Chio/Docs
LOGIN · JOIN

BuildEconomics

Export Billing Records

Export signed receipt costs as a BillingExport bundle for an accounting system.

Two artifacts, not one

This page covers two things an operator confuses at their peril. chio evidence export writes a signed-receipt evidence package: thirteen files of receipts, lineage, and a hash manifest. create_billing_export in chio-metering turns a slice of CostMetadata into a BillingExport. The CLI does not call it, and the package it writes contains no BillingRecord. The receipt log is where the two meet: a BillingRecord carries the receipt_id its cost metadata named, and the evidence package is how you prove that receipt exists.

Prerequisites

  • A receipt store, named by --receipt-db or reached through --control-url. Exactly one of the two; passing both is refused.
  • A tenant read boundary. --tenant <id> for one tenant, or --admin-all for an explicit cross-tenant operator read. The export refuses without one.
  • Cost metadata, if you want billing records. create_billing_export reads CostMetadata values your integration builds and stores. Nothing in the kernel writes them onto a receipt today, so the pipeline that collects them is yours. The money field the kernel itself writes is metadata.financial.cost_charged, on priced calls only.
  • A Rust build, if you take the library path. chio-metering is a crate, not a CLI verb.

Why Export Billing Records

Per-call budget checks answer 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 receipts.

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

crates/economy/chio-metering/src/export.rs20-51rust
pub struct BillingRecord {
    /// Schema version.
    pub schema: String,
    /// Receipt ID.
    pub receipt_id: String,
    /// Unix timestamp (seconds) of the invocation.
    pub timestamp: u64,
    /// ISO 8601 timestamp for human-readable export.
    pub timestamp_iso: String,
    /// Session ID.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,
    /// Agent that triggered the cost.
    pub agent_id: String,
    /// Tool server.
    pub tool_server: String,
    /// Tool name.
    pub tool_name: String,
    /// Compute time in milliseconds.
    pub compute_time_ms: u64,
    /// Total data transferred in bytes.
    pub data_bytes: u64,
    /// Monetary cost amount (minor units).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cost_units: Option<u64>,
    /// Currency code for cost_units.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub currency: Option<String>,
    /// Upstream provider that charged the cost.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,
}

Four values are derived rather than copied. compute_time_ms is the saturating sum of every ComputeTime dimension; data_bytes is bytes read plus bytes written across every DataVolume dimension; provider is the first provider found among the ApiCost dimensions; and timestamp_iso is the RFC 3339 rendering of timestamp, so the two can never disagree in real output.

cost_units is in the currency's minor units. It is copied from the record's total_monetary_cost, which is a stored field rather than a computed one: a CostMetadata carrying ApiCost dimensions but never passed through compute_total_monetary_cost exports with a provider and no cost_units.

Records are collected into a BillingExport envelope:

crates/economy/chio-metering/src/export.rs55-67rust
pub struct BillingExport {
    /// Schema version.
    pub schema: String,
    /// Export timestamp (Unix seconds).
    pub exported_at: u64,
    /// Total number of records in this export.
    pub record_count: u64,
    /// Total monetary cost across all records.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub total_cost: Option<MonetaryAmount>,
    /// The billing records.
    pub records: Vec<BillingRecord>,
}

A real export, from create_billing_export over two cost records naming two receipts that exist in a receipt store:

billing-export · singletranscript
$ cargo run -- single ids.txt
{
  "schema": "chio.billing-export.v1",
  "exported_at": 1777593600,
  "record_count": 2,
  "total_cost": {
    "units": 300,
    "currency": "USD"
  },
  "records": [
    {
      "schema": "chio.billing-export.v1",
      "receipt_id": "084e222b3a438c3893e580da73c01c2f3af56bab6fa899ad54396adb3b3e23ec",
      "timestamp": 1775078745,
      "timestamp_iso": "2026-04-01T21:25: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": "11bcbb02affa85aa3bf5b3a4bb6a568169308b07fb9d0308ec83b27329eade64",
      "timestamp": 1775081400,
      "timestamp_iso": "2026-04-01T22:10: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"
    }
  ]
}
exit 0

The envelope total is the per-currency sum of the records: 100 plus 200 is 300 minor units, three US dollars. And 1775078745 really does render as 2026-04-01T21:25:45Z, because the export computes the string from the integer.

Null fields are omitted

Optional fields (session_id, cost_units, currency, provider, total_cost) carry skip_serializing_if, so they vanish from the JSON rather than serializing as null. The second record above has no session_id line for that reason. When a field is absent, treat it as null, not zero: a record with no ApiCost dimension has no cost_units, not a zero-valued one.

Exporting: CLI and SDK

The two paths produce different artifacts. The CLI writes the signed evidence package an auditor asks for. The Rust library writes the flat billing rows an accounting system ingests. A monthly close usually runs both: the package is the proof, the export is the invoice.

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. Verify the directory with chio evidence verify.

The export requires a backend selector, exactly one of --receipt-db <path> or --control-url <url>. It also fails closed without an explicit tenant read boundary: pass --tenant <id> for a single-tenant export or --admin-all for an explicit cross-tenant operator read.

bash
# Monthly export for a single capability, scoped to one tenant.
# --since is 2026-04-01T00:00:00Z and --until is 2026-05-01T00:00:00Z.
# Both bounds are inclusive, so a receipt landing exactly on the second
# instant appears in this month's export and the next one's.
chio evidence export \
    --receipt-db ./receipts.sqlite \
    --tenant acme \
    --output ./billing-2026-04 \
    --since 1775001600 \
    --until 1777593599 \
    --capability cap-budget-001

# Verify the package
chio evidence verify --input ./billing-2026-04

A comment after a backslash is not a comment

Writing --since 1775001600 \ # start does not annotate the line. The backslash escapes the space that follows it, not the newline, so the command terminates at that point and the shell tries to run --until as a program. Put comments on their own lines, as above.

Verify the result

A whole-store export with no window is the smallest version of the same two commands, and it is the one to run first against a development receipt database to see the shape of the output. The export writes nothing to stdout and exits zero:

receipt-audit · exporttranscript
$ chio --receipt-db ./receipts.db evidence export --admin-all --output ./package
$ ls -1 package
README.txt
capability-lineage.ndjson
checkpoint-consistency-proofs.ndjson
checkpoint-equivocations.ndjson
checkpoint-publications.ndjson
checkpoint-witnesses.ndjson
checkpoints.ndjson
child-receipts.ndjson
inclusion-proofs.ndjson
manifest.json
query.json
receipts.ndjson
retention.json
exit 0

Thirteen files. Twelve of them appear in the manifest's files array with a sha256 and a byte count, which is what verified_files: 12 counts; manifest.json is the thirteenth and does not hash itself. The verify prints a count per artifact class. This run covers three receipts, one allowed and two denied:

receipt-audit · verifytranscript
$ chio evidence verify --input ./package
evidence package verified
tool_receipts:          3
child_receipts:         0
checkpoints:            0
checkpoint_publications: 0
checkpoint_witnesses:   0
checkpoint_consistency_proofs: 0
checkpoint_equivocations: 0
capability_lineage:     3
inclusion_proofs:       0
uncheckpointed_receipts: 3
authorized_receipts:     1
trace_observations:      0
advisory_evaluations:    0
verified_files:         12
child_receipt_scope:    FullQueryWindow
transparency_preview_logs: 0
publication_state:      transparency_preview
exit 0

tool_receipts counts every tool receipt in the window. authorized_receipts counts the allows that also passed every cryptographic check: recomputed id, signature, and action hash. So the difference between the two is not simply the denial count, it is everything that is not a fully verified allow. Read the denial count off the receipts themselves rather than subtracting.

To list the raw receipts that feed a reconciliation, use chio receipt list with the same window and tenant scope. The output is JSON Lines, one receipt per line, and the read boundary is required:

bash
# Emit the receipts for one capability inside the billing window
chio receipt list \
    --receipt-db ./receipts.sqlite \
    --tenant acme \
    --capability cap-budget-001 \
    --since 1775001600 \
    --until 1777593599 \
    --limit 200

Two flags are easy to get wrong here. --min-cost and --max-cost each require --cost-currency, and the command refuses to parse without it. And --limit is clamped to 200 with no warning and a zero exit, so a window with more receipts than that needs the cursor loop from Query & Audit Receipts.

receipt-audit · list-cost-bandtranscript
$ chio --receipt-db ./receipts.db receipt list --admin-all --min-cost 0
error: the following required arguments were not provided:
  --cost-currency <COST_CURRENCY>

Usage: chio receipt list --cost-currency <COST_CURRENCY> --admin-all --min-cost <MIN_COST>

For more information, try '--help'.
exit 2

Rust SDK: create_billing_export

For programmatic exports (nightly jobs, SaaS billing webhooks), call create_billing_export directly from chio-metering. Its signature is create_billing_export(records: &[CostMetadata], exported_at: u64) -> BillingExport: a slice in, an envelope out, no I/O and no filtering.

Where the slice comes from is your integration's problem. CostMetadata documents itself as living on a receipt under the cost metadata key, but no kernel path writes that key and no store exposes a loader for it, so the collection step below is a placeholder for yours.

rust
use chio_metering::export::{create_billing_export, BillingExport};
use chio_metering::cost::CostMetadata;

// Collect the cost metadata your metering pipeline recorded for the window.
// Nothing in chio writes CostMetadata onto a receipt, so this loader is
// yours: whatever your integration persisted alongside each receipt_id.
let records: Vec<CostMetadata> = load_cost_metadata(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.

crates/economy/chio-metering/src/query.rs19-55rust
pub struct CostQuery {
    /// Filter by session ID.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub session_id: Option<String>,

    /// Filter by agent ID.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub agent_id: Option<String>,

    /// Filter by tool server.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_server: Option<String>,

    /// Filter by tool name.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tool_name: Option<String>,

    /// Start of time range (inclusive, Unix seconds).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub since: Option<u64>,

    /// End of time range (exclusive, Unix seconds).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub until: Option<u64>,

    /// Currency filter -- only include costs in this currency.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub currency: Option<String>,

    /// Maximum number of detailed records to return.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit: Option<usize>,

    /// Aggregation group-by dimension.
    #[serde(default)]
    pub group_by: GroupBy,
}

since is inclusive and until is exclusive here, which is the opposite of the CLI: an evidence export's --until is inclusive. A month boundary passed to both therefore selects a receipt landing exactly on midnight in the CLI and drops it in CostQuery. Pick one convention for your close and subtract a second where you need to. limit is capped at MAX_COST_QUERY_LIMIT, 500.

CostQuery has no tenant field. Per-tenant invoicing works through agent_id when each tenant is one agent, or through the CLI's --tenant boundary on the receipt side.

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.tstypescript
import { readFileSync } from "node:fs";
import { spawnSync } 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. There is no
// single-receipt fetch subcommand; list the window and index by id. The
// read fails closed without a tenant scope, so pass --tenant. --limit is
// clamped to 200 with no warning, and the cursor comes back on stderr as
// "next_cursor=<seq> total_count=<n>", so page rather than asking for more.
const RECEIPT_DB = "./receipts.sqlite";
const TENANT = "acme";

const receiptById = new Map<string, any>();
let cursor: string | undefined;
for (;;) {
  const page = [
    "chio receipt list",
    `--receipt-db ${RECEIPT_DB}`,
    `--tenant ${TENANT}`,
    "--limit 200",
    cursor ? `--cursor ${cursor}` : "",
  ].join(" ");
  const proc = spawnSync("bash", ["-c", page], { encoding: "utf8" });
  if (proc.status !== 0) throw new Error(proc.stderr);
  for (const line of proc.stdout.split("\n")) {
    if (line.trim() === "") continue;
    const receipt = JSON.parse(line);
    receiptById.set(receipt.id, receipt);
  }
  const next = /next_cursor=(\d+)/.exec(proc.stderr);
  if (!next) break;
  cursor = next[1];
}

for (const r of exportJson.records) {
  const receipt = receiptById.get(r.receipt_id);
  if (receipt === undefined) {
    throw new Error(`no signed receipt for ${r.receipt_id}`);
  }
  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. Comparing amounts is a check you opt into: cost_units comes from your cost metadata and metadata.financial.cost_charged is written by the budget path, and nothing in the kernel reconciles the two for you. They are at least in the same unit, both minor currency units, so the comparison is meaningful when both are present.
  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.
billing-export · mixedtranscript
$ cargo run -- mixed ids.txt
{
  "schema": "chio.billing-export.v1",
  "exported_at": 1777593600,
  "record_count": 2,
  "records": [
    {
      "schema": "chio.billing-export.v1",
      "receipt_id": "084e222b3a438c3893e580da73c01c2f3af56bab6fa899ad54396adb3b3e23ec",
      "timestamp": 1775078745,
      "timestamp_iso": "2026-04-01T21:25:45Z",
      "agent_id": "agent-main-001",
      "tool_server": "srv-ai-inference",
      "tool_name": "generate_text",
      "compute_time_ms": 100,
      "data_bytes": 256,
      "cost_units": 75,
      "currency": "USD",
      "provider": "openai"
    },
    {
      "schema": "chio.billing-export.v1",
      "receipt_id": "11bcbb02affa85aa3bf5b3a4bb6a568169308b07fb9d0308ec83b27329eade64",
      "timestamp": 1775081400,
      "timestamp_iso": "2026-04-01T22:10:00Z",
      "agent_id": "agent-main-001",
      "tool_server": "srv-ai-inference",
      "tool_name": "generate_text",
      "compute_time_ms": 80,
      "data_bytes": 128,
      "cost_units": 50,
      "currency": "EUR",
      "provider": "mistral"
    }
  ]
}
exit 0

There is no total_cost key at all. The implementation sets the field to None and the field carries skip_serializing_if, so the spec's "MUST be null" shows up on the wire as an absent key. A reconciler comparing with != in JavaScript is fine, since undefined != null is false; one comparing with !== is not.

Produce one export per currency

To keep each export single-currency, 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.shbash
#!/usr/bin/env bash
set -euo pipefail

RECEIPT_DB="./receipts.sqlite"
MONTH_START=$(date -u -d "$(date -u +%Y-%m-01) -1 month" +%s)
# --until is inclusive, so stop one second short of the next month to keep a
# receipt landing exactly on the boundary out of two consecutive closes.
MONTH_END=$(( $(date -u -d "$(date -u +%Y-%m-01)" +%s) - 1 ))
LABEL=$(date -u -d "@$MONTH_START" +%Y-%m)
OUT="./billing-$LABEL"

# Whole-kernel close: --admin-all is the explicit cross-tenant operator read.
chio evidence export \
    --receipt-db "$RECEIPT_DB" \
    --admin-all \
    --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. --admin-all lets the operator read across
# tenants; --capability narrows each bundle to one tenant's token.
for TENANT_CAP in $(cat ./tenant-caps.txt); do
    chio evidence export \
        --receipt-db ./receipts.sqlite \
        --admin-all \
        --output "./billing/$TENANT_CAP/$LABEL" \
        --since "$MONTH_START" \
        --until "$MONTH_END" \
        --capability "$TENANT_CAP"
done

Downstream Systems

The flat export schema maps to most billing systems. Chio does not prescribe a destination. Use this flow:

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

Failures and Recovery

What you seeWhat it meansWhat to do
evidence export requires an explicit receipt read boundaryNo --tenant and no --admin-allName the boundary. The export refuses rather than defaulting to every tenant
use either --receipt-db or --control-url for evidence export, not bothTwo backends named at onceDrop one
output directory must be emptyA previous export is still in the target directoryExport to a fresh path. Re-running a monthly close needs a new directory, not an overwrite
the following required arguments were not provided: --cost-currencyA cost band on receipt list with no currencyAdd --cost-currency USD
no signed receipt for <id> from a reconcilerUsually the 200-row clamp, not a missing receiptPage with the cursor from stderr and compare against total_count before concluding anything is missing
envelope total disagrees with record sumA record set that is not single-currency, or one whose total_monetary_cost was never computedCheck for a second currency first. Then check that each CostMetadata went through compute_total_monetary_cost
cost query truncated, or CostQueryResult.truncated trueThe match set exceeded MAX_COST_QUERY_LIMITNarrow the time window and concatenate the resulting exports
A record with a provider and no cost_unitstotal_monetary_cost was never computed on that recordCall compute_total_monetary_cost before exporting. The export copies the field, it does not derive it

Next Steps

  • Budgets & Metering · configure budget limits and monitor consumption before exporting records
  • Settlement · move capital after the export is reconciled, across Manual, Api, Ach, Wire, Ledger, Sandbox, or Web3 rails.
  • Query & Audit Receipts · query the receipts that feed exports and reconciliation