Every tracker says "server truth." Here is how you check.

Open Chrome DevTools on claude.ai/settings/usage and watch the page's own request to /api/organizations/{org}/usage. The JSON it returns is the truth value Anthropic enforces for the next sixty seconds. Then run one curl against the loopback HTTP server ClaudeMeter starts at 127.0.0.1:63762. If the floats match byte for byte, your tracker is reading the same thing. If they don't, you have your answer. The whole protocol is three checks and roughly one minute.

M
Matthew Diakonov
9 min read

The trust gap with every other tracker

The Claude usage tracker landscape is full of dashboards that print a number and ask you to trust it. The closed-source Chrome extensions show you a percentage that may or may not match the settings page. The local-log readers (ccusage, Claude-Code-Usage-Monitor) sum tokens out of your local JSONL transcript and present the sum as a quota. None of these tools give you a way to walk the data path yourself, from the wire that claude.ai actually serves, all the way to the number on your screen.

ClaudeMeter is built so you can. The fetch path is two independent processes that hit the same internal endpoint, post their results to a loopback HTTP server, and serialise into a Rust struct that is MIT-licensed and 73 lines long (src/models.rs). The verification protocol below uses that loopback server. You do not need to read the Rust source to trust the output. You read one JSON, then read another JSON, then diff.

The three-step verification protocol

1

Step 1: Intercept the request that draws the page.

Open claude.ai/settings/usage in Chrome, press F12 to open DevTools, switch to the Network tab, filter for usage. Hard-refresh the page. You will see a single XHR to /api/organizations/{some-uuid}/usage. Click it. The Response tab shows a JSON body with five_hour, seven_day, seven_day_sonnet, and seven_day_opus fields, each with a utilization float and a resets_at timestamp. That JSON is the truth value for the next 60 seconds.

Copy the Response body to a scratch file. Note your org UUID from the URL path. You now have a ground-truth snapshot you can compare every other reading against.
2

Step 2: Read the same JSON from the localhost bridge.

If ClaudeMeter is running and the extension is loaded, run curl -s http://127.0.0.1:63762/snapshots from any terminal. Pipe through jq. You should see an array of UsageSnapshot objects, one per browser session. Find the entry whose org_uuid matches the UUID from Step 1, then drill into .usage.five_hour.utilization. Compare that float against what you saw in DevTools Response.

If they match, your tracker is reading server truth. If they differ by more than rounding (the menu-bar app rounds with {:.0}% for display, and the underlying float in the JSON is exact), something else is happening. Most likely: the cached bridge snapshot is older than 60 seconds, or the extension is logged into a different account than the DevTools tab.
3

Step 3: Disable the extension and watch the staleness flip.

Go to chrome://extensions, toggle ClaudeMeter off. Wait 120 seconds. Run curl http://127.0.0.1:63762/snapshots | jq '.[0].stale'. You will see the bridge still returns the last cached snapshot, but the menu-bar app's poll_loop has now seen bridge_fresh = false (last_bridge.elapsed() exceeds BRIDGE_FRESHNESS) and switched to its own cookie-decrypt fetch. The badge in the menu bar still updates because the Rust binary reads your Chrome cookie file directly via Safe Storage in the Keychain.

This is the proof that the tracker has two independent paths to the same number. Re-enable the extension and the next POST resets last_bridge to now, the menu bar yields back to the bridge, and the cycle continues.

Step 1: capture the truth value in DevTools

The settings page makes one network call when you load it, and that call is the entire data source. There is no client-side aggregation, no token counter, no estimation. The response body is rendered into the bars and percentages you see. Open DevTools, filter the Network panel for the word usage, reload, and the call is right there.

DevTools → Network → /api/organizations/<uuid>/usage

Three things to copy down: your org UUID (visible in the request URL), the response body (Right-click → Copy Response), and the three required headers (Cookie, Referer, Accept). The Referer is load-bearing. Drop it and the same endpoint returns 403; the server uses it as a CSRF check. Without these three headers a repeat curl will fail, and any third-party tracker that does not send them is doing something other than what the page does.

Step 2: read the same JSON from the bridge

With ClaudeMeter running, the menu-bar binary is listening on 127.0.0.1:63762/snapshots and the browser extension is POSTing the very response you just captured to that URL once a minute. One curl returns the array of snapshots, one per browser session it is tracking. Drill into the object whose org_uuid matches Step 1, then read .usage.five_hour.utilization.

curl 127.0.0.1:63762/snapshots | jq

The float is the same. The timestamp is the same. The fetched_at tells you the snapshot was retrieved less than a minute ago. The browser field is "Chrome" because the menu-bar app identified the POST's peer process via lsof on the TCP port (peer_browser_by_port at src/bin/menubar.rs). Two processes, two paths, one number. If your script reads from the bridge in production, you are reading the same value the settings page would show you if you were watching it.

Step 3: prove there are two independent paths

A claim like "the tracker reads what claude.ai shows" is easier to believe when there are two ways to get the number and either one works on its own. Disable the extension at chrome://extensions and the menu bar should keep updating. That works because the Rust binary has its own fetch path: it reads the encrypted Chrome cookie file via the macOS Keychain, decrypts the session cookie locally, and calls the same endpoint with the same Cookie/Referer headers. After 120 seconds without an extension POST, the menu-bar app takes over.

src/bin/menubar.rs

Watch the bridge response while the handoff happens. The cached snapshot stays in place for a moment, then the menu-bar refresh overwrites it with new numbers. The stale: true flag in the JSON is the single bit that tells you the data is last-seen rather than just-fetched.

watching the staleness flip

Anchor fact: the entire dual-source contract is two constants

The whole reason curl on localhost can return server truth is two lines in src/bin/menubar.rs: BRIDGE_PORT: u16 = 63762 on line 349 and BRIDGE_FRESHNESS: Duration = Duration::from_secs(120) on line 350. The port is where the loopback HTTP server binds, the freshness budget is how long the menu-bar app trusts an extension POST before falling back to its own decrypt-and-fetch. Inside poll_loop the check is one line: if last_bridge.elapsed() < BRIDGE_FRESHNESS the menu-bar yields. That is it. Two integers, one branch, and the verification protocol works.

The sequence below is one full minute of data flow. Note that the only outbound network call is from the extension to claude.ai. Everything else is local IPC over loopback.

One minute of polling, four participants

claude.aiextension127.0.0.1:63762menu-barGET /api/organizations/{org}/usage200 { five_hour: { utilization: 0.42 } }POST /snapshots [snapshot]AppEvent::Snapshots(Ok(snaps))last_bridge = Instant::now()if bridge_fresh: skip fetch_all

What every protocol outcome means

Verification is not just for the happy path. The interesting cases are when Step 1 and Step 2 diverge, when curl returns nothing, when the bridge says stale. Below is the full decision table for what each outcome means and where to look next.

FeatureWhat it meansWhat you see
Step 1 and Step 2 floats match exactlyHealthy. The tracker is reading server truth.Healthy. Use the bridge value in scripts and dashboards.
Step 2 returns nothing (connection refused)ClaudeMeter is not running. brew services start, or relaunch the app.Not a tracker bug, a process bug. Check Activity Monitor.
Step 2 floats are off by more than roundingBridge cache is older than 60s, or extension hit a different org.Filter the array on org_uuid, then re-diff.
Step 2 returns stale: true after Step 3Expected. The extension stopped posting; the menu-bar takes over.Re-enable the extension and the next POST resets the bridge.
Step 1 returns 403Your Cookie or Referer header is missing or wrong.Use DevTools to copy the headers verbatim, including the lastActiveOrg cookie.
Step 1 returns a different float than the settings page UIThe page renders client-side from a stale cached fetch; refresh.Hard-reload the settings page and re-intercept the network call.

The same protocol, applied to other trackers

You can run Step 1 against any tracker. Capture the DevTools response, then look at whatever number the third-party tool is showing in its UI. If the percentages do not match the DevTools-side floats, the tool is reading something else. The local-log tools are honest about this: ccusage's own README explains it tracks tokens out of the local Claude Code transcript, which is a different data source. Closed-source extensions are where it gets tricky, because there is no guarantee about which endpoint they hit. If you cannot run Step 2 against them, run Step 1 against the same browser session they use and see whether their UI matches.

The reason ClaudeMeter ships the bridge in the first place is so you do not have to take a screenshot at face value. The same loopback URL also makes the data scriptable: a Starship prompt or tmux status line can curl the bridge once a minute and render the float without going near a browser. Whatever you build on top of it, the verification protocol is the same.

FeatureWhat you see in DevToolsSource of truth
What you see in DevTools (Step 1 truth value).five_hour.utilization = 0.42 from /api/organizations/{uuid}/usageSame float, parsed via the Window struct in src/models.rs
What curl returns from the bridge (Step 2 echo).usage.five_hour.utilization = 0.42 from 127.0.0.1:63762/snapshotsIdentical to Step 1, decoded once, served unchanged
Where the bridge value originatedextension POST inside the last 120 seconds (BRIDGE_FRESHNESS)or, after 120 seconds without a POST, menu-bar fetch via decrypted Chrome cookie
How fast a divergence shows upWithin 60 seconds of either fetch path completingstale: true flips on the missing account, badge changes color
What you need to trustThe MIT-licensed source for src/api.rs and extension/background.jsOr just diff the two values yourself; the protocol does not require trust

Stuck on a tracker that does not match the settings page?

Bring a screenshot and the curl output and we will walk through the diff together.

Frequently asked questions

Why does ClaudeMeter run a localhost HTTP server on port 63762?

Because two processes on your machine fetch the same data from claude.ai and they need to coordinate. The browser extension already has your session and can call /api/organizations/{org}/usage with credentials: 'include' from the page context. The Rust menu-bar app runs outside the browser and has to read the encrypted Chrome cookie file to do the same call. If both poll independently every minute, you double the request rate to claude.ai and the menu bar shows numbers a few seconds apart from the popover. The extension POSTs every fetched snapshot to http://127.0.0.1:63762/snapshots and the menu-bar app prefers that bridge value if it arrived in the last 120 seconds. BRIDGE_PORT is defined at src/bin/menubar.rs line 349 and BRIDGE_FRESHNESS at line 350.

What exactly is BRIDGE_FRESHNESS and why 120 seconds?

BRIDGE_FRESHNESS = Duration::from_secs(120) at src/bin/menubar.rs line 350. It is the staleness budget the menu-bar app gives the extension. Inside poll_loop the check is bridge_fresh = last_bridge.elapsed() < BRIDGE_FRESHNESS. If the extension's last POST is younger than 120 seconds, the menu bar skips its own cookie-decrypt fetch entirely and waits another tick. 120 is double the 60-second extension poll cadence, so a single missed extension fetch does not force a fallback, but two missed fetches do. The fallback is what guarantees you keep seeing numbers when the browser is closed.

Do I need ClaudeMeter installed to run the verification protocol?

Step 1 (DevTools intercept) works against any Claude account regardless of which tracker you use, including no tracker. Step 2 (curl 127.0.0.1:63762/snapshots) only works if ClaudeMeter is running, because the bridge is the menu-bar app's own loopback HTTP server. Step 3 (staleness flip) requires ClaudeMeter so you can see the badge state change. If you are auditing a different tracker, run Step 1 to capture the truth value, then compare against whatever number that tracker is showing in its UI.

Why is the bridge plaintext HTTP and not HTTPS?

It binds to 127.0.0.1 only. Server::http("127.0.0.1:63762") at src/bin/menubar.rs line 358 means the socket only accepts connections from the loopback interface. Nothing on the network can reach it, including other machines on the same Wi-Fi. Adding TLS would require a self-signed cert and a trust prompt for what is already a local-only socket. The CORS headers at lines 366-370 allow OPTIONS preflight from any origin so the extension's POST works from claude.ai's page context, but the listener itself never sees external traffic.

What if the curl returns a stale snapshot?

The snapshot's stale field flips to true. In the Rust schema at src/models.rs line 71, every UsageSnapshot has a stale: bool. The menu-bar app's merge_with_persisted function (src/bin/menubar.rs lines 840-884) marks an account as stale if the same browser POSTs without that account in the new snapshot, and drops it entirely after a 2-hour cutoff (chrono::Duration::hours(2) at line 865). When you read .[0].stale from the curl, you know whether the number is live or last-seen. The menu-bar UI also paints stale rows with '(stale, last 14:23)' so you can see the staleness in the dropdown without scripting anything.

How does the bridge know which browser POSTed a snapshot?

src/bin/menubar.rs lines 437-462. The bridge reads req.remote_addr() to get the peer's TCP port, runs lsof -nP -iTCP:{port} -sTCP:ESTABLISHED to find the local PID that owns that socket, runs ps -p {pid} -o command= to read the executable path, and matches substrings like /Arc.app/, /Google Chrome.app/, /Brave Browser.app/, /Microsoft Edge.app/ in classify_browser_exe. Falls back to Sec-Ch-Ua sniffing if lsof returns nothing. This is why the dropdown shows your real browser name even when the extension's User-Agent looks generic.

What does jq see if I curl the bridge with no extension running?

If the extension was running in the last 120 seconds the bridge serves the last cached snapshot. After 120 seconds with no POST, the menu-bar app's poll_loop wakes up, runs fetch_all, and the Rust binary's own bridge handler keeps serving until the next refresh writes new snapshots to disk via save_snapshots. So curl 127.0.0.1:63762/snapshots may return last-seen JSON with stale: true rather than a 404. Check fetched_at to see when the data was actually retrieved.

Will Anthropic block this approach?

The endpoint is what claude.ai's own settings page uses, called with the same headers the page sends (Cookie, Referer: https://claude.ai/settings/usage, Accept: */*). It is not a private API key or a scraping path. ClaudeMeter sends one HTTPS request per minute per org membership, identical to what an open browser tab would do if you reloaded the settings page. If Anthropic ships a breaking change to the response shape, the Rust serde deserializer fails fast and the menu bar shows '!' rather than a wrong number. The README documents this risk explicitly.