Chio/Docs

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

This page is about the questions you ask. For the exhaustive parameter list, response shapes, and error codes, jump to the Receipt Query API reference. Treat that page as the contract and this page as the cookbook.

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 list returns one JSON receipt per line (NDJSON), ideal for piping into jq, awk, or a file.
  • HTTP. GET /v1/receipts/query on the trust-control server, with a bearer token, returns a JSON envelope containing a page of receipts plus totalCount and nextCursor.
  • TypeScript SDK. ReceiptQueryClient from @chio-protocol/sdk wraps the HTTP endpoint with typed params and an async-generator paginate().

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.

DimensionCLI flagHTTP paramWhat it scopes
Agent(see note)agentSubjectHex-encoded Ed25519 subject public key. Resolved from receipt attribution metadata when present; otherwise via capability lineage.
Tool server--tool-servertoolServerExact match on the tool server identifier, e.g. filesystem, shell.
Tool--tool-nametoolNameExact match on the tool name within a server, e.g. write_file.
Outcome--outcomeoutcomeOne of allow, deny, cancelled, incomplete.
Capability--capabilitycapabilityIdExact capability token ID. Use this to audit the usage of a single issued token.
Time range--since / --untilsince / untilUnix seconds, inclusive on both ends.
Cost floor--min-costminCostMinimum cost_charged in minor currency units. Excludes receipts without financial metadata.
Cost ceiling--max-costmaxCostMaximum cost_charged in minor units. Same exclusion rule as the floor.
Page size--limitlimitResults per page. Default 50, server cap 200.
Cursor--cursorcursorLast 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.

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

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

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

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

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

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

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

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

typescript
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. totalCount reflects all matches for the filters, independent of limit and cursor. Use it to show "N total" in a UI without walking every page.

A full walk in shell looks like this:

bash
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
done

New receipts keep arriving

The receipt log is append-only. If new receipts land while you are walking pages, the cursor still advances correctly: you will see those receipts on subsequent pages because their 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:

bash
chio receipt list \
  --since "$QUARTER_START" --until "$QUARTER_END" \
  --limit 200 \
  --control-url "$CONTROL_URL" --control-token "$CONTROL_TOKEN" \
  > 2026-Q1-receipts.ndjson

CSV for spreadsheets

Pair NDJSON with jq -r to flatten into CSV. Pick only the columns you need; auditors rarely want the full receipt JSON.

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

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

FlagPurpose
--capabilityScope to a single capability token.
--tool-server / --tool-nameScope to a server and optionally a tool within it.
--outcomeallow · deny · cancelled · incomplete.
--since / --untilTime window in Unix seconds, inclusive.
--min-cost / --max-costCost band in minor currency units.
--limit / --cursorPage size (default 50, cap 200) and resume seq.
--control-url / --control-tokenTrust-control server URL and bearer token.
--receipt-dbLocal SQLite path for offline mode.

Next Steps

Query & Audit Receipts · Chio Docs