Chio/Docs
LOGIN · JOIN

Budgets & Metering

Give an agent a spending allowance, run a priced service against it, and read the receipts that record each charge.

Guide chio@5f63e6e

On this page

Spending authority is part of the kernel's admission decision. Each priced call reserves its worst case against the agent's grant before the service runs, the service reports what it cost, and a signed receipt records the charge.

The example is Larkfield's discount tool as a priced service: each call prices a SKU after a discount and charges USD 0.20, 20 USD minor units. The grant allows 20 per call and 50 in total, so two calls succeed and the kernel refuses the third before the handler starts.

Start

1. Run a service under a 50-unit allowance

Install the CLI, then fetch the project with chio new pricing-metered --example pricing-metered, or download it. Cargo fetches the pinned dependencies on the first run.

What the project supplies
  • A signed capability with a priced grant, and a policy-context hash derived from the run's host policy descriptor.
  • A durable admission authority that coordinates operation, budget, and revocation state, plus a SQLite receipt store installed before issuance.
  • A service manifest the host signs and verifies before registration, and a connection that reports its fixed realized cost.
  • A persistent local-credit payment adapter whose capture equals the authorized amount, so the price and the per-call ceiling match.

The host uses Unix file locking. Its built-in ledger records local credits denominated in USD and moves no external money. The README describes the fresh-run scope, and src/host.rs is the complete host to adapt.

Run your metered servicebash
cd pricing-metered
cargo run --release --locked -- sku-204 15
Output
call-1: ALLOW
  result: {"currency":"USD","percent":15,"price":"49.30","sku":"sku-204"}
  charge: 20 USD; settlement: settled
  budget: 30 available; 20 spent (USD)
  reserved: 0
  handler invocations: 1
call-2: ALLOW
  result: {"currency":"USD","percent":15,"price":"49.30","sku":"sku-204"}
  charge: 20 USD; settlement: settled
  budget: 10 available; 40 spent (USD)
  reserved: 0
  handler invocations: 2
call-3: DENY
  reason: invocation budget exhausted for capability cap-01a12f1d-3f0b-73d5-a642-1d508e92b96a
  charge: 0 USD; settlement: not_applicable
  budget: 10 available; 40 spent (USD)
  reserved: 0
  handler invocations: 2
evidence: runs/80e3c437-e8ab-4345-89b4-4831cf179aec

You should see two calls admitted at 20 units each, each pricing sku-204 at 15 percent off, and a third refused with 10 units left. The arguments are the SKU and the discount in percent. The handler's own log, handler-invocations.log, holds two lines after the run: the refusal stopped a third invocation before it started.

The printed evidence directory holds the complete receipts, the signed capability and manifest, host-policy.json, the SQLite stores, and the host's public verification key.

2. Trace each charge through the hold ledger

The kernel reserves the grant's per-call ceiling against the remaining total before it dispatches discount, then reconciles that hold to the reported cost.

The metered service’s budgetChoose a move.

The per-call ceiling is held before the service runs

The kernel reserves the grant’s max_cost_per_invocation of 20 against max_total_cost of 50 before it dispatches discount. Authoritative spend.

The captured metered run: three discount calls under a grant of 20 per call and 50 in total, in USD minor units. Charges, balances, and the refusal are the run’s own; the local credit ledger moves no external money.

The hold is recorded before the service runs. The service reports its realized cost through invoke_with_cost, the hold reconciles to the 20-unit charge, and the receipt records settlement: settled. A variable price reconciles below the ceiling and releases the unused part of the hold.

Call three needs 20 and 10 remain, so the kernel refuses before dispatch. Its receipt records a charge of 0 with settlement: not_applicable. The local credit adapter here is one payment module; a rail settles money outside the kernel, and the kernel records the outcome it observes.

Read more in Economics.

3. Read the limits in the signed grant

Open capability.json in the evidence directory. The limits belong to the tool grant inside it.

capability.json · scope.grants[0]json
{
  "max_cost_per_invocation": {
    "currency": "USD",
    "units": 20
  },
  "max_total_cost": {
    "currency": "USD",
    "units": 50
  },
  "operations": [
    "invoke"
  ],
  "server_id": "pricing",
  "tool_name": "discount"
}

You should see max_cost_per_invocation at 20 and max_total_cost at 50, both in USD minor units: 100 units is one dollar. The signed capability also binds its subject, issuer, lifetime, and policy context, so keep the whole file when you verify it.

4. Lower the allowance and run again

In src/host.rs, reduce the total to 30. Keep the per-call ceiling equal to the service's fixed 20-unit charge.

src/host.rsrust
pub const CALL_CEILING: u64 = 20;
pub const TOTAL_BUDGET: u64 = 30;
Terminalbash
cargo run --release --locked -- sku-118 10
Output
call-1: ALLOW
  result: {"currency":"USD","percent":10,"price":"21.60","sku":"sku-118"}
  charge: 20 USD; settlement: settled
  budget: 10 available; 20 spent (USD)
  reserved: 0
  handler invocations: 1
call-2: DENY
  reason: invocation budget exhausted for capability cap-01a12f1d-69e7-75e9-a84c-d26cd92a1658
  charge: 0 USD; settlement: not_applicable
  budget: 10 available; 20 spent (USD)
  reserved: 0
  handler invocations: 1
call-3: DENY
  reason: invocation budget exhausted for capability cap-01a12f1d-69e7-75e9-a84c-d26cd92a1658
  charge: 0 USD; settlement: not_applicable
  budget: 10 available; 20 spent (USD)
  reserved: 0
  handler invocations: 1
evidence: runs/a7042bba-9c26-4564-874a-81ca3e825a9d

You should see one call admitted and two refused. Each run issues a fresh capability in its own directory, so the new constant takes effect on the next run and never alters a grant already signed.

5. Read the refusal against the ledger

Return to the 50-unit run. After two charges, 40 units are spent and 10 remain: the balance is above zero, yet too small for another 20-unit reservation. The durable ledger holds the live balance.

call-3-ledger.json · application observationjson
{
  "available": 10,
  "currency": "USD",
  "reserved": 0,
  "spent": 40
}

You should see available at 10 and spent at 40 after the refusal. The refusal charged zero; it did not restore earlier spending. Its receipt names kernel as the deciding guard and records the requested exposure in attempted_cost.

Read the current balance from the ledger and treat each receipt's budget_remaining as a snapshot. A signed grant's limits are fixed: to change an allowance, issue or delegate new authority and keep the old evidence. A refused reservation can succeed later once another call's hold is released.

6. Total the charges from the signed receipts

Inside your run directory, project the three-record file into a total per currency.

Terminal · in your run directorybash
# Group the allowed, settled records by currency.
jq -s '[.[] | select(.decision.verdict == "allow"
    and .metadata.financial.settlement_status == "settled")]
  | group_by(.metadata.financial.currency)
  | map({currency: .[0].metadata.financial.currency,
         recorded_units: (map(.metadata.financial.cost_charged) | add)})' receipts.ndjson

# Successful balance snapshots, in the application's recorded order.
jq 'select(.decision.verdict == "allow")
  | {timestamp, remaining: .metadata.financial.budget_remaining}' receipts.ndjson
Output
[
  {
    "currency": "USD",
    "recorded_units": 40
  }
]

You should see 40 USD units across the two settled receipts. Each one carries its charge in metadata.financial; here is the second.

call-2-receipt.json · metadata.financialjson
{
  "budget_remaining": 10,
  "budget_total": 50,
  "cost_breakdown": {
    "payment": {
      "authorization_id": "hold:dd10f15d49a5f974060c67d0de1db477263075316b1de61e5867f10a6fdf06b5",
      "preauthorized_units": 20,
      "rail": "finding-operator-ledger",
      "rail_mode": "reversible_hold",
      "recorded_units": 20,
      "transaction_id": "capture:8ce40bb1d741924b4f5158d3ac99c215516311dcf93d22cd69d7913ffb19f365"
    }
  },
  "cost_charged": 20,
  "currency": "USD",
  "delegation_depth": 0,
  "grant_index": 0,
  "payment_reference": "capture:8ce40bb1d741924b4f5158d3ac99c215516311dcf93d22cd69d7913ffb19f365",
  "root_budget_holder": "8c8ccb6701deb29d5fba8ea87585145f250dcea086c3d849bd4751d43e756889",
  "settlement_status": "settled"
}

The block records a 20-unit local-credit capture, 10 units remaining, and a payment_reference that joins the receipt to the ledger's capture. The jq projections read content only. Export Billing Records checks each receipt's signature against the host key, excludes the refusal, and produces two accounting rows.

A cost reported above the hold fails the call at finalization

The per-call ceiling bounds what the kernel authorizes. A service that reports more has already run, so the kernel refuses to finalize the charge and leaves the operation for you to resolve.

The service reported more than its authorization

Change PRICE_UNITS to 30 and leave the ceiling at 20. The durable host returns this error, with no normal tool response and no billable receipt:

Durable overrun refusaltext
Error: durable admission failed: reported cost exceeds the durable payment authorization

Preserve the operation and payment state before you rerun. A retry runs the tool again. Align the price and the ceiling, or set the ceiling from the worst case you will pay.

Several kernels draw on one budget

Kernels that serve one budget share the durable admission authority and its fencing. Independent counters on each node multiply the available authority, and multiplying the call ceiling by the node count gives no overrun bound. Configure budget and revocation through the admission authority instead of attaching a separate budget database.

Read more in Budgets Across Nodes.

Budget fields, pricing helpers, and financial receipts

Grant budget fields

Budget limits live on individual grants inside a signed capability. Each grant targets one tool on one server and can carry any combination of max_cost_per_invocation, max_total_cost, and max_invocations. The YAML fragments below are scopes for issuance; issuance signs the capability that contains them.

MonetaryAmount carries exactly two fields, units and currency. Units are the currency's minor units, so 100 USD units is one dollar and the type never holds a float.

Three limits on one grant

Combine a cap per call, an aggregate cap, and a call count. The Python tab builds the same grant with the SDK's typed models (pip install chio-sdk).

priced-scope.yamlpriced_scope.py
grants:
  - server_id: models
    tool_name: complete
    operations: [invoke]
    max_cost_per_invocation:
      units: 100     # USD 1.00 per call
      currency: USD
    max_total_cost:
      units: 5000    # USD 50.00 in total
      currency: USD
    max_invocations: 500

Serializing the model with exclude_none=True yields these grant fields; key order carries no meaning.

scope.model_dump_json(exclude_none=True, indent=2)json
{
  "grants": [
    {
      "server_id": "models",
      "tool_name": "complete",
      "operations": [
        "invoke"
      ],
      "max_invocations": 500,
      "max_cost_per_invocation": {
        "units": 100,
        "currency": "USD"
      },
      "max_total_cost": {
        "units": 5000,
        "currency": "USD"
      }
    }
  ]
}

Call count only

A free tool can still be bounded: max_invocations stops a runaway loop with no monetary accounting.

invocation-limited-scope.yamlyaml
grants:
  - server_id: web
    tool_name: search
    operations: [invoke]
    max_invocations: 100
    # No monetary limits: the tool is free

Aggregate cap only

The schema accepts a total by itself. Monetary admission reserves max_cost_per_invocation and defaults it to zero when absent, and the local-ledger adapter refuses a zero-value authorization, so pair a total with an explicit per-call ceiling when you run the example.

aggregate-scope-fragment.yamlyaml
grants:
  - server_id: models
    tool_name: complete
    operations: [invoke]
    max_total_cost:
      units: 5000    # USD 50.00
      currency: USD

Per-call and total caps

For a tool whose cost varies per call, the per-call cap keeps one expensive call from consuming the whole budget and the total cap bounds aggregate spend.

pay-per-use-scope.yamlyaml
grants:
  - server_id: models
    tool_name: complete
    operations: [invoke]
    max_cost_per_invocation:
      units: 200       # USD 2.00 per call
      currency: USD
    max_total_cost:
      units: 10000     # USD 100.00 in total
      currency: USD

Several grants in one capability

One capability can hold several grants, each with its own budget, so an agent can reach several tools with different spending profiles.

multi-grant-scope.yamlyaml
grants:
  - server_id: models
    tool_name: complete
    operations: [invoke]
    max_cost_per_invocation:
      units: 100
      currency: USD
    max_total_cost:
      units: 5000
      currency: USD
    max_invocations: 500

  - server_id: web
    tool_name: search
    operations: [invoke]
    max_invocations: 200
    # Free tool, no monetary limits

  - server_id: pricing
    tool_name: publish_price
    operations: [invoke]
    max_cost_per_invocation:
      units: 10      # USD 0.10 per published price
      currency: USD
    max_total_cost:
      units: 500     # USD 5.00 in total
      currency: USD

Delegated budgets

A parent narrows a child's total and per-call limits with ReduceTotalCost and ReduceCostPerInvocation, and its call count with ReduceBudget. Parent-child accounting comes from the signed delegation chain and its budget authority; two independently issued tokens hold two separate budgets.

parent-scope.yamlyaml
# Parent token: USD 100.00 in total
grants:
  - server_id: models
    tool_name: complete
    operations: [invoke]
    max_total_cost:
      units: 10000     # USD 100.00
      currency: USD
    max_invocations: 1000
child-scope.yamlyaml
# Child token, attenuated from the parent: USD 10.00
# Created with ReduceTotalCost and ReduceBudget
# (optionally ReduceCostPerInvocation)
grants:
  - server_id: models
    tool_name: complete
    operations: [invoke]
    max_total_cost:
      units: 1000      # USD 10.00, reduced from the parent's USD 100.00
      currency: USD
    max_invocations: 100  # reduced from the parent's 1000

To attribute delegated spend, trace the delegation chain and the budget-authority records. delegation_depth records the chain depth used for the call, and root_budget_holder is populated from the capability issuer. Siblings can also share one family monetary cap, with basis-point shares bounding each child's draw; see shared capacity.

Tool pricing helpers

A tool server declares its price with the NativeTool helpers. per_invocation_price sets the billing unit to "invocation"; flat_price sets one price with no billing unit; per_unit_price prices a custom unit; hybrid_price adds a usage price to a base. The helpers describe a pricing contract. The service measures usage and reports the cost.

priced-tools.rsrust
NativeTool::new("quote", "Quote a SKU", schema.clone())
    .per_invocation_price(1, "USD"); // USD 0.01 per quote
NativeTool::new("read_order", "Read one order", schema.clone())
    .flat_price(500, "USD"); // USD 5.00 per call
NativeTool::new("complete", "Run a model completion", schema.clone())
    .per_unit_price(2, "USD", "1k_tokens"); // USD 0.02 per 1,000 tokens
NativeTool::new("search", "Search competitor prices", schema)
    .hybrid_price(25, 10, "USD", "result"); // USD 0.25 plus 0.10 per result

The host authenticates the manifest before registration. A connection reports its realized charge as a ToolInvocationCost. The default invoke_with_cost returns no amount, and the kernel then debits the authorized ceiling as a conservative charge. The service connection reports explicitly:

src/service.rs · realized monetary chargerust
Some(ToolInvocationCost {
    units: PRICE_UNITS,
    currency: "USD".into(),
    breakdown: Some(json!({"fixed_service_charge": PRICE_UNITS})),
})

Publishing offers and planning a budget from a quote are in Tool Pricing.

Financial receipt metadata

The monetary path records its decision as FinancialReceiptMetadata in metadata.financial. Depending on the outcome, the block holds:

  • grant_index: the grant inside the capability that paid for the call.
  • cost_charged and currency: the recorded amount, read with the settlement status and the rail.
  • budget_remaining and budget_total: a balance snapshot and the grant's original total.
  • delegation_depth and root_budget_holder: the chain depth and the issuing capability key as bare lowercase hex.
  • payment_reference: an optional reference that joins the receipt to an external settlement system.
  • settlement_status: whether the charge is pending, settled, failed, or not applicable.
  • cost_breakdown: optional reporting and payment details, not necessarily an additive cost list.
  • oracle_evidence: the conversion record for a cross-currency charge.
  • attempted_cost: the requested exposure on a monetary refusal.

A receipt without the block makes no statement about price. Invocation-only quotas can refuse with no financial block at all.

Query receipts with financial data

Local reads against a --receipt-db path fail closed without a tenant scope: pass --tenant <id>, or --admin-all for an explicit cross-tenant operator read, on every local query. --min-cost and --max-cost both require --cost-currency.

Terminalbash
# Receipts with cost information for one tool
chio --receipt-db ./receipts.sqlite receipt list --tenant team-a \
    --tool-server models --tool-name complete

# Expensive calls, by minimum cost in minor units
chio --receipt-db ./receipts.sqlite receipt list --tenant team-a \
    --min-cost 100 --cost-currency USD --tool-server models

# Failed settlements in the returned page
chio --receipt-db ./receipts.sqlite receipt list --tenant team-a \
    --tool-server models \
    | jq 'select(.metadata.financial.settlement_status == "failed")'

# Monetary refusals in the returned page
chio --receipt-db ./receipts.sqlite receipt list --tenant team-a --outcome deny \
    | jq 'select(.metadata.financial.attempted_cost != null)'

# One receipt by ID; follow next_cursor for records beyond this page
chio --receipt-db ./receipts.sqlite receipt list --tenant team-a --limit 500 \
    | jq 'select(.id == "d1b874355e0cf8b2d5aea4d5b6bc0c71e231420654798bddf84d20cbd44409d3")'

budget_total minus budget_remaining is cumulative committed usage; a single invocation's reservation is in its budget-authority events. One response is one page of the log: for a shared store, follow the cursor loop in Query & Audit Receipts before you aggregate, and export to a SIEM for continuous dashboards (SIEM export).

Live budget state

The chio mcp serve-http admin router serves a read-only GET /admin/budgets?capability_id=<id>&limit=<n>, gated by --admin-token (environment variable CHIO_ADMIN_TOKEN). It lists the current per-grant invocation counts for one capability.

Budget-authority events

The hold-and-reconcile lifecycle is recorded in metadata.budget_authority: the authorization event, the invocation capture, and a terminal disposition. This capture comes from a simulated prepaid call; the events describe budget accounting, and the rail records settlement separately.

metadata.budget_authority from a prepaid calltranscript
$ 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

Failed settlement records

On the non-durable reconciliation path, a report above the authorization produces a failed financial record. settlement_status is failed; cost_charged holds the reported cost, because recorded_cost is set to actual_cost; and the budget is debited no further than the reservation. In this illustration a 100-unit reservation meets a 220-unit report.

Illustrative non-durable overrun projectionjson
{
  "grant_index": 0,
  "cost_charged": 220,
  "currency": "USD",
  "budget_remaining": 900,
  "budget_total": 1000,
  "settlement_status": "failed",
  "cost_breakdown": {
    "compute": 180,
    "io": 40
  }
}

A failed status records that 220 units were reported, not that they were paid, and a rail failure produces the same status. Inspect the operation's error and payment state before attributing it to the tool.

Cross-currency configuration

When a grant's currency differs from a tool's pricing currency, the kernel resolves the rate through chio-link, a pinned Chainlink and Pyth oracle runtime. Build a PriceOracleConfig in Rust, usually with PriceOracleConfig::base_arbitrum_default(base_rpc, arbitrum_rpc), and hand it to the kernel with set_price_oracle. The struct is deny_unknown_fields with no defaulted fields: a chain inventory (operator.chains), an operator policy block, a typed egress_contract that gates every outbound oracle request fail-closed, and one Chainlink feed address per pair with an optional Pyth fallback and a per-pair policy.

Replace the base-mainnet.example endpoint and its matching egress authority with your RPC before validation. The feed addresses are source defaults.

price-oracle-config.jsonjson
{
  "primary": "chainlink",
  "fallback": "pyth",
  "refresh_interval_seconds": 60,
  "pyth": {
    "hermes_url": "https://hermes.pyth.network"
  },
  "operator": {
    "global_pause": false,
    "chains": [
      {
        "chain_id": 8453,
        "label": "base-mainnet",
        "caip2": "eip155:8453",
        "rpc_endpoint": "https://base-mainnet.example",
        "enabled": true,
        "sequencer_uptime_feed": "0xBCF85224fc0756B9Fa45aA7892530B47e10b6433",
        "sequencer_grace_period_seconds": 300
      }
    ],
    "pair_overrides": [],
    "monitoring": {
      "alert_on_fallback": true,
      "alert_on_degraded": true,
      "alert_on_pause": true,
      "alert_on_sequencer": true
    }
  },
  "egress_contract": {
    "tenant_egress_namespace": "chio-link",
    "allowed_schemes": ["https"],
    "allowed_authority_set": ["hermes.pyth.network", "base-mainnet.example"],
    "deny_loopback": true,
    "deny_link_local": true,
    "deny_ipv6_ula": true,
    "max_redirect_chain": 1,
    "max_response_bytes": 4194304
  },
  "pairs": [
    {
      "base": "USDC",
      "quote": "USD",
      "chain_id": 8453,
      "chainlink": {
        "address": "0x7e860098F58bBFC8648a4311b374B1D669a2bc6B",
        "decimals": 8,
        "heartbeat_seconds": 68400
      },
      "pyth": {
        "id": "0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a"
      },
      "policy": {
        "max_age_seconds": 600,
        "divergence_threshold_bps": 500,
        "exchange_rate_margin_bps": 200,
        "twap_enabled": false,
        "twap_window_seconds": 600,
        "twap_max_observations": 10,
        "stable_pair": true,
        "degraded_mode": {
          "enabled": false,
          "max_stale_age_seconds": 300,
          "extra_margin_bps": 800
        }
      }
    },
    {
      "base": "ETH",
      "quote": "USD",
      "chain_id": 8453,
      "chainlink": {
        "address": "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70",
        "decimals": 8,
        "heartbeat_seconds": 300
      },
      "pyth": {
        "id": "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"
      },
      "policy": {
        "max_age_seconds": 600,
        "divergence_threshold_bps": 500,
        "exchange_rate_margin_bps": 200,
        "twap_enabled": true,
        "twap_window_seconds": 600,
        "twap_max_observations": 10,
        "stable_pair": false,
        "degraded_mode": {
          "enabled": false,
          "max_stale_age_seconds": 300,
          "extra_margin_bps": 800
        }
      }
    }
  ]
}

exchange_rate_margin_bps adds a conservative buffer to the converted rate; one basis point is 0.01%. divergence_threshold_bps trips a circuit breaker when Chainlink and Pyth disagree beyond the threshold, and max_age_seconds bounds how stale a cached rate may be before resolution fails closed. twap_enabled, twap_window_seconds, and twap_max_observations govern time-weighted averaging; stable_pair marks a pegged pair; degraded_mode bounds how far resolution relaxes its staleness and margin ceilings when a feed goes stale.

A successful conversion adds OracleConversionEvidence; a refusal before conversion carries none. source names the backend that produced the rate, authority names the receipt-side FX authority model, and the signed rate is the exact ratio rate_numerator / rate_denominator. Margin is a resolution input, never a field on the evidence. A pinned oracle signer adds oracle_public_key and signature.

Cross-currency financial projection · illustrativejson
{
  "metadata": {
    "financial": {
      "grant_index": 0,
      "cost_charged": 150,
      "currency": "USD",
      "budget_remaining": 850,
      "budget_total": 1000,
      "settlement_status": "pending",
      "oracle_evidence": {
        "schema": "chio.oracle-conversion-evidence.v1",
        "base": "USDC",
        "quote": "USD",
        "authority": "chio_link_runtime_v1",
        "rate_numerator": 100,
        "rate_denominator": 100,
        "source": "chainlink",
        "feed_address": "0x7e860098F58bBFC8648a4311b374B1D669a2bc6B",
        "updated_at": 1710000090,
        "max_age_seconds": 600,
        "cache_age_seconds": 12,
        "original_cost_units": 1500000,
        "original_currency": "USDC",
        "converted_cost_units": 150,
        "grant_currency": "USD"
      }
    }
  }
}

Other budget models

The grant ceiling above is enforced in try_charge_cost() against the durable hold ledger. chio-metering ships two helpers that evaluate budgets independently of that ceiling. Neither signs a kernel receipt or installs itself in the admission path.

Flat enforcer

budget::BudgetEnforcer applies a flat BudgetPolicy scoped to total, per-session, per-agent, and per-tool spend. A charge that breaches any scope returns a BudgetViolation naming the dimension. Its counters are process-local, reset on restart, and need external synchronization; durable spend belongs on the kernel hold ledger.

Budget hierarchy

budget_hierarchy::BudgetTree caps a draft spend at every ancestor BudgetNode across four dimensions: spend, tokens, requests, and warehouse bytes. A denying BudgetDecision reports the offending scope closest to the root through BudgetDenyReason. It suits organization, team, and project hierarchies where no child may exceed an ancestor. BudgetTree evaluates a supplied SpendSnapshot; the caller owns that snapshot's freshness, persistence, and atomic update with the work it authorizes.

Next: export these charges