Skip to content

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:133 decideSpend, src/budget/store-d1.ts:72, src/budget/token.ts:66, migration 0003_model_budgets.sql) and have no caller: nothing outside src/budget/ imports them, facade.ts never touches them, and no metered model-proxy route exists.

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.

The substrate’s metered model proxy enforces two tiers:

  1. Per-execution metering — the AgentBudget pattern, keyed by execution, capped by the run’s declared limits.
  2. 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:

Two-tier spend decision Flowchart, top to bottom. 6 nodes, 5 edges. charge micro-USD, unpriced models at the top rate → execution spent + charge over the run's cap? execution spent + charge over the run's cap? → budget-stop scope execution, meter state [yes]; → consumer spent + charge over the ceiling? [no] consumer spent + charge over the ceiling? → budget-stop scope consumer, meter state [yes]; → charge admitted fits both tiers [no] yes no yes no chargemicro-USD, unpriced models at thetop rate execution spent + chargeover the run's cap? budget-stopscope execution, meter state consumer spent + chargeover the ceiling? budget-stopscope consumer, meter state charge admittedfits both tiers
Two-tier spend decision
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 ok

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

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