Budgets & Metering
Give an agent a spending allowance, run a priced service against it, and read the receipts that record each charge.
On this page
Spending authority is part of the kernel's admission decision. Each priced call reserves its worst case against the agent's grant before the service runs, the service reports what it cost, and a signed receipt records the charge.
The example is Larkfield's discount tool as a priced service: each call prices a SKU after a discount and charges USD 0.20, 20 USD minor units. The grant allows 20 per call and 50 in total, so two calls succeed and the kernel refuses the third before the handler starts.
1. Run a service under a 50-unit allowance
Install the CLI, then fetch the project with chio new pricing-metered --example pricing-metered, or download it. Cargo fetches the pinned dependencies on the first run.
What the project supplies
- A signed capability with a priced grant, and a policy-context hash derived from the run's host policy descriptor.
- A durable admission authority that coordinates operation, budget, and revocation state, plus a SQLite receipt store installed before issuance.
- A service manifest the host signs and verifies before registration, and a connection that reports its fixed realized cost.
- A persistent local-credit payment adapter whose capture equals the authorized amount, so the price and the per-call ceiling match.
The host uses Unix file locking. Its built-in ledger records local credits denominated in USD and moves no external money. The README describes the fresh-run scope, and src/host.rs is the complete host to adapt.
cd pricing-metered
cargo run --release --locked -- sku-204 15call-1: ALLOW
result: {"currency":"USD","percent":15,"price":"49.30","sku":"sku-204"}
charge: 20 USD; settlement: settled
budget: 30 available; 20 spent (USD)
reserved: 0
handler invocations: 1
call-2: ALLOW
result: {"currency":"USD","percent":15,"price":"49.30","sku":"sku-204"}
charge: 20 USD; settlement: settled
budget: 10 available; 40 spent (USD)
reserved: 0
handler invocations: 2
call-3: DENY
reason: invocation budget exhausted for capability cap-01a12f1d-3f0b-73d5-a642-1d508e92b96a
charge: 0 USD; settlement: not_applicable
budget: 10 available; 40 spent (USD)
reserved: 0
handler invocations: 2
evidence: runs/80e3c437-e8ab-4345-89b4-4831cf179aecYou should see two calls admitted at 20 units each, each pricing sku-204 at 15 percent off, and a third refused with 10 units left. The arguments are the SKU and the discount in percent. The handler's own log, handler-invocations.log, holds two lines after the run: the refusal stopped a third invocation before it started.
The printed evidence directory holds the complete receipts, the signed capability and manifest, host-policy.json, the SQLite stores, and the host's public verification key.
2. Trace each charge through the hold ledger
The kernel reserves the grant's per-call ceiling against the remaining total before it dispatches discount, then reconciles that hold to the reported cost.
The per-call ceiling is held before the service runs
The kernel reserves the grant’s max_cost_per_invocation of 20 against max_total_cost of 50 before it dispatches discount. Authoritative spend.
discount calls under a grant of 20 per call and 50 in total, in USD minor units. Charges, balances, and the refusal are the run’s own; the local credit ledger moves no external money.The hold is recorded before the service runs. The service reports its realized cost through invoke_with_cost, the hold reconciles to the 20-unit charge, and the receipt records settlement: settled. A variable price reconciles below the ceiling and releases the unused part of the hold.
Call three needs 20 and 10 remain, so the kernel refuses before dispatch. Its receipt records a charge of 0 with settlement: not_applicable. The local credit adapter here is one payment module; a rail settles money outside the kernel, and the kernel records the outcome it observes.
Read more in Economics.
3. Read the limits in the signed grant
Open capability.json in the evidence directory. The limits belong to the tool grant inside it.
{
"max_cost_per_invocation": {
"currency": "USD",
"units": 20
},
"max_total_cost": {
"currency": "USD",
"units": 50
},
"operations": [
"invoke"
],
"server_id": "pricing",
"tool_name": "discount"
}You should see max_cost_per_invocation at 20 and max_total_cost at 50, both in USD minor units: 100 units is one dollar. The signed capability also binds its subject, issuer, lifetime, and policy context, so keep the whole file when you verify it.
4. Lower the allowance and run again
In src/host.rs, reduce the total to 30. Keep the per-call ceiling equal to the service's fixed 20-unit charge.
pub const CALL_CEILING: u64 = 20;
pub const TOTAL_BUDGET: u64 = 30;cargo run --release --locked -- sku-118 10call-1: ALLOW
result: {"currency":"USD","percent":10,"price":"21.60","sku":"sku-118"}
charge: 20 USD; settlement: settled
budget: 10 available; 20 spent (USD)
reserved: 0
handler invocations: 1
call-2: DENY
reason: invocation budget exhausted for capability cap-01a12f1d-69e7-75e9-a84c-d26cd92a1658
charge: 0 USD; settlement: not_applicable
budget: 10 available; 20 spent (USD)
reserved: 0
handler invocations: 1
call-3: DENY
reason: invocation budget exhausted for capability cap-01a12f1d-69e7-75e9-a84c-d26cd92a1658
charge: 0 USD; settlement: not_applicable
budget: 10 available; 20 spent (USD)
reserved: 0
handler invocations: 1
evidence: runs/a7042bba-9c26-4564-874a-81ca3e825a9dYou should see one call admitted and two refused. Each run issues a fresh capability in its own directory, so the new constant takes effect on the next run and never alters a grant already signed.
5. Read the refusal against the ledger
Return to the 50-unit run. After two charges, 40 units are spent and 10 remain: the balance is above zero, yet too small for another 20-unit reservation. The durable ledger holds the live balance.
{
"available": 10,
"currency": "USD",
"reserved": 0,
"spent": 40
}You should see available at 10 and spent at 40 after the refusal. The refusal charged zero; it did not restore earlier spending. Its receipt names kernel as the deciding guard and records the requested exposure in attempted_cost.
Read the current balance from the ledger and treat each receipt's budget_remaining as a snapshot. A signed grant's limits are fixed: to change an allowance, issue or delegate new authority and keep the old evidence. A refused reservation can succeed later once another call's hold is released.
6. Total the charges from the signed receipts
Inside your run directory, project the three-record file into a total per currency.
# Group the allowed, settled records by currency.
jq -s '[.[] | select(.decision.verdict == "allow"
and .metadata.financial.settlement_status == "settled")]
| group_by(.metadata.financial.currency)
| map({currency: .[0].metadata.financial.currency,
recorded_units: (map(.metadata.financial.cost_charged) | add)})' receipts.ndjson
# Successful balance snapshots, in the application's recorded order.
jq 'select(.decision.verdict == "allow")
| {timestamp, remaining: .metadata.financial.budget_remaining}' receipts.ndjson[
{
"currency": "USD",
"recorded_units": 40
}
]You should see 40 USD units across the two settled receipts. Each one carries its charge in metadata.financial; here is the second.
{
"budget_remaining": 10,
"budget_total": 50,
"cost_breakdown": {
"payment": {
"authorization_id": "hold:dd10f15d49a5f974060c67d0de1db477263075316b1de61e5867f10a6fdf06b5",
"preauthorized_units": 20,
"rail": "finding-operator-ledger",
"rail_mode": "reversible_hold",
"recorded_units": 20,
"transaction_id": "capture:8ce40bb1d741924b4f5158d3ac99c215516311dcf93d22cd69d7913ffb19f365"
}
},
"cost_charged": 20,
"currency": "USD",
"delegation_depth": 0,
"grant_index": 0,
"payment_reference": "capture:8ce40bb1d741924b4f5158d3ac99c215516311dcf93d22cd69d7913ffb19f365",
"root_budget_holder": "8c8ccb6701deb29d5fba8ea87585145f250dcea086c3d849bd4751d43e756889",
"settlement_status": "settled"
}The block records a 20-unit local-credit capture, 10 units remaining, and a payment_reference that joins the receipt to the ledger's capture. The jq projections read content only. Export Billing Records checks each receipt's signature against the host key, excludes the refusal, and produces two accounting rows.
A cost reported above the hold fails the call at finalization
The per-call ceiling bounds what the kernel authorizes. A service that reports more has already run, so the kernel refuses to finalize the charge and leaves the operation for you to resolve.
The service reported more than its authorization
Change PRICE_UNITS to 30 and leave the ceiling at 20. The durable host returns this error, with no normal tool response and no billable receipt:
Error: durable admission failed: reported cost exceeds the durable payment authorizationPreserve the operation and payment state before you rerun. A retry runs the tool again. Align the price and the ceiling, or set the ceiling from the worst case you will pay.
Several kernels draw on one budget
Kernels that serve one budget share the durable admission authority and its fencing. Independent counters on each node multiply the available authority, and multiplying the call ceiling by the node count gives no overrun bound. Configure budget and revocation through the admission authority instead of attaching a separate budget database.
Read more in Budgets Across Nodes.
Budget fields, pricing helpers, and financial receipts
Search for max_total_cost, invoke_with_cost, settlement_status, or chio-link.
Grant budget fields
Budget limits live on individual grants inside a signed capability. Each grant targets one tool on one server and can carry any combination of max_cost_per_invocation, max_total_cost, and max_invocations. The YAML fragments below are scopes for issuance; issuance signs the capability that contains them.
MonetaryAmount carries exactly two fields, units and currency. Units are the currency's minor units, so 100 USD units is one dollar and the type never holds a float.
Three limits on one grant
Combine a cap per call, an aggregate cap, and a call count. The Python tab builds the same grant with the SDK's typed models (pip install chio-sdk).
grants:
- server_id: models
tool_name: complete
operations: [invoke]
max_cost_per_invocation:
units: 100 # USD 1.00 per call
currency: USD
max_total_cost:
units: 5000 # USD 50.00 in total
currency: USD
max_invocations: 500from chio_sdk import ChioScope, MonetaryAmount, Operation, ToolGrant
scope = ChioScope(
grants=[
ToolGrant(
server_id="models",
tool_name="complete",
operations=[Operation.invoke],
max_cost_per_invocation=MonetaryAmount(units=100, currency="USD"),
max_total_cost=MonetaryAmount(units=5000, currency="USD"),
max_invocations=500,
)
]
)Serializing the model with exclude_none=True yields these grant fields; key order carries no meaning.
{
"grants": [
{
"server_id": "models",
"tool_name": "complete",
"operations": [
"invoke"
],
"max_invocations": 500,
"max_cost_per_invocation": {
"units": 100,
"currency": "USD"
},
"max_total_cost": {
"units": 5000,
"currency": "USD"
}
}
]
}Call count only
A free tool can still be bounded: max_invocations stops a runaway loop with no monetary accounting.
grants:
- server_id: web
tool_name: search
operations: [invoke]
max_invocations: 100
# No monetary limits: the tool is freeAggregate cap only
The schema accepts a total by itself. Monetary admission reserves max_cost_per_invocation and defaults it to zero when absent, and the local-ledger adapter refuses a zero-value authorization, so pair a total with an explicit per-call ceiling when you run the example.
grants:
- server_id: models
tool_name: complete
operations: [invoke]
max_total_cost:
units: 5000 # USD 50.00
currency: USDPer-call and total caps
For a tool whose cost varies per call, the per-call cap keeps one expensive call from consuming the whole budget and the total cap bounds aggregate spend.
grants:
- server_id: models
tool_name: complete
operations: [invoke]
max_cost_per_invocation:
units: 200 # USD 2.00 per call
currency: USD
max_total_cost:
units: 10000 # USD 100.00 in total
currency: USDSeveral grants in one capability
One capability can hold several grants, each with its own budget, so an agent can reach several tools with different spending profiles.
grants:
- server_id: models
tool_name: complete
operations: [invoke]
max_cost_per_invocation:
units: 100
currency: USD
max_total_cost:
units: 5000
currency: USD
max_invocations: 500
- server_id: web
tool_name: search
operations: [invoke]
max_invocations: 200
# Free tool, no monetary limits
- server_id: pricing
tool_name: publish_price
operations: [invoke]
max_cost_per_invocation:
units: 10 # USD 0.10 per published price
currency: USD
max_total_cost:
units: 500 # USD 5.00 in total
currency: USDDelegated budgets
A parent narrows a child's total and per-call limits with ReduceTotalCost and ReduceCostPerInvocation, and its call count with ReduceBudget. Parent-child accounting comes from the signed delegation chain and its budget authority; two independently issued tokens hold two separate budgets.
# Parent token: USD 100.00 in total
grants:
- server_id: models
tool_name: complete
operations: [invoke]
max_total_cost:
units: 10000 # USD 100.00
currency: USD
max_invocations: 1000# Child token, attenuated from the parent: USD 10.00
# Created with ReduceTotalCost and ReduceBudget
# (optionally ReduceCostPerInvocation)
grants:
- server_id: models
tool_name: complete
operations: [invoke]
max_total_cost:
units: 1000 # USD 10.00, reduced from the parent's USD 100.00
currency: USD
max_invocations: 100 # reduced from the parent's 1000To attribute delegated spend, trace the delegation chain and the budget-authority records. delegation_depth records the chain depth used for the call, and root_budget_holder is populated from the capability issuer. Siblings can also share one family monetary cap, with basis-point shares bounding each child's draw; see shared capacity.
Tool pricing helpers
A tool server declares its price with the NativeTool helpers. per_invocation_price sets the billing unit to "invocation"; flat_price sets one price with no billing unit; per_unit_price prices a custom unit; hybrid_price adds a usage price to a base. The helpers describe a pricing contract. The service measures usage and reports the cost.
NativeTool::new("quote", "Quote a SKU", schema.clone())
.per_invocation_price(1, "USD"); // USD 0.01 per quote
NativeTool::new("read_order", "Read one order", schema.clone())
.flat_price(500, "USD"); // USD 5.00 per call
NativeTool::new("complete", "Run a model completion", schema.clone())
.per_unit_price(2, "USD", "1k_tokens"); // USD 0.02 per 1,000 tokens
NativeTool::new("search", "Search competitor prices", schema)
.hybrid_price(25, 10, "USD", "result"); // USD 0.25 plus 0.10 per resultThe host authenticates the manifest before registration. A connection reports its realized charge as a ToolInvocationCost. The default invoke_with_cost returns no amount, and the kernel then debits the authorized ceiling as a conservative charge. The service connection reports explicitly:
Some(ToolInvocationCost {
units: PRICE_UNITS,
currency: "USD".into(),
breakdown: Some(json!({"fixed_service_charge": PRICE_UNITS})),
})Publishing offers and planning a budget from a quote are in Tool Pricing.
Financial receipt metadata
The monetary path records its decision as FinancialReceiptMetadata in metadata.financial. Depending on the outcome, the block holds:
grant_index: the grant inside the capability that paid for the call.cost_chargedandcurrency: the recorded amount, read with the settlement status and the rail.budget_remainingandbudget_total: a balance snapshot and the grant's original total.delegation_depthandroot_budget_holder: the chain depth and the issuing capability key as bare lowercase hex.payment_reference: an optional reference that joins the receipt to an external settlement system.settlement_status: whether the charge is pending, settled, failed, or not applicable.cost_breakdown: optional reporting and payment details, not necessarily an additive cost list.oracle_evidence: the conversion record for a cross-currency charge.attempted_cost: the requested exposure on a monetary refusal.
A receipt without the block makes no statement about price. Invocation-only quotas can refuse with no financial block at all.
Query receipts with financial data
Local reads against a --receipt-db path fail closed without a tenant scope: pass --tenant <id>, or --admin-all for an explicit cross-tenant operator read, on every local query. --min-cost and --max-cost both require --cost-currency.
# Receipts with cost information for one tool
chio --receipt-db ./receipts.sqlite receipt list --tenant team-a \
--tool-server models --tool-name complete
# Expensive calls, by minimum cost in minor units
chio --receipt-db ./receipts.sqlite receipt list --tenant team-a \
--min-cost 100 --cost-currency USD --tool-server models
# Failed settlements in the returned page
chio --receipt-db ./receipts.sqlite receipt list --tenant team-a \
--tool-server models \
| jq 'select(.metadata.financial.settlement_status == "failed")'
# Monetary refusals in the returned page
chio --receipt-db ./receipts.sqlite receipt list --tenant team-a --outcome deny \
| jq 'select(.metadata.financial.attempted_cost != null)'
# One receipt by ID; follow next_cursor for records beyond this page
chio --receipt-db ./receipts.sqlite receipt list --tenant team-a --limit 500 \
| jq 'select(.id == "d1b874355e0cf8b2d5aea4d5b6bc0c71e231420654798bddf84d20cbd44409d3")'budget_total minus budget_remaining is cumulative committed usage; a single invocation's reservation is in its budget-authority events. One response is one page of the log: for a shared store, follow the cursor loop in Query & Audit Receipts before you aggregate, and export to a SIEM for continuous dashboards (SIEM export).
Live budget state
The chio mcp serve-http admin router serves a read-only GET /admin/budgets?capability_id=<id>&limit=<n>, gated by --admin-token (environment variable CHIO_ADMIN_TOKEN). It lists the current per-grant invocation counts for one capability.
Budget-authority events
The hold-and-reconcile lifecycle is recorded in metadata.budget_authority: the authorization event, the invocation capture, and a terminal disposition. This capture comes from a simulated prepaid call; the events describe budget accounting, and the rail records settlement separately.
$ jq '.metadata.budget_authority | {authority_profile, guarantee_level, metering_profile, authorize, terminal}' ./bundle.json{
"authority_profile": "authoritative_hold_event",
"guarantee_level": "single_node_atomic",
"metering_profile": "max_cost_preauthorize_then_reconcile_actual",
"authorize": {
"budget_commit_index": 1,
"committed_cost_units_after": 100,
"event_id": "budget-hold:governed-sim-req-1:cap-01a07037-33c8-7d20-a83c-a4a5a131a3d4:0:authorize",
"exposure_units": 100
},
"terminal": {
"budget_commit_index": 3,
"committed_cost_units_after": 100,
"disposition": "reconciled",
"event_id": "budget-hold:governed-sim-req-1:cap-01a07037-33c8-7d20-a83c-a4a5a131a3d4:0:reconcile",
"exposure_units": 100,
"realized_spend_units": 100
}
}Failed settlement records
On the non-durable reconciliation path, a report above the authorization produces a failed financial record. settlement_status is failed; cost_charged holds the reported cost, because recorded_cost is set to actual_cost; and the budget is debited no further than the reservation. In this illustration a 100-unit reservation meets a 220-unit report.
{
"grant_index": 0,
"cost_charged": 220,
"currency": "USD",
"budget_remaining": 900,
"budget_total": 1000,
"settlement_status": "failed",
"cost_breakdown": {
"compute": 180,
"io": 40
}
}A failed status records that 220 units were reported, not that they were paid, and a rail failure produces the same status. Inspect the operation's error and payment state before attributing it to the tool.
Cross-currency configuration
When a grant's currency differs from a tool's pricing currency, the kernel resolves the rate through chio-link, a pinned Chainlink and Pyth oracle runtime. Build a PriceOracleConfig in Rust, usually with PriceOracleConfig::base_arbitrum_default(base_rpc, arbitrum_rpc), and hand it to the kernel with set_price_oracle. The struct is deny_unknown_fields with no defaulted fields: a chain inventory (operator.chains), an operator policy block, a typed egress_contract that gates every outbound oracle request fail-closed, and one Chainlink feed address per pair with an optional Pyth fallback and a per-pair policy.
Replace the base-mainnet.example endpoint and its matching egress authority with your RPC before validation. The feed addresses are source defaults.
{
"primary": "chainlink",
"fallback": "pyth",
"refresh_interval_seconds": 60,
"pyth": {
"hermes_url": "https://hermes.pyth.network"
},
"operator": {
"global_pause": false,
"chains": [
{
"chain_id": 8453,
"label": "base-mainnet",
"caip2": "eip155:8453",
"rpc_endpoint": "https://base-mainnet.example",
"enabled": true,
"sequencer_uptime_feed": "0xBCF85224fc0756B9Fa45aA7892530B47e10b6433",
"sequencer_grace_period_seconds": 300
}
],
"pair_overrides": [],
"monitoring": {
"alert_on_fallback": true,
"alert_on_degraded": true,
"alert_on_pause": true,
"alert_on_sequencer": true
}
},
"egress_contract": {
"tenant_egress_namespace": "chio-link",
"allowed_schemes": ["https"],
"allowed_authority_set": ["hermes.pyth.network", "base-mainnet.example"],
"deny_loopback": true,
"deny_link_local": true,
"deny_ipv6_ula": true,
"max_redirect_chain": 1,
"max_response_bytes": 4194304
},
"pairs": [
{
"base": "USDC",
"quote": "USD",
"chain_id": 8453,
"chainlink": {
"address": "0x7e860098F58bBFC8648a4311b374B1D669a2bc6B",
"decimals": 8,
"heartbeat_seconds": 68400
},
"pyth": {
"id": "0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a"
},
"policy": {
"max_age_seconds": 600,
"divergence_threshold_bps": 500,
"exchange_rate_margin_bps": 200,
"twap_enabled": false,
"twap_window_seconds": 600,
"twap_max_observations": 10,
"stable_pair": true,
"degraded_mode": {
"enabled": false,
"max_stale_age_seconds": 300,
"extra_margin_bps": 800
}
}
},
{
"base": "ETH",
"quote": "USD",
"chain_id": 8453,
"chainlink": {
"address": "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70",
"decimals": 8,
"heartbeat_seconds": 300
},
"pyth": {
"id": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"
},
"policy": {
"max_age_seconds": 600,
"divergence_threshold_bps": 500,
"exchange_rate_margin_bps": 200,
"twap_enabled": true,
"twap_window_seconds": 600,
"twap_max_observations": 10,
"stable_pair": false,
"degraded_mode": {
"enabled": false,
"max_stale_age_seconds": 300,
"extra_margin_bps": 800
}
}
}
]
}exchange_rate_margin_bps adds a conservative buffer to the converted rate; one basis point is 0.01%. divergence_threshold_bps trips a circuit breaker when Chainlink and Pyth disagree beyond the threshold, and max_age_seconds bounds how stale a cached rate may be before resolution fails closed. twap_enabled, twap_window_seconds, and twap_max_observations govern time-weighted averaging; stable_pair marks a pegged pair; degraded_mode bounds how far resolution relaxes its staleness and margin ceilings when a feed goes stale.
A successful conversion adds OracleConversionEvidence; a refusal before conversion carries none. source names the backend that produced the rate, authority names the receipt-side FX authority model, and the signed rate is the exact ratio rate_numerator / rate_denominator. Margin is a resolution input, never a field on the evidence. A pinned oracle signer adds oracle_public_key and signature.
{
"metadata": {
"financial": {
"grant_index": 0,
"cost_charged": 150,
"currency": "USD",
"budget_remaining": 850,
"budget_total": 1000,
"settlement_status": "pending",
"oracle_evidence": {
"schema": "chio.oracle-conversion-evidence.v1",
"base": "USDC",
"quote": "USD",
"authority": "chio_link_runtime_v1",
"rate_numerator": 100,
"rate_denominator": 100,
"source": "chainlink",
"feed_address": "0x7e860098F58bBFC8648a4311b374B1D669a2bc6B",
"updated_at": 1710000090,
"max_age_seconds": 600,
"cache_age_seconds": 12,
"original_cost_units": 1500000,
"original_currency": "USDC",
"converted_cost_units": 150,
"grant_currency": "USD"
}
}
}
}Other budget models
The grant ceiling above is enforced in try_charge_cost() against the durable hold ledger. chio-metering ships two helpers that evaluate budgets independently of that ceiling. Neither signs a kernel receipt or installs itself in the admission path.
Flat enforcer
budget::BudgetEnforcer applies a flat BudgetPolicy scoped to total, per-session, per-agent, and per-tool spend. A charge that breaches any scope returns a BudgetViolation naming the dimension. Its counters are process-local, reset on restart, and need external synchronization; durable spend belongs on the kernel hold ledger.
Budget hierarchy
budget_hierarchy::BudgetTree caps a draft spend at every ancestor BudgetNode across four dimensions: spend, tokens, requests, and warehouse bytes. A denying BudgetDecision reports the offending scope closest to the root through BudgetDenyReason. It suits organization, team, and project hierarchies where no child may exceed an ancestor. BudgetTree evaluates a supplied SpendSnapshot; the caller owns that snapshot's freshness, persistence, and atomic update with the work it authorizes.