Chio/Docs
LOGIN · JOIN

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.

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-url with 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-sim drives 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.

free-tier-token.yamlyaml
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.yamlyaml
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. 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.

production-token.yamlchio-sdk-python
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

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:

scope.model_dump_json(exclude_none=True, indent=2)json
{
  "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.

multi-grant-token.yamlyaml
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.
# --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:

budget-receipts · allowtranscript
$ 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"
}
exit 0
The financial metadata on an allowed prepaid call. root_budget_holder is the issuing capability key rendered as bare lowercase hex, and payment_reference is the adapter authorization id that joins the receipt back to the rail.
sourcecrates/core/chio-core-types/src/receipt/economics.rs:77-106at fe56570

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

budget-receipts · budget-authoritytranscript
$ 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
  }
}
exit 0
The hold, its exposure, and its terminal disposition. The metering profile preauthorizes the maximum cost and reconciles the actual one, so a call that costs less than its cap releases the difference.
sourcecrates/kernel/chio-kernel/src/kernel/validation.rs:2169-2187at fe56570

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

  1. The settlement_status is set to Failed
  2. cost_charged records the actual cost the tool reported, not the authorized amount: on an overrun recorded_cost is set to actual_cost and written straight to the field (crates/kernel/chio-kernel/src/kernel/validation.rs:2253-2257, :2276-2278)
  3. The budget state is not adjusted beyond the authorization: the kernel does not retroactively debit more than what was reserved
metadata.financial on an overrunjson
{
  "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

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")'

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

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.jsonjson
{
  "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.

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 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.yamlyaml
# 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.yamlyaml
# 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.).

budget-receipts · deny-receipttranscript
$ 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"
  }
}
exit 0deny
A refused monetary call and the financial block behind it. attempted_cost records what was asked for, cost_charged is zero, and the budget is untouched at its total.
sourcecrates/kernel/chio-kernel/src/kernel/responses/deny_responses.rs:31-90at fe56570

Four things about that block are worth reading closely, because each of them is easy to guess wrong.

  • decision.guard is "kernel". There is no budget guard: 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_remaining equals budget_total. On the exhaustion path the builder sets budget_remaining: budget_total (deny_responses.rs:41-46), because nothing was committed. A refused call moves no money.
  • There is no evidence array. The monetary deny builder passes no guard evidence, and the field carries skip_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_holder is a bare 64-character lowercase hex public key. It is cap.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 for kernel_key, signature, parameter_hash, content_hash and policy_hash: every one is bare hex, never ed25519: or sha256:.

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

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, select on attempted_cost being present. Filtering on a "budget" guard matches nothing, forever:

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