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.
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.
{
"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.
Try estimated_cost_usd, WindowExpired, or truncated. Each entry has its own link.
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.
#[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
| Variant | Fields | Description |
|---|---|---|
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_ | 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
| Field | Type | Description |
|---|---|---|
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.
| Field | Type | Required | Description |
|---|---|---|---|
schema | string | Yes | MUST be "chio. |
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_ | 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
| Field | Type | Required | Description |
|---|---|---|---|
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:
- Total budget.
total_spent + cost > max_total.units - Per-session budget.
session_spent + cost > max_per_session.units - Per-agent budget.
agent_spent + cost > max_per_agent.units - 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:
| Variant | Fields | Description |
|---|---|---|
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
Node identifier, conventionally scope/name. The tree validates uniqueness and acyclic parents and leaves the string shape to the caller.
BudgetWindow
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
A node may cap one dimension and leave the rest open. currency is required once max_spend_units is set.
BudgetNode
A node with no parent is a root. enabled defaults to true, and a disabled node denies every draft charged through it.
AggregateSpend
The four capped dimensions plus the currency that denominates the first. The same type carries the draft and the recorded spend.
PerWindowSpend
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
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 rejects a duplicate id, a missing parent, and a parent chain that would close a cycle. serialize emits nodes sorted by id.
BudgetDecision
Exactly two outcomes, with no warning state.
BudgetDenyReason
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
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:
- A disabled node records
NodeDisabled. - A snapshot with no
evaluated_at, or a row whosewindow_startdiffers from the node’s bucket start for that instant, recordsWindowExpired. A node with no row reads as zero spend in the current bucket. - 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. - A checked addition that overflows records
ArithmeticOverflowfor that dimension. The hierarchy checks addition where the flat enforcer saturates it, so an overflow here denies where the enforcer would clamp. - 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
| Field | Type | Description |
|---|---|---|
schema | string | MUST be "chio. |
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.
| Field | Type | Description |
|---|---|---|
schema | string | MUST be "chio. |
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.
| Field | Type | Description |
|---|---|---|
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
| Value | Groups by | Group key |
|---|---|---|
none | Nothing: summary only | No grouped rows |
session | Session | Session ID |
agent | Agent | Agent ID |
tool | Tool | "server:tool_name" |
CostQueryResult
| Field | Type | Description |
|---|---|---|
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
| Field | Type | Description |
|---|---|---|
receipt_count | u64 | Matching receipts included after applying the limit |
total_ | u64 | Aggregate compute time |
total_ | u64 | Aggregate data volume |
total_ | 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
| Field | Type | Description |
|---|---|---|
key | string | Group key (session ID, agent ID, or "server:tool") |
receipt_count | u64 | Receipts in this group |
total_ | u64 | Compute time for this group |
total_ | u64 | Data volume for this group |
total_ | 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.
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.
USD 2.00 grant. Cross-currency conversion and the provider’s own billing are left out.Budgets and meteringThe 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.
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_statusisFailed. - 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.
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.
#[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.
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 code | Minor units per major unit | Decimal 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:
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:
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:
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, withauthority_id,lease_id, andlease_epoch.FinancialBudgetAuthorizeReceiptMetadata: the authorize step, withexposure_unitsandcommitted_cost_units_after, the committed cost after the hold opened.FinancialBudgetTerminalReceiptMetadata: the terminal step, with the hold’sdisposition, therealized_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.
Every delegated grant sits within its parent on all three bounds.
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,
}
}
| Variant | Field | Effect on the child token |
|---|---|---|
Attenuation:: | max_ | Tightens |
Attenuation:: | max_total_cost | Tightens |
Attenuation:: | 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_spentat line 191,remainingat 197,policyat 203). - Budget tree:
src/budget_hierarchy.rs(evaluateat line 599, over the ancestor chain built at 548). - Billing records:
src/export.rs(create_billing_exportat line 70). - Query interface:
src/query.rs, with the crate-root re-exports at line 33 ofsrc/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.