Budgets & Metering
Give an agent money to spend under a ceiling the kernel checks before every priced call.
On this page
An agent can buy services and fund other agents’ work with spending authority you give it. In the Quickstart, the grant to fixer carried USD 2.00 beside its resources and expiry, and the run spent 0.48 of it. In part 3, one USD 5.00 pool covered every agent the partner added.
The budget is part of the grant. The capability token carries cost and invocation limits, and the configured budget authority checks them before dispatch. Delegation can only narrow them, and each priced call leaves an accounting record on its receipt.
Two calls fit; the third is refused before dispatch
Give the tester its own slice of the model route to reproduce the flaky test in issue #416: USD 0.50 in total, and no more than USD 0.20 on any one call. The grant uses the project and fork from the Quickstart.
const tester = await project.agent("tester");
const authority = fork.grant({
to: tester,
resources: { "/workspace": ["read"], "/models/default": ["invoke"] },
budget: USD(0.5),
perCall: USD(0.2),
});
const run = await tester.run("reproduce issue #416", { on: fork, authority });
for await (const event of run.events()) console.log(event.summary);00:00 admitted tester runs "reproduce issue #416" on fork 51c07ae 00:02 read /workspace/tests/test_pricing.py 00:04 invoke /models/default, held USD 0.20, charged USD 0.20 00:19 invoke /models/default, held USD 0.20, charged USD 0.20 00:31 refused invoke /models/default: budget, needs USD 0.20, USD 0.10 left spent USD 0.40 of 0.50
tester = project.agent("tester")
authority = fork.grant(
to=tester,
resources={"/workspace": ["read"], "/models/default": ["invoke"]},
budget=USD(0.5),
per_call=USD(0.2),
)
run = tester.run("reproduce issue #416", on=fork, authority=authority)
for event in run.events():
print(event.summary)00:00 admitted tester runs "reproduce issue #416" on fork 51c07ae 00:02 read /workspace/tests/test_pricing.py 00:04 invoke /models/default, held USD 0.20, charged USD 0.20 00:19 invoke /models/default, held USD 0.20, charged USD 0.20 00:31 refused invoke /models/default: budget, needs USD 0.20, USD 0.10 left spent USD 0.40 of 0.50
let tester = project.agent("tester").await?;
let authority = fork.grant(
Grant::to(&tester)
.resource("/workspace", ["read"])
.resource("/models/default", ["invoke"])
.budget(USD(0.5))
.per_call(USD(0.2)),
)?;
let mut run = tester.run("reproduce issue #416").on(&fork).authority(authority).start().await?;
let mut events = run.events();
while let Some(event) = events.next().await {
println!("{}", event?.summary());
}00:00 admitted tester runs "reproduce issue #416" on fork 51c07ae 00:02 read /workspace/tests/test_pricing.py 00:04 invoke /models/default, held USD 0.20, charged USD 0.20 00:19 invoke /models/default, held USD 0.20, charged USD 0.20 00:31 refused invoke /models/default: budget, needs USD 0.20, USD 0.10 left spent USD 0.40 of 0.50
With 20 reserved per call and a total of 50, 2 of 3 calls are admitted. Call 3 would bring the committed total to 60, above 50, so admission refuses it before dispatch.
In Rust · budget_store.rs:62421 lines
/// Atomically check monetary budget limits and record provisional exposure if within bounds.
///
/// Checks:
/// 1. `invocation_count < max_invocations` (if set)
/// 2. `cost_units <= max_cost_per_invocation` (if set)
/// 3. `(total_cost_exposed + total_cost_realized_spend + cost_units)
/// <= max_total_cost_units` (if set)
///
/// On pass: increments `invocation_count` by 1 and `total_cost_exposed` by
/// `cost_units`, allocates a new replication seq, returns `Ok(true)`.
/// On any limit exceeded: rolls back, returns `Ok(false)`.
// … HA overrun note
fn try_charge_cost(
&self,
capability_id: &str,
grant_index: usize,
max_invocations: Option<u32>,
cost_units: u64,
max_cost_per_invocation: Option<u64>,
max_total_cost_units: Option<u64>,
) -> Result<bool, BudgetStoreError>;
Each call reserves its per-call ceiling before it runs. The first two reservations fit and each settles at USD 0.20, leaving 0.30 and then 0.10. The third would need another 0.20 against a remaining 0.10, so admission refuses it before dispatch, the model route never sees the request, and nothing is charged.
The kernel’s budget authority keeps the holds and charges in a persistent store beside the receipts, so each receipt carries the amount held and the amount settled. The ledger separates available, spent, and reserved amounts. Payment to the model provider is a separate settlement step; Reconciliation covers it.
Read more in Budgets.
Three independent limits live in the grant
Each ToolGrant in a capability token carries up to three budget constraints. max_cost_per_invocation bounds the exposure one invocation may authorize, max_total_cost bounds what the grant may spend across calls, and max_invocations bounds how many calls may use it.
grants:
- server_id: models
tool_name: default
operations: [invoke]
# USD 0.25 per call
max_cost_per_invocation:
units: 25
currency: USD
# USD 2.00 total
max_total_cost:
units: 200
currency: USD
# At most 100 invocations
max_invocations: 100The limits are independent and optional, and a grant can combine them freely. Setting only max_invocations creates a free tier with a call-count limit; setting only max_total_cost creates a total-cost bound in the schema. The runnable local-credit adapter also needs an explicit nonzero per-call ceiling, because it requires a valid reservation amount.
The configured budget authority checks the applicable limits before dispatch, and a refusal records the decision and the available accounting evidence. The runtime’s store and authority configuration determine how usage is coordinated, where the atomicity boundary lies, and whether usage survives a restart.
Read more in Kernel budget accounting.
A hold becomes a charge or a reversal
Before dispatch, the kernel authorizes a hold for the worst-case exposure: the grant’s max_cost_per_invocation, or the quoted cost when one is present. The tool runs and reports its cost, and the kernel reconciles the hold against that report.
- A reported cost below the hold credits the difference back to the budget.
- A reported cost equal to the hold needs no adjustment.
- A reported cost above the hold records the overrun in the receipt, with
settlement_statusset toFailed. - A guard or validation step that denies the call after authorization triggers
reverse_budget_charge(), which fully reverses the hold, so the agent is not charged for a tool call that never ran.
A hold reserves spending capacity, a charge records the amount accounted to the call, and a payment adapter supplies the settlement behavior. After a refusal, inspect the stage and the receipt before drawing a conclusion about effects. A refusal before dispatch stops the tool. After dispatch, releasing an accounting hold does not undo an external action.
Read more in Reconciliation.
A quoted prepayment charges its quote
Quoted prepayment follows its own accounting path. A compute call under a $1.00 per-call cap and a $10.00 total records its quote of 100 minor units as the charge, even though the tool reports 75; the difference is not credited back. A second run with no payment adapter is refused, because approval cannot satisfy a missing settlement requirement.
Inspect the quoted-prepayment run and its receipts
The shared example invokes governed-sim-srv/compute. Its grant caps one call at $1.00 and total spending at $10.00. The $1.00 intent crosses a $0.50 approval threshold, so the runner signs an approval bound to that request before submitting it.
This is an executable kernel example using an ephemeral store and a simulated payment adapter. The approval is created programmatically; it does not exercise a human interface or a wait-and-resume cycle. Settlement makes no external payment.
adapter configured: allow. Charged $1.00; $9.00 of $10.00 remaining.
governed_validation.rs:1457Self::validate_metered_billing_context(intent, charge_currency, now)?;
if Self::mustprepay_prepaid_units(intent).is_some() && self.payment_adapter.is_none() {
return Err(KernelError::GovernedTransactionDenied(
"governed intent mandates prepayment (settlement_mode=MustPrepay) but no payment adapter is configured"
.to_string(),
));
}
The tool implementation reports 75 minor units, but this MustPrepay path records the quote of 100 minor units. Read the receipt's charged amount when explaining this example: it does not demonstrate a $0.25 refund.
| Captured outcome | Charged | Remaining |
|---|---|---|
Allow with simulated prepayment | $1.00 | $9.00 |
Deny with no payment adapter | $0.00 | $10.00 |
The denial uses a fresh kernel with the same limits and a freshly signed approval. Removing the payment adapter prevents mandatory prepayment, so the kernel refuses the call. Approval cannot satisfy a missing settlement requirement. These are two independent runs, not consecutive debits from one budget.
Run the approval and budget simulation
Use the installed chio. governed-sim supplies its own in-process tool, policy, and simulated payment adapter; it needs no funded account. The command and its recorded result are below.
$ chio mcp governed-sim --governed-mustprepay --payment-adapter sim --out ./compute-allow.jsonExpected output
$ jq '{tool_server, tool_name, decision, receipt_kind, financial: .metadata.financial, budget_authority: .metadata.budget_authority}' ./compute-allow.json{
"tool_server": "governed-sim-srv",
"tool_name": "compute",
"decision": {
"verdict": "allow"
},
"receipt_kind": "mediated_decision",
"financial": {
"budget_remaining": 900,
"budget_total": 1000,
"cost_breakdown": {
"payment": {
"adapter_metadata": {
"adapter": "sim",
"commerce": null,
"governed": {
"approvalTokenId": "governed-sim-approval-1",
"intentHash": "47767e1b5eb79ddbcdb0b2394e957d606ac50bc6b94ab33c2183b3703c8999d7",
"intentId": "governed-sim-intent-1",
"purpose": "no-key CI smoke",
"serverId": "governed-sim-srv",
"toolName": "compute"
},
"mode": "prepaid_no_broadcast"
},
"authorization_id": "sim-268afdc9129342029708eb795c87da08",
"preauthorized_units": 100,
"recorded_units": 100
}
},
"cost_charged": 100,
"currency": "USD",
"delegation_depth": 0,
"grant_index": 0,
"payment_reference": "sim-268afdc9129342029708eb795c87da08",
"root_budget_holder": "e13e1a500b08c64049cf9f9a180fbfef011a1d37ee0205ee6daa3d9acb5aa89b",
"settlement_status": "settled"
},
"budget_authority": {
"authority": {
"authority_id": "kernel:e13e1a500b08c64049cf9f9a180fbfef011a1d37ee0205ee6daa3d9acb5aa89b",
"lease_epoch": 1,
"lease_id": "single-node"
},
"authority_profile": "authoritative_hold_event",
"authorize": {
"budget_commit_index": 1,
"committed_cost_units_after": 100,
"event_id": "budget-hold:governed-sim-req-1:cap-01a07d0c-0238-7443-a0f6-d8a2bac4db3d:0:authorize",
"exposure_units": 100
},
"budget_term": "kernel:e13e1a500b08c64049cf9f9a180fbfef011a1d37ee0205ee6daa3d9acb5aa89b:1",
"guarantee_level": "single_node_atomic",
"hold_id": "budget-hold:governed-sim-req-1:cap-01a07d0c-0238-7443-a0f6-d8a2bac4db3d:0",
"invocation_capture": {
"budget_commit_index": 2,
"event_id": "budget-hold:governed-sim-req-1:cap-01a07d0c-0238-7443-a0f6-d8a2bac4db3d:0:capture-invocation:1",
"invocation_count_after": 1
},
"metering_profile": "max_cost_preauthorize_then_reconcile_actual",
"terminal": {
"budget_commit_index": 3,
"committed_cost_units_after": 100,
"disposition": "reconciled",
"event_id": "budget-hold:governed-sim-req-1:cap-01a07d0c-0238-7443-a0f6-d8a2bac4db3d:0:reconcile",
"exposure_units": 100,
"realized_spend_units": 100
}
}
}To put spending limits on a service you own, continue with the complete metered-service application. It includes the host, tool, signed grant, persistent accounting, and receipts.
Inspect the simulation's missing-payment refusal
When it refuses
$ chio mcp governed-sim --governed-mustprepay --payment-adapter none --out ./compute-deny.jsonerror[CHIO-ECONOMY-PREPAYMENT-REQUIRED]: the governed intent requires prepayment (settlement_mode=MustPrepay) and no payment adapter is configured help: pass --payment-adapter sim for a local run, or --payment-adapter http-x402 with a facilitator URL code: urn:chio:error:economy:prepayment-required docs: https://chio.computer/docs/reference/errors#error-economy-prepayment-required
$ jq '{tool_server, tool_name, decision, financial: .metadata.financial}' ./compute-deny.json{
"tool_server": "governed-sim-srv",
"tool_name": "compute",
"decision": {
"verdict": "deny",
"reason": "governed transaction denied: governed intent mandates prepayment (settlement_mode=MustPrepay) but no payment adapter is configured",
"guard": "kernel"
},
"financial": {
"attempted_cost": 100,
"budget_remaining": 1000,
"budget_total": 1000,
"cost_charged": 0,
"currency": "USD",
"delegation_depth": 0,
"grant_index": 0,
"root_budget_holder": "d91861f13c66c366fd59a0c762982d5ea462c4cb78e10ea6b759f853ad5337ae",
"settlement_status": "not_applicable"
}
}A delegated allowance stays within its parent
A coordinator can delegate a smaller allowance to an agent, which can delegate a smaller allowance in turn. In the Quickstart’s partner run, their coordinator gave fixer-1, fixer-2, and tester each a narrower slice. Each delegation may narrow any of the three limits and cannot increase them.
Every delegated grant sits within its parent on all three bounds.
In Rust · scope.rs:14728 lines
// If parent has an invocation cap, child must too and it must be <= parent
if let Some(parent_max) = parent.max_invocations {
match self.max_invocations {
Some(child_max) if child_max <= parent_max => {}
None => return false, // child is uncapped but parent is capped
Some(_) => return false, // child exceeds parent
}
}
// … constraints
// If parent has a per-invocation cost cap, child must too and it must be <=
if let Some(ref parent_cost) = parent.max_cost_per_invocation {
match &self.max_cost_per_invocation {
Some(child_cost)
if child_cost.currency == parent_cost.currency
&& child_cost.units <= parent_cost.units => {}
_ => return false,
}
}
// If parent has a total cost cap, child must too and it must be <=
if let Some(ref parent_cost) = parent.max_total_cost {
match &self.max_total_cost {
Some(child_cost)
if child_cost.currency == parent_cost.currency
&& child_cost.units <= parent_cost.units => {}
_ => return false,
}
}
The kernel requires each attenuated value to be less than or equal to the corresponding parent value, and it rejects a delegation whose child budget exceeds its parent’s when the token is issued. A wider token would also fail signature-chain verification. An agent can authorize a smaller unit of work while keeping a ceiling on the authority it delegates.
Scope attenuation checks each child’s grant values against its own parent. Coordinating consumption across related grants requires the configured budget authority and shared runtime state: token validation establishes the child’s limits, and shared accounting establishes the family’s consumption. Shared capacity covers dividing one parent’s capacity among siblings.
Read more in Budget attenuation in delegation.
Limits bound what a compromised agent can repeat
A grant’s limits still hold when its agent follows hostile text. Suppose the tester follows the injected comment in pull request #421 and retries until an upload works. Scope, guards, and flow checks decide each attempt; the limits decide how many attempts reach a tool and how much they can spend.
- Retrying until a call gets through meets
max_invocations. Each call that reaches dispatch captures one invocation. At the cap, admission refuses every further call before dispatch withBudgetExhausted, whatever its arguments. - Spreading the loop across new sub-agents meets the aggregate invocation budget with
scope: delegation_family. Every descendant carries the root’s signed binding unchanged and counts against one maximum, so a new sub-agent adds no invocation quota. - Spending on a priced tool the grant permits meets
max_total_cost. Each hold must fit the remaining total. In the tester’s run above, admission refuses the third reservation before dispatch, and the model route runs twice however often the tester retries.
The family maximum is the limit a compromised delegate cannot route around. Attenuation keeps each child’s max_invocations at or below its parent’s, yet six children with four uses each could still make twenty-four calls. Under a family maximum of four, the swarm runner’s budget scenario accepts four sends across six workers and refuses both workers of the third graph. Once the family is exhausted, the kernel refuses every descendant, whatever its own max_invocations.
A call refused before dispatch releases its reservation, so refused attempts leave the invocation quota intact, and each one still produces a signed deny receipt. Size the counts to the task: Task-Scoped Authority mints a narrow grant per subtask, and Invocation limits runs a delegation family to its last use, with two workers racing for it.
Choose the accounting guarantee you operate
Choose an accounting authority that can serve the workers sharing a budget and survive the failures you expect. A single kernel serving the pricing project uses persistent single-node authority and retains its hold events; the compute simulation uses ephemeral state. A deployed monetary workflow needs durable admission, store, and authority configuration for its intended failure model.
Each budget store declares the guarantee behind its holds with BudgetGuaranteeLevel:
single_node_atomic: hold and reconcile are atomic on one node.ha_linearizable: the hold is linearizable across a replicated quorum store. A store can report it only when such a store backs the hold.partition_escrowed: the hold is escrowed so a network partition cannot double-spend it.advisory_posthoc: no enforced hold. Spend is recorded after the fact and is not authorization.
The control-plane runtime’s advisory budget-store path reports advisory_posthoc. Deterministic leader selection and replicated usage records do not establish quorum-backed linearizable admission. Select and verify a hold authority that supplies the guarantee you need, and describe the deployment with the level attached to that authority’s evidence.
The authorize and reconcile records together determine spend. A supported denial or abort path reverses the budget hold; inspect the payment adapter’s terminal state independently when explaining settlement.
Read more in Authoritative spend.