Export Billing Records
This guide covers the invoicing workflow: taking the metered cost metadata that chio already attaches to receipts and turning it into a flat, denormalized BillingExport bundle suitable for CSV, JSON, or JSON-lines ingestion by external accounting systems. If you are still configuring budgets or monitoring live consumption, start with Budgets & Metering instead. This page picks up after the month is closed and the receipts are already signed.
Receipts are the source of truth
BillingRecord derives from a single CostMetadata block on one receipt, so the full export is independently re-verifiable against the underlying receipts at any time.Why Export Billing Records
Real-time budget monitoring (covered in the Budgets guide) answers can this call proceed? Billing export answers a different set of questions:
- Invoicing: produce a record per tool call, suitable for ingestion by QuickBooks, NetSuite, Stripe Billing, or an internal invoicing pipeline.
- Chargeback: attribute compute, data, and monetary costs back to the agent, session, or tenant that caused them.
- Reconciliation: compare the denormalized export against the signed receipt log to detect drift, missing costs, or ingestion bugs.
- Compliance: hand auditors a flat, timestamped record set with a schema version and a one-to-one mapping back to signed evidence.
What a BillingRecord Contains
Each record is emitted by create_billing_export in chio-metering::export. The schema is identified by the constant BILLING_EXPORT_SCHEMA, which equals "chio.billing-export.v1".
| Field | Type | Description |
|---|---|---|
schema | string | Always "chio.billing-export.v1" |
receipt_id | string | ID of the underlying signed receipt |
timestamp | u64 | Unix seconds at invocation |
timestamp_iso | string | ISO 8601 UTC form, e.g. "2023-11-14T22:13:20Z" |
session_id | string, nullable | Session that produced the receipt |
agent_id | string | Agent that made the invocation |
tool_server | string | Tool server identifier |
tool_name | string | Tool name within the server |
compute_time_ms | u64 | Sum of all ComputeTime dimensions |
data_bytes | u64 | Sum of read + written bytes across DataVolume dimensions |
cost_units | u64, nullable | Aggregate monetary cost in minor currency units |
currency | string, nullable | ISO 4217 code for cost_units |
provider | string, nullable | First upstream provider found among ApiCost dimensions |
Records are collected into a BillingExport envelope with a schema, an exported_at Unix timestamp, a record_count, and an optional total_cost:
{
"schema": "chio.billing-export.v1",
"exported_at": 1712102400,
"record_count": 2,
"total_cost": {
"units": 300,
"currency": "USD"
},
"records": [
{
"schema": "chio.billing-export.v1",
"receipt_id": "rcpt-001",
"timestamp": 1712012345,
"timestamp_iso": "2024-04-01T22:05:45Z",
"session_id": "sess-42",
"agent_id": "agent-main-001",
"tool_server": "srv-ai-inference",
"tool_name": "generate_text",
"compute_time_ms": 200,
"data_bytes": 1536,
"cost_units": 100,
"currency": "USD",
"provider": "openai"
},
{
"schema": "chio.billing-export.v1",
"receipt_id": "rcpt-002",
"timestamp": 1712015000,
"timestamp_iso": "2024-04-01T22:50:00Z",
"agent_id": "agent-main-001",
"tool_server": "srv-ai-inference",
"tool_name": "generate_text",
"compute_time_ms": 180,
"data_bytes": 1024,
"cost_units": 200,
"currency": "USD",
"provider": "anthropic"
}
]
}Null fields are omitted
session_id, cost_units, currency, provider, total_cost) are serialized with skip_serializing_if. When a field is absent in the JSON, treat it as null, not zero. A receipt with no ApiCost dimension has no cost_units, not a zero-valued one.Exporting: CLI and SDK
Two paths produce a billing export. The CLI wraps the same code path for operator use; the Rust SDK is appropriate when the export needs to be embedded in a larger pipeline (warehouse sync, accounting webhook, nightly cron).
CLI: chio evidence export
The operator-facing command is chio evidence export. It writes a verifiable evidence directory covering a time window and an optional capability filter. The directory is end-to-end signature-checkable with chio evidence verify.
# Monthly export for a single capability
chio evidence export \
--output ./billing-2026-04 \
--since 1711929600 \ # 2026-04-01 00:00:00 UTC
--until 1714521600 \ # 2026-05-01 00:00:00 UTC
--capability cap-budget-001
# Verify the package end-to-end
chio evidence verify --input ./billing-2026-04To list the raw receipts that feed the export, use the receipt list command with the same time window. The output is JSON-lines, one receipt per line, suitable for piping to jq or a downstream ingestion job.
# Emit the receipts that back the export
chio receipt list \
--capability cap-budget-001 \
--since 1711929600 \
--until 1714521600 \
--min-cost 0Rust SDK: create_billing_export
For programmatic exports (nightly jobs, SaaS billing webhooks), call create_billing_export directly from chio-metering. It takes a slice of CostMetadata plus an exported_at timestamp and returns a BillingExport.
use chio_metering::export::{create_billing_export, BillingExport};
use chio_metering::cost::CostMetadata;
// Load cost metadata from your receipt store. The exact loader depends on
// your backend (SqliteReceiptStore, a cloud store adapter, etc.).
let records: Vec<CostMetadata> = receipt_store
.load_cost_metadata_between(since_unix, until_unix)?;
let exported_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs();
let export: BillingExport = create_billing_export(&records, exported_at);
// Write JSON
std::fs::write(
"./billing-2026-04.json",
serde_json::to_vec_pretty(&export)?,
)?;
// Or JSON-lines (one BillingRecord per line)
let mut lines = String::new();
for record in &export.records {
lines.push_str(&serde_json::to_string(record)?);
lines.push('\n');
}
std::fs::write("./billing-2026-04.jsonl", lines)?;CSV is schema-compatible
METERING.md §4.3) states that CSV export, where supported, MUST use the same field names as the JSON schema with one record per row and a header line. Convert the JSON-lines output with any standard flattener; no chio field has nested structure.Filtering Exports
create_billing_export itself does not filter: it serializes every record you pass in. Filtering is done upstream, either via chio receipt list / chio evidence export flags or by pre-filtering the CostMetadata slice with CostQuery from chio-metering::query.
| Filter | CostQuery field | Use case |
|---|---|---|
| Time range | since / until | Monthly close, billing period alignment. Inclusive start, exclusive end, Unix seconds. |
| Tenant / agent | agent_id | Per-tenant invoicing where each tenant is one agent. |
| Session | session_id | Per-session chargeback, for example one customer support ticket. |
| Capability / tool | tool_server, tool_name | Isolate cost for a specific capability or SKU. |
| Currency | currency | Produce a single-currency export (see the cross-currency section below). |
| Cap size | limit | Bounded pages; server cap is MAX_COST_QUERY_LIMIT = 500. |
use chio_metering::query::{CostQuery, GroupBy, execute_cost_query};
use chio_metering::export::create_billing_export;
// 1. Filter with CostQuery.
let query = CostQuery {
since: Some(month_start),
until: Some(month_end),
agent_id: Some("agent-tenant-acme".to_string()),
currency: Some("USD".to_string()),
group_by: GroupBy::None,
..Default::default()
};
let query_result = execute_cost_query(&all_records, &query);
if query_result.truncated {
// Matching set exceeded MAX_COST_QUERY_LIMIT (500). Page by narrowing the
// time range and concatenating the resulting exports.
log::warn!("cost query truncated; paginate by time window");
}
// 2. Feed the filtered set into create_billing_export. CostQuery does not
// return CostMetadata directly, so re-filter the source slice by the same
// predicates, or use your receipt store's range loader.
let filtered: Vec<_> = all_records.iter()
.filter(|r| r.timestamp >= month_start && r.timestamp < month_end)
.filter(|r| r.agent_id == "agent-tenant-acme")
.cloned()
.collect();
let export = create_billing_export(&filtered, now_unix);Cost queries are bounded
MAX_COST_QUERY_LIMIT caps a single query at 500 records. When CostQueryResult.truncated is true, narrow the time window and concatenate the resulting exports. This is by design: it prevents a single unbounded query from pulling the entire receipt log into memory.Reconciling Export to Receipts
Every BillingRecord.receipt_id points back to a signed receipt in the kernel log. Reconciliation is the process of confirming that each export row has a matching receipt, and that the totals agree. This is the check you run before handing an invoice to a customer or uploading a batch to an accounting system.
import { readFileSync } from "node:fs";
import { execSync } from "node:child_process";
interface BillingRecord {
schema: string;
receipt_id: string;
timestamp: number;
cost_units?: number;
currency?: string;
}
interface BillingExport {
schema: string;
exported_at: number;
record_count: number;
total_cost?: { units: number; currency: string };
records: BillingRecord[];
}
const exportJson: BillingExport = JSON.parse(
readFileSync("./billing-2026-04.json", "utf8"),
);
// Re-derive the total from records and compare to the envelope.
const bucketByCurrency = new Map<string, number>();
for (const r of exportJson.records) {
if (r.cost_units != null && r.currency != null) {
bucketByCurrency.set(
r.currency,
(bucketByCurrency.get(r.currency) ?? 0) + r.cost_units,
);
}
}
if (bucketByCurrency.size > 1) {
// Mixed currencies: the envelope MUST have total_cost = null per spec.
if (exportJson.total_cost != null) {
throw new Error("mixed currencies but total_cost is not null");
}
} else if (bucketByCurrency.size === 1) {
const [currency, units] = [...bucketByCurrency.entries()][0];
if (
exportJson.total_cost?.units !== units ||
exportJson.total_cost?.currency !== currency
) {
throw new Error("envelope total disagrees with record sum");
}
}
// Cross-check each record against the signed receipt log.
for (const r of exportJson.records) {
const receiptJson = execSync(
`chio receipt show ${r.receipt_id}`,
).toString();
const receipt = JSON.parse(receiptJson);
const financial = receipt.metadata?.financial;
if (r.cost_units != null && financial?.cost_charged !== r.cost_units) {
throw new Error(`cost mismatch on ${r.receipt_id}`);
}
}Three checks cover the common drift modes:
- Envelope vs records. The per-currency sum of
records[].cost_unitsmust equaltotal_cost.unitsfor single-currency exports. For mixed-currency exports,total_costMUST be null (METERING.md §4.2). - Record vs receipt. Each
BillingRecord.receipt_idmust resolve to a signed receipt whosemetadata.financial.cost_chargedmatches the record'scost_units. - Record count.
record_countin the envelope must equalrecords.length. Any mismatch indicates a truncated or corrupted export.
Cross-Currency Handling
A single invocation can accrue ApiCost dimensions in different currencies (for example, a tool that chains USD and EUR providers). The export has three rules for this case, implemented in create_billing_export:
- Per-record:
cost_unitsandcurrencycome from the receipt'stotal_monetary_cost, which only sums dimensions that share a currency with the first one seen. Cross-currency amounts are ignored on the receipt itself. - Export total: the envelope's
total_costsums across records only while every record uses the same currency. As soon as a second currency appears,total_costis set to null. - Conversion: chio does not convert currencies during export. Cross-currency amounts require an oracle feed, which is only available at budget-enforcement time via chio-link. Any FX conversion done downstream (for invoicing in a single reporting currency) must attach its own evidence outside the billing export.
{
"schema": "chio.billing-export.v1",
"exported_at": 1712102400,
"record_count": 2,
"records": [
{
"schema": "chio.billing-export.v1",
"receipt_id": "rcpt-usd",
"timestamp": 1712010000,
"timestamp_iso": "2024-04-01T21:40:00Z",
"agent_id": "agent-x",
"tool_server": "srv-a",
"tool_name": "call",
"compute_time_ms": 100,
"data_bytes": 256,
"cost_units": 75,
"currency": "USD",
"provider": "openai"
},
{
"schema": "chio.billing-export.v1",
"receipt_id": "rcpt-eur",
"timestamp": 1712011000,
"timestamp_iso": "2024-04-01T21:56:40Z",
"agent_id": "agent-x",
"tool_server": "srv-b",
"tool_name": "call",
"compute_time_ms": 80,
"data_bytes": 128,
"cost_units": 50,
"currency": "EUR",
"provider": "mistral"
}
]
}Produce one export per currency
CostQuery.currency before filtering. Each resulting bundle has a non-null total_cost and maps cleanly to a single-currency journal in your accounting system.Common Patterns
Monthly Close
Run a cron job on the first day of every month that exports the prior month's receipts, verifies the bundle, and hands it to the accounting pipeline.
#!/usr/bin/env bash
set -euo pipefail
MONTH_START=$(date -u -d "$(date -u +%Y-%m-01) -1 month" +%s)
MONTH_END=$(date -u -d "$(date -u +%Y-%m-01)" +%s)
LABEL=$(date -u -d "@$MONTH_START" +%Y-%m)
OUT="./billing-$LABEL"
chio evidence export \
--output "$OUT" \
--since "$MONTH_START" \
--until "$MONTH_END"
chio evidence verify --input "$OUT"
# Hand off to accounting (script supplied by operator)
./scripts/upload-to-accounting.sh "$OUT"Per-Agent Chargeback
Split a single kernel's cost across multiple internal teams by emitting one export per agent_id. Each team's export is fully self-contained and can be invoiced or booked against an internal cost center without exposing other teams' activity.
use std::collections::BTreeMap;
use chio_metering::{cost::CostMetadata, export::create_billing_export};
fn exports_by_agent(
records: Vec<CostMetadata>,
now: u64,
) -> BTreeMap<String, chio_metering::BillingExport> {
let mut bucketed: BTreeMap<String, Vec<CostMetadata>> = BTreeMap::new();
for r in records {
bucketed.entry(r.agent_id.clone()).or_default().push(r);
}
bucketed
.into_iter()
.map(|(agent_id, rs)| (agent_id, create_billing_export(&rs, now)))
.collect()
}Multi-Tenant SaaS Billing
If each tenant is represented by a distinct capability token, use chio evidence export --capability to produce one tenant-scoped bundle per billing period. The capability filter ensures the export contains only receipts signed under that tenant's token, which is exactly the set of costs the tenant owes.
# Per-tenant monthly exports
for TENANT_CAP in $(cat ./tenant-caps.txt); do
chio evidence export \
--output "./billing/$TENANT_CAP/$LABEL" \
--since "$MONTH_START" \
--until "$MONTH_END" \
--capability "$TENANT_CAP"
doneDownstream Systems
The export schema is deliberately flat so it maps cleanly onto any billing-side system. chio itself has no opinion about the destination; the pattern is always:
- Call
chio evidence exportorcreate_billing_exportto produce aBillingExport. - Run
chio evidence verifyon the produced directory to confirm it is signature-checkable. - Transform the JSON-lines into the downstream system's ingest format (CSV for QuickBooks, line items for Stripe Billing, a row per record for BigQuery, etc.).
Because BillingRecord has no nested structure, a naive flattener is sufficient for CSV targets. The receipt_id column doubles as a primary key for idempotent upserts, which lets you re-run an export safely against the same downstream bucket or table.
Do not re-number records downstream
receipt_id is the only stable cross-system identifier. If your downstream pipeline assigns a new ID, preserve the original receipt_id as a column so reconciliation can still walk back to signed receipts.Next Steps
- Budgets & Metering · configure budget limits and monitor real-time consumption before the records that this guide exports even exist.
- Settlement · move capital after the export is reconciled, across Manual, Api, Ach, Wire, Ledger, Sandbox, or Web3 rails.
- Query & Audit Receipts · the underlying receipt query surface that feeds every export and every reconciliation.