Skip to content

Add Ledger: a menu-bar app for Cursor spend#103

Open
kyleve wants to merge 12 commits into
mainfrom
cursor/ledger-spend-menubar
Open

Add Ledger: a menu-bar app for Cursor spend#103
kyleve wants to merge 12 commits into
mainfrom
cursor/ledger-spend-menubar

Conversation

@kyleve

@kyleve kyleve commented Jul 17, 2026

Copy link
Copy Markdown
Owner

What

A new native-macOS menu bar app, Ledger, that shows your current-cycle Cursor spend at a glance (with a year-to-date total in the popover). Building it also restores the native-macOS build infrastructure that was removed with Foreman (#68) — the .macOS platform, a .mac app target + hostless macOS test bundle, the platform-scoped CI scheme, and the test-macos job.

Menu-bar title shows the live cycle-to-date amount (a $— placeholder until the first fetch); clicking opens a popover with this cycle's spend + date range, year-to-date, included-usage used/limit, and plan tier.

Data source

Works for individual accounts by calling the same undocumented endpoints the cursor.com/dashboard/usage page uses (the team Admin API only sees team accounts):

  • GET /api/usage-summary — current cycle dates, plan, and live usage-based spend (individualUsage.onDemand.used).
  • POST /api/dashboard/get-monthly-invoice — per-month billed totals, summed across the year for the YTD figure.

Auth is zero-config: it auto-detects your Cursor session (read-only, from the local app's state.vscdbcursorAuth/accessToken) and builds the required WorkosCursorSessionToken=<userId>::<jwt> cookie, deriving userId from the JWT sub (a bare JWT 401s). Settings › Account has an optional paste-a-token override (stored in the Keychain) for when auto-detect fails or the session expires.

Structure

  • Ledger/LedgerCore (macOS-only SPM library) — LedgerServices (@MainActor @Observable root with a single LoadState), SessionToken/CursorLocalTokenSource, DashboardProvider/CursorDashboardAPI, UsageSummary/MonthlyInvoice/SpendSnapshot, KeychainStore, and LoginItemController (launch-at-login, restored from Foreman).
  • Ledger/Ledger (.mac, LSUIElement) — the AppKit NSStatusItem + NSPopover shell (title driven by an Observations loop, deliberately not MenuBarExtra), a System-Settings-style sidebar, and the thin LedgerSession facade.
  • Build wiring: .macOS(.v26) + LedgerCore in Package.swift; Ledger/LedgerCoreTests targets, Ledger/Ledger-macOS-Tests schemes in Project.swift; the test-macos CI job; root AGENTS.md + per-module README.md/AGENTS.md.

Notes / caveats

  • The dashboard endpoints are undocumented and can change without notice.
  • It rides your Cursor web session (re-open Cursor when it expires).
  • On a plan with usage-based pricing off, the $ figures reflect included-compute value, not money owed.
  • Only the current cycle is "live"; YTD is prior months' billed invoices + the current cycle's live figure (the current month's invoice lags until charges post).

Verification

  • ./swiftformat --lint — clean
  • tuist generate --no-open — clean
  • tuist test Ledger-macOS-Tests — 26 tests pass (incl. a real SQLite round-trip for the token reader and the JWT→cookie derivation)
  • tuist build Stuff-iOS-Tests — iOS scheme unaffected
Open in Web Open in Cursor 

kyleve added 12 commits July 17, 2026 15:15
The model layer for a new Ledger menu-bar app: fetches current-cycle Cursor
spend from the Admin API (POST /teams/spend, Basic auth) and reduces it to the
signed-in team member via a single observable LoadState. The Admin API key
lives in the Keychain (KeychainStore); only the email + refresh interval
persist as JSON (LedgerConfigStore). Restores the .macOS(.v26) platform and a
LedgerCore product/target in Package.swift.

Groundwork for the Ledger app (next commit); builds via swift build.
The native-macOS shell over LedgerCore: an NSStatusItem whose title shows the
current-cycle spend (updated via an Observations loop) toggling an NSPopover
that renders the LoadState, plus a System-Settings-style sidebar (General +
Account panes) where the email and Keychain-stored Admin API key are set.

Wires the Ledger app target (.mac, LSUIElement), the hostless LedgerCoreTests
bundle, the Ledger / Ledger-macOS-Tests schemes, and restores the test-macos
CI job. Verified: swiftformat --lint, tuist generate, and tuist test
Ledger-macOS-Tests all pass (30 tests).
Adds README.md + AGENTS.md for Ledger/Ledger and Ledger/LedgerCore, and
restores the native-macOS references in the root AGENTS.md (deployment row,
the two-platform CI-scheme note, directory-layout mention, and the macOS test
command). CLAUDE.md files regenerated via ./sync-agents (gitignored).
The Admin API (/teams/spend) only sees team accounts, so an individual
account can't use it. Re-point the data layer at the same undocumented
endpoints the cursor.com dashboard itself calls:

- GET /api/usage-summary — current cycle dates, plan, and live usage-based
  spend (individualUsage.onDemand.used).
- POST /api/dashboard/get-monthly-invoice — per-month billed totals, summed
  across the year for a year-to-date figure (the original yearly ask, now
  feasible).

Auth is the WorkOS session cookie, value "<userId>::<jwt>". SessionToken
derives the userId from the JWT sub (a bare JWT 401s). CursorLocalTokenSource
auto-detects it read-only from the local Cursor app's state.vscdb; a pasted
token in the Keychain overrides it. Drops the email + Admin API key UI.

Verified against a live account (usage-summary + invoices) and via
Ledger-macOS-Tests (26 tests); swiftformat --lint clean.
The status item was icon-only until the first fetch, making it easy to lose
among other menu-bar items. Show the current-cycle amount as the title always,
with a "$—" placeholder while loading, so it's a wider, findable target.
Opening the menu-bar popover fired a network fetch every time, which is too
frequent. Rely on the periodic 15-minute refresh loop (which drives both the
title and the popover) plus the explicit Refresh button; opening now just
shows the latest fetched state.
Host a SwiftUI MenuBarLabel in the status item (via a click-through
NSHostingView, so the button still toggles the popover) instead of setting a
plain NSButton title, and give both it and the popover's headline amount
.contentTransition(.numericText()) so digit changes roll over. The hosted
label observes the session directly, replacing the manual Observations title
loop.
A variable-length NSStatusItem doesn't auto-size to a hosted SwiftUI view, so
only the leading icon was visible and the amount was clipped. MenuBarLabel now
reports its rendered width (fixedSize + onGeometryChange) and the app delegate
sets the item's length to match.
Adds an included-usage progress bar (usage-summary totalPercentUsed) with
Cursor's own status messages, and a top-models-this-cycle breakdown from
get-aggregated-usage-events. The per-model figure measures compute differently
from the billed on-demand headline (they don't reconcile), so models are shown
as usage *shares*, not dollars. The aggregated fetch is best-effort — a
failure logs and yields no models rather than failing the whole load.
A script to run Ledger standalone without Xcode: regenerates the project,
builds the Release configuration via xcodebuild (ad-hoc signed, so no Apple
Developer account needed), installs to /Applications (quitting any running
copy first), and launches it. --no-open skips the launch.
quit is asynchronous, so poll until the installed binary has actually exited
(force-killing as a fallback) before rm/cp — making repeated runs race-free.
Silences the 'No App Category is set' build warning by tagging Ledger as a
developer-tools app in its Info.plist.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant