ReferenceSpec
Metering Spec
Chio metering 1.0: cost dimensions, budget enforcement, billing-export records, and the query interface the chio-metering crate implements.
Source
This page normatively reflects spec/METERING.md in the chio repository. Status: Normative. Version 1.0, dated 2026-04-14. The keywords MUST, SHOULD, and MAY are normative.
Behavior comes from the crate that implements the spec, crates/economy/chio-metering: src/cost.rs for the dimensions and receipt cost metadata, src/budget.rs for the flat enforcer, src/budget_hierarchy.rs for the tree, src/export.rs for billing records, and src/query.rs for the query interface. Where the crate and the spec disagree, the crate decides and the difference is named where it falls.
chio-metering is a library. It is not on the kernel receipt path: the only workspace crate that depends on it is crates/guards/chio-data-guards, whose WarehouseCostGuard imports CostDimension. Read the types below as a contract a host implements, and the kernel's own monetary blocks in Receipt Format as a separate object.
Synopsis
A cost dimension is an internally tagged enum whose tag key is dimension; the crate defines five variants at crates/economy/chio-metering/src/cost.rs:15-62. The two metering schema identifiers are chio.cost-metadata.v1 and chio.billing-export.v1.
#[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>,
},
}Cost dimensions
Each receipt MAY carry zero or more cost dimensions. Each serializes with its snake-case variant name under the dimension tag.
The crate carries one dimension the spec text omits
spec/METERING.md section 2.1 tabulates four variants and omits WarehouseQuery. The enum in crates/economy/chio-metering/src/cost.rs defines five. The enum decides what deserializes, so the variant table follows it.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_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 |
MonetaryAmount
| Field | Type | Description |
|---|---|---|
units | u64 | Cost in minor currency units (e.g., cents for USD) |
currency | string | ISO 4217 currency code (e.g., "USD", "EUR") |
The Custom variant allows operators to track domain-specific metrics (e.g., token counts, request counts) without modifying the protocol.
The WarehouseQuery variant records the dry-run cost estimate a warehouse cost guard reads off a tool call before the query runs. It serializes under the dimension tag "warehouse_query". estimated_cost_usd is a decimal string rather than a fixed-width integer: the guard parses it against its max_cost_per_query_usd limit and preserves the value verbatim on the receipt for auditability. This follows the precedent set by Constraint::MaxTransactionAmountUsd in the capability scope types.
CostMetadata
Per-receipt cost metadata, serialized as JSON. The doc comment on the struct places it in the receipt's metadata map under the cost key.
cost is not one of the kernel's reserved metadata keys, and no crate in the workspace writes it. 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. The kernel writes its own typed block under each of those, merges them last, and rejects a pre-existing collision from caller or hook metadata. Placing cost is therefore the host's job, and what it writes there is ordinary caller metadata: the kernel signs it and does not author it.
| Field | Type | Required | Description |
|---|---|---|---|
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
The total_monetary_cost field is computed by summing all ApiCost dimension amounts. Implementations MUST use saturating addition to prevent overflow.
When multiple ApiCost dimensions use different currencies, only amounts in the first currency encountered are summed. Cross-currency amounts require oracle conversion and are excluded from the automatic total. Implementations SHOULD document this behavior to operators.
When no ApiCost dimensions are present, total_monetary_cost MUST be null.
Budget enforcement
Budget enforcement tracks cumulative spending and rejects invocations that would exceed configured limits. Enforcement is fail-closed: any error during budget evaluation MUST deny the request.
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 the following order. 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 rather than wrapping.
A cost of zero MUST always pass budget checks regardless of current spending levels.
BudgetViolation
When a budget check fails, the enforcer 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
After a tool invocation succeeds and the receipt is signed, the enforcer records the cost against all applicable counters (total, session, agent, tool). Recording uses saturating addition.
The record operation does not enforce limits; it only updates tracking counters. Budget enforcement happens exclusively in the check operation before invocation.
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 rather than wrapping, and policy() borrows the active BudgetPolicy. The per-session, per-agent, and per-tool maps stay private; a caller reads them only through a check violation. Defined at crates/economy/chio-metering/src/budget.rs:191, budget.rs:197, and budget.rs:203.
Budget hierarchy
BudgetEnforcer holds one flat policy. Above it sits a second model the specification text does not cover: a tree of nodes, one per organizational scope, where every ancestor caps its descendants. crates/economy/chio-metering/src/lib.rs declares the module at line 20 and re-exports its public types at lines 27 to 30, so a caller reaches them as chio_metering::BudgetTree and siblings.
The tree owns shape and limits, never storage. A caller reads current spend from its own store into a SpendSnapshot and passes it in for each decision.
| Type | Fields or variants | Behavior |
|---|---|---|
BudgetNodeId | String, transparent | Node identifier, conventionally scope/name. The tree validates uniqueness and acyclic parents; it does not validate the string shape. |
BudgetWindow | Daily, Monthly, Rolling { seconds } | Tagged under 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_bytes | Every field is optional, so 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, enabled | One 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_bytes | The four capped dimensions plus the currency that denominates the first. Used for both the draft and the recorded spend. |
PerWindowSpend | window_start, current | One node's spend inside one window bucket. The bucket start is compared against the node's own window, so a stale row denies rather than under-counting. |
SpendSnapshot | evaluated_at, per_node | The caller-supplied read of the store. A snapshot with no evaluated_at denies as an expired window rather than defaulting to now. |
BudgetTree | insert, validate, ancestors, descendants, evaluate, serialize, deserialize | The 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 } | Tagged under decision. There is no third outcome and no warning state. |
BudgetDenyReason | NodeDisabled, DimensionExceeded, CurrencyMismatch, ArithmeticOverflow, WindowExpired, UnknownNode | Tagged under 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, InvalidSerialization | Construction and deserialization failures. Distinct from a deny: a tree that cannot be built 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 is read 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 rather than saturating it, so an overflow denies where the flat 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 is the one from the node closest to the root and an operator reads the widest policy boundary first. A walk that records nothing returns Allow. Defined at crates/economy/chio-metering/src/budget_hierarchy.rs:599, over the ancestor chain built at budget_hierarchy.rs:548.
Billing export
Billing export transforms Chio cost metadata into flat, denormalized records suitable for ingestion by external billing systems.
BillingRecord
| Field | Type | Description |
|---|---|---|
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 (e.g., "2023-11-14T22:13:20Z") |
session_id | string | Session ID (nullable) |
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 (nullable) |
currency | string | ISO 4217 currency code (nullable) |
provider | string | Upstream provider (nullable) |
All timestamps MUST use ISO 8601 format with UTC timezone and Z suffix. Implementations MUST fall back to "unix:<timestamp>" when the Unix timestamp cannot be converted to a calendar date.
BillingExport
A batch of billing records.
| Field | Type | Description |
|---|---|---|
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 (null if mixed currencies) |
records | BillingRecord[] | The billing records |
When records contain costs in multiple currencies, total_cost MUST be null rather than summing incompatible amounts.
One function builds the batch: create_billing_export(records, exported_at) at crates/economy/chio-metering/src/export.rs:70. 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 null as soon as a second currency appears.
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.
The crate meets the MUST and leaves the SHOULD to its caller. BillingRecord and BillingExport derive Serialize, which gives JSON through serde_json. There is no CSV writer in crates/economy/chio-metering/src/export.rs, and the crate takes no CSV dependency in its manifest. A host that wants the CSV form derives it from the record fields.
Query interface
The query interface supports cost aggregation across multiple dimensions.
crates/economy/chio-metering/src/lib.rs re-exports CostQuery, CostQueryResult, and CostSummary at line 33, and stops there. GroupBy, CostGroup, execute_cost_query, and MAX_COST_QUERY_LIMIT are public on the module, so a caller reaches them at chio_metering::query:: and not at the crate root.
CostQuery
All filter fields are optional. When omitted, the filter matches all records. Multiple filters are ANDed together.
| 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 detailed records to return |
group_by | GroupBy | Aggregation dimension |
GroupBy
| Value | Description |
|---|---|
none | No grouping; return individual receipt costs |
session | Group by session ID |
agent | Group by agent ID |
tool | Group by tool key ("server:tool_name") |
CostQueryResult
| Field | Type | Description |
|---|---|---|
summary | CostSummary | Aggregate statistics across all matching records |
groups | CostGroup[] | Grouped rows (empty when group_by is none) |
truncated | bool | Whether the result was truncated due to limit |
CostSummary
| Field | Type | Description |
|---|---|---|
receipt_count | u64 | Total matching receipts |
total_compute_time_ms | u64 | Aggregate compute time |
total_data_bytes | u64 | Aggregate data volume |
total_monetary_cost | MonetaryAmount | Aggregate cost (null if mixed currencies) |
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_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 (null if mixed currencies) |
Limits
The maximum number of records returned by a single query MUST NOT exceed MAX_COST_QUERY_LIMIT, which the crate sets to 500 at crates/economy/chio-metering/src/query.rs:12. When the matching set exceeds the limit, the result MUST set truncated to true and return only the first limit records. An operator-provided limit is capped at that constant.
Related
- Pricing · what a call costs before metering measures it
- Reconciliation · adjusting a kernel budget hold to realized cost, which this crate does not do
- Economics · budgets and metering as a concept, without the field shapes
- Receipt Format · the kernel-written monetary blocks on a signed receipt