The confusion this exists to end
A delegation was refused with "the $50.00 daily spend limit has been hit". The user went to
platform.claude.com to see the $50 — and found nothing, because no money had been spent. The
engine had been running on their Claude subscription.
Chasing that took an hour and produced a wrong first answer from me, twice. The reason it was
hard is not a bug in one function. It is that the ledger has no column for who pays.
The model we have
ai_usage records tokens and one cost_micros, plus cost_source (migration 0080): 'reported'
when Claude Code computed the figure, NULL when we did.
That axis answers "whose arithmetic produced this number?" — a minor provenance detail. It was
then used everywhere as if it answered "is this number money?", which it does not, and which
nothing in the schema answers at all.
What the vendor documentation actually says
From Manage costs effectively (Anthropic, current):
Claude Code computes the dollar figure locally from token counts priced at standard list
rates, so it doesn't reflect promotional pricing or contracted discounts and may differ from
your actual bill. For authoritative billing, see the Usage page in the Claude Console.
Claude Max and Pro subscribers have usage included in their subscription, so the session cost
figure isn't relevant for billing purposes.
So total_cost_usd is the same kind of object as our own estimate — tokens × list price. It is
not a measurement of money, in any auth mode. And on a subscription it corresponds to no charge
at all.
Note also that subscription limits are not denominated in dollars: they are a rolling 5-hour
window and a weekly window, shared across Claude chat, Claude Code and Cowork. There is no
subscription number a dollar ceiling could ever be comparing against.
The model we should have
Three distinct facts, currently collapsed into one column:
| Fact |
Always true? |
Have it? |
| Consumption — input / output / cache-read / cache-write tokens |
yes |
✅ stored |
| Notional value — tokens × list price, "what this would cost on the API" |
yes, as an estimate |
✅ cost_micros |
| Charge — money someone actually owes |
depends on the payer |
❌ missing |
The missing thing is not a better cost number. It is payer.
payer:
byok-api — the user's own provider API key. Real money, their provider account.
subscription — a Claude/ChatGPT subscription. No marginal charge; draws a plan allowance.
platform — PAGS pays (Workers AI, platform models).
unknown — we could not determine it. Say so; never guess.
provider (anthropic / openai / platform) is the vendor, not the payer, and cannot
substitute: the same vendor is reached both ways.
With payer present, everything downstream becomes correct by construction rather than by
special-case:
Determining the payer honestly
resolveEngineAuth (packages/browser-runner/src/coding/engine-auth.ts:63) already derives
api-key / subscription / machine-login from the merged spawn env, and authResolved already
reaches the cloud on the capture snapshot (workers/api/src/routes/coding.ts:734) and in
diagnostics (:1680). It is displayed and then discarded — recordEngineUsage
(workers/api/src/lib/usage.ts:147) never receives it and the ledger row has nowhere to put it.
Persisting it is most of the mechanical work.
The honest gap: machine-login does not mean "subscription". It means neither credential was
in the env and the CLI used whatever login it has stored — which could be either. Mapping it to
subscription would be the same species of confident-but-unfounded inference this ticket exists
to remove, so it must map to unknown.
Which leaves a genuine design decision, and it should be taken deliberately:
- Treating
unknown as billable is fail-safe against a surprise bill, but machine-login is the
most common resolution (it is what auto produces), so this preserves today's bug for most
users.
- Treating it as non-billable removes the false block but could let real API spend run unbounded.
Recommended: stop inferring and ask. A one-time per-machine declaration — "Claude Code on this
machine signs in with: ( ) my Claude subscription ( ) an API key" — is a fact the user knows for
certain and we cannot derive. Keep the env-derived signal as a cross-check and warn on
conflict, exactly as engineAuthWarning already does for mode-vs-resolved. Until it is answered,
unknown bounds by tokens (never blocked over money) and the Usage page says "payer not
established" rather than picking one.
One caveat to state in the UI rather than model: a subscriber drawing on usage credits is
being charged. We cannot detect that, so we should say we cannot, not imply we can.
Scope
- Migration:
ai_usage.payer TEXT (NULL = unknown, no backfill — pre-existing rows genuinely are
unknown).
- Thread
authResolved → recordEngineUsage → the row. Platform-paid rows get platform; rows
made on a vault provider key get byok-api (those we know for certain — we injected the key).
- Per-machine payer declaration + conflict warning.
- Consumers read
payer, not cost_source.
Verification
- A row's payer is never inferred from
machine-login; that resolves to unknown in the ledger.
- Summing
cost_micros without filtering on payer is not done anywhere after this lands — a
test asserting the dollar aggregate filters is worth more than the aggregate itself.
- A user with no declaration and a
machine-login engine session is never blocked by a money
limit, and can see on the Usage page that the payer is unestablished and why.
Related
The confusion this exists to end
A delegation was refused with "the $50.00 daily spend limit has been hit". The user went to
platform.claude.comto see the $50 — and found nothing, because no money had been spent. Theengine had been running on their Claude subscription.
Chasing that took an hour and produced a wrong first answer from me, twice. The reason it was
hard is not a bug in one function. It is that the ledger has no column for who pays.
The model we have
ai_usagerecords tokens and onecost_micros, pluscost_source(migration 0080):'reported'when Claude Code computed the figure, NULL when we did.
That axis answers "whose arithmetic produced this number?" — a minor provenance detail. It was
then used everywhere as if it answered "is this number money?", which it does not, and which
nothing in the schema answers at all.
What the vendor documentation actually says
From Manage costs effectively (Anthropic, current):
So
total_cost_usdis the same kind of object as our own estimate — tokens × list price. It isnot a measurement of money, in any auth mode. And on a subscription it corresponds to no charge
at all.
Note also that subscription limits are not denominated in dollars: they are a rolling 5-hour
window and a weekly window, shared across Claude chat, Claude Code and Cowork. There is no
subscription number a dollar ceiling could ever be comparing against.
The model we should have
Three distinct facts, currently collapsed into one column:
cost_microsThe missing thing is not a better cost number. It is
payer.provider(anthropic/openai/platform) is the vendor, not the payer, and cannotsubstitute: the same vendor is reached both ways.
With
payerpresent, everything downstream becomes correct by construction rather than byspecial-case:
byok-api. A circuit breaker for money stops counting things thatare not money. (→ [bug] The $50 circuit breaker is denominated in dollars for work nobody is billed for — subscription engine usage blocks delegations #343)
message. Different resource, different number, different sentence. (→ [bug] The $50 circuit breaker is denominated in dollars for work nobody is billed for — subscription engine usage blocks delegations #343)
and "Drawn from your Claude subscription". (→ [bug] The platform calls engine cost "measured" — Anthropic's docs say it is a list-price estimate #347)
cost_sourceshrinks back to what it is: a provenance note about whose arithmetic ran, withno authority attached. Both values are list-price estimates.
Determining the payer honestly
resolveEngineAuth(packages/browser-runner/src/coding/engine-auth.ts:63) already derivesapi-key/subscription/machine-loginfrom the merged spawn env, andauthResolvedalreadyreaches the cloud on the capture snapshot (
workers/api/src/routes/coding.ts:734) and indiagnostics (
:1680). It is displayed and then discarded —recordEngineUsage(
workers/api/src/lib/usage.ts:147) never receives it and the ledger row has nowhere to put it.Persisting it is most of the mechanical work.
The honest gap:
machine-logindoes not mean "subscription". It means neither credential wasin the env and the CLI used whatever login it has stored — which could be either. Mapping it to
subscriptionwould be the same species of confident-but-unfounded inference this ticket existsto remove, so it must map to
unknown.Which leaves a genuine design decision, and it should be taken deliberately:
unknownas billable is fail-safe against a surprise bill, butmachine-loginis themost common resolution (it is what
autoproduces), so this preserves today's bug for mostusers.
Recommended: stop inferring and ask. A one-time per-machine declaration — "Claude Code on this
machine signs in with: ( ) my Claude subscription ( ) an API key" — is a fact the user knows for
certain and we cannot derive. Keep the env-derived signal as a cross-check and warn on
conflict, exactly as
engineAuthWarningalready does for mode-vs-resolved. Until it is answered,unknownbounds by tokens (never blocked over money) and the Usage page says "payer notestablished" rather than picking one.
One caveat to state in the UI rather than model: a subscriber drawing on usage credits is
being charged. We cannot detect that, so we should say we cannot, not imply we can.
Scope
ai_usage.payer TEXT(NULL = unknown, no backfill — pre-existing rows genuinely areunknown).
authResolved→recordEngineUsage→ the row. Platform-paid rows getplatform; rowsmade on a vault provider key get
byok-api(those we know for certain — we injected the key).payer, notcost_source.Verification
machine-login; that resolves tounknownin the ledger.cost_microswithout filtering onpayeris not done anywhere after this lands — atest asserting the dollar aggregate filters is worth more than the aggregate itself.
machine-loginengine session is never blocked by a moneylimit, and can see on the Usage page that the payer is unestablished and why.
Related
without it. Payouts in particular must never pay out against notional subscription value.