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.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.
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: 500Multiple 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
$ chio --receipt-db ./receipts.sqlite receipt list --tenant acme \
--min-cost 100 --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 == "rcpt-econ-001")'A receipt for a successful invocation shows cost_charged with the reconciled amount and budget_remaining reflecting the post-invocation state:
{
"metadata": {
"financial": {
"grant_index": 0,
"cost_charged": 75,
"currency": "USD",
"budget_remaining": 4925,
"budget_total": 5000,
"delegation_depth": 0,
"root_budget_holder": "agent-main-001",
"settlement_status": "pending",
"cost_breakdown": {
"compute": 60,
"io": 15
}
}
}
}Handling 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. Point every kernel in an HA pool at one shared store with the global --budget-db <path> flag (an optional SQLite database path for durable shared capability budget state) so the atomic authorization in try_charge_cost() commits against one durable counter instead of one per process.
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 - The receipt records both
cost_charged(what was authorized) and the actual cost reported by the tool - The budget state is not adjusted beyond the authorization: the kernel does not retroactively debit more than what was reserved
{
"metadata": {
"financial": {
"grant_index": 0,
"cost_charged": 100,
"currency": "USD",
"budget_remaining": 900,
"budget_total": 1000,
"settlement_status": "failed",
"cost_breakdown": {
"compute": 180,
"io": 40
}
}
}
}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")'
# Compare the cost_charged vs actual cost_breakdown totals
# cost_charged = authorized amount (max_cost_per_invocation)
# sum(cost_breakdown) = actual cost reported by the toolCross-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 today it 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": 150,
"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.
Monitoring Budget 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, ReduceTotalCost, and ReduceMaxInvocations.
# 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 + ReduceMaxInvocations
# (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.
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.).
{
"id": "rcpt-deny-budget-001",
"timestamp": 1710001000,
"capability_id": "cap-budget-001",
"tool_server": "srv-ai-inference",
"tool_name": "generate_text",
"action": {
"parameters": {
"prompt": "Write a summary",
"max_tokens": 1000
},
"parameter_hash": "sha256:a1b2c3..."
},
"decision": {
"verdict": "deny",
"reason": "budget exhausted: max_total_cost exceeded (950/1000 USD charged, 100 USD required)",
"guard": "budget"
},
"content_hash": "sha256:0000000000000000...",
"policy_hash": "abc123def456",
"evidence": [
{"guard_name": "budget", "verdict": false, "details": "max_total_cost would be exceeded: 950 + 100 > 1000 USD"}
],
"metadata": {
"financial": {
"grant_index": 0,
"cost_charged": 0,
"currency": "USD",
"budget_remaining": 50,
"budget_total": 1000,
"delegation_depth": 0,
"root_budget_holder": "agent-main-001",
"settlement_status": "not_applicable",
"attempted_cost": 100
}
},
"kernel_key": "ed25519:pub:9c7b3f...",
"signature": "ed25519:d4e5f6a7..."
}Budget exhaustion is final
To detect budget exhaustion programmatically, check for denial receipts where the guard is "budget" and attempted_cost is present:
# Find all budget exhaustion denials
$ chio --receipt-db ./receipts.sqlite receipt list --tenant acme --outcome deny \
| jq 'select(.decision.guard == "budget" and .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 — and 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