ADR-0009: Two-tier budgets — the substrate meters executions and caps consumers
- Status: Proposed
- Date: 2026-08-06
- Implementation:
partial— both tiers exist as pure logic (src/budget/meter.ts:133decideSpend,src/budget/store-d1.ts:72,src/budget/token.ts:66, migration0003_model_budgets.sql) and have no caller: nothing outsidesrc/budget/imports them,facade.tsnever touches them, and no metered model-proxy route exists.
Context
Section titled “Context”Both consumers built spend fences at different granularity: fractalbot’s BudgetLedger is
conversation-scoped and provider-window-aware (per-task dollar cap, “unmeasured is not free”,
per-minute token pacing); flare-dispatch’s AgentBudget DO meters per execution behind its model
proxy, with typed stop reasons. They meter different loops and neither survives the other’s failure:
a buggy or compromised consumer — or a leaked fleet of per-execution tokens — can drain the org’s
model budget through a proxy that meters but never refuses. Within the components we own, the
substrate is the only place a spend cap holds without consumer cooperation.
One platform-side cap now sits below it: AI Gateway’s identity-aware mode (open beta, 2026-08-05) carries per-user spend limits that block or downgrade to a cheaper model when a bucket is spent. It does not replace either tier here. Identity comes from SAML SSO, and the announcement describes no mechanism by which a headless caller — a service binding, or a container holding a per-execution proxy token — obtains one, so a substrate execution has no user to bill against. Re-evaluate a third, gateway-held tier when non-human identity or IdP-group limits ship; until then the metered proxy is the enforcement floor.
Decision
Section titled “Decision”The substrate’s metered model proxy enforces two tiers:
- Per-execution metering — the AgentBudget pattern, keyed by execution, capped by the run’s declared limits.
- Per-consumer ceiling — a second budget DO keyed by consumer identity on the service binding; the hard stop that holds when a consumer’s own ledger is wrong.
A charge clears only when it fits both tiers, checked execution-first:
Diagram source
flowchart TB accTitle: Two-tier spend decision charge["**charge**<br/>micro-USD, unpriced models at the top rate"] --> exec{"execution spent + charge<br/>over the run's cap?"} exec -->|yes| stopE["**budget-stop**<br/>scope `execution`, meter state"] exec -->|no| cons{"consumer spent + charge<br/>over the ceiling?"} cons -->|yes| stopC["**budget-stop**<br/>scope `consumer`, meter state"] cons -->|no| ok["**charge admitted**<br/>fits both tiers"] class stopE,stopC danger class ok okBudget stops cross the facade as typed refusals carrying meter state — never as opaque model-call
failures. Consumer-side budgets (fractalbot’s BudgetLedger, its token pacing, all budget UX) stay
consumer-side as the product layer above the floor.
Consequences
Section titled “Consequences”- A BYOC operator gets one place to cap total spend per consumer.
- The substrate needs consumer identity on every facade call — the service binding provides it; the substrate never trusts a consumer-supplied identity field.