The number does not change when the range does
usage_summary on production, 2026-08-13, same account, three ranges:
| range |
totals.costMicros |
totals.chargedCostMicros |
totals.calls |
7d |
9,584,796,135 |
36,352,782 |
3,370 |
30d |
9,621,218,575 |
36,352,782 |
5,721 |
all |
9,621,218,575 |
36,352,782 |
5,721 |
2,351 additional calls appear when the window widens from 7d to 30d, worth $36.42 of notional value — and not one cent of it lands in the charged figure. The console renders this as Est. billed (store/console/src/pages/Usage.tsx:280), so the owner comparing his Anthropic invoice against a 30-day PAGS figure is looking at roughly seven days of it.
Mechanism — the payer column has a start date and nothing says so
payer was added by workers/api/migrations/0092_ai_usage_payer.sql, committed 2026-08-07 (6141dad). The migration is explicit that this is deliberate:
NULL is the honest default and there is no backfill: every pre-existing row genuinely does not record who paid, and stamping one on retrospectively would be the same confident-but-unfounded inference this column exists to remove.
That reasoning is right and should not be reversed. The defect is not the NULLs — it is that the aggregate presents "charged over this range" without disclosing that the range and the payer's coverage are different windows.
workers/api/src/lib/usage.ts:369:
if (isCharged(r.payer)) totals.chargedCostMicros += r.cost_micros || 0;
isCharged(null) is false (lib/usage-payer.ts:47-49), so every pre-0092 row contributes 0. The arithmetic is correct; the label is not.
Quantified, exactly
The 30d daily series, summed for every day before 2026-08-07:
07-14 454,706 07-15 8,109 07-18 188,499 07-19 210,180
08-01 6,676,369 08-02 7,915,468 08-03 12,854,308 08-04 4,456,429
08-05 3,658,372 08-06 8,287,110
total 44,709,550 micros = $44.71
And the difference between the two unknown-payer buckets is 9,584,865,793 − 9,548,443,353 = 36,422,440 — exactly the sum of days 07-14 through 08-05 above. That is a precise confirmation that every row outside the 7d window has payer IS NULL, with no exceptions.
Every one of those pre-0092 rows is non-engine (byKind.engine is byte-identical at 7d and 30d — 56,035 input / 3,283,599 output / 449 calls — so all 449 engine rows are inside the last 7 days). Non-engine rows come from recordUsage (hardcoded 'byok-api', usage.ts:97) and recordPlatformUsage (hardcoded 'platform', usage.ts:237). Both are charged payers. So had the column existed, essentially the whole $44.71 would have been charged.
Reported 30d charged: $36.35. Actual: ≈ $81.06. The page understates by ~55%.
(Verified: the $44.71 total, and that all 449 engine rows are inside 7d. Inferred: that every pre-0092 row would have resolved to a charged payer — from the two recorders' hardcoded values plus the absence of engine rows in that period, not from the rows themselves.)
What to do, cheapest first
- Return the coverage, do not backfill.
GET /v1/usage already scans every row in range; add to the response:
payerCoverage: {
knownCalls, unknownCalls,
knownCostMicros, unknownCostMicros,
earliestKnownAt // MIN(created_at) WHERE payer IS NOT NULL, over the range
}
aggregateUsage can compute all five in the same loop it already runs.
- Say it on the page. Under
Est. billed, one sentence: "Payer has been recorded since 7 Aug. 2,351 earlier calls ($36.42 of value) predate it and are not counted here." The page already carries a good unknown payer note (Usage.tsx:44); this is the same honesty applied to the time axis rather than the credential axis.
- Nothing to change in the ledger. No migration, no backfill, no new column.
Alternatives considered and rejected
- Backfill
payer from provider (provider='platform' → platform, else byok-api). Rejected: it is exactly the inference 0092's header forbids, and it would be wrong for any pre-0092 engine row that happened to exist — provider='anthropic' is the vendor, reached both by a metered key and by a subscription.
- Backfill only rows written by
recordUsage/recordPlatformUsage. Rejected as a migration: ai_usage has no column recording which recorder wrote a row, so the predicate would have to be reconstructed from kind/provider, which is the same inference in a longer form. If it is ever wanted, it belongs as a display-time treatment behind the disclosed coverage figure, not as a UPDATE.
- Clamp the range to the payer's start when charged is requested. Rejected: it silently changes what "30 days" means, and the notional column beside it would still be a genuine 30 days. Two different windows in one table is the defect, not the fix.
Acceptance criteria
GET /v1/usage?range=30d returns a coverage object; for this account it reports unknownCalls ≈ 2,351 and earliestKnownAt on 2026-08-07.
- The console states the coverage wherever
Est. billed appears.
usage.test.ts covers a fixture spanning the boundary (rows with and without payer) and asserts the coverage counts, so the disclosure cannot silently drop out.
- MCP
usage_summary passes the field through (it forwards the payload).
Regression risk
Low — additive. The one thing to get right is that the coverage counts must be computed over the same filtered row set the aggregate uses, not a separate query with its own WHERE; a second query is how the two figures start to disagree. Compute it inside aggregateUsage.
Related: #346 (added the payer axis), #543 (per-agent charged figures — the other half of "what did this agent cost me").
The number does not change when the range does
usage_summaryon production, 2026-08-13, same account, three ranges:totals.costMicrostotals.chargedCostMicrostotals.calls7d30dall2,351 additional calls appear when the window widens from 7d to 30d, worth $36.42 of notional value — and not one cent of it lands in the charged figure. The console renders this as
Est. billed(store/console/src/pages/Usage.tsx:280), so the owner comparing his Anthropic invoice against a 30-day PAGS figure is looking at roughly seven days of it.Mechanism — the payer column has a start date and nothing says so
payerwas added byworkers/api/migrations/0092_ai_usage_payer.sql, committed 2026-08-07 (6141dad). The migration is explicit that this is deliberate:That reasoning is right and should not be reversed. The defect is not the NULLs — it is that the aggregate presents "charged over this range" without disclosing that the range and the payer's coverage are different windows.
workers/api/src/lib/usage.ts:369:isCharged(null)isfalse(lib/usage-payer.ts:47-49), so every pre-0092 row contributes0. The arithmetic is correct; the label is not.Quantified, exactly
The 30d daily series, summed for every day before 2026-08-07:
And the difference between the two
unknown-payer buckets is9,584,865,793 − 9,548,443,353 = 36,422,440— exactly the sum of days 07-14 through 08-05 above. That is a precise confirmation that every row outside the 7d window haspayer IS NULL, with no exceptions.Every one of those pre-0092 rows is non-engine (
byKind.engineis byte-identical at 7d and 30d — 56,035 input / 3,283,599 output / 449 calls — so all 449 engine rows are inside the last 7 days). Non-engine rows come fromrecordUsage(hardcoded'byok-api',usage.ts:97) andrecordPlatformUsage(hardcoded'platform',usage.ts:237). Both are charged payers. So had the column existed, essentially the whole $44.71 would have been charged.Reported 30d charged: $36.35. Actual: ≈ $81.06. The page understates by ~55%.
(Verified: the $44.71 total, and that all 449 engine rows are inside 7d. Inferred: that every pre-0092 row would have resolved to a charged payer — from the two recorders' hardcoded values plus the absence of engine rows in that period, not from the rows themselves.)
What to do, cheapest first
GET /v1/usagealready scans every row in range; add to the response:aggregateUsagecan compute all five in the same loop it already runs.Est. billed, one sentence: "Payer has been recorded since 7 Aug. 2,351 earlier calls ($36.42 of value) predate it and are not counted here." The page already carries a goodunknownpayer note (Usage.tsx:44); this is the same honesty applied to the time axis rather than the credential axis.Alternatives considered and rejected
payerfromprovider(provider='platform'→ platform, else byok-api). Rejected: it is exactly the inference 0092's header forbids, and it would be wrong for any pre-0092 engine row that happened to exist —provider='anthropic'is the vendor, reached both by a metered key and by a subscription.recordUsage/recordPlatformUsage. Rejected as a migration:ai_usagehas no column recording which recorder wrote a row, so the predicate would have to be reconstructed fromkind/provider, which is the same inference in a longer form. If it is ever wanted, it belongs as a display-time treatment behind the disclosed coverage figure, not as aUPDATE.Acceptance criteria
GET /v1/usage?range=30dreturns a coverage object; for this account it reportsunknownCalls ≈ 2,351andearliestKnownAton 2026-08-07.Est. billedappears.usage.test.tscovers a fixture spanning the boundary (rows with and withoutpayer) and asserts the coverage counts, so the disclosure cannot silently drop out.usage_summarypasses the field through (it forwards the payload).Regression risk
Low — additive. The one thing to get right is that the coverage counts must be computed over the same filtered row set the aggregate uses, not a separate query with its own
WHERE; a second query is how the two figures start to disagree. Compute it insideaggregateUsage.Related: #346 (added the payer axis), #543 (per-agent charged figures — the other half of "what did this agent cost me").