Chio/Docs

BuildEconomics

Budgets & Metering

Set capability-token spending limits, price tools, and inspect receipts for charges, overruns, and denials.

Read Economics first

This guide assumes familiarity with Economics, which covers the conceptual model: MonetaryAmount, the three-tier budget model, and the authorize/capture/reconcile cycle.
rendering…
Budget accounting: atomic authorize, capture, then reconcile to actual cost.

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.

free-tier-token.yaml
grants:
  - server_id: srv-search
    tool_name: web_search
    operations: [invoke]
    max_invocations: 100
    # No monetary limits, tool is free

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

pay-per-use-token.yaml
grants:
  - server_id: srv-ai-inference
    tool_name: generate_text
    operations: [invoke]
    max_total_cost:
      units: 5000    # $50.00
      currency: USD

Three Budget Limits

Combine all three limits when you need a cap per call, an aggregate cap, and a call-count limit.

production-token.yaml
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: 500

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.

multi-grant-token.yaml
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: USD

Configuring 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".

rust
NativeTool::new("greet", "Return a greeting", schema)
    .per_invocation_price(25, "USD")
    // Each call costs $0.25

Flat Pricing

A single fixed price with no billing unit. Use this for tools with a constant cost regardless of input size.

rust
NativeTool::new("lookup", "Look up a record", schema)
    .flat_price(500, "USD")
    // Each call costs $5.00

Per-Unit Pricing

Scale cost by a custom billing unit. The tool reports the number of units consumed in the ToolInvocationCost.

rust
NativeTool::new("tokenize", "Tokenize text", schema)
    .per_unit_price(2, "USD", "1k_tokens")
    // $0.02 per 1,000 tokens

Hybrid Pricing

Combine a base price with a per-unit price. The base price is charged on every invocation; the unit price scales with usage.

rust
NativeTool::new("search", "Search documents", schema)
    .hybrid_price(25, 10, "USD", "document")
    // $0.25 base + $0.10 per document returned

Pricing is in the manifest

Tool pricing is embedded in the signed tool manifest. The kernel verifies the manifest signature before trusting the declared prices. A tool server cannot claim a lower price than what it signed.

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:

FieldWhat it tells you
cost_chargedHow much this invocation actually cost (after reconciliation)
budget_remainingHow much budget is left after this invocation
budget_totalThe original total budget for this grant
cost_breakdownItemized cost categories (compute, I/O, etc.)
settlement_statusWhether 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.

bash
# 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:

receipt-budget-status.json
{
  "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:

  1. The settlement_status is set to Failed
  2. The receipt records both cost_charged (what was authorized) and the actual cost reported by the tool
  3. The budget state is not adjusted beyond the authorization: the kernel does not retroactively debit more than what was reserved
receipt-overrun.json
{
  "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

A properly implemented tool should never report a cost exceeding 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:

bash
# 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 tool

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

json
{
  "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:

cross-currency-receipt.json
{
  "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.

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

For continuous monitoring, export receipts to your SIEM system (Splunk, Elasticsearch) and build dashboards that track budget consumption, cost-per-call distributions, and settlement failure rates. See the Receipts page for SIEM configuration.

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.

yaml
grants:
  - server_id: srv-search
    tool_name: web_search
    operations: [invoke]
    max_invocations: 100
    # No max_cost_per_invocation
    # No max_total_cost

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

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

Delegated 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.yaml
# 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.yaml
# 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.).

receipt-budget-exhausted.json
{
  "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

Once a grant's budget is exhausted, no further calls can be made against that grant. The agent must either use a different grant, receive a new capability token with fresh budget, or the operator must issue a replacement token. There is no way to top up an existing grant's budget: this is by design, as modifying an issued token would break the signature chain.

To detect budget exhaustion programmatically, check for denial receipts where the guard is "budget" and attempted_cost is present:

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