{"slug": "the-budget-panel-said-189-left-the-console-said-291-both-numbers-were-wrong", "title": "The budget panel said $189 left. The console said $291. Both numbers were wrong.", "summary": "A developer building a Claude Code status-pane plugin found that its budget display and the provider's console reported conflicting remaining balances ($189.20 versus $291.77) because the pane compared a real spend figure against an unmeasured placeholder budget of $200 and blended two incompatible accounting sources — the OpenAI-compatible billing endpoint and the new-api console's quota fields. The fix replaces arithmetic reconciliation with a discriminated union tagging each figure by origin ('console' or 'billing') and a merge function that only carries values across snapshots of the same origin, so the two accountings can never be silently combined.", "body_md": "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.\n\nBoth 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.\n\nIt'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.\n\nThe 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.\n\nThe pane compared a spend figure against `budgetUsd`, a config value with a default of `200`:\n\n``` js\nconst DEFAULT_BUDGET_USD = 200\n```\n\nNobody 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.\n\nThe spend figure came from the billing API:\n\n```\nGET /v1/dashboard/billing/usage\n{\"total_usage\": 3004}   // cents\n```\n\nThat endpoint is the OpenAI-compatible one, and on this provider it reports a *different* number from the console. More on that below.\n\nThe console exposes the account itself:\n\n```\nGET /api/user/self\nAuthorization: Bearer <console token>\nNew-Api-User: <numeric user id>\n\n{\"data\": {\"quota\": 145883249, \"used_quota\": 16616751}, \"success\": true}\n```\n\n`quota` is what's left; `used_quota` is what's been burned. Both are in **quota units**, not cents, not dollars.\n\nTwo gotchas, both of which cost me a round trip:\n\n`New-Api-User` header is not optional.\nI 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.\n\nSo 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.)\n\n| Source | Spent | \n|---|---|\n| `/v1/dashboard/billing/usage` | $30.04 | \n| Console ( `used_quota` ) | $33.23 | \n\nNeither 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.\n\nThat's a discriminated union and a `null`:\n\n```\nexport type QuotaOrigin = 'console' | 'billing'\n\nexport type QuotaBudget = {\n  at: number\n  origin: QuotaOrigin\n  balanceUsd: number | null   // console only\n  spentUsd: number | null     // billing only\n  budgetUsd: number\n  // ...\n}\n```\n\nAnd the merge function only carries figures across from a previous snapshot **of the same origin**:\n\n``` js\nconst merged = (was: QuotaBudget | null, now: QuotaBudget): QuotaBudget => {\n  const same = was !== null && was.origin === now.origin ? was : null\n\n  return {\n    origin: now.origin,\n    balanceUsd: now.balanceUsd ?? same?.balanceUsd ?? null,\n    spentUsd: now.spentUsd ?? same?.spentUsd ?? null,\n    // ...\n  }\n}\n```\n\nThe parsing is deliberately strict about what counts as an answer:\n\n``` js\nconst balanceOf = (body: unknown, quotaPerUnit: number) => {\n  if (typeof body !== 'object' || body === null) return null\n  const data = (body as Record<string, unknown>).data\n  if (typeof data !== 'object' || data === null) return null\n\n  const { quota } = data as Record<string, unknown>\n  if (typeof quota !== 'number' || !Number.isFinite(quota) || quota < 0) return null\n\n  return { balanceUsd: quota / quotaPerUnit }\n}\n```\n\nAnything 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.\n\nThe 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:\n\n``` js\nconst budgetPercent = (shown: QuotaBudget) =>\n  shown.spentUsd === null || shown.budgetUsd <= 0\n    ? 0\n    : Math.max(0, Math.min(100, (shown.spentUsd / shown.budgetUsd) * 100))\n```\n\nThe most useful test in the suite is the one that asserts a request was *not* made:\n\n```\ntest('balance comes from the console and the billing API is never asked', async ($, on) => {\n  const asked: string[] = []\n  on('http.fetch', (_$, e) => {\n    asked.push(e.url)\n    if (e.url.includes('/api/user/self')) return { value: SELF }\n    if (e.url.includes('/api/log/self?p=1')) return { value: logPage(ones(1), 1) }\n    return { value: { status: 404, ok: false, headers: {}, text: '' } }\n  })\n\n  // ...\n  expect(await ui.find({ text: /^balance \\$291\\.77$/ })).toBeDefined()\n  expect(asked.some(url => url.includes('/v1/dashboard/billing/usage'))).toBe(false)\n})\n```\n\nPlus 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:\n\n```\nbalance $291.77 · 5 min ago · response 500\n```\n\nStale-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.\n\n`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.\nThe pane now reads `$291.77 left`, which is what the provider's own console says. It stopped being a guess.\n\nThe 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.\n\nPrior art that shaped how I thought about this, from the DEV archive:", "url": "https://wpnews.pro/news/the-budget-panel-said-189-left-the-console-said-291-both-numbers-were-wrong", "canonical_source": "https://dev.to/mystical/the-budget-panel-said-189-left-the-console-said-291-both-numbers-were-wrong-2el6", "published_at": "2026-10-17 01:48:35+00:00", "updated_at": "2026-10-17 01:57:05.811584+00:00", "lang": "en", "topics": ["ai-tools", "ai-agents", "developer-tools"], "entities": ["Claude Code", "new-api", "one-api", "OpenAI"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/the-budget-panel-said-189-left-the-console-said-291-both-numbers-were-wrong", "markdown": "https://wpnews.pro/news/the-budget-panel-said-189-left-the-console-said-291-both-numbers-were-wrong.md", "text": "https://wpnews.pro/news/the-budget-panel-said-189-left-the-console-said-291-both-numbers-were-wrong.txt", "jsonld": "https://wpnews.pro/news/the-budget-panel-said-189-left-the-console-said-291-both-numbers-were-wrong.jsonld"}}