The mechanism

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.

200 · pass402 · refused
Attribution

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

customerfeatureteamvirtual_keyorg

Attribution headers

  • x-sd-customer-id

    The end client — resolves to customers.external_id.

  • x-sd-feature

    An internal feature or product — features.key.

  • x-sd-team

    An 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.

Reserve → commit → refund

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.

01

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.

200 · pass402 · refused
02

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.

200 · pass
03

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 · pass
Reserve — held (estimate high)$0.084000
Commit — spent (real usage)$0.061247
Released — held → 0$0.022753

Reserved 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).

Budget resolution

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

daily
Resets each day, anchored to a time-of-day anchor.
weekly
Resets each week, anchored to a day of the week.
monthly
Resets each month, anchored to a day of the month.
rolling_24h
A sliding 24-hour window.
rolling_30d
A sliding 30-day window.
total
Never resets — reset_at is null.

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.

Fail posture

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 · refused

Block 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 · pass

Let 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.

Source of truth

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 · canonical

The 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 · accelerator

The 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.

The 402 contract

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

Two integration modes

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
See what this buys you, per use case
Become a design partner

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.

No credit card. Run the gateway in your own VPC. We only email about the beta.

By submitting, you consent to us processing your email to manage your early-access request and tell you when we launch (art. 6.1(a) GDPR). Details in the Privacy policy.

Basic data-protection information
Controller:
The owner of SixDecimal (an individual). Full identification in the Legal notice.
Purpose:
Managing your early-access request and telling you when we launch. If you tick the box, also sending you product news.
Legal basis:
Your consent when you submit this form (art. 6.1(a) GDPR). You can withdraw it at any time.
Retention:
Until launch or until you withdraw your consent.
Recipients:
Providers acting as processors (transactional email, hosting). We never hand your data to third parties for their own marketing.
Rights:
Access, rectification, erasure, objection, restriction and portability, via the privacy contact in the Privacy policy. You can also complain to the AEPD (www.aepd.es).
More information:
Privacy policy · Legal notice
How it works · SixDecimal