# The budget panel said $189 left. The console said $291. Both numbers were wrong.

> Source: <https://dev.to/mystical/the-budget-panel-said-189-left-the-console-said-291-both-numbers-were-wrong-2el6>
> Published: 2026-10-17 01:48:35+00:00

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.

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](https://github.com/Calcium-Ion/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.

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

``` js
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.

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:

`New-Api-User` header is not optional.
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.)

| 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**:

``` js
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:

``` js
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:

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

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.

`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.
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.

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