This is a hands-on kata, not a think-piece. The goal: take a raw AI usage event - a chunk of Claude tokens, a tool call, a credit burn - and turn it into a stable, auditable invoice line using UsageBox, in about 30 minutes, without standing up a billing database of your own. Every call below hits the real metering API. By the end you will have idempotent ingest, a month-to-date total that is cheap to read, an audit trail behind every charge, and a closed period you can actually invoice against.
If you have read why a plain SQL usage_events table quietly breaks, this is the other half: what it looks like to just use a store that holds the billing invariants for you.
Step 1: send your first usage event
Usage goes in as events. Each one carries a stable event_id (your idempotency key), the account_id you are billing, what was metered (meter_id, optionally model_id and product_id), a quantity, and a timestamp. Batch them - one request can carry many events:
curl -X POST https://api.usagebox.com/v1/usage/batch \
-H "Authorization: Bearer $USAGEBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"event_id": "evt_2026-06-16_acct42_0001",
"account_id": "acct_42",
"meter_id": "claude_tokens",
"model_id": "claude-opus-4-8",
"quantity": 18450,
"unit": "tokens",
"timestamp": "2026-06-16T10:14:05Z"
}]
}'
The response tells you exactly what happened, per event:
{ "accepted": 1, "duplicates": 0, "conflicts": 0, "rejected": 0 }
That event is now durable - written to a write-ahead log and fsynced before you got the ack. A crash one millisecond later does not lose it.
Step 2: make retries safe (the part that actually matters)
Usage collectors retry. Networks drop acks. So the real test is: what happens when the same event arrives twice? Re-send the identical request from Step 1:
{ "accepted": 0, "duplicates": 1, "conflicts": 0, "rejected": 0 }
The second copy is recognized by its event_id and counted as a duplicate, not new usage. Your customer is billed once. This is the single most important property in the stack: idempotency is enforced at write time, not bolted on later.
Now send the same event_id with a different quantity (say 18451). You do not get a silent overwrite - you get a conflict:
{ "accepted": 0, "duplicates": 0, "conflicts": 1, "rejected": 0 }
A conflict almost always means a buggy collector sending the same id for different usage. UsageBox surfaces it instead of swallowing it, so you find the bug in your logs and not in a billing dispute three weeks later.
Step 3: the number that goes on the invoice
Send a few hundred more events through the month, then ask for the account total. This is the figure the invoice shows ("18.4M tokens in June"), so it has to be fast - no SUM() over millions of rows under a lock at billing time:
curl "https://api.usagebox.com/v1/accounts/acct_42/usage?from=2026-06-01T00:00:00Z&to=2026-07-01T00:00:00Z&group_by=meter_id" \
-H "Authorization: Bearer $USAGEBOX_KEY"
{ "source": "rollup", "groups": [{ "meter_id": "claude_tokens", "sum": 18412900, "count": 1043 }] }
Note "source": "rollup". Completed hours are pre-aggregated in the background, so the monthly read is cheap and never contends with live ingestion. The open (current) hour falls back to raw automatically, so the number is still correct, not just fast.
Step 4: the audit trail behind a disputed line
A customer disputes the June charge. "Trust our current total" is not an answer - you need to show what they actually did. The raw events are immutable and queryable:
curl "https://api.usagebox.com/v1/accounts/acct_42/explain?from=2026-06-01T00:00:00Z&to=2026-07-01T00:00:00Z" \
-H "Authorization: Bearer $USAGEBOX_KEY"
/explain returns the breakdown, the segment provenance (which immutable file each number came from), and any corrections applied. That is the evidence behind the invoice line, not a recomputed guess.
Step 5: close the month and freeze the invoice
When June ends, close the period. That captures a frozen snapshot - the quantity, the event count, and a watermark - so the invoice number stops moving:
curl -X POST https://api.usagebox.com/v1/accounts/acct_42/periods/2026-06/close \
-H "Authorization: Bearer $USAGEBOX_KEY"
From now on, a new Usage event dated in June is rejected at ingest - it cannot silently change a number you already billed. But a genuine correction is still accepted as a first-class event (a negative-quantity Correction that references the original via correction_ref), and it lands as a pending adjustment, never an edit to history. Read the period back:
{
"status": "closed",
"frozen": { "quantity": 18412900, "event_count": 1043 },
"pending_adjustments": [{ "quantity": -5000, "correction_ref": "evt_..._0099" }],
"adjustments_quantity": -5000,
"net_total": 18407900
}
The frozen total is what your original invoice showed. net_total is what a corrected invoice would show today. Both are visible and auditable - the original never mutates.
Step 6: prove the fast number equals the true number
The worst bug in billing is a fast total and a true total that disagree. UsageBox ships a built-in check that scans raw events and compares them against the rollups:
curl "https://api.usagebox.com/v1/accounts/acct_42/verify?from=2026-06-01T00:00:00Z&to=2026-07-01T00:00:00Z" \
-H "Authorization: Bearer $USAGEBOX_KEY"
Run it before you generate invoices. If raw and rollup ever drift, you find out here - not from a customer.
Production notes before you ship it
- Dimensions. Attach up to 16 dimension keys per event (region, feature, agent, plan tier) and
group_byany of them later. You do not have to decide your invoice breakdown up front. - Rollup vs raw. Account totals default to
source=rollupfor speed; passsource=rawto force a full scan when you want belt-and-suspenders correctness. - Corrections, not edits. Refunds and fixes are
Correction/Retractionevents that net against the original. History is never rewritten, which is exactly what makes a dispute winnable. - No lock-in. Your raw events export back out. The audit trail is yours.
Kata variations to try
- Per-model cost. Re-run Step 3 with
group_by=model_idto split Opus vs Sonnet vs Haiku spend per account. - Live spend cap. Query the open hour and alert when an account crosses a threshold mid-month, before the bill lands.
- Reconcile a vendor bill. Pull your provider's invoice total and compare it against
/explainto catch the gap between list price and real cost. - Ad-hoc SQL. Hit
/v1/query/sqlwith aSELECT meter_id, SUM(quantity) ... GROUP BY meter_idwhen you want a one-off slice without a new endpoint.
Kata FAQ
Do I have to run the database myself? No. UsageBox hosts the metering engine; you send events and read totals over HTTP. The append-only store, WAL, rollups, and crash recovery are handled for you.
What happens if my collector sends an event twice? It is deduped by event_id and counted as a duplicate. Same id with a different payload is flagged as a conflict so you can fix the collector.
What about a correction that arrives after I have already invoiced? It is accepted as an adjustment against the closed period and shown in net_total; the frozen snapshot behind your original invoice never changes.
Can I get my data out? Yes. Raw events are exportable - the immutable trail is yours, no lock-in.
What you just avoided building
In six steps you got idempotent ingest, a durable write path, cheap month-to-date totals, an immutable audit trail, period close with frozen snapshots, first-class corrections, and a raw-vs-rollup reconciliation check. Built in-house, that is a write-ahead log, crash recovery, compaction, dedupe-under-retry, and two aggregation paths you have to keep consistent - a real database project, not a weekend usage_events table. That is the same realization driving the 2026 metering acquisition wave: Stripe bought Metronome and Adyen bought Orb because metering is strategic infrastructure that is hard to get right and expensive to get wrong.
Keep reading: idempotency and late events in depth, why metering needs its own database, and the usage-based billing guide.