TL;DR: I built 5 Hour Window, a small Chrome extension that shows my Claude, Cursor and Z.ai usage limits in one popup, using the sessions I am already logged in with. Two of the three providers were easy. Z.ai taught me that "it works when I open the URL" and "it works from an extension" are different claims. Code is on GitHub, MIT licensed.

I pay for three AI coding plans, and each one meters me differently. Claude has a 5-hour session window and a weekly cap. Cursor has a monthly plan plus a separate weekly bucket for its Grok model. Z.ai has a 5-hour token window and a monthly tool-call quota. Checking all of them meant three tabs, three dashboards, and a lot of mental arithmetic about when each window resets.

All three dashboards fetch their numbers from a JSON endpoint. So the plan was simple: call those endpoints from an extension, normalize the answers, show bars.

No cookie copying

The first question was auth. I did not want to paste session cookies into a settings page, both because it is clumsy and because cookies expire.

It turns out I did not need to. A manifest v3 extension with host_permissions for a site can fetch that site from its service worker with credentials: 'include', and the browser attaches your existing cookies. If you are logged in to claude.ai in that profile, the extension is too.

"host_permissions": [
  "https://claude.ai/*",
  "https://cursor.com/*",
  "https://api.z.ai/*",
  "https://z.ai/*"
]

One catch: those requests carry a chrome-extension:// origin, which is exactly what CSRF checks are designed to reject. A declarativeNetRequest session rule rewrites Origin and Referer to the real site, scoped with tabIds: [TAB_ID_NONE] so it only touches requests that come from the service worker and never your actual tabs.

Claude also needs an organization id in the URL. That one is free: claude.ai keeps it in a lastActiveOrg cookie, which the cookies permission can read, with /api/organizations as a fallback.

Every provider gets the same shape

Each provider has a different response, so each one gets a small adapter that returns the same thing:

// { ok, plan?, meters: [{ id, label, percent, resetsAt, weekly? }], extra? }

percent is always 0 to 100 and resetsAt is always epoch milliseconds. Claude gives ISO strings, Cursor gives ISO strings on one endpoint and stringified milliseconds on another, Z.ai gives raw milliseconds. After the adapter, the popup does not care.

The service worker polls on a chrome.alarms timer, keeps the last snapshot plus 14 days of history in storage.local, and draws a sparkline for each weekly meter so I can see how fast the week is going. The badge shows the highest weekly percentage, green, amber or red. Dates are DD/MM HH:mm, because that is the only order a date should be in.

Cursor had two meters, I found one

The first version of the Cursor card showed exactly one line: Weekly, 0.1%. I was clearly using Cursor more than that.

The number was not wrong. get-sand-usage-status returns usagePercent: 0.129813, which is already a percentage, and Cursor's own dashboard rounds the same value up to "1% used". The problem was that this endpoint only describes the weekly Grok bucket. My actual Pro plan usage lives on a second endpoint, get-current-period-usage, which splits into "Cursor Models" and "Other Models", 12% and 34% at the time.

So the Cursor adapter now calls both with Promise.allSettled, and if one of them fails the card still shows the other. Labels match the dashboard, so I do not have to translate in my head.

Z.ai does not use cookies at all

Z.ai showed "Not logged in", and I had a strong argument that it should not: the quota URL returned perfect JSON in my browser.

What I had actually done was copy that JSON from the DevTools network tab while the dashboard was open. That request was made by the dashboard's own code. So I asked the API directly, with cookies only:

code 1001: Authentication parameter not received in Header, unable to authenticate

The page lives on z.ai, the API lives on api.z.ai, and the API never looks at cookies. The dashboard reads a token out of localStorage and sends it as an Authorization header. Claude and Cursor authenticate with same-site session cookies, so the extension inherited my login for free. Z.ai authenticates with a header that only the page knows how to build.

The fix is a content script on z.ai that reads the token and hands it to the service worker. My first version took the first JWT-shaped value it found. There are two:

localStorage key Result
z-ai-website-token 401: token expired or incorrect
z-ai-open-platform-token-production 200, real data

The script now prefers the open-platform key. Both Bearer <token> and the raw token work, for the record.

Two smaller Z.ai details. It returns HTTP 200 even when auth fails, with the real error code inside the JSON body, so a status-code check alone sees success. And the limit windows are encoded as unit and number: unit: 3, number: 5 is five hours, unit: 5, number: 1 is one month.

The cost of the header approach is a one-time step: visit any z.ai page while logged in, then open the popup. After that it refreshes in the background until the token expires. A Z.ai API key in Settings works too, if you would rather not depend on the session at all.

Small things that made it nicer

  • Drag to reorder. Cards only become draggable while the grip is held, so links and text inside a card still behave normally. The order lives under its own storage key, so reordering does not trigger a refetch.
  • No redraw mid-drag. If a background refresh lands while you are dragging, the popup waits until you drop before rendering, so the card does not snap back.
  • One notification per window. Alerts are keyed by provider, meter and reset time, so crossing 80% notifies you once per window, not once per poll.
  • Real errors. The card shows what the server said, like HTTP 401: token expired, instead of a generic "Not logged in". That one change would have saved me the Z.ai detour.

What I would tell myself at the start

"It works in my browser" was true for all three providers and meant three different things. For Claude and Cursor it meant "your cookies are enough". For Z.ai it meant "the page's JavaScript is enough", which is a much stronger dependency. The quickest way to tell them apart is to replay the request with nothing but cookies and read what comes back.

The extension is plain JavaScript with no build step: load the folder unpacked and it runs. Code is on GitHub, MIT licensed. Adding another provider is one adapter function plus its domain in the manifest and the header rules.