UsageBox Kata #2: Live Spend Caps and Real-Time Usage

Catch and cap AI spend before the bill lands. A hands-on kata against the real metering API: read an account month-to-date total fast from rollups, understand why the open current hour falls back to raw so the live number is both fast and current, compute headroom against a budget, run a real-time burn-rate check, and act at the threshold - soft caps that alert and hard caps your app enforces (the meter measures, your app gates). Plus per-meter caps with group_by, production notes, variations (Slack alerts, per-model caps, prepaid-credit countdowns), and FAQ.

7 min read

usagebox kataspend capsreal-time usagebudget alertsusage-based billingAI cost controlmetering APIrollups2026

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_at if 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-05 to 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.

Key Topics

  • usagebox kata
  • spend caps
  • real-time usage
  • budget alerts
  • usage-based billing
  • AI cost control
  • metering API
  • rollups
  • 2026

Related Articles

Explore more articles on similar topics to deepen your understanding of usage-based billing.

UsageBox Kata #1: From Token Event to Invoice Line in 30 Minutes

A hands-on kata: take a raw AI usage event - a chunk of Claude tokens, a tool call, a credit burn - and turn it into a s...

7 min readRead more

Reconcile an AI Vendor Bill Against Your Usage Meter

A hands-on AI billing reconciliation pattern: compare your monthly meter rollups with the provider bill, localize gaps b...

8 min readRead more

Per-Customer, Per-Model AI Cost Without Arbitrary Dimensions

Use the actual UsageBox routing axes—subscription, product item and meter—to preserve the customer/model cuts you will n...

8 min readRead more

Explore More Articles

Discover our complete collection of usage-based billing guides and implementation patterns.

View all articles