Chio/Docs

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

The billing export is a projection of the signed receipt log. Every 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".

FieldTypeDescription
schemastringAlways "chio.billing-export.v1"
receipt_idstringID of the underlying signed receipt
timestampu64Unix seconds at invocation
timestamp_isostringISO 8601 UTC form, e.g. "2023-11-14T22:13:20Z"
session_idstring, nullableSession that produced the receipt
agent_idstringAgent that made the invocation
tool_serverstringTool server identifier
tool_namestringTool name within the server
compute_time_msu64Sum of all ComputeTime dimensions
data_bytesu64Sum of read + written bytes across DataVolume dimensions
cost_unitsu64, nullableAggregate monetary cost in minor currency units
currencystring, nullableISO 4217 code for cost_units
providerstring, nullableFirst 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:

billing-export.json
{
  "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

Optional fields (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.

bash
# 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-04

To 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.

bash
# Emit the receipts that back the export
chio receipt list \
    --capability cap-budget-001 \
    --since 1711929600 \
    --until 1714521600 \
    --min-cost 0

Rust 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.

rust
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

The spec (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.

FilterCostQuery fieldUse case
Time rangesince / untilMonthly close, billing period alignment. Inclusive start, exclusive end, Unix seconds.
Tenant / agentagent_idPer-tenant invoicing where each tenant is one agent.
Sessionsession_idPer-session chargeback, for example one customer support ticket.
Capability / tooltool_server, tool_nameIsolate cost for a specific capability or SKU.
CurrencycurrencyProduce a single-currency export (see the cross-currency section below).
Cap sizelimitBounded pages; server cap is MAX_COST_QUERY_LIMIT = 500.
rust
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.

reconcile.ts
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:

  1. Envelope vs records. The per-currency sum of records[].cost_units must equal total_cost.units for single-currency exports. For mixed-currency exports, total_cost MUST be null (METERING.md §4.2).
  2. Record vs receipt. Each BillingRecord.receipt_id must resolve to a signed receipt whose metadata.financial.cost_charged matches the record's cost_units.
  3. Record count. record_count in the envelope must equal records.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_units and currency come from the receipt's total_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_cost sums across records only while every record uses the same currency. As soon as a second currency appears, total_cost is 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.
mixed-currency-export.json
{
  "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

The simplest compliant workflow is to emit one export per currency by setting 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.

monthly-close.sh
#!/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.

rust
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.

bash
# 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"
done

Downstream 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:

  1. Call chio evidence export or create_billing_export to produce a BillingExport.
  2. Run chio evidence verify on the produced directory to confirm it is signature-checkable.
  3. 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.
Export Billing Records · Chio Docs