Chio/Docs
LOGIN · JOIN

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:

rust
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 codeMinor units per major unit
USD, EUR, GBP100
JPY1
USDC, USDT1_000_000
BTC100_000_000
ETH, LINK1_000_000_000_000_000_000
The minor-unit scale chio-link pins per currency code for cross-currency conversion. A code outside the set is rejected rather than assigned a default.
sourcecrates/economy/chio-link/src/convert.rs:5-16at fe56570

Integer amounts

Budget checks, cost comparisons, and attenuation validation use integer minor-unit arithmetic. Conversion runs in 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.

yaml
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: 200

Independent limits

A grant can set any combination of the three limits. Setting only 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.

StepKernel functionEffect on the budget
Authorizecheck_and_increment_budgetOpens a hold for max_cost_per_invocation and increments the invocation count, or denies before the tool runs
CaptureToolInvocationCostThe tool reports what the call actually cost; a tool that reports nothing leaves the worst-case debit standing
Reconcilefinalize_budgeted_tool_output_with_cost_and_metadataSettles the hold against the realized spend and writes the financial record onto the receipt
Reversereverse_budget_chargeReverses the hold in full when a guard or validation step denies after authorization
The three phases the kernel runs for a priced tool call, with the kernel function that carries each. The reversal path a post-authorize denial takes is the fourth row.
sourcecrates/kernel/chio-kernel/src/kernel/validation.rs:1459-2003at fe56570

Phase 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 against max_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:

rust
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_status is set to Failed and the overrun is recorded in the receipt
  • Actual = charged: exact match, no adjustment needed

Guard failure after authorize

If a guard or validation step denies the request after the hold has already been authorized, the kernel calls 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.

tests/e2e/tests/full_flow.rs744-793rust
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());
}
run the budget testbash
cargo test -p chio-e2e --test full_flow full_flow_budget_exhaustion -- --exact

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

crates/kernel/chio-kernel/src/kernel/error.rsrust
    #[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:

rust
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:

rust
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_units and the committed_cost_units_after the hold was opened.
  • FinancialBudgetTerminalReceiptMetadata: the terminal step: the hold's disposition, the realized_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:

rust
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

A cross-currency receipt includes the rate, feed timestamp, and oracle authority. When the quote is signed, it also includes the oracle's 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 VariantEffect
Attenuation::ReduceCostPerInvocationTightens max_cost_per_invocation on the child token
Attenuation::ReduceTotalCostTightens max_total_cost on the child token
Attenuation::ReduceBudgetLowers 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.

bash
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 further

Child budgets cannot exceed parent budgets

The kernel rejects any delegation attempt where a child's budget exceeds the parent's. The check occurs when the token is issued. A wider token also fails signature-chain verification.

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 exercised
  • total_cost_exposed: the authorized exposure that has been held against this grant
  • total_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:

LevelWhat it claims
single_node_atomicHold and reconcile are atomic on one node
ha_linearizableThe hold is linearizable across a replicated quorum store
partition_escrowedThe hold is escrowed so a network partition cannot double-spend it
advisory_posthocNo 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, and chio-listing turn priced tool calls into biddable, listable offers.
  • Credit and insurance. chio-credit models credit facilities, bonds, scorecards, and exposure ledgers; chio-underwriting turns receipt history into underwriting input.
  • Settlement and anchoring. chio-settle, chio-anchor, and chio-web3 carry 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.