My company recently started supporting Cursor, so I jumped in. But checking usage meant opening the dashboard every time — and if Max mode was on without me noticing, requests burned through fast.
I wanted a glanceable counter, always visible.
So I built one.
V1: SwiftUI — Done in a Weekend, Regretted in a Week
The first version took two days. SwiftUI's MenuBarExtra made it trivial to get something on screen. I was proud of it.
Then I checked Activity Monitor.
56 MB of memory. For a menu bar app that displays a single number.
I tried to optimize — removed animations, simplified views, cached aggressively. Nothing moved the needle. The SwiftUI runtime itself was the cost. MenuBarExtra allocates a full SwiftUI rendering pipeline whether you need it or not.
For a "set and forget" menu bar app that runs 24/7, 56MB felt like a tax I shouldn't be paying.
The Rewrite: Burning It All Down
I threw out the entire UI layer and rebuilt everything in pure AppKit. NSStatusItem, NSPopover, NSViewController — the APIs your framework tries to hide from you.
It was painful. SwiftUI's declarative bindings became manual NSTextField updates. Layout constraints replaced VStack. Every state change needed explicit UI synchronization.
But the result:
| SwiftUI | AppKit | |
|---|---|---|
| Memory (RSS) | ~56 MB | ~13 MB |
| Lines of UI code | ~200 | ~660 |
3x more code, 4x less memory. For a menu bar app, I'd make that trade every time.
Other SwiftUI-based menu bar apps I've seen typically sit around 30–50MB. The framework overhead is real.
The API That Doesn't Exist
Cursor has no public API for usage data. But their dashboard has to get the numbers from somewhere.
Browser DevTools → Network tab → three requests:
-
/api/usage— per-model request counts -
/api/usage-summary— billing cycle, plan usage in USD cents -
/api/auth/me— user info
No documentation. Cookie-based auth. The response shape has changed at least once since I started.
This meant two things:
1. No hardcoding. The /api/usage response contains model names as dynamic keys. If Cursor adds claude-4-opus tomorrow, the parser picks it up automatically — no app update needed.
2. Expect failure. Both API calls run in parallel. If /api/usage-summary fails but /api/usage succeeds, the app shows what it can. If both fail, it shows the last known data. The app never crashes on an API change — it just degrades gracefully.
Zero Dependencies, on Purpose
The Package.swift imports nothing. No Alamofire, no KeychainAccess, no SwiftyJSON. Just macOS SDK frameworks.
This wasn't ideology. It was pragmatism:
- The app does three HTTP requests. I don't need a networking library for that.
- Keychain access is ~80 lines of
Securityframework calls. A dependency would be longer. - JSON decoding is
Codable. It's built in.
The side benefit: no supply chain to audit, no version conflicts, no "this dependency requires macOS 15 but I target 14" surprises.
What I Actually Learned
AppKit isn't dead — it's just unfashionable. For long-running, low-footprint apps, the memory savings are real and measurable. SwiftUI is better for most things. But not this.
Swift 6 strict concurrency is worth the pain. The API client is an actor. The UI is @MainActor. All models are Sendable. It took effort to get it compiling, but an entire category of bugs simply can't happen now.
Build for the API to break. When you depend on undocumented endpoints, "it works today" is the only guarantee. Dynamic parsing + graceful degradation isn't a nice-to-have — it's the whole architecture.
The App
CursorMeter is open source (MIT). macOS 14+, zero dependencies, ~13MB memory.
curl -sL https://raw.githubusercontent.com/WoojinAhn/CursorMeter/main/Scripts/install.sh | bash
If you use Cursor daily and want to keep an eye on your usage without opening a browser, give it a try. Issues and feedback are always welcome.


Top comments (0)