Chio/Docs

Verify Commerce Evidence

Run one verifier over a signed commerce bundle, read each record it checks, and watch four tampered copies fail.

Guide chio@5f63e6e

On this page

A reviewer can check an agent's spending record on their own machine, with keys they chose. A Proof Room bundle carries the mandate, the kernel's receipts, and the settlement packet for one purchase, and chio proof verify recomputes each claim from those records.

The fixture is a USD 42 single-use allowance, a signed refusal, a signed allowed call, and an offline settlement projection. Verification reads recorded evidence: it contacts no payment rail and holds or moves no funds.

Run the verifier

The verifier accepts the signed commerce bundle

Generate the built-in fixture with the installed chio, then verify it with five claim families required. The commands here use jq, and its filter narrows the chio.transaction.verifier-report.v1 report to its verdict.

Terminalbash
chio proof fixture generate commerce-transaction-passport --out ./commerce
BUNDLE=./commerce/proof-room-bundle
chio proof verify "$BUNDLE" --trust ./commerce/trust.toml \
  --require denials --require commerce --require settlement \
  --require risk --require trust-market
Output
generated  commerce-transaction-passport  ./commerce
trust      ./commerce/trust.toml  (fixture profile)
VERIFIED   passport-public-settlement-valid  23 claims
required   denials, commerce, settlement, risk, trust-market: all covered
trust      ./commerce/trust.toml (fixture keys)

You should see VERIFIED and 23 claims. Add --json for the full chio.transaction.verifier-report.v1 report, whose verified_claims names each claim. The verifier takes its keys from the trust file you pass, never from the bundle, so a bundle cannot vouch for itself. Each --require names a family the report must cover: denials for the terminal refusal, commerce for the mandate and order, settlement for the packet, and risk and trust-market for the risk-comptroller and trust-scorecard checks. A missing record, a broken binding, or a failed signature rejects the bundle.

Bundle to verdictChoose a lens.
κmandatemandate-commerce-001ρreceiptsssettlement packetσtrusted keysvverifierρVerifiedverifier-report.v1Rejectedexit 1κmandatemandate-commerce-001ρreceiptsssettlementσtrusted keysvverifierρVerifiedverifier-report.v1Rejectedexit 1

The mandate bounds what the agent may spend

mandate-commerce-001 caps spend at max_amount_minor 4200 (USD 42.00) with max_occurrences 1. The commerce requirement checks the ledger schema, the bounds, and the hash of each entry in protocol_projections. Verify commerce evidence.

The records of the offline chio.proof-room.bundle.v1 commerce fixture meeting the verifier under keys the reviewer configures. The receipts are fixture receipts signed with a deterministic test key, and no rail is contacted; the risk and trust-market records are left out.

The fixture's records are signed with deterministic test keys suited only to this exercise, and the bundle is the same chio.proof-room.bundle.v1 format the Proof Room server verifies.

The mandate caps the purchase at USD 42.00 and one use

mandate-allowance-ledger.json (chio.commerce.mandate-allowance-ledger.v1) holds the allowance the agent spent against.

Terminalbash
jq '{id, max_amount_minor, max_occurrences, currency}' \
  "$BUNDLE/mandate-allowance-ledger.json"
Output
{
  "id": "mandate-commerce-001",
  "max_amount_minor": 4200,
  "max_occurrences": 1,
  "currency": "USD"
}

You should see max_amount_minor 4200, which is USD 42.00, and max_occurrences 1 on a single_use allowance. The commerce requirement checks the ledger schema, these bounds, and that each entry in protocol_projections hashes to its payload under protocol-payloads/: x402, AP2, and ACP-Commerce payloads, each hashed and checked, none dispatched.

Read the full mandate and its five protocol projections

The ledger also carries an issued_at and one top-level digest per projected protocol; hashes are abbreviated.

mandate-allowance-ledger.jsonjson
{
  "schema": "chio.commerce.mandate-allowance-ledger.v1",
  "id": "mandate-commerce-001",
  "currency": "USD",
  "max_amount_minor": 4200,
  "max_occurrences": 1,
  "single_use": true,
  "used_occurrences": 1,
  "merchant_subject": "merchant:stripe:coffee-shop",
  "order_id": "order-public-settlement-valid",
  "valid_from": "2026-06-10T00:00:00Z",
  "expires_at": "2026-06-10T01:00:00Z",
  "quote_sha256": "9ead9dbbe19e0e2428a6bc6f..."
}
protocol_projections (excerpt)json
{
"protocol_projections": [
  { "protocol": "ap2", "purpose": "checkout_mandate",
    "amount_minor": 4200, "currency": "USD",
    "payload_path": "protocol-payloads/ap2-checkout-mandate.json" },
  { "protocol": "ap2", "purpose": "payment_mandate",
    "payload_path": "protocol-payloads/ap2-payment-mandate.json" },
  { "protocol": "acp-commerce", "purpose": "delegated_payment_token",
    "payload_path": "protocol-payloads/acp-commerce-delegated-payment-token.json" },
  { "protocol": "x402", "purpose": "payment_requirements",
    "payload_path": "protocol-payloads/x402-payment-requirements.json" },
  { "protocol": "chio", "purpose": "authority_projection",
    "payload_path": "protocol-payloads/chio-authority-projection.json" }
]
}

Two receipts record a refusal and an allowed call under one key

Both receipts use chio.receipt.v1 and the same signing key, with independent ids and signatures.

Terminalbash
for r in denial allow; do
  jq -c '{terminal_status, receipt_id, kernel_key}' \
    "$BUNDLE/commerce-terminal-$r-receipt.json"
done
Output
{"terminal_status":"denied_guard_request","receipt_id":"receipt-commerce-terminal-denial","kernel_key":"e8da63a40ca687c87cfce05cb24a786c7e75cc49c70db5573f026f1c6a86ceaa"}
{"terminal_status":"allowed_executed","receipt_id":"receipt-commerce-terminal-allow","kernel_key":"e8da63a40ca687c87cfce05cb24a786c7e75cc49c70db5573f026f1c6a86ceaa"}

You should see denied_guard_request and allowed_executed under one kernel_key and one policy_digest. The denials requirement checks that the refusal verifies under its key and that the receipt-coverage matrix records a runtime_terminal_denial row for it. Each receipt carries a receipt_id, a policy_digest, the kernel key, and a signature, and neither carries an amount or a mandate reference, so the two are separate records rather than two uses of one mandate. Their terminal statuses come from the fixture generator.

Read both abbreviated receipts

Hashes and signatures are abbreviated; the verifier reads the full values in the bundle.

commerce-terminal-*-receipt.jsonjson
{
  "schema": "chio.receipt.v1",
  "receipt_id": "receipt-commerce-terminal-denial",
  "terminal_status": "denied_guard_request",
  "policy_digest": "824f6c706f1092e0e84e5c9b...",
  "kernel_key": "e8da63a40ca687c87cfce05c...",
  "signature": "425dc80cbb68c93823659daa..."
}
{
  "schema": "chio.receipt.v1",
  "receipt_id": "receipt-commerce-terminal-allow",
  "terminal_status": "allowed_executed",
  "policy_digest": "824f6c706f1092e0e84e5c9b...",
  "kernel_key": "e8da63a40ca687c87cfce05c...",
  "signature": "7036a4e3310e43037ef358a4..."
}

The settlement packet binds to the order and the quote

The settlement requirement checks the packet schema and that its order_id and quote_sha256 match the mandate.

Terminalbash
jq '{schema, status, psp, settlement_rail}' "$BUNDLE/settlement-packet.json"
Output
{
  "schema": "chio.commerce.settlement-packet.v1",
  "status": "settled",
  "psp": "stripe-shaped-offline",
  "settlement_rail": "offline-psp-ledger"
}

You should see status settled on an offline projection: psp is stripe-shaped-offline and settlement_rail is offline-psp-ledger. The packet settlement-packet-commerce-001 also records amount_minor 4200 in USD, payment_intent_id pi_commerce_001, order_id order-public-settlement-valid, dispatch_receipt_ref receipt-settlement-dispatch-commerce-001, and reconciliation_ref reconciliation-commerce-001.

A live deployment records the same packet from an observed rail outcome, and holds and caps stop spend before dispatch. Authoritative spend states the predicate a receipt meets to count as authoritative spend.

Four tampered bundles each fail on the rule they break

The fixture ships a negative catalog under negatives/catalog/: complete bundles with one record altered. Verify the four commerce cases in one run and state that you expect each to be refused.

Terminalbash
NEG="$BUNDLE/negatives/catalog"
chio proof verify "$NEG/commerce-payment-before-budget" "$NEG/commerce-mandate-occurrence-limit" \
  "$NEG/commerce-expired-mandate" "$NEG/commerce-payment-amount-mismatch" \
  --trust ./commerce/trust.toml --require commerce --expect refuse
Output
REFUSED   commerce-payment-before-budget     commerce replay failed: unknown commerce transition: provider_admitted -> payment_verified via verify_payment
REFUSED   commerce-mandate-occurrence-limit  commerce mandate failed: mandate occurrence limit exceeded
REFUSED   commerce-expired-mandate           commerce mandate failed: mandate expired before payment capture
REFUSED   commerce-payment-amount-mismatch   commerce payment failed: payment amount or currency mismatch
4 of 4 refused, as expected
trust     ./commerce/trust.toml (fixture keys)

You should see four REFUSED lines and an exit of 0, because every input met --expect refuse. A refusal is a result, not an error: without --expect, the same run exits 1, and a case that verified would print VERIFIED with expected refuse and fail the run. The occurrence case raises used_occurrences to 2 against a max_occurrences of 1 on the same mandate-commerce-001 at 4200 minor units. It is a byte-for-byte copy of the offline-psp-valid commerce fixture's ledger with that one field changed, which is why its order_id and payload digests differ from the bundle's ledger.

CaseTampered recordFailure
commerce-payment-before-budgetevent-log.jsonunknown commerce transition
commerce-mandate-occurrence-limitmandate-allowance-ledger.jsonmandate occurrence limit exceeded
commerce-expired-mandatemandate-allowance-ledger.jsonmandate expired before payment capture
commerce-payment-amount-mismatchpayment-lifecycle.jsonpayment amount or currency mismatch

Every fixture chio proof fixture generate writes is verified by the release's qualification run, including each negative, so a case that refuses for the wrong reason, or passes, fails the release.

Verify a bundle another operator produced

Run the same command over a bundle your partner produced, with the partner's keys pinned from a channel you trust: its partner card, exchanged when the two kernels paired. A key that arrives inside the bundle establishes self-consistency, not the producer's identity.

Terminalbash
chio trust pin proof-room.bundle-signer ./halden.card
chio trust pin proof-room.receipt-kernel ./halden.card
chio proof verify ./halden-bounty-417 --require commerce --require settlement
Output
pinned     halden  proof-room.bundle-signer   ed25519:9b07…d31c
pinned     halden  proof-room.receipt-kernel  ed25519:9b07…d31c
file       ~/.chio/trust.toml
VERIFIED   passport-halden-bounty-417  19 claims
required   commerce, settlement: all covered
trust      ~/.chio/trust.toml
  • A signature fails or the signer is untrusted. Compare the expected signer configuration with the original signed bytes, then restore the intended trust or obtain an intact bundle.
  • A required claim is absent. The bundle lacks a record that family needs; obtain it before accepting the claim.
  • A tampered bundle fails with a different error. Compare it with the table above; an unrelated error leaves the intended rejection unshown.
  • A tampered bundle is accepted. Stop using the result as evidence for that check and investigate the fixture and the verifier together.

Next: test whether a receipt is authoritative spend