Audit APIs
Give an auditor a signed, bounded export of an agent's receipts, verify it offline, and show the window is complete.
On this page
An auditor can take a signed slice of one agent's receipt history, confirm that the operator did not edit it on the way out, and show that the slice holds every matching record.
The slice comes from one read-only route, GET /regulatory/receipts, which the operator's HTTP adapter mounts behind a regulator token. The handler reads a filtered window of the receipt log, wraps the rows in an envelope signed with the kernel's receipt-signing key, and returns it. The running example asks for ninety days of one deployed agent's decisions.
A valid signature over part of the window
350 receipts match and 200 return, the most one response carries. The signature still verifies: the envelope is authentic, and the window is incomplete.
A regulator token buys one bounded, read-only query
The operator provisions a token bound to one RegulatorIdentity. The auditor sends it in X-Regulatory-Token with the agent's subject key and a window in Unix seconds, here the ninety days from 1740000000 to 1747776000.
curl --fail-with-body --silent --show-error --get \
'https://kernel.larkfield.example/regulatory/receipts' \
--data-urlencode 'agent=<agent-subject-hex>' \
--data-urlencode 'after=1740000000' \
--data-urlencode 'before=1747776000' \
--data-urlencode 'limit=200' \
-H 'X-Regulatory-Token: <regulator-token>' \
--output export.json{
"body": {
"schema": "chio.regulatory.receipt-export.v1",
"agentId": "[64 hex, the agent subject key]",
"after": 1740000000,
"before": 1747776000,
"matchingReceipts": 142,
"generatedAt": 1747776123,
"receipts": []
},
"signerKey": "[64 hex, the kernel receipt-signing public key]",
"signature": "[128 hex]"
}You should see a SignedExportEnvelope with three keys: body, signerKey, and signature. The signature covers the canonical JSON of body alone. matchingReceipts counts every receipt that matched the query, and receipts carries at most 200 of them. The result omits all 142 receipt bodies and abbreviates the keys, so verify the full response saved in export.json.
Replace the host, agent key, and token with your deployment's values. The query has no tenant field: the adapter and its receipt-source integration decide which data a regulator may read, and the agent filter only narrows a query that is already authorized.
Read more in Query Audit Receipts, which runs the same queries from the operator's side.
The envelope verifies offline against the kernel's key
verify_regulatory_export checks the schema id, confirms the body canonicalizes, and then checks the signature against the kernel public key the auditor expects, the deployment's ChioKernel::public_key().
use chio_http_core::regulatory_api::{
verify_regulatory_export, SignedRegulatoryReceiptExport,
};
let envelope: SignedRegulatoryReceiptExport =
serde_json::from_reader(File::open("export.json")?)?;
let kernel_public_key = load_kernel_public_key();
let verified = verify_regulatory_export(&envelope, &kernel_public_key)?;
assert!(verified, "envelope signature did not match kernel key");The function separates three outcomes. A body whose schema differs from chio.regulatory.receipt-export.v1 is a BadRequest naming the schema it found. A body that fails to canonicalize is a Signing error, so a malformed body reads differently from a bad signature. A signer key other than the expected one returns Ok(false).
The signed envelope is the primary audit record. Retain it with the fingerprint of the kernel public key that verified it, and the same envelope re-verifies years later without the live kernel.
A signed count shows whether the window is complete
Compare body.receipts.length with body.matchingReceipts. The count is taken before the 200-row limit, so a valid signature over 200 returned rows still leaves an export of 350 matches incomplete.
- Counts agree. The response contains the source's matches for that query at query time. Retain its bounds and its signed count with the records.
- Truncated. Split the inclusive integer-second range
[after, before]at a midpoint into[after, midpoint]and[midpoint + 1, before], then repeat the count check on each half. - One crowded second. When a single second for one agent still holds more than 200 matches, stop: the query has no cursor or sequence-range parameter. Collect those rows through a separately authorized store export and retain this response as incomplete.
- Combining exports. Keep every signed envelope and deduplicate receipt identities across them. If history can change during collection, use the backing integration's stable snapshot or retention procedure, because each query reads the store once.
Narrowing the window is the only pagination, which keeps the handler stateless and the export deterministic. A complete export records what the authorized source returned at query time; actions that never reached the kernel, and receipts the source no longer retains, sit outside any envelope.
The query has no decision-class filter. To review refusals, read the decision field on each returned receipt and keep the variants in scope.
An evidence package ties each receipt to a signed checkpoint
A ChioReceipt carries its signature, content hash, and policy hash, and no Merkle path. To show that a receipt was committed to a checkpoint, the auditor asks the operator for an evidence export.
$ chio evidence export \
--output ./pkg \
--receipt-db ./receipts.db \
--admin-all \
&& ls ./pkgREADME.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
You should see a directory that pairs the receipts with the signed KernelCheckpoint batch and a list of ReceiptInclusionProof records (checkpoint_seq, leaf_index, merkle_root, proof). Each proof checks the receipt's canonical bytes against the checkpoint's Merkle root. manifest.json, schema chio.evidence_export_manifest.v1, lists every file with its SHA-256 and byte count beside the receipt, checkpoint, and inclusion-proof counts, so the directory is tamper-evident before anyone opens a receipt.
The export fails closed on the read boundary. Without --admin-all or an explicit --tenant, it refuses to choose whose evidence the caller may read and exits 1:
$ chio evidence export \
--output ./pkg-unscoped \
--receipt-db ./receipts.dberror[CHIO-KERNEL-RECEIPT-READ-CONTEXT-MISSING]: evidence export needs a read boundary help: pass --tenant <id> to export one tenant's receipts, or --admin-all to export every tenant as the operator code: urn:chio:error:kernel:receipt-read-context-missing docs: https://chio.computer/docs/reference/errors#error-kernel-receipt-read-context-missing
Read more in Verifiable evidence, which covers checkpoint chains and offline verification.
Framework mappings cite the export as recording evidence
Each framework page maps a clause to the Chio record that answers it. The export envelope answers the recording and retention clauses; other mechanisms answer access, security, and third-party clauses.
| Chio mechanism | Framework | Clause |
|---|---|---|
| Regulatory export envelope | EU AI Act | Art. 12, Art. 19 |
| Regulatory export envelope | ISO/IEC 42001 | 9.1 |
| Receipt signature and checkpoint inclusion | NIST AI RMF | MEASURE-2.7 |
| Capability scope and authority chain | NIST AI RMF | GOVERN-2.1 |
| Settlement watchdog outcomes | ISO/IEC 42001 | 8.1 |
| Compliance certificates and bilateral peer attestations | NIST AI RMF | GOVERN-6.1 |
The framework pages carry the full mappings: NIST AI RMF across the GOVERN, MAP, MEASURE, and MANAGE functions; the EU AI Act for automatic recording (Article 12), provider log retention (Article 19), technical documentation and retention (Annex IV), human oversight (Article 14), and monetary risk-management accountability (Article 9); ISO/IEC 42001 clause by clause; and Compliance Frameworks for PCI DSS, the OWASP LLM Top 10, and the cross-framework view.
The operator owns the obligations around the record: which personal and cardholder data may enter a receipt, how long receipts are kept, and how an erasure request is answered. KYC and AML workflows, sanctions screening, travel-rule handling, and licensing also stay with the operator. The HIPAA, PCI DSS, and GDPR entries give the details for each.
Route, envelope, errors, and data protection
Search for RegulatorIdentity, matchingReceipts, or retention_days.
The handler, query, envelope, and errors live in crates/platform/chio-http-core/src/regulatory_api.rs.
Route and query
chio-http-core embeds no HTTP server. A framework adapter wires handle_regulatory_receipts_signed into its own router and forwards query-string fields through RegulatoryReceiptsQuery. The route constant is REGULATORY_RECEIPTS_PATH, /regulatory/receipts, registered as a GET.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RegulatoryReceiptsQuery {
/// Filter by agent subject (hex-encoded Ed25519 public key).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent: Option<String>,
/// Include only receipts with `timestamp >= after`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<u64>,
/// Include only receipts with `timestamp <= before`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub before: Option<u64>,
/// Maximum rows to return (capped at
/// [`MAX_REGULATORY_EXPORT_LIMIT`]).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
}limit_or_default() clamps the caller's limit to the range 1 through 200 and defaults to 200. Filters are optional: an unfiltered query returns the whole authorized source when it has at most 200 matches.
Handler order
The handler runs four steps in order:
- It refuses a caller with no
RegulatorIdentity. - It refuses a window whose
afteris greater than itsbefore, with the message"after must be <= before". - It queries the store under
ReceiptReadContext::admin_service(). - It signs the body.
The handler is read-only by construction. The store trait it consumes, RegulatoryReceiptSource, exposes one method, query_receipts, and the backing store is append-only. The API never creates, supersedes, archives, or erases receipt state; retention rotates aged receipts into a separate archive.
Export body and envelope
pub const REGULATORY_RECEIPT_EXPORT_SCHEMA: &str = "chio.regulatory.receipt-export.v1";
pub const MAX_REGULATORY_EXPORT_LIMIT: usize = 200;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RegulatoryReceiptExport {
pub schema: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub before: Option<u64>,
pub matching_receipts: u64,
pub generated_at: u64,
pub receipts: Vec<ChioReceipt>,
}
pub type SignedRegulatoryReceiptExport = SignedExportEnvelope<RegulatoryReceiptExport>;SignedExportEnvelope is rename_all = "camelCase" and deny_unknown_fields over exactly three keys: body, signerKey, and signature. The signature covers the canonical JSON of body, which is camelCase too. matching_receipts is the full count before the limit; receipts holds up to limit_or_default() rows.
An absent value is omitted, never sent as null: agentId, after, and before carry skip_serializing_if, so an unfiltered export body has four keys: schema, matchingReceipts, generatedAt, and receipts.
Errors
RegulatoryApiError has four variants. Each carries a status, a stable code, and a message, and body() renders them as {"error": <code>, "message": <message>}.
| Variant | Status | Code | Cause |
|---|---|---|---|
BadRequest(String) | 400 | bad_request | Malformed query, including after > before. |
Unauthorized | 401 | unauthorized | No authorized Regulator reached the handler. Message: "regulatory API access denied". |
Store | 503 | store_ | The backing receipt store could not respond. |
Signing(String) | 500 | signing_error | Canonical-JSON encoding or signing failed. |
Authorization
The framework adapter validates the caller and passes an authorized RegulatorIdentity to the handler:
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegulatorIdentity {
/// Stable identifier for audit logging (e.g. regulator name,
/// agency id).
pub id: String,
}The header has its own constant, REGULATORY_TOKEN_HEADER, X-Regulatory-Token. Its doc comment states the adapter's rule: an adapter must not expose /regulatory/* without requiring that header, and the handler fails closed when the caller identity is missing. Map each token to one RegulatorIdentity and log every successful query. The kernel imposes no token format, so the adapter uses whatever mechanism a security review requires.
Jurisdiction is the operator's rule. The adapter decides which regulators see which slices of the receipt log, the same way capability scoping handles cross-tenant isolation elsewhere.
Compliance score route
The same route group registers POST /compliance/score (COMPLIANCE_SCORE_PATH, handler in crates/platform/chio-http-core/src/compliance.rs). It takes an agent subject and a time window and returns a ComplianceScore on a 0..=1000 scale, computed by compliance_score in chio-kernel. It is a compute-only rollup: the receipt export is the evidence, the score summarizes it, and as_underwriting_evidence projects the score into underwriting.
Tax and billing records
Receipt-driven tax reporting consumes the same billing pipeline as ordinary reconciliation. The regulatory export gives auditors the canonical receipts; the billing export (chio.billing-export.v1 from chio-metering) gives finance the per-receipt cost rollup. The regulatory envelope is signed. The billing export is an unsigned projection, so retain its source receipts and the export procedure for independent comparison.
- Select the reporting window your finance process requires, export the intended receipt set, and retain its selection criteria.
- Apply the agent and tool filters your integration supports. A jurisdiction annotation is application metadata; retain its meaning and source.
- Cross-reference the regulatory export for any period under audit. Receipt ids appear in both, and the rolled-up cost in the billing export must reconcile to the per-receipt amounts in the regulatory export.
Reconciliation covers the cycle that produces these records, and Export Billing Records the operator workflow.
HIPAA
Configure which Protected Health Information (PHI) may enter the recorded evidence. Chio's data guards (Data Layer) redact configured content before the receipt is sealed; verify the resulting receipt. The regulatory export returns the stored form and performs no redaction of its own, so keep unredacted PHI out of signed envelopes. HIPAA compliance remains the operator's program.
PCI DSS
Keep cardholder data out of receipt contents and adapter metadata. For payment linkage, use the rail's opaque payment_reference, which is the transaction_id the adapter returned. Payment rails plug in as optional modules, and three adapters ship: X402PaymentAdapter (an x402 payment reference), AcpPaymentAdapter (an ACP-Commerce shared-payment-token id), and SimPaymentAdapter (a deterministic id that performs no HTTP and moves no funds).
A card-network bridge is an adapter the operator writes against the PaymentAdapter trait, and that integration enforces the data boundary: identifiers and metadata accept arbitrary strings, and the trait leaves cardholder-data screening to the integration. The regulator cross-references the rail's own logs through the operator's PCI-scoped systems.
GDPR
- Data minimization. The export supports agent and time filters plus a 200-row cap. Filters are optional, so enforce the permitted scope in the adapter and receipt source; a row limit authorizes nothing.
- Audit trail. Every export carries
generated_at, and the framework adapter logs the regulator id. The export is a discoverable record for the data subject's right of access. - Right to erasure. Receipts are append-only and signed, with no per-record deletion or in-place erasure path. Time- and size-based retention governs the log (
RetentionConfigonKernelConfig:retention_daysdefault 90,max_size_bytesdefault 10 GB). Aged receipts rotate into a separate archive database and stay verifiable against the Merkle checkpoint roots preserved there. Chio ships no Article 17 erasure mechanism, and archive rotation is not erasure: the operator defines retention and deletion procedures for the deployment and verifies any configured redaction before sealing.
For breach notification (GDPR Article 33), the operator joins the receipt log against access logs to establish whose data was affected; the regulatory API is the read side auditors use to check the operator's account after the incident.
Anchored inclusion proofs
Where the optional web3 anchoring module publishes checkpoint roots on chain, the inclusion proof takes the form of an AnchorInclusionProof (chio.anchor-inclusion-proof.v1). It bundles the receipt, a Web3ReceiptInclusion, the signed Web3CheckpointStatement, and an optional chain-anchor record tying the checkpoint root to the root published in the ChioRootRegistry contract. verify_anchor_inclusion_proof checks the receipt signature, the checkpoint statement signature, and the Merkle inclusion path in one pass.