I Built a Cursor IDE Extension to Check My AI Requests Without Leaving the Editor

March 15, 2026 (1mo ago)

Samyak Jain

Samyak Jain

Introduction

I live in Cursor most days. For months I kept doing the same dance: Cmd+Tab to a browser, open settings or the dashboard, squint at how many fast requests were left, then tab back. It’s not a crisis. It’s just… a lot of context switching for a number I wanted maybe ten times a day.

So I built a tiny extension: remaining fast requests in the status bar. Click to refresh. It goes red when you’re low. That’s the whole product surface.

Cursor Usage Monitor

Below is how it actually works — where the token lives, which API answered, and how I read the DB without doing something dumb as state.vscdb keeps growing (chats and global state pile into that file over time).

It’s on Open VSX: open-vsx.org/extension/Sammy970/cursor-usage

Table of Contents

The Problem

On Pro you get something like 500 fast requests a month for the good models. After that you’re on slow mode — still usable, but you feel it.

Cursor doesn’t surface that counter in the editor. Officially you check the web. Fine for monthly accounting; annoying when you’re mid-flow and just want a gut check.

I wanted a number I could see without leaving the window.

Why the Obvious Approach Doesn't Work

First thing I tried was the obvious dashboard-style URL:

GET https://cursor.com/api/usage?user=<USER_ID>

That route wants the browser cookie WorkosCursorSessionToken. Extensions run in Node inside the IDE — not in a tab — so there’s no cookie jar to pull from. I could see the network calls in DevTools; I couldn’t replay them honestly from extension code.

fetch(..., { credentials: "include" }) doesn’t magically attach your Chrome session. You get redirects and 401s. Dead end.

Digging Into How Cursor Stores Auth

Cursor is Electron. Like VS Code, a pile of stuff lives in a SQLite file:

PlatformPath
macOS~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
Windows%APPDATA%\Cursor\User\globalStorage\state.vscdb
Linux~/.config/Cursor/User/globalStorage/state.vscdb

I poked around:

SELECT key, value FROM ItemTable WHERE key LIKE 'cursorAuth%';

Useful rows included:

KeyValue
cursorAuth/accessTokeneyJhbGciOiJIUzI1NiIs... (JWT)
cursorAuth/refreshTokeneyJhbGciOiJIUzI1NiIs... (JWT)
cursorAuth/cachedEmailyou@example.com
cursorAuth/stripeMembershipTypepro

Decode the access token and the payload points at real API hosts — in my case aud was https://api2.cursor.sh. That’s the backend audience, not the marketing site. Worth following.

Finding the Right API Endpoint

With a bearer token, probing api2.cursor.sh eventually landed here:

GET https://api2.cursor.sh/auth/usage
Authorization: Bearer <accessToken>

Example payload:

{
  "gpt-4": {
    "numRequests": 150,
    "numRequestsTotal": 150,
    "numTokens": 132713206,
    "maxRequestUsage": 500,
    "maxTokenUsage": null
  },
  "startOfMonth": "2026-03-01T00:00:00.000Z"
}

No user id in the query string. Remaining fast requests ≈ maxRequestUsage - numRequests for the gpt-4 bucket (that’s what the UI calls “fast” in practice).

Building the Extension

Still four TypeScript files — small on purpose:

src/
├── auth.ts       — token from state.vscdb (see below)
├── api.ts        — GET api2.cursor.sh/auth/usage
├── statusBar.ts  — status item + tooltip
└── extension.ts  — activate, refresh command, 5 min timer

Reading the Token

This part changed after v1 shipped. Two things were true:

  1. state.vscdb doesn’t stay tiny. Global storage grows. A friend hit a multi‑gigabyte file; Node’s single-shot readFile path caps around 2 GiB and blew up. So we don’t rely on “read the whole file in one syscall” anymore for the fallback path.

  2. We only need one string. Loading the entire DB into WASM just to read one cell is wasteful when the runtime can open SQLite by path.

Preferred path: Node’s built‑in node:sqlite (DatabaseSync), when the editor’s Node/Electron actually exposes it. Open state.vscdb read‑only on disk, run SELECT value FROM ItemTable WHERE key = ?, done. SQLite pulls pages as needed — you’re not malloc’ing the whole file in JS for one JWT.

Fallback: sql.js (SQLite compiled to WASM). It only accepts an in‑memory copy of the DB, so we still read the file into a buffer — but we read it in chunks so we stay under Node’s limits. Yes, that means heavy RAM use on huge DBs; that’s why the native sqlite path is first.

Rough shape of the real code:

// 1) Try node:sqlite — file-backed, read-only
const { DatabaseSync } = await import("node:sqlite");
const db = new DatabaseSync(dbPath, { readOnly: true });
const row = db
  .prepare("SELECT value FROM ItemTable WHERE key = ? LIMIT 1")
  .get("cursorAuth/accessToken") as { value: string } | undefined;
db.close();
 
// 2) If import or open fails → sql.js + chunked read into Buffer, then SQL.Database(buffer)

I didn’t switch to better-sqlite3 — native addons have to match Electron’s ABI, and everyone’s on a slightly different Cursor build. A built-in module plus a pure-JS fallback keeps installs boring.

The extension never persists the token; it’s read, used for one HTTPS request, then dropped.

Calling the API

No axios — just Node’s https module:

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

Bottom right: lightning icon, “N left”, warning background under 50. Tooltip has the breakdown.

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

extension.ts registers the refresh command, runs an initial fetch, and sets a 5‑minute interval:

export function activate(context: vscode.ExtensionContext): void {
  statusBarItem = new UsageStatusBarItem();
 
  const command = vscode.commands.registerCommand("cursorUsage.refresh", () => {
    refresh();
  });
 
  context.subscriptions.push(command);
 
  refresh();
 
  refreshTimer = setInterval(() => refresh(), 5 * 60 * 1000);
}

Security Considerations

Before publishing I went through the boring checklist:

Token — Read from disk per refresh, never written by the extension, never logged. On API errors I don’t dump response bodies (they could theoretically echo weird stuff).

Network — Only https://api2.cursor.sh/auth/usage. No analytics domain, no random POST. You can grep the repo.

Errors — Status bar text stays generic so we don’t leak home-directory paths in tooltips.

DB — Read path only (readOnly when using node:sqlite; sql.js path is read-only file access, no writes back into Cursor’s DB).

How to Install It

Cursor pulls from Open VSX for the Extensions panel.

  1. Cmd+Shift+X (or your platform’s shortcut)
  2. Search Cursor Usage
  3. Install Sammy970

CLI:

cursor --install-extension Sammy970.cursor-usage

Source: github.com/Sammy970/cursor-usage-extension

Conclusion

The fun bit was the scavenger hunt: where Electron stashes tokens, which aud the JWT expects, which hostname actually answers GET /auth/usage. The extension itself is a thin wrapper.

If you’re poking at Cursor or VS Code tooling, state.vscdb is a useful artifact — just remember it’s not a static file; it’ll swell as you use the product. Designing around “one row read” instead of “slurp entire file” was worth the extra branch in auth.ts.

MIT licensed. If it saves you a tab, cool — star or PRs welcome.