BuildEconomics
Budgets & Metering
Set capability-token spending limits, price tools, and inspect receipts for charges, overruns, and denials.
Read Economics first
MonetaryAmount, the three-tier budget model, and the authorize/capture/reconcile cycle.Prerequisites
- A capability token you can issue or attenuate. Budgets live on grants inside the token, not on a policy file.
- A receipt store to read spend back out of:
--receipt-db <path>for a local read, or--control-urlwith a token for a shared trust-control read. - A tool server that reports a cost. Pricing is declared in the tool manifest; a tool that reports nothing charges nothing. To see the fields without one,
chio mcp governed-simdrives one priced call through an ephemeral kernel, which is what every capture on this page came from. - For cross-currency grants, a configured price oracle. Without one the kernel denies the conversion rather than guessing a rate.
Setting Budget Limits
Budget limits are set on individual grants within a capability token. Each grant targets a specific tool on a specific server and can carry any combination of the three budget fields: max_cost_per_invocation, max_total_cost, and max_invocations.
Invocation Limit Only
The simplest budget: limit how many times a tool can be called with no monetary cap. This is useful for free-tier tools where you want to prevent runaway loops.
grants:
- server_id: srv-search
tool_name: web_search
operations: [invoke]
max_invocations: 100
# No monetary limits, tool is freeMonetary Cap Only
Set an aggregate spending limit with no per-call cap. The agent can make expensive calls as long as the total stays under budget.
grants:
- server_id: srv-ai-inference
tool_name: generate_text
operations: [invoke]
max_total_cost:
units: 5000 # $50.00
currency: USDThree Budget Limits
Combine all three limits when you need a cap per call, an aggregate cap, and a call-count limit. The YAML tab is the token document; the Python tab builds the identical grant from the typed models in chio_sdk (pip install chio-sdk-python), which is what you want when the budget is computed rather than written down.
grants:
- server_id: srv-ai-inference
tool_name: generate_text
operations: [invoke]
max_cost_per_invocation:
units: 100 # $1.00 per call
currency: USD
max_total_cost:
units: 5000 # $50.00 aggregate
currency: USD
max_invocations: 500from chio_sdk import ChioScope, MonetaryAmount, Operation, ToolGrant
scope = ChioScope(
grants=[
ToolGrant(
server_id="srv-ai-inference",
tool_name="generate_text",
operations=[Operation.invoke],
max_cost_per_invocation=MonetaryAmount(units=100, currency="USD"),
max_total_cost=MonetaryAmount(units=5000, currency="USD"),
max_invocations=500,
)
]
)The two produce the same document. Serializing the Python object gives back exactly the fields the YAML declares, in the order the kernel reads them:
{
"grants": [
{
"server_id": "srv-ai-inference",
"tool_name": "generate_text",
"operations": [
"invoke"
],
"max_invocations": 500,
"max_cost_per_invocation": {
"units": 100,
"currency": "USD"
},
"max_total_cost": {
"units": 5000,
"currency": "USD"
}
}
]
}MonetaryAmount carries exactly two fields, units and currency. Units are minor units of the currency, so 100 USD units is one dollar and the type never holds a float.
Multiple Grants with Different Budgets
A single capability token can contain multiple grants, each with its own independent budget. This lets you give an agent access to several tools with different spending profiles.
grants:
- server_id: srv-ai-inference
tool_name: generate_text
operations: [invoke]
max_cost_per_invocation:
units: 100
currency: USD
max_total_cost:
units: 5000
currency: USD
max_invocations: 500
- server_id: srv-search
tool_name: web_search
operations: [invoke]
max_invocations: 200
# Free tool, no monetary limits
- server_id: srv-storage
tool_name: store_document
operations: [invoke]
max_cost_per_invocation:
units: 10 # $0.10 per store
currency: USD
max_total_cost:
units: 500 # $5.00 aggregate
currency: USDConfiguring Tool Pricing
Tool pricing is declared on the tool server side using NativeTool pricing helpers (per_invocation_price, flat_price, per_unit_price, hybrid_price). The kernel reads this pricing metadata from the tool manifest during economic checks.
Per-Invocation Pricing
Charge a fixed amount for each call. The billing unit is automatically set to "invocation".
NativeTool::new("greet", "Return a greeting", schema)
.per_invocation_price(25, "USD")
// Each call costs $0.25Flat Pricing
A single fixed price with no billing unit. Use this for tools with a constant cost regardless of input size.
NativeTool::new("lookup", "Look up a record", schema)
.flat_price(500, "USD")
// Each call costs $5.00Per-Unit Pricing
Scale cost by a custom billing unit. The tool reports the number of units consumed in the ToolInvocationCost.
NativeTool::new("tokenize", "Tokenize text", schema)
.per_unit_price(2, "USD", "1k_tokens")
// $0.02 per 1,000 tokensHybrid Pricing
Combine a base price with a per-unit price. The base price is charged on every invocation; the unit price scales with usage.
NativeTool::new("search", "Search documents", schema)
.hybrid_price(25, 10, "USD", "document")
// $0.25 base + $0.10 per document returnedPricing is in the manifest
Reading Budget Status from Receipts
Every receipt for a tool call that exercises a monetary grant includes FinancialReceiptMetadata in the metadata.financial field. Receipt metadata includes grant_index, cost_charged, currency, budget_remaining, budget_total, delegation_depth, root_budget_holder, payment_reference (an optional reference for external settlement systems), settlement_status, cost_breakdown, oracle_evidence, and attempted_cost. The key fields for monitoring budget consumption are:
| Field | What it tells you |
|---|---|
cost_charged | How much this invocation actually cost (after reconciliation) |
budget_remaining | How much budget is left after this invocation |
budget_total | The original total budget for this grant |
cost_breakdown | Itemized cost categories (compute, I/O, etc.) |
settlement_status | Whether the charge is pending, settled, or failed |
Use the CLI to 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.
# Show receipts with cost information for a specific tool
$ chio --receipt-db ./receipts.sqlite receipt list --tenant acme \
--tool-server srv-ai-inference --tool-name generate_text
# Filter by minimum cost (in minor currency units) to find expensive calls.
# --min-cost and --max-cost both require --cost-currency.
$ chio --receipt-db ./receipts.sqlite receipt list --tenant acme \
--min-cost 100 --cost-currency USD --tool-server srv-ai-inference
# Look up one receipt by id (list emits JSON Lines; filter with jq)
$ chio --receipt-db ./receipts.sqlite receipt list --tenant acme --limit 500 \
| jq 'select(.id == "d1b874355e0cf8b2d5aea4d5b6bc0c71e231420654798bddf84d20cbd44409d3")'A receipt for a successful invocation shows cost_charged with the reconciled amount and budget_remaining reflecting the post-invocation state. chio mcp governed-sim drives one governed prepaid call through an ephemeral kernel and writes the signed bundle, which is the smallest way to see the block without standing up a priced tool server:
$ chio --session-db ./s.sqlite mcp governed-sim \
--governed-mustprepay --payment-adapter sim --out ./bundle.json
$ jq '.metadata.financial' ./bundle.json{
"budget_remaining": 900,
"budget_total": 1000,
"cost_breakdown": {
"payment": {
"adapter_metadata": {
"adapter": "sim",
"commerce": null,
"governed": {
"approvalTokenId": "governed-sim-approval-1",
"intentHash": "f57e0f00b78fe5267f81f92696a0fe9a32339cb02d57b81c9e15f59996f3062d",
"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": "0473a15a01ca05a11b0558cd9fd1edd8bddd26f3f598f1c168aaec97dd7f031b",
"settlement_status": "settled"
}crates/core/chio-core-types/src/receipt/economics.rs:77-106at fe56570The budget itself is not a counter the receipt merely reports. It is a hold-and-reconcile lifecycle recorded in metadata.budget_authority, with the authorization event, the invocation capture, and a terminal disposition:
$ 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
}
}crates/kernel/chio-kernel/src/kernel/validation.rs:2169-2187at fe56570Handling Cost Overruns
A cost overrun occurs when a tool reports an actual cost greater than the authorized amount (max_cost_per_invocation). This should not happen with correctly implemented tools, but the kernel handles it defensively. In an HA deployment the worst-case overrun bound is max_cost_per_invocation.units * active_node_count, reflecting the maximum cost that can escape the atomic authorization window if nodes race.
The active_node_count term only bites when nodes keep independent budget state. Every kernel in an HA pool has to authorize against one durable counter, and the way to get that is the durable admission authority rather than a separate budget store: it owns revocation and budget state so every admission participant shares one fenced transaction coordinator. Passing --budget-db alongside it is refused with the durable admission authority owns revocation and budget state (crates/platform/chio-control-plane/src/durable_admission.rs:138-159), and durable admission cannot be turned off to work around that outside unsafe development with ephemeral receipts (crates/kernel/chio-kernel/src/admission_operation/identity.rs:393-404).
To read live budget state without walking the receipt log, the chio mcp serve-http admin router serves a read-only GET /admin/budgets?capability_id=<id>&limit=<n>, gated by --admin-token (env CHIO_ADMIN_TOKEN). It lists the current per-grant invocation counts for a capability.
When an overrun is detected during reconciliation:
- The
settlement_statusis set toFailed cost_chargedrecords the actual cost the tool reported, not the authorized amount: on an overrunrecorded_costis set toactual_costand written straight to the field (crates/kernel/chio-kernel/src/kernel/validation.rs:2253-2257,:2276-2278)- The budget state is not adjusted beyond the authorization: the kernel does not retroactively debit more than what was reserved
{
"grant_index": 0,
"cost_charged": 220,
"currency": "USD",
"budget_remaining": 900,
"budget_total": 1000,
"settlement_status": "failed",
"cost_breakdown": {
"compute": 180,
"io": 40
}
}Read the gap. The tool asked for 220 minor units, so cost_charged is 220, and the budget moved by only the authorized 100, so budget_remaining is 900. That gap between what was charged and what the budget moved by is the overrun signal, and it is why realized_budget_units keeps the authorized value rather than the reported one (validation.rs:2181-2187).
Overruns indicate a tool bug
max_cost_per_invocation. If you see settlement_status: "failed" in receipts, investigate the tool server. The tool's declared pricing may be incorrect, or the tool may not be respecting its own cost bounds.To debug overruns, query for failed settlements:
# Find all receipts with failed settlement
$ chio --receipt-db ./receipts.sqlite receipt list --tenant acme --tool-server srv-ai-inference \
| jq 'select(.metadata.financial.settlement_status == "failed")'
# cost_charged and sum(cost_breakdown) both carry the actual reported cost.
# The authorized amount is what budget_total - budget_remaining moved by,
# so an overrun shows as cost_charged exceeding that difference.Cross-Currency Setup
When a grant's budget currency differs from a tool's pricing currency, the kernel resolves the exchange rate through chio-link, a pinned Chainlink-plus-Pyth oracle runtime. The conversion evidence is embedded in every cross-currency receipt for auditability.
chio-link is not a generic feed-URI list and is not loaded from a standalone config file. It is a PriceOracleConfig struct built in Rust, typically through the PriceOracleConfig::base_arbitrum_default(base_rpc, arbitrum_rpc) builder, then handed to the kernel with set_price_oracle. The struct is deny_unknown_fields with no defaulted fields; serialized, it pins a chain inventory (operator.chains), an operator policy block, a typed egress_contract that gates every outbound oracle dispatch fail-closed, and one explicit Chainlink feed address per pair with an optional Pyth fallback and a per-pair policy block:
{
"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
}
}
}
]
}The policy block's exchange_rate_margin_bps adds a conservative buffer to the converted rate; one basis point equals 0.01%. divergence_threshold_bps trips a circuit breaker when the Chainlink and Pyth prices disagree beyond the threshold, and max_age_seconds bounds how stale a cached rate may be before resolution fails closed. The remaining fields are required too: twap_enabled (with twap_window_seconds and twap_max_observations) governs time-weighted averaging, stable_pair marks a pegged pair, and degraded_mode bounds how far resolution may relax its staleness and margin ceilings when a feed goes stale.
When a cross-currency invocation occurs, the receipt includes OracleConversionEvidence:
{
"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"
}
}
}
}source labels the backend that produced the rate (chainlink or pyth) and authority names the receipt-side FX authority model. The signed rate is the exact integer ratio rate_numerator / rate_denominator; margin is a chio-link policy input applied during resolution, not a field on the signed evidence. When the runtime pins an oracle signer, the optional oracle_public_key and signature fields carry its attestation.
Verify the result: monitoring consumption
Budget consumption can be tracked over time by querying the receipt log. Each receipt records the budget_remaining at the time of invocation, giving you a running view of spend.
# chio receipt list emits one JSON receipt per line (JSON Lines).
# Use jq's slurp mode (-s) where aggregation across lines is needed.
# Total cost charged for a specific capability token
$ chio --receipt-db ./receipts.sqlite receipt list --tenant acme --capability cap-budget-001 \
| jq -s '[.[] | .metadata.financial.cost_charged] | add'
# Budget consumption over time (timestamp + remaining)
$ chio --receipt-db ./receipts.sqlite receipt list --tenant acme --capability cap-budget-001 \
| jq '{ts: .timestamp, remaining: .metadata.financial.budget_remaining}'
# Group costs by tool name
$ chio --receipt-db ./receipts.sqlite receipt list --tenant acme --tool-server srv-ai-inference \
| jq -s 'group_by(.tool_name) | map({tool: .[0].tool_name, total: [.[].metadata.financial.cost_charged] | add})'Export receipts to a SIEM
Common Patterns
Free Tier with Invocation Limits
For tools that have no per-call cost but should not be called unboundedly. The invocation limit prevents runaway loops without requiring any monetary accounting.
grants:
- server_id: srv-search
tool_name: web_search
operations: [invoke]
max_invocations: 100
# No max_cost_per_invocation
# No max_total_costPay-Per-Use with Monetary Caps
For metered tools where cost varies per call. The per-invocation cap prevents a single expensive call from consuming the entire budget, while the total cap limits aggregate spending.
grants:
- server_id: srv-ai-inference
tool_name: generate_text
operations: [invoke]
max_cost_per_invocation:
units: 200 # $2.00 max per call
currency: USD
max_total_cost:
units: 10000 # $100.00 total budget
currency: USDDelegated Budgets
A parent agent with a $100 budget delegates $10 to a child agent. The child's spending counts against the parent's total, but the child can never exceed its own $10 limit. Cost attenuations for delegation use ReduceCostPerInvocation and ReduceTotalCost. The invocation count is narrowed by ReduceBudget, which carries a max_invocations field; the seven variants are RemoveTool, RemoveOperation, AddConstraint, ReduceBudget, ShortenExpiry, ReduceCostPerInvocation and ReduceTotalCost(crates/core/chio-core-types/src/capability/attenuation.rs:172-210). They are internally tagged on type and renamed to snake_case on the wire.
# Parent token: $100 total budget
grants:
- server_id: srv-ai-inference
tool_name: generate_text
operations: [invoke]
max_total_cost:
units: 10000 # $100.00
currency: USD
max_invocations: 1000# Child token (attenuated from parent): $10 budget
# Created via ReduceTotalCost + ReduceBudget
# (optionally ReduceCostPerInvocation)
grants:
- server_id: srv-ai-inference
tool_name: generate_text
operations: [invoke]
max_total_cost:
units: 1000 # $10.00 (reduced from parent's $100)
currency: USD
max_invocations: 100 # (reduced from parent's 1000)The delegation chain preserves the root_budget_holder field in receipts, so you can always trace spending back to the original budget owner. The delegation_depth field indicates how deep in the chain the invocation occurred.
Failures and recovery: budget exhaustion
When a budget is exhausted, whether by invocation count or monetary cap, the kernel denies further calls and produces a receipt with financial metadata that records the denied attempt.
The denial receipt includes attempted_cost: the cost that would have been charged if the budget were sufficient. This distinguishes budget exhaustion from other denial reasons (guard failures, expired tokens, etc.).
$ jq '{decision, financial: .metadata.financial}' ./bundle-deny.json{
"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": "95dd6964b2c519849a0f53ded5c810fc87791bb622ace9b887537de4c51a2c59",
"settlement_status": "not_applicable"
}
}crates/kernel/chio-kernel/src/kernel/responses/deny_responses.rs:31-90at fe56570Four things about that block are worth reading closely, because each of them is easy to guess wrong.
decision.guardis"kernel". There is nobudgetguard: monetary denials are refused by the kernel before any guard runs, and both monetary deny builders hardcode the string (crates/kernel/chio-kernel/src/kernel/responses/deny_responses.rs:84-86,:195-198).budget_remainingequalsbudget_total. On the exhaustion path the builder setsbudget_remaining: budget_total(deny_responses.rs:41-46), because nothing was committed. A refused call moves no money.- There is no
evidencearray. The monetary deny builder passes no guard evidence, and the field carriesskip_serializing_if = "Vec::is_empty"(crates/core/chio-core-types/src/receipt/body.rs:76-77), so the key is absent rather than empty. root_budget_holderis a bare 64-character lowercase hex public key. It iscap.issuer.to_hex()(deny_responses.rs:37), and Ed25519 renders with no prefix at all (crates/core/chio-core-types/src/crypto.rs:575-583). The same holds forkernel_key,signature,parameter_hash,content_hashandpolicy_hash: every one is bare hex, nevered25519:orsha256:.
On a genuine invocation-budget exhaustion the reason string comes from KernelError::BudgetExhausted and reads invocation budget exhausted for capability <capability-id> (crates/kernel/chio-kernel/src/kernel/error.rs:152-154), raised for both the monetary and the invocation-count cases at validation.rs:1784-1785.
Budget exhaustion is final
To detect budget exhaustion programmatically, select on attempted_cost being present. Filtering on a "budget" guard matches nothing, forever:
# Find all budget exhaustion denials
$ chio --receipt-db ./receipts.sqlite receipt list --tenant acme --outcome deny \
| jq 'select(.metadata.financial.attempted_cost != null)'Other Budget Models
Everything above is the per-grant ceiling: the three-tier budget carried on a ToolGrant and enforced in try_charge_cost(). chio-metering ships two further budget models that enforce independently of that per-grant ceiling.
Flat Enforcer
budget::BudgetEnforcer applies a flat BudgetPolicy scoped to total, per-session, per-agent, and per-tool spend. A charge that would breach any scope returns a BudgetViolation naming the breached dimension. This is the model to reach for when the constraint is "this agent may spend at most X per session," independent of which grant funded any single call.
Budget Hierarchy
budget_hierarchy::BudgetTree is tree-shaped. Every ancestor BudgetNode caps a draft spend across four dimensions: spend, tokens, requests, and warehouse bytes. A BudgetDecision that denies reports the offending scope closest to the root via BudgetDenyReason. Use it for parent-capped org/team/project hierarchies where a child scope must never exceed any ancestor's limit.
Both are crate-root exports of chio-metering and hold no state beyond a single call: no persistence, no receipt signing. They compose with the per-grant ceiling this guide otherwise covers.
Next Steps
- Economics · the conceptual model behind budgets, including cross-currency enforcement and attenuation
- Receipts · receipt structure, including financial metadata fields
- Native Tool Server · how to declare tool pricing in native server manifests