Chio/Docs
LOGIN · JOIN

Metering Spec

Find the field, budget rule, or export type for a metered call, from the host’s cost record to the kernel’s signed spend.

Reference chio@5f63e6e

On this page

A cost record attributes one receipt’s resource use (compute time, data volume, upstream API spend, a warehouse scan, or a custom count) to the agent, session, and tool that produced it.

Synopsis

The chio-metering library builds three host tools on that record: budget checks before a call, flat billing records after it, and grouped cost queries. The host supplies the measurements and the storage. Two schema identifiers cover the library: chio.cost-metadata.v1 for the record and chio.billing-export.v1 for billing output. MUST, SHOULD, and MAY carry their normative meaning. The record below is the fixer’s USD 0.21 model call from the Quickstart: one compute measurement and one upstream charge.

cost.jsonjson
{
  "schema": "chio.cost-metadata.v1",
  "receipt_id": "rcpt-01a8…5d17",
  "timestamp": 1700000000,
  "agent_id": "did:chio:z6MkrT2…8wd",
  "tool_server": "models",
  "tool_name": "default",
  "dimensions": [
    { "dimension": "compute_time", "duration_ms": 420 },
    {
      "dimension": "api_cost",
      "amount": { "units": 21, "currency": "USD" },
      "provider": "/models/default"
    }
  ],
  "total_monetary_cost": { "units": 21, "currency": "USD" }
}

The kernel keeps its own accounting for the budgets a grant carries. Its hold ledger is the one consumption authority: it holds the worst case before dispatch, reconciles to the reported cost, and writes metadata.financial on the signed receipt. Those contracts are under Kernel budget accounting, and Budgets & Metering explains the model.

Cost dimensions

Each receipt MAY carry zero or more cost dimensions. CostDimension is an internally tagged enum: each variant serializes with its snake-case name under the dimension tag. The crate defines five variants.

crates/economy/chio-metering/src/cost.rs15-62rust
#[serde(tag = "dimension", rename_all = "snake_case")]
pub enum CostDimension {
    /// Wall-clock compute time in milliseconds.
    ComputeTime {
        /// Duration in milliseconds.
        duration_ms: u64,
    },
    /// Data volume transferred in bytes.
    DataVolume {
        /// Bytes read from upstream.
        bytes_read: u64,
        /// Bytes written to upstream.
        bytes_written: u64,
    },
    /// Monetary cost charged by an upstream API.
    ApiCost {
        /// The cost amount in minor currency units.
        amount: MonetaryAmount,
        /// Provider that charged this cost (e.g. "openai", "anthropic").
        provider: String,
    },
    /// Warehouse query cost recorded by the `WarehouseCostGuard` in
    /// `chio-data-guards`.  Captures the dry-run cost estimate for a
    /// warehouse-class query (BigQuery, Snowflake, Redshift, etc.).
    ///
    /// `estimated_cost_usd` is a decimal string rather than a fixed-width
    /// integer because `rust_decimal` is not part of the Chio workspace;
    /// this follows the precedent set by
    /// `Constraint::MaxTransactionAmountUsd` in `chio-core-types`.
    WarehouseQuery {
        /// Bytes the warehouse reported it will scan to satisfy the query.
        bytes_scanned: u64,
        /// Decimal-string estimate of the monetary cost in USD (e.g.
        /// `"0.25"`).  Parsed against the guard's `max_cost_per_query_usd`
        /// limit; preserved verbatim on the receipt for auditability.
        estimated_cost_usd: String,
    },
    /// Custom cost dimension for extensibility.
    Custom {
        /// Name of the custom dimension.
        name: String,
        /// Numeric value.
        value: u64,
        /// Optional unit label (e.g. "tokens", "requests").
        #[serde(default, skip_serializing_if = "Option::is_none")]
        unit: Option<String>,
    },
}

CostDimension

VariantFieldsDescription
ComputeTime
duration_ms: u64
Wall-clock compute time in milliseconds
DataVolume
bytes_read: u64, bytes_written: u64
Data volume transferred in bytes
ApiCost
amount: MonetaryAmount, provider: string
Monetary cost charged by an upstream API
WarehouseQuery
bytes_scanned: u64, estimated_cost_usd: string
Dry-run cost estimate for a warehouse-class query (BigQuery, Snowflake, Redshift, and similar)
Custom
name: string, value: u64, unit: string?
Extensibility dimension with a numeric value

Custom lets an operator track a domain-specific count, such as tokens or requests, without a protocol change.

WarehouseQuery records the dry-run cost estimate a warehouse cost guard reads off a tool call before the query runs, under the tag "warehouse_query". estimated_cost_usd is a decimal string: the guard parses it against its max_cost_per_query_usd limit and keeps the value verbatim on the receipt for audit. The same convention appears in Constraint::MaxTransactionAmountUsd in the capability scope types.

MonetaryAmount

FieldTypeDescription
units
u64
Cost in minor currency units (cents for USD)
currency
string
ISO 4217 currency code, such as "USD" or "EUR"

CostMetadata

Per-receipt cost metadata, serialized as JSON. It lives in the receipt’s metadata map under the cost key, and the host writes it there.

The kernel reserves admission_operation, attribution, budget_authority, channel, chio_receipt_signing_nonce, delivery_contract, financial, finding_delivery, finding_recovery, governed_transaction, and original_metadata, declared in crates/core/chio-core-types/src/receipt/metadata.rs, crates/core/chio-core-types/src/receipt/signing.rs, and crates/kernel/chio-kernel/src/admission_operation.rs. It writes its own typed block under each of those keys, merges them last, and rejects a pre-existing collision from caller or hook metadata. cost sits outside that set, so the block under it is ordinary caller metadata: the kernel signs it and the host authors it.

FieldTypeRequiredDescription
schema
string
Yes
MUST be "chio.cost-metadata.v1"
receipt_id
string
Yes
Receipt ID this cost belongs to
timestamp
u64
Yes
Unix timestamp (seconds) of the receipt
session_id
string
No
Session that produced this receipt
agent_id
string
Yes
Agent that made the invocation
tool_server
string
Yes
Tool server that handled the invocation
tool_name
string
Yes
Tool that was invoked
dimensions
CostDimension[]
Yes
Individual cost measurements
total_monetary_cost
MonetaryAmount
No
Aggregate monetary cost across ApiCost dimensions

Total monetary cost computation

compute_total_monetary_cost refreshes total_monetary_cost after dimensions are added. It sums every ApiCost amount. Implementations MUST use saturating addition to prevent overflow.

When ApiCost dimensions use different currencies, only amounts in the first currency encountered are summed. Cross-currency amounts require oracle conversion and stay out of the automatic total. Implementations SHOULD document this behavior to operators.

With no ApiCost dimension, the method leaves total_monetary_cost as None, and serialization omits the field.

Budget enforcement

BudgetEnforcer checks a proposed cost against total, session, agent, and tool limits, and the host uses the returned violation to refuse work. Costs arrive already denominated in the policy currency; conversion belongs to the caller.

BudgetEnforcer is marked #[deprecated] with the note “process-local counters reset on restart; use chio-kernel BudgetStore”. Execution limits that survive a restart live in the kernel’s Budget Store. The types below describe what a host integration built on the enforcer reads and returns.

BudgetPolicy

FieldTypeRequiredDescription
max_total
MonetaryAmount
Yes
Maximum total spending across all dimensions
max_per_session
MonetaryAmount
No
Per-session spending limit
max_per_agent
MonetaryAmount
No
Per-agent spending limit
max_per_tool
map[string, MonetaryAmount]
No
Per-tool limits; key format "server:tool"
currency
string
Yes
Currency for budget enforcement

Enforcement semantics

Budget checks evaluate in this order, and the first violation encountered MUST be returned:

  1. Total budget. total_spent + cost > max_total.units
  2. Per-session budget. session_spent + cost > max_per_session.units
  3. Per-agent budget. agent_spent + cost > max_per_agent.units
  4. Per-tool budget. tool_spent + cost > max_per_tool[key].units

All arithmetic MUST use saturating addition. Overflow MUST saturate to u64::MAX instead of wrapping.

A zero proposed cost runs through the same comparisons, so a counter already past its cap returns a violation for a zero-cost check too. A total limit of 100 with 101 already recorded rejects a proposed cost of 0.

BudgetViolation

A failed check MUST return a typed violation:

VariantFieldsDescription
Total
limit_units, current_units, requested_units, currency
Total budget exceeded
Session
session_id, limit_units, current_units, requested_units, currency
Per-session budget exceeded
Agent
agent_id, limit_units, current_units, requested_units, currency
Per-agent budget exceeded
Tool
tool_key, limit_units, current_units, requested_units, currency
Per-tool budget exceeded

Recording

The host calls record to add a booked cost to the applicable total, session, agent, and tool counters, with saturating addition. record only updates counters; limits are enforced in check, before invocation.

The check and the counter update are separate operations, so the host coordinates concurrent calls itself.

Three accessors read the enforcer without changing it. total_spent() returns the running total in policy currency units. remaining() returns max_total.units minus that total under saturating subtraction, so it floors at zero. policy() borrows the active BudgetPolicy. The per-session, per-agent, and per-tool maps stay private; a caller reads them through a check violation.

Budget hierarchy

BudgetTree represents organizational scopes (a team, its projects, and their agents) where BudgetEnforcer holds one flat policy. Each ancestor can cap spending, tokens, requests, and warehouse bytes, and a leaf must satisfy the applicable limits along its path to the root.

The tree owns shape and limits; storage stays with the caller. A caller reads current spend from its own store into a SpendSnapshot and passes it in for each decision.

BudgetNodeId

StringTransparent

Node identifier, conventionally scope/name. The tree validates uniqueness and acyclic parents and leaves the string shape to the caller.

BudgetWindow

Daily | Monthly | Rolling { seconds }Tag: kind

duration_seconds() returns 86400, 30 days, or the given seconds. bucket_start(ts) returns ts - (ts % duration), and 0 when the duration is 0.

BudgetLimits

max_spend_units, currency, max_tokens, max_requests, max_warehouse_bytesAll optional

A node may cap one dimension and leave the rest open. currency is required once max_spend_units is set.

BudgetNode

id, parent, limits, window, enabledOne scope

A node with no parent is a root. enabled defaults to true, and a disabled node denies every draft charged through it.

AggregateSpend

spend_units, currency, tokens, requests, warehouse_bytesDraft and recorded spend

The four capped dimensions plus the currency that denominates the first. The same type carries the draft and the recorded spend.

PerWindowSpend

window_start, currentOne node, one bucket

One node’s spend inside one window bucket. The bucket start is compared against the node’s own window, so a stale row denies instead of under-counting.

SpendSnapshot

evaluated_at, per_nodeCaller-supplied

The caller’s read of its store. A snapshot with no evaluated_at denies as an expired window instead of defaulting to now.

BudgetTree

insert, validate, ancestors, descendants, evaluate, serialize, deserializeThe node map

insert rejects a duplicate id, a missing parent, and a parent chain that would close a cycle. serialize emits nodes sorted by id.

BudgetDecision

Allow | Deny { reason }Tag: decision

Exactly two outcomes, with no warning state.

BudgetDenyReason

NodeDisabled | DimensionExceeded | CurrencyMismatch | ArithmeticOverflow | WindowExpired | UnknownNodeTag: reason

DimensionExceeded names the dimension as one of spend, tokens, requests, or warehouse_bytes, and formats the cap and the projected value as decimal strings.

BudgetError

InvalidLimits | Cycle | MissingParent | Duplicate | InvalidSerializationConstruction failures

Construction and deserialization failures, distinct from a deny: a tree that fails to build never evaluates.

Evaluation rule

BudgetTree::evaluate(id, draft, current) takes the leaf the charge belongs to, the draft spend, and the snapshot. A leaf absent from the tree denies at once with UnknownNode. Otherwise the walk runs from that leaf to the root, and at each node in turn:

  1. A disabled node records NodeDisabled.
  2. A snapshot with no evaluated_at, or a row whose window_start differs from the node’s bucket start for that instant, records WindowExpired. A node with no row reads as zero spend in the current bucket.
  3. A monetary cap with no node currency, or with a draft or snapshot currency that differs from it, records CurrencyMismatch. A zero amount carrying no currency is accepted against the node currency.
  4. A checked addition that overflows records ArithmeticOverflow for that dimension. The hierarchy checks addition where the flat enforcer saturates it, so an overflow here denies where the enforcer would clamp.
  5. A projection over a cap records DimensionExceeded, for spend, tokens, requests, and warehouse bytes in that order.

Each node overwrites the recorded reason, so the reason returned comes from the node closest to the root and an operator reads the widest policy boundary first. A walk that records nothing returns Allow.

Billing export

Billing export turns cost metadata into flat, denormalized records that an external billing system ingests.

BillingRecord

FieldTypeDescription
schema
string
MUST be "chio.billing-export.v1"
receipt_id
string
Receipt ID
timestamp
u64
Unix timestamp (seconds)
timestamp_iso
string
ISO 8601 timestamp in UTC, such as "2023-11-14T22:13:20Z"
session_id
string
Session ID; omitted when absent
agent_id
string
Agent that triggered the cost
tool_server
string
Tool server
tool_name
string
Tool name
compute_time_ms
u64
Total compute time in milliseconds
data_bytes
u64
Total data transferred in bytes
cost_units
u64
Monetary cost in minor units; omitted when absent
currency
string
ISO 4217 currency code; omitted when absent
provider
string
Upstream provider; omitted when absent

timestamp_iso is a UTC calendar string with a Z suffix. When the Unix timestamp has no calendar date, the formatter writes "unix:<timestamp>".

BillingExport

A batch of billing records.

FieldTypeDescription
schema
string
MUST be "chio.billing-export.v1"
exported_at
u64
Unix timestamp when the export was created
record_count
u64
Total number of records in this export
total_cost
MonetaryAmount
Aggregate cost; omitted if currencies differ or no monetary costs are present
records
BillingRecord[]
The billing records

create_billing_export(records, exported_at) builds the batch. It flattens each CostMetadata into one BillingRecord, takes provider from the first ApiCost dimension it finds, sums total_monetary_cost with saturating addition while the currency holds, and sets the export’s total_cost to None as soon as a second currency appears. Serialized JSON omits a None total, and a consumer treats the absent field as null.

Export formats

Implementations MUST support JSON export. Implementations SHOULD support CSV export. CSV output MUST use the same field names as the JSON schema with one record per row and a header line.

BillingRecord and BillingExport derive Serialize, so serde_json produces the JSON form, which is the one format the crate writes. A host that wants CSV writes it from the record fields under the rule above.

Query interface

The query interface aggregates cost across sessions, agents, and tools. The crate root re-exports CostQuery, CostQueryResult, and CostSummary. GroupBy, CostGroup, execute_cost_query, and MAX_COST_QUERY_LIMIT are public on the module, at chio_metering::query::.

CostQuery

Every filter field is optional, and an omitted filter matches all records. Filters combine with AND.

FieldTypeDescription
session_id
string
Filter by session ID
agent_id
string
Filter by agent ID
tool_server
string
Filter by tool server
tool_name
string
Filter by tool name
since
u64
Start of time range, inclusive (Unix seconds)
until
u64
End of time range, exclusive (Unix seconds)
currency
string
Only include costs in this currency
limit
usize
Maximum matching records included in the summary and groups; capped at 500
group_by
GroupBy
Aggregation dimension

GroupBy

ValueGroups byGroup key
none
Nothing: summary only
No grouped rows
session
Session
Session ID
agent
Agent
Agent ID
tool
Tool
"server:tool_name"

CostQueryResult

FieldTypeDescription
summary
CostSummary
Aggregate statistics over the first matching records, up to the limit
groups
CostGroup[]
Grouped rows from the same limited set; omitted from JSON when empty
truncated
bool
True when more records matched than the limit admitted

CostSummary

FieldTypeDescription
receipt_count
u64
Matching receipts included after applying the limit
total_compute_time_ms
u64
Aggregate compute time
total_data_bytes
u64
Aggregate data volume
total_monetary_cost
MonetaryAmount
Aggregate cost; omitted if currencies differ or no monetary costs are present
distinct_agents
u64
Number of distinct agents
distinct_tools
u64
Number of distinct tools

CostGroup

FieldTypeDescription
key
string
Group key (session ID, agent ID, or "server:tool")
receipt_count
u64
Receipts in this group
total_compute_time_ms
u64
Compute time for this group
total_data_bytes
u64
Data volume for this group
total_monetary_cost
MonetaryAmount
Cost for this group; omitted if currencies differ or no monetary costs are present

Limits

execute_cost_query filters the supplied collection, takes the first matching records in input order, and aggregates that subset. The default and maximum limit is MAX_COST_QUERY_LIMIT = 500. The result holds a summary and optional groups, without individual receipt records.

When more records match, truncated is true and counts and totals describe only the included subset: 600 matching receipts with a limit of 100 produce a summary of the first 100. A limit of zero produces an empty summary and still reports truncation when records match.

Kernel budget accounting

The kernel accounts for the budget limits a capability grant carries, apart from the chio-metering types above. Its contracts are the enforcement phases, integer money and currency conversion, the financial receipt metadata, the delegation variants, and the persistent budget store.

The enforcement flow

On a cost-reporting path the success cycle is authorize, capture, and reconcile: the kernel holds exposure before dispatch and reconciles it to the reported cost. A denial or validation failure after authorization takes a fourth path, where reverse_budget_charge reverses the hold.

Model spend on the hold ledgerChoose a move.

The worst case is held before dispatch

Before a model call leaves the kernel, the hold ledger reserves its worst case against the grant’s USD 2.00 budget: the quoted cost when a quote is present, otherwise max_cost_per_invocation. The call dispatches only after the hold is recorded. Authoritative spend.

The hold ledger for the fixer’s priced calls under its USD 2.00 grant. Cross-currency conversion and the provider’s own billing are left out.Budgets and metering

The call dispatches only after its hold is recorded. A reply that never returns keeps its hold until the outcome is resolved, and the kernel never dispatches that call again. Quoted prepayment has its own accounting path: the compute example charges the quote instead of refunding the difference from the tool’s report.

Phase 1: authorize

Before the tool executes, the kernel calls check_and_increment_budget(). In one step it increments the invocation count and checks it against max_invocations, and it debits the worst-case cost from the running total and checks it against max_total_cost. The worst case is quote.quoted_cost when a quote exists, otherwise max_cost_per_invocation. If either check fails, the kernel refuses the call before tool code runs and signs a deny receipt.

Phase 2: capture

The tool executes and reports its cost through ToolInvocationCost. The selected payment mode and adapter determine capture and release. A failed operation alone does not show that a payment was reversed.

ToolInvocationCostrust
pub struct ToolInvocationCost {
    /// Actual cost in minor units.
    pub units: u64,
    /// ISO 4217 currency code.
    pub currency: String,
    /// Optional cost breakdown as an arbitrary JSON value (tool-defined keys).
    pub breakdown: Option<serde_json::Value>,
}

The breakdown lets a tool itemize costs, separating compute, I/O, and network charges for example. Its keys are tool-defined and its shape is opaque JSON, which the kernel copies into the financial receipt for audit.

Phase 3: reconcile

After the tool returns, the kernel calls finalize_budgeted_tool_output_with_cost_and_metadata() to reconcile the authorized amount with the actual cost. It takes the request, the tool output, the elapsed time, the timestamp, the matched grant index, and a FinalizeToolOutputCostContext (the charge result, the reported cost, the payment authorization, and the cap). An unbudgeted call has no charge to reconcile and goes through finalize_tool_output_with_metadata_and_payee_binding.

A budgeted call settles the difference in one of four ways:

  • Reported cost below the hold. The difference returns to the budget.
  • Reported cost equal to the hold. No adjustment.
  • Reported cost above the hold. The receipt records the overrun and settlement_status is Failed.
  • A guard or validation step denies after authorization. reverse_budget_charge() fully reverses the hold, so the agent pays nothing for a tool call that never ran, and no authoritative spend receipt exists for it.

Exhausting the invocation count

The full-flow budget test drives the count cap to its edge against a full kernel. It issues a grant with max_invocations: Some(2), spends both invocations, and asserts that the third call is denied.

tests/e2e/tests/full_flow.rs744-793rust
async fn full_flow_budget_exhaustion() {
    let (kernel, _ca_kp) = make_kernel_bare();
    let agent_kp = Keypair::generate();

    // Issue a capability with max_invocations = 2.
    let cap = kernel
        .issue_capability(
            &agent_kp.public_key(),
            ChioScope {
                grants: vec![ToolGrant {
                    server_id: "srv".to_string(),
                    tool_name: "echo".to_string(),
                    operations: vec![Operation::Invoke],
                    constraints: vec![],
                    max_invocations: Some(2),
                    max_cost_per_invocation: None,
                    max_total_cost: None,
                    dpop_required: None,
                }],
                ..ChioScope::default()
            },
            300,
        )
        .expect("issue budgeted cap");

    // First two calls succeed.
    for i in 0..2 {
        let req = make_request(
            &format!("req-budget-{i}"),
            &cap,
            "echo",
            serde_json::json!({"i": i}),
        );
        let resp = kernel.evaluate_tool_call(&req).await.unwrap();
        assert_eq!(resp.verdict, Verdict::Allow, "call {i} should succeed");
        assert!(resp.receipt.verify_signature().unwrap());
    }

    // Third call should be denied due to budget exhaustion.
    let req = make_request("req-budget-2", &cap, "echo", serde_json::json!({"i": 2}));
    let resp = kernel.evaluate_tool_call(&req).await.unwrap();
    assert_eq!(resp.verdict, Verdict::Deny);
    let reason = resp.reason.as_deref().unwrap_or("");
    assert!(
        reason.contains("budget"),
        "expected budget exhaustion, got: {reason}"
    );
    assert!(resp.receipt.is_denied());
    assert!(resp.receipt.verify_signature().unwrap());
}

The last two assertions check that the denied call produces a receipt and that the receipt is signed. The refusal reason comes from a KernelError variant, so the type fixes its text. The metered service’s refusal reads invocation budget exhausted for the same reason, even when a total-cost ceiling stopped the call.

crates/kernel/chio-kernel/src/kernel/error.rsrust
    #[error("invocation budget exhausted for capability {0}")]
    BudgetExhausted(CapabilityId),

    #[error("captured budget replay denied for capability {0}")]
    CapturedBudgetReplay(CapabilityId),

Every Chio release passes the full-flow budget exhaustion gate before it is tagged. It drives a grant to its limit through the hosted kernel and checks the refusal above. chio release verify --evidence qualification shows its result for the release you run.

Minor-unit arithmetic

The kernel represents money with the same MonetaryAmount shape: minor-unit integers with an ISO 4217 currency code.

MonetaryAmountrust
pub struct MonetaryAmount {
    /// Value in the currency's minor unit (e.g., cents for USD).
    pub units: u64,
    /// ISO 4217 currency code (e.g., "USD", "EUR", "USDC").
    pub currency: String,
}

Monetary values are u64 minor units. Budget checks, cost comparisons, and attenuation validation all use integer minor-unit arithmetic, with no floating-point step.

currency is a bare string, so the kernel compares currencies without interpreting them. One place does interpret them: chio-link’s minor_units_for_currency pins a scale per code so a cross-currency conversion can move between two minor units. A code outside this set returns PriceOracleError::InvalidConfiguration instead of a guessed scale.

Currency codeMinor units per major unitDecimal places
USD, EUR, GBP
100
2
JPY
1
0
USDC, USDT
1_000_000
6
BTC
100_000_000
8
ETH, LINK
1_000_000_000_000_000_000
18

Conversion runs in u128 and returns an overflow error instead of rounding through a float.

Cross-currency enforcement

A capability token’s budget may be denominated in one currency while a tool reports its cost in another, such as a USD budget invoking a tool that charges in USDC. The kernel catches the mismatch at reconcile time, when it compares the reported cost against the hold, and calls resolve_cross_currency_cost to convert the reported amount into the grant currency.

The configured PriceOracle supplies the rate. chio-link defines this trait, and the deployment installs its implementation with set_price_oracle. With no oracle installed, the call fails with KernelError::NoCrossCurrencyOracle.

The conversion uses integer arithmetic. The rate is a rate_numerator/rate_denominator pair, and the cost travels as both original_cost_units (what the tool charged) and converted_cost_units (what the budget was debited). Three fields bound the quote’s freshness: updated_at records when the feed reported the rate, max_age_seconds caps its age at use, and cache_age_seconds records how old the cached rate was at conversion. The receipt carries this as OracleConversionEvidence:

OracleConversionEvidencerust
pub struct OracleConversionEvidence {
    /// Wire schema id ("chio.oracle-conversion-evidence.v1").
    pub schema: String,
    /// Base currency of the quoted pair.
    pub base: String,
    /// Quote currency of the quoted pair.
    pub quote: String,
    /// Identifier of the oracle authority that issued the quote.
    pub authority: String,
    /// Exchange-rate numerator (integer representation).
    pub rate_numerator: u64,
    /// Exchange-rate denominator.
    pub rate_denominator: u64,
    /// Oracle source identifier.
    pub source: String,
    /// Address of the price feed the rate was read from.
    pub feed_address: String,
    /// Timestamp the feed reported the rate.
    pub updated_at: u64,
    /// Maximum age (seconds) the rate may be at use.
    pub max_age_seconds: u64,
    /// Age (seconds) of the cached rate at conversion time.
    pub cache_age_seconds: u64,
    /// Converted cost in the grant currency's minor units.
    pub converted_cost_units: u64,
    /// Original cost in the tool's charged-currency minor units.
    pub original_cost_units: u64,
    /// Currency the tool actually charged in.
    pub original_currency: String,
    /// Currency the grant budget is denominated in.
    pub grant_currency: String,
    /// Oracle's public key, when the quote is signed.
    pub oracle_public_key: Option<PublicKey>,
    /// Oracle's signature over the quote, when signed.
    pub signature: Option<Signature>,
}

A cross-currency receipt includes the rate, the feed timestamp, and the oracle authority. A signed quote also adds the oracle’s oracle_public_key and signature, so a verifier checks the rate against the oracle’s key.

Financial receipt metadata

Every tool invocation that exercises a monetary grant produces a FinancialReceiptMetadata record in the receipt’s metadata field, holding the economic state of the transaction:

FinancialReceiptMetadatarust
pub struct FinancialReceiptMetadata {
    /// Index of the grant within the capability token.
    pub grant_index: u32,
    /// Actual cost charged (after reconciliation).
    pub cost_charged: u64,
    /// Currency of the charge.
    pub currency: String,
    /// Budget remaining after this invocation.
    pub budget_remaining: u64,
    /// Total budget for the grant.
    pub budget_total: u64,
    /// Depth in the delegation chain (0 = root).
    pub delegation_depth: u32,
    /// Identity of the root budget holder.
    pub root_budget_holder: String,
    /// External payment reference for settlement.
    pub payment_reference: Option<String>,
    /// Settlement status of this charge.
    pub settlement_status: SettlementStatus,
    /// Itemized cost breakdown from the tool, as an arbitrary JSON value.
    /// Keys are tool-defined.
    pub cost_breakdown: Option<serde_json::Value>,
    /// Oracle conversion evidence (for cross-currency).
    pub oracle_evidence: Option<OracleConversionEvidence>,
    /// Cost that was attempted (present in denial receipts).
    pub attempted_cost: Option<u64>,
}

SettlementStatus tracks the lifecycle of the financial transaction:

SettlementStatusrust
pub enum SettlementStatus {
    /// No monetary cost (e.g., free-tier grant).
    NotApplicable,
    /// Cost recorded, awaiting settlement.
    Pending,
    /// Settlement completed successfully.
    Settled,
    /// Settlement failed (e.g., cost overrun).
    Failed,
}

Budget hold lineage

FinancialReceiptMetadata is the settled summary. Beneath it, the authorize path records the budget hold itself. When check_and_increment_budget authorizes a charge it opens a hold through authorize_budget_hold, and three sibling records capture that hold’s lineage on the receipt:

  • FinancialBudgetHoldAuthorityMetadata: the authority behind the hold, with authority_id, lease_id, and lease_epoch.
  • FinancialBudgetAuthorizeReceiptMetadata: the authorize step, with exposure_units and committed_cost_units_after, the committed cost after the hold opened.
  • FinancialBudgetTerminalReceiptMetadata: the terminal step, with the hold’s disposition, the realized_spend_units, and the committed total after reconcile.

The kernel merges FinancialReceiptMetadata into the receipt under the reserved financial metadata key (FINANCIAL_METADATA_KEY), so a reader finds it at metadata.financial on the signed receipt. A receipt carries the block only when the call exercised a monetary grant or a prepaid authorization. Receipts covers the rest of the receipt body, including what the evidence array holds and when it is present.

Budget attenuation in delegation

When an agent delegates a capability token to a child agent, the economic bounds may narrow. Three variants of the Attenuation enum govern them directly, and each child grant is checked against its own parent.

Three delegated grants, each bound drawn inside its parent’s allowancescaled to the root grant
gi⊑gi−1  ⟺  ∀k: ck≤pk\htmlClass{role-call}{g_i} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_{i-1}} \iff \forall k:\ \htmlClass{role-call}{c_k} \le \htmlClass{cor-parent}{p_k}
g0orchestratorroot granttotalmax_total_cost = 1000per callmax_cost_per_invocation = 100invocationsmax_invocations = 200g1research
g1⊑g0\htmlClass{role-call}{g_1} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_0}
total
500≤1000\htmlClass{role-call}{500} \le \htmlClass{cor-parent}{1000}
ReduceTotalCost
per call
50≤100\htmlClass{role-call}{50} \le \htmlClass{cor-parent}{100}
ReduceCostPerInvocation
invocations
50≤200\htmlClass{role-call}{50} \le \htmlClass{cor-parent}{200}
ReduceBudget
g2sub-agent
g2⊑g1\htmlClass{role-call}{g_2} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_1}
total
100≤500\htmlClass{role-call}{100} \le \htmlClass{cor-parent}{500}
ReduceTotalCost
per call
25≤50\htmlClass{role-call}{25} \le \htmlClass{cor-parent}{50}
ReduceCostPerInvocation
invocations
10≤50\htmlClass{role-call}{10} \le \htmlClass{cor-parent}{50}
ReduceBudget
Three delegated grants, each bound drawn inside its parent’s allowancescaled to the root grantg0orchestratorroot granttotal1000per call100invocations200g1research
g1⊑g0\htmlClass{role-call}{g_1} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_0}
total
500≤1000\htmlClass{role-call}{500} \le \htmlClass{cor-parent}{1000}
per call
50≤100\htmlClass{role-call}{50} \le \htmlClass{cor-parent}{100}
invocations
50≤200\htmlClass{role-call}{50} \le \htmlClass{cor-parent}{200}
g2sub-agent
g2⊑g1\htmlClass{role-call}{g_2} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_1}
total
100≤500\htmlClass{role-call}{100} \le \htmlClass{cor-parent}{500}
per call
25≤50\htmlClass{role-call}{25} \le \htmlClass{cor-parent}{50}
invocations
10≤50\htmlClass{role-call}{10} \le \htmlClass{cor-parent}{50}

Every delegated grant sits within its parent on all three bounds.

gi⊑gi−1  ⟺  ∀k: ck≤pk\htmlClass{role-call}{g_i} \mathrel{\htmlClass{cor-relation}{\sqsubseteq}} \htmlClass{cor-parent}{g_{i-1}} \iff \forall k:\ \htmlClass{role-call}{c_k} \le \htmlClass{cor-parent}{p_k}
In Rust · scope.rs:14728 lines
// If parent has an invocation cap, child must too and it must be <= parent
if let Some(parent_max) = parent.max_invocations {
    match self.max_invocations {
        Some(child_max) if child_max <= parent_max => {}
        None => return false, // child is uncapped but parent is capped
        Some(_) => return false, // child exceeds parent
    }
}
// … constraints
// If parent has a per-invocation cost cap, child must too and it must be <=
if let Some(ref parent_cost) = parent.max_cost_per_invocation {
    match &self.max_cost_per_invocation {
        Some(child_cost)
            if child_cost.currency == parent_cost.currency
                && child_cost.units <= parent_cost.units => {}
        _ => return false,
    }
}

// If parent has a total cost cap, child must too and it must be <=
if let Some(ref parent_cost) = parent.max_total_cost {
    match &self.max_total_cost {
        Some(child_cost)
            if child_cost.currency == parent_cost.currency
                && child_cost.units <= parent_cost.units => {}
        _ => return false,
    }
}
An illustrative three-level delegation chain, in USD minor units and invocations. Each grant is checked against its own parent; siblings are not summed against a parent here. Cost bounds also require the same currency.Budgets and metering
VariantFieldEffect on the child token
Attenuation::ReduceCostPerInvocation
max_cost_per_invocation
Tightens
Attenuation::ReduceTotalCost
max_total_cost
Tightens
Attenuation::ReduceBudget
max_invocations
Lowers

In the chain drawn above, values are USD minor units and invocation counts: the orchestrator holds a 1,000 total, 100 per call, and 200 invocations; the research agent 500, 50, and 50; the sub-agent 100, 25, and 10. A parent cap obliges the child to carry the same cap at or below it, and cost bounds also require the same currency.

The same enum carries four non-monetary narrowings: RemoveTool and RemoveOperation drop a tool or operation from scope, AddConstraint tightens a tool’s parameter constraints, and ShortenExpiry pulls the expiry earlier. Each variant narrows the parent token.

Lean 4 property P1, capability attenuation, proves that narrowing over the bounded ChioScope model; the proof manifest lists 6 named proofs under it. The theorem covers that model, and the Rust budget store sits outside it.

Budget state persistence

A BudgetStore holds the running state. SqliteBudgetStore in chio-store-sqlite persists it in the capability_grant_budgets table, whose primary key is (capability_id, grant_index), so grants inside one token keep independent budgets. The kernel’s own InMemoryBudgetStore holds the same shape without durability.

A BudgetUsageRecord tracks three running values per key: invocation_count, how many times this grant has been exercised; total_cost_exposed, the authorized exposure held against this grant; and total_cost_realized_spend, the spend reconciled back from completed calls. committed_cost_units() sums the two cost columns, and a limit check compares that total against max_total_cost.

SqliteBudgetStore opens one write transaction per hold: the read of current usage, the check against the limits, and the write of the new state commit together. Of two concurrent invocations against the same grant, only one passes a check that has room for one.

Specification and source files

The metering specification states the protocol requirements, and the Rust implementation in crates/economy/chio-metering fixes the library behavior.

  • Cost dimensions and cost metadata: src/cost.rs (the enum at lines 15 to 62).
  • Flat enforcer: src/budget.rs (total_spent at line 191, remaining at 197, policy at 203).
  • Budget tree: src/budget_hierarchy.rs (evaluate at line 599, over the ancestor chain built at 548).
  • Billing records: src/export.rs (create_billing_export at line 70).
  • Query interface: src/query.rs, with the crate-root re-exports at line 33 of src/lib.rs.
  • Minor-unit scales: crates/economy/chio-link/src/convert.rs, lines 5 to 16.

chio-metering is a library that sits beside the kernel receipt path. WarehouseCostGuard in crates/guards/chio-data-guards imports its CostDimension. The kernel’s monetary blocks on a signed receipt are on Receipt Format.