Query & Audit Receipts
Every tool call that crosses a chio kernel produces a signed receipt. This guide is the operator playbook for asking useful questions of that log: who ran what, what was denied, which guard fired, how much was spent, and where the evidence is. It pairs with the machine reference for full parameter schemas.
Machine-readable schemas live elsewhere
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 surfaces share the same filter vocabulary. Pick whichever is most ergonomic for the task at hand. Filters combine with AND semantics: supplying more filters always narrows the result set, never widens it.
Filter Dimensions
Every audit question reduces to a combination of these filters. Omit a filter to disable it entirely.
| Dimension | CLI flag | HTTP param | What it scopes |
|---|---|---|---|
| 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. Excludes receipts without financial metadata. |
| Cost ceiling | --max-cost | maxCost | Maximum cost_charged in minor units. Same exclusion rule as the floor. |
| 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 and policies: the guard name lives inside each receipt's decision payload (for example { deny: { guard: "path_allowlist" } }). It is not a first-class filter on the query endpoint. You filter by outcome=deny server-side and then narrow client-side with jq on the decision object. The same applies to policy hashes and reason strings. Patterns for both are in the cookbook below.
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 are the audit questions operators actually ask, translated into runnable queries. 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.deny.guard, reason: .decision.deny.reason}'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/tool pair.
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.
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(.decision.deny.guard == "secret-leak")
| {id, ts: .timestamp, server: .tool_server, tool: .tool_name, reason: .decision.deny.reason}'Swap secret-leak for any guard identifier your policy loads: path_allowlist, monetary_budget, velocity, or a custom guard you built with Custom Guards.
4. Budget-exceeded events for a workload
The monetary_budget guard writes "budget exhausted" or a similar reason into the deny decision, and the financial block records the attempted cost against the remaining budget.
chio receipt list \
--capability cap-workload-xyz \
--outcome deny \
--limit 200 \
--control-url "$CONTROL_URL" \
--control-token "$CONTROL_TOKEN" \
| jq -c 'select(.decision.deny.guard == "monetary_budget")
| {id,
ts: .timestamp,
attempted: .metadata.financial.attempted_cost,
remaining: .metadata.financial.budget_remaining,
total: .metadata.financial.budget_total,
currency: .metadata.financial.currency}'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. Divide by 100 for USD dollars when reporting.
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. Together they answer "what did this token do, 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}'SDK Cookbook
The TypeScript SDK is the right tool when you are building an internal dashboard, a compliance bot, or a nightly summarizer. The same filter vocabulary applies; cursor pagination is handled for you.
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;
if (receipt.decision.deny) {
console.log(receipt.id, receipt.decision.deny.guard, receipt.decision.deny.reason);
}
}
}
console.log(`${total} file-writes this week`);For a single-agent view, build the client once and narrow the generator with agentSubject on every call. The SDK also surfaces QueryError and TransportError so you can distinguish a 401 from a dropped connection.
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;
}
}Paginating 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.
A full walk in shell looks like this:
cursor=""
while : ; do
args=(--outcome deny --limit 200 --control-url "$CONTROL_URL" --control-token "$CONTROL_TOKEN")
[ -n "$cursor" ] && args+=(--cursor "$cursor")
page=$(chio receipt list "${args[@]}")
[ -z "$page" ] && break
echo "$page" >> all-denies.ndjson
# The last seq in the page becomes the next cursor.
cursor=$(echo "$page" | tail -n 1 | jq -r '.seq')
[ "$cursor" = "null" ] && break
doneNew receipts keep arriving
seq is greater than your current cursor. There is no re-shuffling and no risk of missing history.Exporting for Audit
Queries are great for operators. Auditors want files. Chio gives you three off-ramps, each optimized for a different consumer.
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 | keys[0]),
(.metadata.financial.cost_charged // 0),
(.metadata.attribution.subject_key // "")] | @csv' \
>> receipts.csvForwarding 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. Chio ships an chio-siem forwarder that reads the receipt database with its own seq-cursor and fans events out to registered exporters. See SIEM Export for the end-to-end setup.
Offline evidence packages
For bilateral audit with an external counterparty, use chio evidence export. It produces a directory containing the filtered receipts, the policy file, and any checkpoint roots needed to prove log integrity. The counterparty can verify it with chio evidence verify --input ./pkg on an air-gapped machine.
Composing with Verification
Querying shows you what happened. Verification proves you can trust what you are looking at. The two are meant to compose: run a query to narrow the receipts of interest, then verify that subset offline against the issuer's public key and the log's checkpoint roots.
Typical audit pipeline:
- 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
The full flag set for chio receipt list, kept short so you can keep it on a sticky note.
| 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. |
--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. |
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