This is a hands-on kata, not a think-piece. The goal: catch and cap an account's spend before the bill lands, using UsageBox, in about 30 minutes. By the end you will read a live month-to-date total per meter, compute headroom against a budget, run a cheap check loop, measure a burn rate from raw records, and act at the threshold - with one honest constraint baked in: UsageBox meters usage, it does not gate it. The cap lives in your app; the meter tells you the truth in real time.
If you have done Kata #1, metering a usage event to an invoice line, you already have idempotent ingest and cheap totals. This kata builds the spend guardrail on top of that same store.
Step 1: read the month-to-date total
The number you guard against is the account's spend since month-start. Rollups are keyed by UTC calendar month, so asking without a month gives you exactly month-to-date, broken out per meter:
curl "https://api.usagebox.com/api/v1/usage/rollups?subscription=acme-prod" \
-H "x-api-key: $USAGEBOX_KEY"
{
"month": "2026-06",
"rollups": [
{ "month": "2026-06", "meter_key": "claude-tokens", "meter_unit": "tokens",
"aggregation": "sum", "value": 41280400, "last_record_at": "2026-06-16T14:29:51.204Z" },
{ "month": "2026-06", "meter_key": "tool-calls", "meter_unit": "calls",
"aggregation": "sum", "value": 18900, "last_record_at": "2026-06-16T14:28:07.881Z" }
]
}
One row per meter, already aggregated. This read does not scan raw records and does not contend with ingestion, which matters more here than in Kata #1: you are going to call it on an interval, not once at billing time.
Step 2: is the total actually current?
A spend cap is only useful if "month-to-date" includes the last few minutes. The classic trap is a rollup that is rebuilt on a schedule, so a fast read silently lags reality - exactly when a runaway account does its damage.
UsageBox does not have that gap, because there is no schedule. Accepted batches are processed inline: by the time POST /api/v1/usage returns, the rollup already reflects those records. The last_record_at field on every row is your proof - compare it to the timestamp of the batch you just sent and they match. Nothing is queued and nothing is pending.
Step 3: set a budget and compute headroom
The cap itself lives in your system - a per-account budget you store next to the customer record. Convert the metered quantities into spend with your own price book, then measure how close you are. The threshold logic is plain client code:
const budgetUsd = 500.00; // this account's monthly cap
const price = { "claude-tokens": 0.000009, "tool-calls": 0.002 };
const spend = rollups.reduce((acc, r) => acc + r.value * (price[r.meter_key] || 0), 0);
const pct = spend / budgetUsd; // fraction of budget consumed
const headroom = budgetUsd - spend; // dollars left this month
if (pct >= 0.80) {
// soft cap territory - warn
}
if (pct >= 1.00) {
// hard cap territory - stop serving this account
}
UsageBox gives you the quantities; the budget, the price book, and the 80 percent line are yours. Keep them in your app so a customer-specific exception is a config change, not an API call.
Step 4: the check loop
Run the rollup read on a short interval - every minute or two - and recompute headroom each tick. Because processing is inline, each tick reflects everything ingested up to that moment:
async function checkSpend(subscriptionKey) {
const url = "https://api.usagebox.com/api/v1/usage/rollups?subscription="
+ encodeURIComponent(subscriptionKey);
const res = await fetch(url, { headers: { "x-api-key": process.env.USAGEBOX_KEY } });
const { rollups } = await res.json();
return rollups; // feed into the Step 3 math
}
setInterval(() => checkSpend("acme-prod"), 90000);
Step 5: burn rate from the raw records
Rollups answer "how much this month". They cannot answer "how fast, right now", because they are aggregated per calendar month. For a burn rate, read the raw records instead - they come back newest first with their timestamps, up to 200 at a time - and window them yourself:
curl "https://api.usagebox.com/api/v1/usage?subscription=acme-prod&limit=200" \
-H "x-api-key: $USAGEBOX_KEY"
const cutoff = Date.now() - 10 * 60 * 1000; // last ten minutes
const { usage_events } = await res.json();
const recent = usage_events.filter((e) => new Date(e.timestamp).getTime() >= cutoff);
const burnUsdPerMin = recent.reduce(
(acc, e) => acc + e.value * (price[e.meter_key] || 0), 0) / 10;
Project that against the headroom from Step 3 and you get the hour an account is on course to cross its cap. Note the honest limit: 200 records is the ceiling on one read, so on a very high-volume account a ten-minute window may be truncated. Treat the burn rate as a signal, not an audit figure - the rollup is the audit figure.
Step 6: act at the threshold - soft cap vs hard cap
This is the honest part. UsageBox meters usage; it does not block it. There is no endpoint that says "stop this account" - and that is correct, because the meter should never be in the request path of your product. So the cap has two flavors, and both live in your code:
- Soft cap (warn). At 80 percent, alert and let traffic continue. Post to Slack, email the account owner, raise a flag in your dashboard. Nothing is throttled - you just stop being surprised.
- Hard cap (stop). At 100 percent, your application stops serving further billable requests for that account until the next period or a manual override. UsageBox told you the number; your gateway enforces the consequence.
if (pct >= 1.00) {
await denyFurtherRequests(subscriptionKey); // YOUR gateway, not UsageBox
await notify("#billing", subscriptionKey + " hit hard cap at $" + spend.toFixed(2));
} else if (pct >= 0.80) {
await notify("#billing", subscriptionKey + " at " + Math.round(pct * 100) + "% of budget");
}
Keep the gate fast and local (a cached flag your request handler checks), and treat the UsageBox read as the source of truth that flips that flag. Never put a network call to the meter on the hot path of every request - poll it, cache the verdict, enforce locally.
Step 7: per-meter ceilings
A single dollar cap is blunt. Often one expensive meter - a vision model, a long-context call - is the real risk, and you want to throttle just that without starving the account's cheap traffic. The rollup read already returns one row per meter, so you can apply a separate ceiling to each, or scope the read to the meter you care about:
curl "https://api.usagebox.com/api/v1/usage/rollups?subscription=acme-prod&meter=vision-pages" \
-H "x-api-key: $USAGEBOX_KEY"
Now your decision logic reads each meter against its own budget, so you can hard-cap vision-pages while leaving claude-tokens running. Meters are the unit of separation here: if you want to cap a product feature or a single model independently, give it its own meter at ingest time rather than trying to split one meter afterwards.
Production notes before you ship it
- Poll, do not block. The meter read belongs in a background loop, not in your request handler. Cache the verdict and enforce the cap locally so the cap costs you nothing per request.
- Inline means current. Rollups are updated as batches are accepted, so a one-minute loop is both cheap and live. Check
last_record_atif you ever doubt it. - The cap is yours, the truth is theirs. UsageBox never stops a request. Budgets, thresholds, price book, and the actual gate all live in your application.
- One meter per thing you might cap. Separation happens at ingest. A meter you did not create is a cut you cannot make later.
- Time math. Rollups are bucketed by UTC calendar month. Define month-start in UTC or your headroom will drift by a timezone offset at the edges of the month.
Kata variations to try
- Projected overage. Combine the Step 5 burn rate with the Step 3 headroom to estimate the hour an account will cross its cap, and alert a day ahead.
- Per-model cap. Give each model its own meter at ingest, then run Step 7 per model to cap Opus spend separately from Sonnet on the same account.
- Last month versus this. Pass
month=2026-05to the same read and compare the two figures to spot an account whose usage doubled. - Fleet sweep. Loop your own subscription list and call Step 1 per subscription to find every account near its cap.
Kata FAQ
Does UsageBox stop usage when an account hits its cap? No. UsageBox meters usage; it does not gate it. It gives you a current total - your application decides whether to warn (soft cap) or stop serving the account (hard cap).
Is the live total actually up to date, or does it lag? It is current. Batches are processed inline, so a record is in the rollup by the time the ingest call returns. last_record_at on each row shows you the moment it was last touched.
How often can I safely poll? Often. The rollup read is aggregated and does not contend with ingestion, so a per-minute loop is fine. Cache the verdict and enforce the cap locally rather than calling the meter on every request.
Can I cap one expensive meter without throttling everything? Yes. The rollup read returns a row per meter, and ?meter= scopes it to one, so a vision model can be hard-capped while cheap token traffic keeps flowing.
What you just avoided building
In seven steps you got a live month-to-date total per meter, a headroom calculation, a poll loop, a burn-rate window off the raw trail, soft and hard cap decision logic, and per-meter ceilings - without standing up your own aggregation tier. Built in-house, "fast and current at the same time" is the hard part: you would be running a streaming aggregator alongside a batch rollup and reconciling the two on every read, which is precisely the consistency problem that makes a plain SQL usage table buckle under billing load. The meter holds that invariant so your guardrail can be a 90-second loop.
Keep reading: Kata #1, meter a usage event to an invoice line, Kata #3, reconcile a vendor bill against your meter, Kata #4, per-customer per-model cost with dimensions, and how to instrument AI usage for visibility.