LearnAnatomy of a Governed Call
Budgets & Metering
A capability token can limit both the tools an agent may call and the amount it may spend.
A budget travels with the token through delegation. Each charge records its path to the root budget holder, making the token a spending authorization. Autonomous Commerce introduces that identity. Each charge described here is recorded as a priced, signed receipt; Receipts covers the receipt format.
MonetaryAmount
MonetaryAmount represents money as minor-unit integers with an ISO 4217 currency code:
pub struct MonetaryAmount {
/// Value in the currency's minor unit (e.g., cents for USD).
pub units: u64,
/// ISO 4217 currency code (e.g., "USD", "EUR", "USDC").
pub currency: String,
}Monetary values use u64minor units. Budget calculations do not use floating-point values.
currency is a bare string, so the kernel compares currencies rather than interpreting them. One place does interpret them: chio-link's minor_units_for_currency pins a scale per code so a cross-currency conversion can move between two minor units. A code outside this set returns PriceOracleError::InvalidConfiguration rather than a guessed scale.
| Currency code | Minor units per major unit |
|---|---|
USD, EUR, GBP | 100 |
JPY | 1 |
USDC, USDT | 1_000_000 |
BTC | 100_000_000 |
ETH, LINK | 1_000_000_000_000_000_000 |
crates/economy/chio-link/src/convert.rs:5-16at fe56570Integer amounts
u128 and returns an overflow error rather than rounding through a float.Three-tier budget model
Each ToolGrant in a capability token carries up to three independent budget constraints: per-invocation (max_cost_per_invocation), per-grant total (max_total_cost), and an invocation count cap (max_invocations). All three are optional and can be combined freely:
The kernel enforces all three limits atomically before the tool runs. If any limit would be exceeded, the call is denied and a receipt is produced with the attempted cost recorded.
grants:
- server_id: srv-ai-inference
tool_name: generate_text
operations: [invoke]
# Per-call cap: no single call can cost more than $0.50
max_cost_per_invocation:
units: 50
currency: USD
# Aggregate cap: total spend cannot exceed $10.00
max_total_cost:
units: 1000
currency: USD
# Call count cap: maximum 200 invocations
max_invocations: 200Independent limits
max_invocations creates a free-tier with a call count limit. Setting only max_total_cost creates a pay-per-use budget with no per-call cap.Budget enforcement flow
The success path runs in three phases: authorize, capture, reconcile. The kernel reserves the worst-case cost before dispatch, captures the cost the tool reports, then settles the reservation against it. A denial or validation failure after authorization takes a fourth path instead: reverse_budget_charge reverses the hold.
| Step | Kernel function | Effect on the budget |
|---|---|---|
| Authorize | check_and_increment_budget | Opens a hold for max_cost_per_invocation and increments the invocation count, or denies before the tool runs |
| Capture | ToolInvocationCost | The tool reports what the call actually cost; a tool that reports nothing leaves the worst-case debit standing |
| Reconcile | finalize_budgeted_tool_output_with_cost_and_metadata | Settles the hold against the realized spend and writes the financial record onto the receipt |
| Reverse | reverse_budget_charge | Reverses the hold in full when a guard or validation step denies after authorization |
crates/kernel/chio-kernel/src/kernel/validation.rs:1459-2003at fe56570Phase 1: authorize
Before the tool executes, the kernel calls check_and_increment_budget(). This function atomically:
- Increments the invocation count and checks it against
max_invocations - Debits the worst-case cost (
max_cost_per_invocation) from the running total and checks it againstmax_total_cost
If either check fails, the kernel denies the call before tool code runs. Authorization reserves the maximum max_cost_per_invocation before the tool runs.
Phase 2: capture
The tool executes. On success the kernel captures the actual cost the tool reports via ToolInvocationCost; if the call fails, the hold is released and nothing is charged:
pub struct ToolInvocationCost {
/// Actual cost in minor units.
pub units: u64,
/// ISO 4217 currency code.
pub currency: String,
/// Optional cost breakdown as an arbitrary JSON value (tool-defined keys).
pub breakdown: Option<serde_json::Value>,
}The breakdown field allows tools to itemize costs: for example, separating compute, I/O, and network charges. Keys are tool-defined and the shape is opaque JSON, which the kernel copies through into the financial receipt for auditability.
Phase 3: reconcile
After the tool returns, the kernel calls finalize_budgeted_tool_output_with_cost_and_metadata() to reconcile the authorized amount with the actual cost. It takes the request, the tool output, the elapsed time, the timestamp, the matched grant index, and a FinalizeToolOutputCostContext (the charge result, the reported cost, the payment authorization, and the cap). When there was no budget charge to reconcile, an unbudgeted call, it falls back to finalize_tool_output_with_metadata_and_payee_binding. When there was, it settles the difference:
- Actual < charged: the difference is credited back to the budget
- Actual > charged: a cost overrun has occurred;
settlement_statusis set toFailedand the overrun is recorded in the receipt - Actual = charged: exact match, no adjustment needed
Guard failure after authorize
reverse_budget_charge() to fully reverse the debit. The agent is not charged for a tool call that never executed.Exhausting the invocation count
tests/e2e/tests/full_flow.rs drives the count cap to its edge against the real kernel. It issues a grant with max_invocations: Some(2), spends both invocations, and asserts the third call is denied.
async fn full_flow_budget_exhaustion() {
let (kernel, _ca_kp) = make_kernel_bare();
let agent_kp = Keypair::generate();
// Issue a capability with max_invocations = 2.
let cap = kernel
.issue_capability(
&agent_kp.public_key(),
ChioScope {
grants: vec![ToolGrant {
server_id: "srv".to_string(),
tool_name: "echo".to_string(),
operations: vec![Operation::Invoke],
constraints: vec![],
max_invocations: Some(2),
max_cost_per_invocation: None,
max_total_cost: None,
dpop_required: None,
}],
..ChioScope::default()
},
300,
)
.expect("issue budgeted cap");
// First two calls succeed.
for i in 0..2 {
let req = make_request(
&format!("req-budget-{i}"),
&cap,
"echo",
serde_json::json!({"i": i}),
);
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Allow, "call {i} should succeed");
assert!(resp.receipt.verify_signature().unwrap());
}
// Third call should be denied due to budget exhaustion.
let req = make_request("req-budget-2", &cap, "echo", serde_json::json!({"i": 2}));
let resp = kernel.evaluate_tool_call(&req).await.unwrap();
assert_eq!(resp.verdict, Verdict::Deny);
let reason = resp.reason.as_deref().unwrap_or("");
assert!(
reason.contains("budget"),
"expected budget exhaustion, got: {reason}"
);
assert!(resp.receipt.is_denied());
assert!(resp.receipt.verify_signature().unwrap());
}cargo test -p chio-e2e --test full_flow full_flow_budget_exhaustion -- --exactThe last two assertions are the reason this is worth running: the denied call still produces a receipt, and that deny receipt is signed. The refusal reason comes from a KernelError variant, so its text is fixed by the type rather than by the call site.
#[error("invocation budget exhausted for capability {0}")]
BudgetExhausted(CapabilityId),
#[error("captured budget replay denied for capability {0}")]
CapturedBudgetReplay(CapabilityId),Financial receipt metadata
Every tool invocation that exercises a monetary grant produces a FinancialReceiptMetadata record embedded in the receipt's metadata field. This struct captures the complete economic state of the transaction:
pub struct FinancialReceiptMetadata {
/// Index of the grant within the capability token.
pub grant_index: u32,
/// Actual cost charged (after reconciliation).
pub cost_charged: u64,
/// Currency of the charge.
pub currency: String,
/// Budget remaining after this invocation.
pub budget_remaining: u64,
/// Total budget for the grant.
pub budget_total: u64,
/// Depth in the delegation chain (0 = root).
pub delegation_depth: u32,
/// Identity of the root budget holder.
pub root_budget_holder: String,
/// External payment reference for settlement.
pub payment_reference: Option<String>,
/// Settlement status of this charge.
pub settlement_status: SettlementStatus,
/// Itemized cost breakdown from the tool, as an arbitrary JSON value.
/// Keys are tool-defined.
pub cost_breakdown: Option<serde_json::Value>,
/// Oracle conversion evidence (for cross-currency).
pub oracle_evidence: Option<OracleConversionEvidence>,
/// Cost that was attempted (present in denial receipts).
pub attempted_cost: Option<u64>,
}The SettlementStatus enum tracks the lifecycle of the financial transaction:
pub enum SettlementStatus {
/// No monetary cost (e.g., free-tier grant).
NotApplicable,
/// Cost recorded, awaiting settlement.
Pending,
/// Settlement completed successfully.
Settled,
/// Settlement failed (e.g., cost overrun).
Failed,
}Budget hold lineage
FinancialReceiptMetadata is the settled summary. Underneath it, the authorize path records the budget hold itself. When check_and_increment_budget authorizes a charge it opens a hold via authorize_budget_hold, and three sibling records capture that hold's lineage on the receipt:
FinancialBudgetHoldAuthorityMetadata: the authority behind the hold:authority_id,lease_id,lease_epoch.FinancialBudgetAuthorizeReceiptMetadata: the authorize step:exposure_unitsand thecommitted_cost_units_afterthe hold was opened.FinancialBudgetTerminalReceiptMetadata: the terminal step: the hold'sdisposition, therealized_spend_units, and the committed total after reconcile.
Together, these records identify the hold authority, authorization, and terminal reconciliation.
The kernel merges FinancialReceiptMetadata into the receipt under the reserved financial metadata key (FINANCIAL_METADATA_KEY), so a reader finds it at metadata.financial on the signed receipt. A receipt carries the block only when the call exercised a monetary grant or a prepaid authorization. Receipts covers the rest of the receipt body, including what the evidence array holds and when it is present.
Cross-currency enforcement
A capability token's budget may be denominated in one currency while a tool reports its cost in another. For example, a grant with a USD budget invoking a tool that charges in USDC. The kernel notices the mismatch at reconcile time, when it compares the reported cost against the hold, and calls resolve_cross_currency_cost to convert the reported amount into the grant currency. The rate comes from the configured PriceOracle, the trait chio-link defines and a deployment installs with set_price_oracle. Without one, the call fails with KernelError::NoCrossCurrencyOracle.
The conversion uses integer arithmetic. The rate is a rate_numerator/rate_denominator pair, and the cost is carried as both original_cost_units (what the tool charged) and converted_cost_units (what the budget was debited). The quote's freshness is bounded explicitly: updated_at records when the feed reported the rate, max_age_seconds caps how stale it may be at use, and cache_age_seconds records how old the cached rate actually was at conversion time.
The conversion evidence is embedded in the receipt as OracleConversionEvidence:
pub struct OracleConversionEvidence {
/// Wire schema id ("chio.oracle-conversion-evidence.v1").
pub schema: String,
/// Base currency of the quoted pair.
pub base: String,
/// Quote currency of the quoted pair.
pub quote: String,
/// Identifier of the oracle authority that issued the quote.
pub authority: String,
/// Exchange-rate numerator (integer representation).
pub rate_numerator: u64,
/// Exchange-rate denominator.
pub rate_denominator: u64,
/// Oracle source identifier.
pub source: String,
/// Address of the price feed the rate was read from.
pub feed_address: String,
/// Timestamp the feed reported the rate.
pub updated_at: u64,
/// Maximum age (seconds) the rate may be at use.
pub max_age_seconds: u64,
/// Age (seconds) of the cached rate at conversion time.
pub cache_age_seconds: u64,
/// Converted cost in the grant currency's minor units.
pub converted_cost_units: u64,
/// Original cost in the tool's charged-currency minor units.
pub original_cost_units: u64,
/// Currency the tool actually charged in.
pub original_currency: String,
/// Currency the grant budget is denominated in.
pub grant_currency: String,
/// Oracle's public key, when the quote is signed.
pub oracle_public_key: Option<PublicKey>,
/// Oracle's signature over the quote, when signed.
pub signature: Option<Signature>,
}Verifying a conversion
oracle_public_key and signature, so the rate can be checked against the oracle's key.Budget attenuation in delegation
When an agent delegates a capability token to a child agent, the economic constraints may be narrowed. The Attenuation enum. Three of its variants govern the budget tiers directly:
| Attenuation Variant | Effect |
|---|---|
Attenuation::ReduceCostPerInvocation | Tightens max_cost_per_invocation on the child token |
Attenuation::ReduceTotalCost | Tightens max_total_cost on the child token |
Attenuation::ReduceBudget | Lowers max_invocations on the child token |
The same enum carries four non-monetary narrowings: RemoveTool and RemoveOperation drop a tool or operation from scope, AddConstraint tightens a tool's parameter constraints, and ShortenExpiry pulls the expiry. Each variant narrows the parent token.
The kernel requires each attenuated value to be less than or equal to the corresponding parent value. Lean 4 property P1, capability attenuation, proves that narrowing over the bounded ChioScope model; the proof manifest lists 6 named proofs under it. The theorem does not cover the Rust budget store.
Orchestrator token (root):
max_total_cost: 1000 USD ($10.00)
max_cost_per_invocation: 100 USD ($1.00)
max_invocations: 200
└─ Research agent (delegated):
max_total_cost: 500 USD ($5.00) ← tightened
max_cost_per_invocation: 50 USD ($0.50) ← tightened
max_invocations: 50 ← tightened
└─ Sub-agent (delegated again):
max_total_cost: 100 USD ($1.00) ← tightened further
max_cost_per_invocation: 25 USD ($0.25) ← tightened further
max_invocations: 10 ← tightened furtherChild budgets cannot exceed parent budgets
Budget state persistence
A BudgetStore holds the running state. SqliteBudgetStore in chio-store-sqlite persists it in the capability_grant_budgets table, whose primary key is (capability_id, grant_index), so grants inside one token keep independent budgets. The kernel's own InMemoryBudgetStore holds the same shape without durability.
A BudgetUsageRecord tracks three running values per key:
invocation_count: how many times this grant has been exercisedtotal_cost_exposed: the authorized exposure that has been held against this granttotal_cost_realized_spend: the spend reconciled back from completed calls
committed_cost_units() sums the two cost columns, and that total is what a limit check compares against max_total_cost.
The HA control plan replicates budgets as monotonic usage records keyed by capability and grant, and routes every increment to a write leader so exhaustion stays strong across nodes. The leader is the lexicographically smallest healthy advertised URL in the cluster membership set, which is a deterministic election with a repair loop rather than consensus.
Atomicity at the store level
SqliteBudgetStore opens one write transaction per hold: the read of current usage, the check against the limits, and the write of the new state commit together. Two concurrent invocations against the same grant cannot both pass a check that only one of them fits inside.Budget guarantee levels
Each budget store uses BudgetGuaranteeLevel to declare the guarantee behind its holds. A store can report ha_linearizable only when a replicated quorum store backs the hold. The levels:
| Level | What it claims |
|---|---|
single_node_atomic | Hold and reconcile are atomic on one node |
ha_linearizable | The hold is linearizable across a replicated quorum store |
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 level accompanies the hold model described above. The kernel authorizes the maximum exposure before the call (the max_cost_per_invocation, or the quoted cost when present), then reconciles the hold to the realized spend after the tool returns. A denial or abort reverses the full hold. The authorize and reconcile records together determine spend. See Authoritative Spend for the receipt field and validation rules, and Reconciliation for the same reconciliation cycle.
Related components
Per-call budgets and the authorize / capture / reconcile cycle support other components. Each charge has a signed receipt linked to its capability and budget lineage:
- Markets.
chio-market,chio-open-market, andchio-listingturn priced tool calls into biddable, listable offers. - Credit and insurance.
chio-creditmodels credit facilities, bonds, scorecards, and exposure ledgers;chio-underwritingturns receipt history into underwriting input. - Settlement and anchoring.
chio-settle,chio-anchor, andchio-web3carry a settled charge onto external and on-chain rails.
Those components share the per-call budget mechanics described here. A signed receipt records the authorization, cost, budget holder, and any oracle rate used for the call. Autonomous Commercedescribes how these records support settlement and underwriting.