Our little status pane said $10.80 spent of $200.00, $189.20 left. The provider's console, open in the next tab, said $291.77 remaining. Same account, same day.

Both numbers were wrong, and for different reasons. This is the write-up of finding the number that is actually true, and of the one-line-shaped design change that stops the pane from ever mixing two accountings again.

The setup

It's a plugin for the Claude Code agent harness: a mod that registers a UI pane and redraws it on a timer. It shows the account's remaining balance next to the rate-limit windows, the context fill, and the session cost. It runs in TypeScript, it is loaded as source at session start, and it has a test suite that drives it through a mocked engine.

The provider is a reseller running new-api, the open-source console that a lot of LLM API resellers are built on (its ancestor, one-api, behaves the same way). That detail matters later.

Bug one: a budget nobody set

The pane compared a spend figure against budgetUsd, a config value with a default of 200:

const DEFAULT_BUDGET_USD = 200 

Nobody measured that 200. It was a placeholder that shipped, and because it was rendered with two decimal places next to a real spend number, it read as data. The pane confidently announced that 189 dollars were left, which is exactly the kind of wrong answer that is worse than "unknown" — you act on it.

The spend figure came from the billing API:

GET /v1/dashboard/billing/usage {"total_usage": 3004} // cents 

That endpoint is the OpenAI-compatible one, and on this provider it reports a different number from the console. More on that below.

Bug two: it was counting the wrong thing

The console exposes the account itself:

GET /api/user/self Authorization: Bearer <console token> New-Api-User: <numeric user id> {"data": {"quota": 145883249, "used_quota": 16616751}, "success": true} 

quota is what's left; used_quota is what's been burned. Both are in quota units, not cents, not dollars.

Two gotchas, both of which cost me a round trip:

  • The New-Api-User header is not optional. Without it the request fails in a way that looks like a bad token, so you go and re-paste the token for no reason.
  • You need the unit ratio. new-api and one-api default to 500,000 units per US dollar, but it's configurable per deployment, so it is not something to hardcode a second time.

I didn't have to look the ratio up, though. The tell was in the numbers: quota + used_quota came to 162,500,000, and the console's own page showed a tidy $325.00 total. Round totals don't happen by accident. 162,500,000 / 325 = 500,000.

So the honest reading was: $291.77 left, $33.23 spent, $325.00 total. (The total is what pinned the ratio down; the pane itself no longer carries it — more at the end.)

The two accountings do not reconcile

Source Spent
/v1/dashboard/billing/usage $30.04
Console (used_quota) $33.23

Neither is a rounding error. If you average them you get a number that is true in no system. The fix is not arithmetic — it's to decide which source is authoritative and to label every figure with where it came from, so two accountings can never be silently blended.

That's a discriminated union and a null:

export type QuotaOrigin = 'console' | 'billing' export type QuotaBudget = { at: number origin: QuotaOrigin balanceUsd: number | null // console only spentUsd: number | null // billing only budgetUsd: number // ... } 

And the merge function only carries figures across from a previous snapshot of the same origin:

const merged = (was: QuotaBudget | null, now: QuotaBudget): QuotaBudget => { const same = was !== null && was.origin === now.origin ? was : null return { origin: now.origin, balanceUsd: now.balanceUsd ?? same?.balanceUsd ?? null, spentUsd: now.spentUsd ?? same?.spentUsd ?? null, // ... } } 

The parsing is deliberately strict about what counts as an answer:

const balanceOf = (body: unknown, quotaPerUnit: number) => { if (typeof body !== 'object' || body === null) return null const data = (body as Record<string, unknown>).data if (typeof data !== 'object' || data === null) return null const { quota } = data as Record<string, unknown> if (typeof quota !== 'number' || !Number.isFinite(quota) || quota < 0) return null return { balanceUsd: quota / quotaPerUnit } } 

Anything that isn't a finite, non-negative number returns null, and null renders as "no data" — not as zero, and not as last week's number dressed up as this week's.

The console is now the primary source, and the billing API survives only as a fallback for when the console token isn't configured, because it works with the session token that's always present. The percentage is the fallback's alone — a share of a limit that the same line spells out — while the console's line is one number with no denominator to divide by:

const budgetPercent = (shown: QuotaBudget) => shown.spentUsd === null || shown.budgetUsd <= 0 ? 0 : Math.max(0, Math.min(100, (shown.spentUsd / shown.budgetUsd) * 100)) 

The test that keeps it honest

The most useful test in the suite is the one that asserts a request was not made:

test('balance comes from the console and the billing API is never asked', async ($, on) => { const asked: string[] = [] on('http.fetch', (_$, e) => { asked.push(e.url) if (e.url.includes('/api/user/self')) return { value: SELF } if (e.url.includes('/api/log/self?p=1')) return { value: logPage(ones(1), 1) } return { value: { status: 404, ok: false, headers: {}, text: '' } } }) // ... expect(await ui.find({ text: /^balance \$291\.77$/ })).toBeDefined() expect(asked.some(url => url.includes('/v1/dashboard/billing/usage'))).toBe(false) }) 

Plus one for a configured unit ratio, and one where the console answers 500 and the pane is required to show the previous console balance, marked stale, with the reason:

balance $291.77 · 5 min ago · response 500 

Stale-and-labelled beats fresh-and-fabricated. Both assertions are anchored to the whole line, so a second dollar figure reappearing next to the balance fails the suite rather than sneaking back in.

What I'd do differently

  • Never ship a plausible default for a measurement. 200 as a "budget" was the actual bug. A default that looks like data will be read as data. If the value isn't known, the UI should say so.
  • Make the unit ratio config, not a second constant. The ratio is a property of the deployment, and if it's configurable on the server side, it will eventually be configured.
  • Give "unknown" its own state. Empty, zero, and stale are three different things. Collapsing them is how a panel ends up confidently wrong.
  • Only send the session token to the host it was configured for. This pane reads an env var that also carries the session credential; it checks that the request's host matches the configured provider base URL before attaching it, and a test pins that behaviour. A panel is a nice place to accidentally exfiltrate a token from.

The pane now reads $291.77 left, which is what the provider's own console says. It stopped being a guess.

The console's total went with it, and so did the bar showing what share of the top-up was gone. Once the remaining balance is the number you act on, those were two more figures answering a question nobody asked — and the bar's percentage had no denominator left on screen to mean anything.

Community wisdom

Prior art that shaped how I thought about this, from the DEV archive: