Metering tells you what happened. A gate decides what happens next.
SixDecimal sits in front of every provider as an OpenAI-compatible proxy (Anthropic Messages pass through natively). On each call it attributes the spend, reserves the estimated cost against every applicable budget before the request leaves your network, and then commits the real cost — or refunds it — the moment the response lands. Postgres holds the canonical number; Redis just makes the check fast.
Every call carries who it's for — no app rewrite.
Tag each request with three headers, or let a virtual key's scope defaults fill them in. Either way the gateway resolves the call to a set of dimensions before it decides anything.
You attribute a call one of two ways. Send the attribution headers explicitly from your app (or the thin SDK injects them), or mint a virtual key whose scope carries defaults — default_customer_id, default_feature, default_team — so an unadorned call still lands on the right client.
If a header is missing and the key has no default for it, that dimension is left unattributed (null) and the call still proceeds — unless a budget on another dimension stops it. Nothing here asks you to change your business logic: point at the gateway, or change one base URL.
Resolves to
Attribution headers
x-sd-customer-idThe end client — resolves to customers.external_id.
x-sd-featureAn internal feature or product — features.key.
x-sd-teamAn internal team — teams.key.
Missing a header? The virtual key's scope default fills it in. No default either → that dimension is null, the call still proceeds.
One atomic transaction, wrapped around the provider call.
This is a real three-step sequence, not a metaphor. The hold goes up before the request leaves, and it always resolves — to real spend, or back to zero.
Reserve
Estimate the cost high — price book × estimated input tokens, a deliberately conservative upper bound. In one atomic Lua round-trip, check spent + held + estimate ≤ limit against EVERY applicable budget and hold the estimate on all of them at once. If any single budget would exceed its limit, the request fails closed with a 402 BUDGET_EXCEEDED and the provider is never called.
Commit
The call returns and the gateway reads the real usage off the response — non-stream from the body, streaming by accumulating the final SSE chunk. It reprices at the real token counts and moves the money held → spent: subtract the reservation, add the real cost. That delta is what persists to usage_events and budget_windows.
Refund
If the upstream call fails — network error or a status ≥ 400 — the hold is released and nothing is counted as spend. The reservation returns to zero; the budget is exactly where it was before the attempt. Reserve-then-refund is why a failed call never quietly eats a client's ceiling.
Reserve math · example figures
200 · passReserved high, committed at the real count, the over-estimate handed straight back. Money is bigint micro-USD (1 USD = 1,000,000) — carried to the sixth decimal, never rounded on the hot path.
Reserving above the eventual cost is the whole point: under a thousand concurrent calls, the ceiling is what the gate promised, not an average it drifted past. The estimate is conservative on purpose — better to hold a few micro-dollars too many for a few milliseconds than to let a budget slip.
Every op lands in the ledger with its outcome: committed (charged), blocked (refused at reserve, no provider call), error and refunded (held then released). The write is idempotent, deduped on (provider, request_id).
The most restrictive budget wins.
Every active budget that applies to the resolved dimensions loads on the request. They're evaluated together — if any one is exhausted, the gate refuses. Each budget carries its own window.
A single call can sit under budgets at the org, team, feature, customer and virtual-key level at once. The reserve holds against all of them atomically — all-or-nothing. The moment one can't take the hold, the request is refused and the other budgets are moot.
That's the safe default: a generous monthly org cap can't rescue a client who blew a tight daily one. Whichever ceiling is nearest decides, and it decides in the request path — not in a report you read tomorrow.
Budgets on one request · example
- org · monthly200 · pass
- team · rolling_24h200 · pass
- customer · daily402 · refused
- virtual_key · total200 · pass
One exhausted cap refuses the call. The other three are never spent against — the most restrictive budget is the answer.
Each budget has its own window
The window materializes a start and end; the Redis key's TTL is set to expire at the window end, so the reset is exact.
When the accelerator is down, you choose what the gate does.
Redis is fast but volatile. If it — or the control-plane — is unreachable at reserve time, the gate falls back to Postgres under a policy you set per budget (and an org-wide default).
fail-closed · default
402 · refusedBlock the call
If the gate can't confirm the budget, it refuses rather than risk uncounted spend. The safest posture for money: a call you can't meter is a call you don't make. This is the default.
fail-open
200 · passLet it through, reconcile after
The call proceeds in degraded mode and the spend is reconciled against Postgres once the accelerator is back. For paths where availability outranks the ceiling. Configurable per budget, or org-wide via default_fail_mode.
Postgres is canonical. Redis is a cache you can throw away.
The cap has to be both atomic and hot-path fast — two requirements that pull apart. So the durable ledger and the fast check are different stores, with a strict rule about who wins.
Postgres
durable · canonicalThe number of record
budget_windows.spent_micro_usd is the canonical balance; usage_events and ledger_journal are the immutable record, deduped on (provider, request_id). Transactional and auditable — it's what the dashboard, billing and margin read.
Redis
volatile · acceleratorThe fast check
The atomic reserve/commit runs here in a single Lua round-trip for hot-path latency. On boot the gateway rebuilds Redis from Postgres — seeds spent from the active windows, sets held to 0 — so a flush or a restart never underestimates, and a re-seed never lowers a spent that was already ahead.
A Redis ↔ Postgres discrepancy always resolves in favor of Postgres. Redis you can drop and rebuild; Postgres you can't lose. That property is exercised in the gateway's test suite — 1,000 concurrent goroutines land on exactly the cap, and the anti-stale rebuild is verified after a Redis flush.
A refusal your code can read, not a stack trace.
When a budget is exhausted the gateway returns HTTP 402 with an exact JSON body. Every amount is an integer of micro-USD — not a float, not a string — so the caller can act on the number without guessing.
- scope and scope_id name exactly which ceiling refused — customer, feature, team, org or virtual_key.
- reset_at tells the caller when it clears — and is null when the window is total (it never resets).
- The blocked attempt is still written to the ledger as blocked — you can see the refusal, even though the provider was never called.
HTTP 402 · BUDGET_EXCEEDED
402 · refused{
"error": "BUDGET_EXCEEDED",
"scope": "customer",
"scope_id": "cus_8f21",
"limit_micro_usd": 5000000,
"spent_micro_usd": 5000000,
"reset_at": "2026-08-01T00:00:00.000Z"
}Example values · exact shape from the protocol
Change a base URL, or speak HTTP. Same ledger either way.
SixDecimal never asks you to rewrite the app. Route traffic through the gateway one of two ways — the caps, the reserve/commit/refund and the Postgres ledger are identical.
Mode A · thin SDK
Repoint the official SDK
The wrapper re-points the OpenAI/Anthropic SDK's base_url at the gateway and injects the attribution headers. Zero business-logic change; the cap logic lives in the gateway, not the SDK.
const openai = sixdecimal.openai({
apiKey,
baseURL: "https://<your-gateway>/v1",
customerId, feature,
});
await openai.chat.completions.create({ model, messages });Mode B · inline gateway
Point your HTTP at it
Send requests straight to /v1/chat/completions or /v1/messages and add the headers by hand. Language-agnostic — any stack that speaks HTTP, no SDK dependency.
POST https://<your-gateway>/v1/chat/completions
authorization: Bearer sd_live_a1b2c3d4…
x-sd-customer-id: acme-corp
x-sd-feature: support-bot
{ "model": "gpt-4o", "messages": [ … ] }- Same reserve → commit → refund
- Same Postgres ledger & dedupe
- Same 402 at the ceiling
Put a hard ceiling on every client's spend — before the invoice does it for you.
We're pre-revenue and onboarding a handful of agencies and studios as design partners. Attribute cost per client, cap runaway agents, and see real margin per account.