BuildAudit
Query & Audit Receipts
Query the signed receipt log by actor, action, outcome, guard, cost, and time range.
Machine-readable schemas live elsewhere
Prerequisites
- A receipt store, and one of two ways in. Locally, a sqlite file named by
--receipt-db. Remotely, achio trust servepointed at one, reached through--control-urland--control-token. - A read boundary, on the local path only.
--tenant <id>or--admin-all. A local read with neither is refused. A remote read with either is also refused, because the token already carries the scope. - An admin token for the analytics endpoint.
/v1/receipts/analyticsrequires admin read authority; a tenant-scoped token gets 403. - jq. Every filter this page cannot push to the server is a jq expression over JSON Lines.
The refusal on a local read with no boundary is the first thing most readers hit:
$ chio --receipt-db ./receipts.db receipt listerror [urn:chio:error:cli:other]: --tenant <id> or --admin-all is required for local receipt reads
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.What You Can Ask
The chio receipt log is a cursor-paginated, multi-filter stream of signed decisions. Everything an agent tried to do, along with the guard that allowed or blocked it, ends up here. You can reach it three ways:
- CLI.
chio receipt listreturns one JSON receipt per line (NDJSON), ideal for piping intojq,awk, or a file. - HTTP.
GET /v1/receipts/queryon the trust-control server, with a bearer token, returns a JSON envelope containing a page of receipts plustotalCountandnextCursor. - TypeScript SDK.
ReceiptQueryClientfrom@chio-protocol/sdkwraps the HTTP endpoint with typed params and an async-generatorpaginate().
All three interfaces use the same filters. Filters combine with AND semantics, so each additional filter narrows the result set. These are the read routes, as the trust-control router registers them:
| Method | Path | Handler |
|---|---|---|
| GET | /v1/receipts/query | handle_query_receipts |
| GET | /v1/agents/{subject_key}/receipts | handle_agent_receipts |
| GET | /v1/receipts/analytics | handle_receipt_analytics |
| GET | /v1/lineage/{capability_id} | handle_get_lineage |
| GET | /v1/lineage/{capability_id}/chain | handle_get_delegation_chain |
/v1/lineage without a capability id is a POST that records a snapshot, so a GET against it answers 405. The two readable lineage routes both take the capability id in the path.
Filter Dimensions
Combine these filters to form an audit query. Omit a filter to leave it unconstrained.
| Dimension | CLI flag | HTTP param | What it scopes |
|---|---|---|---|
| Tenant scope | --tenant <id> / --admin-all | (from token) | Required whenever --receipt-db is used; the two flags are mutually exclusive and omitting both refuses the read rather than defaulting to all tenants. Remote --control-url reads derive scope from the bearer token and reject these two flags if supplied. |
| Agent | (see note) | agentSubject | Hex-encoded Ed25519 subject public key. Resolved from receipt attribution metadata when present; otherwise via capability lineage. |
| Tool server | --tool-server | toolServer | Exact match on the tool server identifier, e.g. filesystem, shell. |
| Tool | --tool-name | toolName | Exact match on the tool name within a server, e.g. write_file. |
| Outcome | --outcome | outcome | One of allow, deny, cancelled, incomplete. |
| Capability | --capability | capabilityId | Exact capability token ID. Use this to audit the usage of a single issued token. |
| Time range | --since / --until | since / until | Unix seconds, inclusive on both ends. |
| Cost floor | --min-cost | minCost | Minimum cost_charged in minor currency units. Requires --cost-currency alongside it, and excludes receipts without financial metadata. |
| Cost ceiling | --max-cost | maxCost | Maximum cost_charged in minor units. Same currency requirement and same exclusion rule as the floor. |
| Cost currency | --cost-currency | costCurrency | ISO 4217 code the cost band applies to. Mandatory whenever a floor or ceiling is set; both clap and the query builder refuse a band without it. |
| Page size | --limit | limit | Results per page. Default 50, server cap 200. |
| Cursor | --cursor | cursor | Last seq value seen. Pagination is forward-only, seq-ordered. |
A note on guards: there is no guard-name filter on the query endpoint, and decision.guard is not where a guard name lives. Every kernel-built deny receipt sets decision.guard to kernel, and the reason string names the pipeline rather than the guard inside it. The guard that fired appears in evidence[], as an entry whose verdict is false. Filter outcome=deny on the server, then narrow with jq over that array. The same applies to policy hashes and reason strings.
Two denials from two different guards, read three ways in one line:
$ chio --receipt-db ./receipts.db receipt list --admin-all \
| jq -c '{tool: .tool_name, verdict: .decision.verdict,
$ guard: .decision.guard,
$ denied_by: [.evidence[]? | select(.verdict == false) | .guard_name]}'{"tool":"run_command","verdict":"allow","guard":null,"denied_by":[]}
{"tool":"read_file","verdict":"deny","guard":"kernel","denied_by":["forbidden-path"]}
{"tool":"run_command","verdict":"deny","guard":"kernel","denied_by":["shell-command"]}For agent-scoped lookups there is also a shorter URL: GET /v1/agents/{subject_key}/receipts. It accepts only limit and cursor and is equivalent to passing agentSubject on the general query endpoint.
Common Queries Cookbook
These runnable queries cover common operator tasks. Each example assumes you have exported CONTROL_URL and CONTROL_TOKEN in your shell, or that your chio config resolves them automatically.
1. Every denial in the last 24 hours
The daily triage query. Pipe through jq to pull the essentials.
SINCE=$(date -u -d '24 hours ago' +%s 2>/dev/null || date -u -v-24H +%s)
chio receipt list \
--outcome deny \
--since "$SINCE" \
--control-url "$CONTROL_URL" \
--control-token "$CONTROL_TOKEN" \
--limit 200 \
| jq -c '{id, ts: .timestamp, server: .tool_server, tool: .tool_name,
guard: .decision.guard,
denied_by: [.evidence[]? | select(.verdict == false) | .guard_name],
reason: .decision.reason}'guard reads kernel on every row, because that is the layer that refused. denied_by is the column that separates a forbidden path from a shell-command block. A revocation denial has no guard evidence at all: it is refused before the pipeline runs, and its reason reads capability has been revoked: <id>.
The decision object is flat
Decision is an internally tagged enum (#[serde(tag = "verdict")] at crates/core/chio-core-types/src/receipt/decision.rs), so a denial serializes as {"verdict":"deny","reason":...,"guard":...} with no nested deny object. Read .decision.guard, not .decision.deny.guard: the latter is valid jq and returns null on every row, which is the failure mode that looks like an empty result set.2. Every file-write by agent X last week
Combine an agent subject filter with a tool server and tool name. Works equally well for any server and tool pair. The subject key is the hex string a receipt carries at .metadata.attribution.subject_key; read one off the CSV export below rather than typing it, since it is 64 characters and a single wrong nibble matches nothing silently.
AGENT=7b0f6f631f6e66207140ead0b6b2e9418916d2c4b3c7448ba5f7ed27f5c8d038
START=$(date -u -d '7 days ago' +%s 2>/dev/null || date -u -v-7d +%s)
END=$(date -u +%s)
chio receipt list \
--tool-server filesystem \
--tool-name write_file \
--since "$START" \
--until "$END" \
--limit 200 \
--control-url "$CONTROL_URL" \
--control-token "$CONTROL_TOKEN" \
| jq --arg a "$AGENT" -c 'select(.metadata.attribution.subject_key == $a)'The server also accepts agentSubject directly over HTTP. The jq form above is useful when you want to see only the receipts with explicit attribution metadata, as opposed to those resolved via capability lineage. It reads one page: 200 is the server cap, so if the week holds more than that, wrap it in the cursor loop from the pagination section below.
3. Everything blocked by the secret-leak guard
Guard names live inside the decision payload, so filter outcome=deny server-side, then narrow on the guard name.
chio receipt list \
--outcome deny \
--limit 200 \
--control-url "$CONTROL_URL" \
--control-token "$CONTROL_TOKEN" \
| jq -c 'select(any(.evidence[]?; .guard_name == "secret-leak" and .verdict == false))
| {id, ts: .timestamp, server: .tool_server, tool: .tool_name, reason: .decision.reason}'Swap secret-leak for any guard the runtime can register. These are the names guard.name() returns, which is what lands in guard_name. They are kebab-case, and they are not the snake_case rule-block keys you write in a policy: the secret_patterns block compiles to the secret-leak guard, and path_allowlist compiles to path-allowlist.
agent-velocity, behavioral-sequence, browser-automation, code-execution, computer-use, content-review, data-flow, egress-allowlist, embedding-anomaly, forbidden-path, input-injection-capability, internal-network, jailbreak, mcp-tool, memory-governance, patch-integrity, path-allowlist, prompt-injection, remote-desktop-side-channel, response-sanitization, secret-leak, shell-command, velocity. A custom guard you built with Custom Guards reports the name it registers under.
4. Budget-exceeded events for a workload
A budget refusal is a kernel denial, not a guard one, so there is no guard name to select on. What separates it is the financial metadata block: the kernel attaches it on a monetary deny with cost_charged zeroed and attempted_cost set to what the call would have spent. Select on the block existing, and read the reason for which limit was hit.
chio receipt list \
--capability cap-workload-xyz \
--outcome deny \
--limit 200 \
--control-url "$CONTROL_URL" \
--control-token "$CONTROL_TOKEN" \
| jq -c 'select(.metadata.financial.attempted_cost != null)
| {id,
ts: .timestamp,
reason: .decision.reason,
attempted: .metadata.financial.attempted_cost,
remaining: .metadata.financial.budget_remaining,
total: .metadata.financial.budget_total,
currency: .metadata.financial.currency}'The reason string for an exhausted invocation budget reads invocation budget exhausted for capability <id>. A deny that never matched a monetary grant carries no financial block at all, which is why the selector tests for the field rather than comparing a guard name.
5. Top 10 tools by cost this month
Do not aggregate this yourself. The analytics endpoint already groups by tool for exactly this shape of question. Call it with a month-wide since.
MONTH_START=$(date -u -d "$(date -u +%Y-%m-01)" +%s 2>/dev/null \
|| date -u -j -f "%Y-%m-%d" "$(date -u +%Y-%m-01)" +%s)
curl -sS \
-H "Authorization: Bearer $CONTROL_TOKEN" \
"$CONTROL_URL/v1/receipts/analytics?since=$MONTH_START&timeBucket=day&groupLimit=10" \
| jq '.byTool
| sort_by(-.metrics.totalCostCharged)
| .[:10]
| map({server: .toolServer, tool: .toolName, cost: .metrics.totalCostCharged, calls: .metrics.totalReceipts})'totalCostCharged is in minor currency units, so divide by 100 for USD dollars when reporting. Both analytics calls need admin read authority: a tenant-scoped token gets 403 rather than a narrowed result. complianceRate and reliabilityScore are omitted from a row whose denominator is zero, so read them with a // default.
6. Per-agent spend summary
The same analytics endpoint groups by agent. Pair with a time range for monthly reports.
curl -sS \
-H "Authorization: Bearer $CONTROL_TOKEN" \
"$CONTROL_URL/v1/receipts/analytics?since=$MONTH_START&until=$MONTH_END&groupLimit=50" \
| jq '.byAgent
| map({agent: .subjectKey,
calls: .metrics.totalReceipts,
spend_minor: .metrics.totalCostCharged,
denies: .metrics.denyCount,
compliance: .metrics.complianceRate})
| sort_by(-.spend_minor)'7. Trace a capability token's full usage lineage
When an incident implicates a specific capability, pull every receipt it produced, then resolve its delegation chain from /v1/lineage/{capability_id}/chain. Together they answer what this token did, and who issued it to whom.
CAP=cap-abc123
# Every receipt this capability produced, in order.
chio receipt list \
--capability "$CAP" \
--limit 200 \
--control-url "$CONTROL_URL" \
--control-token "$CONTROL_TOKEN" \
> receipts-$CAP.ndjson
# The delegation chain (root to leaf).
curl -sS \
-H "Authorization: Bearer $CONTROL_TOKEN" \
"$CONTROL_URL/v1/lineage/$CAP/chain" \
| jq '.[] | {subject: .subject_key, issuer: .issuer_key, depth: .delegation_depth, expires: .expires_at}'The lineage chain is the one response on this API whose keys are snake_case: CombinedDelegationChainEntry carries no rename_all, so subject_key and delegation_depth are the literal wire names. Do not camelCase them to match the query endpoint.
SDK Cookbook
An internal dashboard, a compliance bot, or a nightly summary reaches for a client instead of the CLI. Two ship today, one per ecosystem, and they are the same object: a constructor taking a control-plane URL and a bearer token, a query for one page, and a paginate that walks the cursor for you. The filter names are the camelCase forms from the wire, identical in both.
$ npm install @chio-protocol/sdk
$ pip install chio-sdkThe Python distribution chio-sdk installs the import package chio. Count the matches first, then walk every page:
import { ReceiptQueryClient } from "@chio-protocol/sdk";
const client = new ReceiptQueryClient(
process.env.CONTROL_URL!,
process.env.CONTROL_TOKEN!,
);
// How many receipts match, without fetching them all.
const head = await client.query({
outcome: "deny",
since: Math.floor(Date.now() / 1000) - 86_400,
limit: 1,
});
console.log(`${head.totalCount} denials in the last 24 hours`);
// Walk every matching page with the async generator.
let total = 0;
for await (const page of client.paginate({
toolServer: "filesystem",
toolName: "write_file",
since: Math.floor(Date.now() / 1000) - 7 * 86_400,
})) {
for (const receipt of page) {
total += 1;
// decision is optional: a trace or advisory receipt carries none.
if (receipt.decision?.verdict === "deny") {
console.log(receipt.id, receipt.decision.guard, receipt.decision.reason);
}
}
}
console.log(`${total} file-writes this week`);import os
import time
from chio import ReceiptQueryClient
client = ReceiptQueryClient(os.environ["CONTROL_URL"], os.environ["CONTROL_TOKEN"])
# How many receipts match, without fetching them all.
head = client.query({"outcome": "deny", "since": int(time.time()) - 86_400, "limit": 1})
print(f"{head['totalCount']} denials in the last 24 hours")
# Walk every matching page. paginate() is a generator over pages.
total = 0
for page in client.paginate({"toolServer": "filesystem", "toolName": "write_file"}):
for r in page:
total += 1
# decision is optional: a trace or advisory receipt carries none.
decision = r.get("decision") or {}
if decision.get("verdict") == "deny":
print(r["id"], decision["guard"], decision["reason"])
print(f"{total} file-writes this week")Both clients speak GET /v1/receipts/query with an Authorization: Bearer header, and both encode filters as query parameters in the order the caller wrote them. The Python block above issues three requests: the counting call, the first page, and the same page filters plus the cursor the first response returned.
/v1/receipts/query?outcome=deny&since=1785521720&limit=1
/v1/receipts/query?toolServer=filesystem&toolName=write_file
/v1/receipts/query?toolServer=filesystem&toolName=write_file&cursor=42The TypeScript tab passes an extra since to paginate, so its last two URLs carry that parameter as well. nextCursor is a number, and both clients require it to strictly advance: a server that repeats a cursor or hands one back that moves backwards raises rather than looping forever. Python holds the full set of cursors it has seen; TypeScript compares against the previous one.
The same query straight over HTTP, so you can see the envelope the SDKs unwrap. This is the deny lane of a store holding one allow and two denials:
$ curl -sS -H "Authorization: Bearer $CONTROL_TOKEN" \
"$CONTROL_URL/v1/receipts/query?outcome=deny&limit=1" \
| jq '{totalCount, nextCursor, receipts: [.receipts[] | {tool_name, decision}]}'{
"totalCount": 2,
"nextCursor": 2,
"receipts": [
{
"tool_name": "read_file",
"decision": {
"guard": "kernel",
"reason": "guard denied the request: guard \"guard-pipeline\" denied the request",
"verdict": "deny"
}
}
]
}totalCount is 2 while the page holds 1, because the count ignores limit and cursor. And decision comes back flat, with guard reading kernel and the reason naming the pipeline.
For a single-agent view, build the client once and narrow the generator with agentSubject on every call. Each SDK also separates a rejected query from a dropped connection, so a 401 and a dead socket are different except branches: QueryError and TransportError in TypeScript, ChioQueryError and ChioTransportError in Python.
import { QueryError, TransportError } from "@chio-protocol/sdk";
try {
const { totalCount, receipts } = await client.query({
agentSubject: "7b0f6f63...",
outcome: "deny",
limit: 50,
});
console.log(`${totalCount} total denials for this agent`);
for (const r of receipts) {
console.log(r.timestamp, r.tool_server, r.tool_name, r.decision);
}
} catch (err) {
if (err instanceof QueryError) {
console.error("Control plane rejected the query", err.status, err.message);
} else if (err instanceof TransportError) {
console.error("Could not reach the control plane", err.message);
} else {
throw err;
}
}from chio import ChioQueryError, ChioTransportError
try:
response = client.query({
"agentSubject": "7b0f6f63...",
"outcome": "deny",
"limit": 50,
})
print(f"{response['totalCount']} total denials for this agent")
for r in response["receipts"]:
print(r["timestamp"], r["tool_server"], r["tool_name"], r["decision"])
except ChioQueryError as err:
print("Control plane rejected the query", err.status, err)
except ChioTransportError as err:
print("Could not reach the control plane", err)Run against a control plane that rejects the bearer token, then against one that is not listening, the Python branch prints:
Control plane rejected the query 401 receipt query failed with status 401
Could not reach the control plane failed to fetch receiptsPaginating Large Result Sets
Pagination is cursor-based and forward-only. The server assigns each receipt a monotonically increasing seq and returns the last seq of each page as nextCursor. Pass that value back as cursor on the next call to pick up where you left off.
- Default page size. 50.
- Server cap. 200. Requesting more is silently clamped.
- End-of-stream. A response without
nextCursor(or with it set to null) means you have reached the last page for the current filter set. - Total count.
totalCountreflects all matches for the filters, independent oflimitandcursor. Use it to show "N total" in a UI without walking every page.
Retrieving every matching page from a shell has one trap in it.
A receipt line carries no seq
seq is the store's row number, not a receipt field, so it is never serialized into the JSON Lines stream. The CLI writes the cursor on stderr instead, as next_cursor=<seq> total_count=<n>. A loop that reads .seq off the last line gets null on page one and stops there, having collected a single page of what it promised to collect in full.$ chio --control-url "$CONTROL_URL" --control-token "$CONTROL_TOKEN" \
receipt list --limit 2 2>cursor.err 1>page.ndjson
$ echo "stdout: $(wc -l < page.ndjson) receipt lines"
$ echo "stderr: $(cat cursor.err)"stdout: 2 receipt lines stderr: next_cursor=2 total_count=3
So the loop splits the two streams and reads the cursor out of stderr:
$ cursor=""
$ : > all-denies.ndjson
$ while : ; do
$ args=(--outcome deny --limit 1 --control-url "$CONTROL_URL" --control-token "$CONTROL_TOKEN")
$ [ -n "$cursor" ] && args+=(--cursor "$cursor")
$
$ # Receipts go to stdout; "next_cursor=<seq> total_count=<n>" goes to stderr.
$ page=$(chio receipt list "${args[@]}" 2>page.err)
$ [ -n "$page" ] && printf '%s\n' "$page" >> all-denies.ndjson
$
$ cursor=$(sed -n 's/^next_cursor=\([0-9]*\).*/\1/p' page.err)
$ [ -z "$cursor" ] && break
$ done
$ rm -f page.err
$ wc -l < all-denies.ndjson2
Two denials, collected a page at a time, with the loop stopping on the request that comes back short and therefore carries no next cursor. Over HTTP the cursor is in the response envelope instead, as nextCursor, and no stream juggling is needed.
New receipts keep arriving
seq is greater than your current cursor. There is no re-shuffling and no risk of missing history.Exporting for Audit
Export queries as files for audit and downstream systems. Chio supports three formats and delivery paths.
NDJSON (one receipt per line)
The default output of chio receipt list. Stable, streamable, and the native input format of most log tooling. Redirect straight to a file:
chio receipt list \
--since "$QUARTER_START" --until "$QUARTER_END" \
--limit 200 \
--control-url "$CONTROL_URL" --control-token "$CONTROL_TOKEN" \
> 2026-Q1-receipts.ndjsonCSV for spreadsheets
Pair NDJSON with jq -r to flatten into CSV. Pick only the columns you need; auditors rarely want the full receipt JSON.
echo 'id,timestamp,server,tool,outcome,cost_charged,agent' > receipts.csv
chio receipt list \
--since "$MONTH_START" --limit 200 \
--control-url "$CONTROL_URL" --control-token "$CONTROL_TOKEN" \
| jq -r '[.id,
.timestamp,
.tool_server,
.tool_name,
(.decision.verdict // ""),
(.metadata.financial.cost_charged // 0),
(.metadata.attribution.subject_key // "")] | @csv' \
>> receipts.csvThe outcome column is .decision.verdict, with a // default because a trace or advisory receipt has no decision at all. Do not reach for (.decision | keys[0]): the decision is internally tagged, so that yields the literal string verdict on an allow and guard on a deny, never an outcome. The same run against a local store:
$ echo 'id,timestamp,server,tool,outcome,cost_charged,agent'
$ chio --receipt-db ./receipts.db receipt list --admin-all \
| jq -r '[.id, .timestamp, .tool_server, .tool_name,
$ (.decision.verdict // ""),
$ (.metadata.financial.cost_charged // 0),
$ (.metadata.attribution.subject_key // "")] | @csv'id,timestamp,server,tool,outcome,cost_charged,agent "ed6c03fe092c83e54898702714592bc9e196a4120f58616840259b4bf4ce372f",1788610104,"*","run_command","allow",0,"ee7d2c513773e84d23ea9ade9bf0f7ffd9cd0c6c4545135ad0a441d1cb607fc3" "f0dea3dd23ccc35f7a15f6e753b3a095ba04ffa1f4fed79b1593f05666cc0736",1788610105,"*","read_file","deny",0,"e7e09911a3f1f8fbe85d5fe2859dcf4a3ffb56e792e79f097a609e2b9845486c" "406ad0272531e42e4fee6a503fd24e6a0ba8cc705db01852dcaf5fe0de9e227e",1788610107,"*","run_command","deny",0,"8aaa4bfda187af829dcfebd3d702fee1483a0eaaa22bf249be7ee60b3686ac6a"
Forwarding to a SIEM
Ad-hoc exports are fine for quarterly reviews. For continuous compliance, stream receipts into Splunk, Elasticsearch, or whatever your SOC already runs. The chio-siem crate is the library for that: its ExporterManager opens a read-only connection to the receipt database, pulls with its own seq cursor, and fans out to registered exporters with backoff and a bounded dead-letter queue. It has no binary and no CLI verb, so a deployment embeds it. See SIEM Export for setup instructions.
Offline evidence packages
For bilateral audit with an external counterparty, use chio evidence export. It produces a directory containing the filtered receipts, the capability lineage, and any checkpoint roots needed to prove log integrity, plus a manifest that hashes every one of them. Pass --policy-file to add the policy source and its metadata; the package does not carry a policy otherwise. The counterparty can verify it with chio evidence verify --input ./pkg on an air-gapped machine.
Failures and Recovery
| What you see | What it means | What to do |
|---|---|---|
--tenant <id> or --admin-all is required for local receipt reads | A --receipt-db read named no boundary | Add one. The read fails closed rather than defaulting to every tenant |
receipt list read-boundary flags apply to local --receipt-db | A --control-url read passed --tenant or --admin-all | Drop the flag. The control token already carries the scope |
the following required arguments were not provided: --cost-currency | A cost band with no currency | Add --cost-currency USD. The query builder refuses the band a second time server-side |
| An empty result where you expected rows | Often a jq path that is valid and always null: .decision.deny.guard, or a guard name compared against .decision.guard | Print one raw row first. Guard names live in .evidence[].guard_name |
| A page that stops after 200 rows | --limit above the cap is clamped silently, with no warning and exit 0 | Page with the cursor loop. Compare what you collected against total_count on stderr |
HTTP 403 on /v1/receipts/analytics | The token is tenant-scoped and analytics needs admin read authority | Use an admin token, or aggregate from receipt list yourself |
HTTP 405 on GET /v1/lineage | That path is the POST that records a snapshot | Use /v1/lineage/{capability_id} or its /chain suffix |
$ chio --control-url "$CONTROL_URL" --control-token "$CONTROL_TOKEN" \
receipt list --tenant acmeerror [urn:chio:error:cli:other]: receipt list read-boundary flags apply to local --receipt-db; remote reads derive scope from the control token
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.$ chio --receipt-db ./receipts.db receipt list --admin-all --min-cost 0error: 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'.
Composing with Verification
Querying finds receipts; verification checks their signatures and log evidence. Query the receipts of interest, then verify that subset offline against the issuer's public key and the log's checkpoint roots.
A typical audit workflow:
- Query the log with the filters for your incident or reporting window. Save the NDJSON.
- Export an evidence package covering the same window (or the same capability ID) so the checkpoint roots travel with the receipts.
- Run offline verification against the package. Any tampered or missing receipt fails the verifier.
For the verification mechanics, see Verify Receipts Offline. If you are building a dashboard that does both live queries and evidence checks, the Receipt Dashboard reference shows how the operator console wires them together.
Flag Summary
Reference flags for chio receipt list.
| Flag | Purpose |
|---|---|
--capability | Scope to a single capability token. |
--tool-server / --tool-name | Scope to a server and optionally a tool within it. |
--outcome | allow · deny · cancelled · incomplete. |
--since / --until | Time window in Unix seconds, inclusive. |
--min-cost / --max-cost | Cost band in minor currency units. Either one requires --cost-currency. |
--cost-currency | ISO 4217 code the cost band applies to. Mandatory alongside a floor or a ceiling. |
--limit / --cursor | Page size (default 50, cap 200) and resume seq. |
--control-url / --control-token | Trust-control server URL and bearer token. |
--receipt-db | Local SQLite path for offline mode. |
--tenant / --admin-all | Tenant read boundary, required with --receipt-db (mutually exclusive). Omitting both refuses the read; remote --control-url mode rejects them. |
Next Steps
- Receipt Query API · the machine-readable reference with full parameter and response schemas
- Verify Receipts Offline · prove the receipts you just queried are authentic
- SIEM Export · stream receipts into Splunk, Elasticsearch, or a generic webhook
- Receipt Dashboard · the operator console that wraps these queries in a UI