Samyak Jain
Introduction
I use Cursor every day. And every day, at some point, I'd open a browser, navigate to the Cursor dashboard, and check how many fast requests I had left for the month.
It was a tiny thing. But it was annoying enough that I decided to fix it.
The result: a Cursor IDE extension that shows your remaining AI requests directly in the status bar — no browser, no dashboard, no context switching.

That's it. Always visible. Click to refresh. Turns red when you're running low. In this post I'll walk you through how it works, what I had to reverse engineer to build it, and the security decisions I made before publishing it.
The extension is live on the Open VSX registry: open-vsx.org/extension/Sammy970/cursor-usage
Table of Contents
The Problem
Cursor gives you 500 fast requests per month on the Pro plan. Fast requests are the ones that use the best models — GPT-4o, Claude Sonnet, etc. Once you hit the limit, you're bumped down to slow requests, which are rate-limited and noticeably laggy.
The only way to check your usage is to open cursor.com/settings in a browser. Every time I wanted to know where I stood, I had to leave the editor entirely. For something I check multiple times a day, it was a constant small friction.
What I wanted was simple: a number in the corner of my screen, always there, that I could glance at without breaking my flow.
Why the Obvious Approach Doesn't Work
My first instinct was to call the same API the Cursor dashboard uses:
GET https://cursor.com/api/usage?user=<USER_ID>
Simple enough. But this endpoint requires the browser session cookie WorkosCursorSessionToken — the one that gets set when you log into cursor.com in Chrome or Safari.
A VS Code extension runs in Node.js. It has no access to browser cookies. So even though I could find the right endpoint, I had no way to authenticate against it from inside the IDE.
I also tried making the request with credentials: "include" — that only works inside a browser context. In Node.js it's silently ignored. The server returns a 308 redirect to the non-www version and then a 401.
Back to square one.
Digging Into How Cursor Stores Auth
Cursor is built on Electron — which means it's essentially a Chromium browser wrapped around a Node.js runtime. Like VS Code, it stores all its persistent state in a local SQLite database:
| Platform | Path |
|---|---|
| macOS | ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb |
| Windows | %APPDATA%\Cursor\User\globalStorage\state.vscdb |
| Linux | ~/.config/Cursor/User/globalStorage/state.vscdb |
I opened this database and queried the ItemTable for anything auth-related:
SELECT key, value FROM ItemTable WHERE key LIKE 'cursorAuth%';The results were exactly what I needed:
| Key | Value |
|---|---|
cursorAuth/accessToken | eyJhbGciOiJIUzI1NiIs... (JWT) |
cursorAuth/refreshToken | eyJhbGciOiJIUzI1NiIs... (JWT) |
cursorAuth/cachedEmail | you@example.com |
cursorAuth/stripeMembershipType | pro |
The accessToken is a JWT that the Cursor app itself uses to communicate with its own backend. I decoded the payload:
{
"sub": "google-oauth2|user_01K1J3DH0GPY9ZT8J4ANY3Z1F5",
"time": 1741234567,
"exp": 1741238167,
"iss": "https://cursor.com",
"scope": "openid profile email",
"aud": "https://api2.cursor.sh"
}Notice the aud field: https://api2.cursor.sh. That's the audience this token is meant for — Cursor's actual backend API, not the public website. This was the lead I needed.
Finding the Right API Endpoint
With the token in hand, I started probing api2.cursor.sh. After a few attempts I hit the right endpoint:
GET https://api2.cursor.sh/auth/usage
Authorization: Bearer <accessToken>
Response:
{
"gpt-4": {
"numRequests": 150,
"numRequestsTotal": 150,
"numTokens": 132713206,
"maxRequestUsage": 500,
"maxTokenUsage": null
},
"startOfMonth": "2026-03-01T00:00:00.000Z"
}No user ID parameter needed. No browser cookie. Just the Bearer token from the local database — and it works perfectly. Remaining requests = maxRequestUsage - numRequests.
Building the Extension
The extension is four TypeScript files:
src/
├── auth.ts — reads the token from state.vscdb using sql.js
├── api.ts — calls api2.cursor.sh/auth/usage
├── statusBar.ts — renders the status bar item and tooltip
└── extension.ts — activation, refresh command, 5-min auto-refresh
Reading the Token
I used sql.js — a pure WebAssembly build of SQLite — to read the database without any native compilation. This means it works on macOS, Windows, and Linux out of the box with no node-gyp build step.
export async function getAccessToken(): Promise<string> {
const dbPath = getStateDbPath(); // platform-specific path
const initSqlJs = require("sql.js");
const SQL = await initSqlJs();
const fileBuffer = fs.readFileSync(dbPath);
const db = new SQL.Database(fileBuffer);
const result = db.exec(
`SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken' LIMIT 1`
);
db.close(); // closed immediately after read
const token = result[0].values[0][0] as string;
return token;
}The database is opened read-only and closed immediately after the single query. The token is never stored anywhere by the extension itself.
Calling the API
The API call is made using Node's built-in https module — no third-party HTTP libraries:
export async function fetchUsage(token: string): Promise<UsageData> {
const body = await httpsGet("https://api2.cursor.sh/auth/usage", token);
const json = JSON.parse(body) as {
"gpt-4"?: { numRequests?: number; maxRequestUsage?: number };
startOfMonth?: string;
};
const model = json["gpt-4"] ?? {};
const fastUsed = model.numRequests ?? 0;
const fastLimit = model.maxRequestUsage ?? 500;
return {
fastRequestsUsed: fastUsed,
fastRequestsLimit: fastLimit,
fastRequestsRemaining: Math.max(0, fastLimit - fastUsed),
startOfMonth: json.startOfMonth ?? "",
};
}The Status Bar Item
The status bar item lives in the bottom-right corner of Cursor. It shows the remaining count, turns warning-red below 50 requests, and displays a full breakdown on hover:
setUsage(data: UsageData, updatedAt: Date): void {
const remaining = data.fastRequestsRemaining;
this.item.text = `$(zap) ${remaining} left`;
if (remaining < 50) {
this.item.backgroundColor = new vscode.ThemeColor(
"statusBarItem.warningBackground"
);
}
this.item.tooltip = new vscode.MarkdownString([
`**Cursor AI Usage**`,
``,
`⚡ Fast requests used: ${data.fastRequestsUsed} / ${data.fastRequestsLimit}`,
`✅ Remaining: **${remaining}**`,
``,
`_Since ${new Date(data.startOfMonth).toLocaleDateString()}_`,
`_Last updated: ${updatedAt.toLocaleTimeString()}_`,
``,
`_Click to refresh_`,
].join("\n"));
}Wiring it all Together
The extension.ts entry point handles activation, the refresh command, and the 5-minute auto-refresh timer:
export function activate(context: vscode.ExtensionContext): void {
statusBarItem = new UsageStatusBarItem();
const command = vscode.commands.registerCommand("cursorUsage.refresh", () => {
refresh();
});
context.subscriptions.push(command);
// Initial fetch on startup
refresh();
// Auto-refresh every 5 minutes
refreshTimer = setInterval(() => refresh(), 5 * 60 * 1000);
}Security Considerations
Before publishing to the Open VSX registry, I audited the extension carefully. Here's what I checked:
1. Token Handling
The access token is read from disk on each refresh call and used immediately. It is never written to disk by the extension, never logged to the output channel, and never stored in VS Code's settings or secret storage. Once the API call completes, it's out of scope and garbage collected.
2. Network Calls
The token is sent exclusively to https://api2.cursor.sh/auth/usage over HTTPS. There are no third-party servers, no telemetry endpoints, no analytics calls. You can verify this by reading the source — api2.cursor.sh is the only outbound URL in the entire codebase.
3. Error Message Sanitisation
Two specific error messages needed sanitising before publishing:
- API errors — originally included the raw response body, which could theoretically echo back auth material. Fixed to surface only the HTTP status code.
- Missing database errors — originally included the full filesystem path to
state.vscdb, which would expose the user's home directory in the status bar tooltip. Fixed to show a generic message.
4. Read-Only Database Access
state.vscdb is opened with fs.readFileSync (read-only) and the database is closed immediately after extracting the single token value. The extension cannot modify Cursor's internal state.
How to Install It
The extension is published on the Open VSX registry, which is what Cursor uses for its Extensions panel.
- Open the Extensions panel (
Cmd+Shift+X) - Search for Cursor Usage
- Install the one by
Sammy970
Or install it directly from your terminal:
cursor --install-extension Sammy970.cursor-usage
The source code is on GitHub: github.com/Sammy970/cursor-usage-extension
Conclusion
The most interesting part of this project wasn't the 200 lines of TypeScript — it was the investigation. Finding the right API by probing endpoints, reading JWTs from a local SQLite database, understanding what Cursor's Electron shell stores and where.
If you're building tools around Cursor or VS Code, the state.vscdb file is worth exploring. It stores everything from auth tokens to recently opened folders to per-extension state. A lot is possible if you know where to look.
The extension is open source under the MIT licence. If you use Cursor daily and find it useful, feel free to star the repo, open issues, or contribute improvements.