Chio/Docs
LOGIN · JOIN

Budgets & Metering

Give an agent money to spend under a ceiling the kernel checks before every priced call.

Concept chio@5f63e6e

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.

quickstart.tsquickstart.pysrc/main.rs · in main
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);
Output
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
Three metered calls against a total budget: realized spend, the worst-case reservation, and the limitcaptured run · 20 per call, 50 total020406080USD minor unitsc = 20call 1allow · 20handler 1S = 20+ccall 2allow · 20handler 2S = 40+c60 > 50call 3denyhandler 2B = 50
admitk  ⟺  Sk+Ek+c≤B\mathrm{admit}_k \iff \htmlClass{role-seal}{S_k} + \htmlClass{role-call}{E_k} + \htmlClass{role-call}{c} \le \htmlClass{role-grant}{B}
k=3:  40+0+20=60 >50k = 3:\ \ 40 + 0 + 20 = 60 \htmlClass{role-deny}{\,> 50}
nothing charged: S3 stays 40\text{nothing charged: }\htmlClass{role-seal}{S_3} \text{ stays } 40
refused before dispatchnothing held, nothing run2 of 3 admitted · 10 available
Three metered calls against a total budget: realized spend, the worst-case reservation, and the limitcaptured run · 20 per call, 50 total020406080c = 20call 1allowH=1S = 20+ccall 2allowH=2S = 40+c60 > 50call 3denyH=2B = 50call 3: 60 > 50, refused before dispatch

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.

admitk  ⟺  Sk+Ek+c≤Bk=3:  40+0+20=60 >50nothing charged: S3 stays 40\begin{aligned} &\mathrm{admit}_k \iff \htmlClass{role-seal}{S_k} + \htmlClass{role-call}{E_k} + \htmlClass{role-call}{c} \le \htmlClass{role-grant}{B} \\[4pt] &k = 3:\ \ 40 + 0 + 20 = 60 \htmlClass{role-deny}{\,> 50} \\ &\text{nothing charged: }\htmlClass{role-seal}{S_3} \text{ stays } 40 \end{aligned}
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>;
The tester’s grant holds USD 0.20 per model call against a USD 0.50 total, in USD minor units; moving c or B recomputes the same check. Settlement with the model provider is outside the figure.

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.

A grant on the model route · budget fieldsyaml
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: 100

The 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_status set to Failed.
  • 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.

Two captured runs of the same approved compute request: with a payment adapter it is allowed and charged $1.00; without one it is denied and charged nothingcaptured rungrantapprovalprepaymentverdictbudget, $10.00adapter configuredadapter: "sim"allow$1.00 charged$9.00 leftno payment adapterpayment_adapter: Nonedeny$0.00 charged$10.00 leftverdict: "allow" cost_charged: 100budget_remaining: 900 settlement_status: "settled"
Two captured runs of the same approved compute request: with a payment adapter it is allowed and charged $1.00; without one it is denied and charged nothingcaptured runadapter configuredadapter: "sim"grantapprovalprepaymentallow$1.00 charged$9.00 leftno payment adapterpayment_adapter: Nonegrantapprovalprepaymentdeny$0.00 charged$10.00 leftverdict: "allow"cost_charged: 100budget_remaining: 900settlement_status: "settled"

adapter configured: allow. Charged $1.00; $9.00 of $10.00 remaining.

allow(r)  ⟺  r∈scope(κ)∧ approved(r)∧ (MustPrepay⇒adapter≠∅)charged(r)=q if allowed, else 0\begin{aligned} &\htmlClass{role-call}{\text{allow}}(r) \iff r \in \text{scope}(\kappa) \\ &\quad \wedge\ \htmlClass{role-grant}{\text{approved}}(r) \\ &\quad \wedge\ \big(\text{MustPrepay} \Rightarrow \htmlClass{role-seal}{\text{adapter}} \ne \varnothing\big) \\[6pt] &\text{charged}(r) = q \text{ if } \htmlClass{role-call}{\text{allowed}} \text{, else } 0 \end{aligned}
In Rust · governed_validation.rs:1457
Self::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 two captured runs of the compute simulation, each in a fresh kernel with the same limits and a freshly signed approval. Only the payment adapter differs. Every value is the run's own receipt field; settlement is simulated.

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

Run the approved compute requesttranscript
$ chio mcp governed-sim --governed-mustprepay --payment-adapter sim --out ./compute-allow.json
exit 0

Expected output

Allowed receipt: approval binding and budget recordtranscript
$ 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
    }
  }
}
exit 0allow

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

Repeat with no payment adaptertranscript
$ chio mcp governed-sim --governed-mustprepay --payment-adapter none --out ./compute-deny.json
error[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
exit 1
Denied receipt: no chargetranscript
$ 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"
  }
}
exit 0deny

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.

Three delegated grants, each bound drawn inside its parent’s allowancescaled to the root grant
gi⊑gi−1  ⟺  ∀k: ck≤pk\htmlClass{role-call}{g_i} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_{i-1}} \iff \forall k:\ \htmlClass{role-call}{c_k} \le \htmlClass{cor-parent}{p_k}
g0orchestratorroot granttotalmax_total_cost = 1000per callmax_cost_per_invocation = 100invocationsmax_invocations = 200g1research
g1⊑g0\htmlClass{role-call}{g_1} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_0}
total
500≤1000\htmlClass{role-call}{500} \le \htmlClass{cor-parent}{1000}
ReduceTotalCost
per call
50≤100\htmlClass{role-call}{50} \le \htmlClass{cor-parent}{100}
ReduceCostPerInvocation
invocations
50≤200\htmlClass{role-call}{50} \le \htmlClass{cor-parent}{200}
ReduceBudget
g2sub-agent
g2⊑g1\htmlClass{role-call}{g_2} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_1}
total
100≤500\htmlClass{role-call}{100} \le \htmlClass{cor-parent}{500}
ReduceTotalCost
per call
25≤50\htmlClass{role-call}{25} \le \htmlClass{cor-parent}{50}
ReduceCostPerInvocation
invocations
10≤50\htmlClass{role-call}{10} \le \htmlClass{cor-parent}{50}
ReduceBudget
Three delegated grants, each bound drawn inside its parent’s allowancescaled to the root grantg0orchestratorroot granttotal1000per call100invocations200g1research
g1⊑g0\htmlClass{role-call}{g_1} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_0}
total
500≤1000\htmlClass{role-call}{500} \le \htmlClass{cor-parent}{1000}
per call
50≤100\htmlClass{role-call}{50} \le \htmlClass{cor-parent}{100}
invocations
50≤200\htmlClass{role-call}{50} \le \htmlClass{cor-parent}{200}
g2sub-agent
g2⊑g1\htmlClass{role-call}{g_2} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_1}
total
100≤500\htmlClass{role-call}{100} \le \htmlClass{cor-parent}{500}
per call
25≤50\htmlClass{role-call}{25} \le \htmlClass{cor-parent}{50}
invocations
10≤50\htmlClass{role-call}{10} \le \htmlClass{cor-parent}{50}

Every delegated grant sits within its parent on all three bounds.

gi⊑gi−1  ⟺  ∀k: ck≤pk\htmlClass{role-call}{g_i} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_{i-1}} \iff \forall k:\ \htmlClass{role-call}{c_k} \le \htmlClass{cor-parent}{p_k}
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,
    }
}
An illustrative three-level delegation chain, in USD minor units and invocations. Each grant is checked against its own parent; siblings are not summed against a parent here. Cost bounds also require the same currency.

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 with BudgetExhausted, 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.

Tie payment to what is delivered