Skip to content

Receipt format#

This page specifies a stored receipt as the API serves it (GET $WS/agents/{agent}/runs/{run}/receipt), so you can check one with your own tools as well as with astra anemoi receipts verify.

a stored receipt (shortened)
{
  "receipt": {
    "version": 1,
    "run": "researcher-d2b797",
    "task": "researcher-d2b797-0",
    "namespace": "ws-3f9a1c07b2e4",
    "agent": "researcher",
    "agent_version": 1,
    "spec_digest": "sha256:…",
    "policy_digests": {"tools": "sha256:…", "sandbox": "sha256:…", "guardrails": ["no-deletes"]},
    "input_digest": "sha256:…",
    "output_digest": "sha256:…",
    "started_at": "2026-10-01T19:38:31Z",
    "finished_at": "2026-10-01T19:39:52Z",
    "exit": {"state": "Completed", "code": 0},
    "calls": {"count": 14, "trace_entries": 9, "activity_entries": 5, "merkle_root": "sha256:…"},
    "approvals": [],
    "totals": {"model_calls": 6, "input_tokens": 41210, "output_tokens": 1830, "cache_read_tokens": 0,
               "cache_write_tokens": 0, "server_tool_calls": 0, "tool_calls": 3, "denied": 1},
    "machine": "gpu-01"
  },
  "machine_signature": {"alg": "ES256", "public_key": "BC…", "certificates": ["-----BEGIN CERTIFICATE-----…"], "signature": "MEU…"},
  "countersignature": {"jws": "eyJ…", "claims": {"iss": "…", "namespace": "ws-3f9a1c07b2e4", "agent": "researcher",
    "run": "researcher-d2b797", "node": "gpu-01", "receipt_digest": "sha256:…", "machine_key": "sha256:…",
    "seq": 42, "prev": "sha256:…", "iat": 1790883593}}
}

receipt#

Field Type Description
version integer The format: 1.
run, task string The run and its worker, local names.
namespace string The workspace's namespace on the cluster.
agent, agent_version string, integer The agent and the version the run was made from.
spec_digest string sha256: of the version's specification as canonical JSON.
policy_digests.tools string sha256: of the tool policy text as applied: the version's, with the guardrails appended.
policy_digests.sandbox string sha256: of the sandbox policy text as the run got it.
policy_digests.guardrails strings The guardrails appended (names; the organisation's are org-<name>).
input_digest string sha256: of the input.
output_digest string sha256: of the output: the worker's log (standard output and error) as the machine's runtime returned it when it ended.
started_at, finished_at RFC 3339 When the worker started and ended.
exit object state (Completed, Failed, Cancelled), code (when it exited), reason.
calls.count integer Leaves of the tree: trace entries, then activity entries.
calls.trace_entries, calls.activity_entries integer How many of each.
calls.merkle_root string The tree's root, below.
calls.truncated bool Older entries had been rotated away before the receipt was made: the tree covers what was kept.
approvals list [{name, decision, by, at}]: approvals the run used; decision is Approved, Denied, Expired or Used.
totals object model_calls, input_tokens (cached included), output_tokens, cache_read_tokens, cache_write_tokens, server_tool_calls, cost_usd (when priced), tool_calls (MCP, HTTP, web), denied (steps refused).
machine string The machine it ran on.

A receipt holds no argument, URL, prompt or answer.

Canonical JSON#

What is hashed and signed is always the canonical form: object keys sorted by their UTF-8 bytes, no whitespace, strings and numbers as written by serde_json. Save a receipt byte for byte as served: parsing and writing it again may change numbers (0.0 to 0).

The steps' Merkle root#

Each step is one line, {"entry": <the entry>, "source": "trace" | "activity"} in canonical JSON. The export GET $WS/tasks/{run}-0/trace?canonical=1&format=jsonl gives these lines in order (format=json gives {"lines": [...], "total", "frozen"}). The root is the RFC 6962 Merkle tree hash of the lines:

  • a leaf is SHA-256(0x00 ‖ line);
  • a node is SHA-256(0x01 ‖ left ‖ right), the lines split at the largest power of two smaller than their count;
  • no lines: SHA-256("").

The machine's signature#

machine_signature.signature is over astraeus.receipt.v1\n followed by the canonical receipt:

alg Key public_key signature
ES256 The machine's node identity key (ECDSA P-256), its certificate in certificates (then the node CA, when the machine knows it) base64, the uncompressed point base64, ASN.1 DER
EdDSA An Ed25519 key of the machine's own (a machine with no node certificate) base64, 32 bytes base64

The countersignature#

countersignature.jws is a compact JWS — header {"alg": "EdDSA", "typ": "astraeus-receipt+jws", "kid"} — signed with the cluster's key published in its JWKS (GET $WS/identity/jwks, an OKP/Ed25519 key with that kid). Its payload:

Claim Description
iss The cluster.
namespace, agent, run Whose receipt (local names).
node The machine that reported it.
receipt_digest sha256: of the canonical receipt.
machine_key sha256: of the machine's public key bytes.
seq Its place in the agent's chain, from 1.
prev The previous receipt's chain digest; empty for the first.
iat When it was stored (Unix seconds).

countersignature.claims repeats the payload for reading; verification reads it from the JWS.

The chain digest of a stored receipt is sha256: of its countersignature's JWS text. Receipt n follows receipt n−1 when its prev is n−1's chain digest and its seq is one more.

Verifying it yourself#

  1. Check receipt.version is 1.
  2. Verify machine_signature over astraeus.receipt.v1\n + canonical receipt with public_key.
  3. Optionally check the first certificate names the machine and was issued by your node CA.
  4. Verify the JWS with the JWKS key of its kid; check receipt_digest, machine_key, run, node and namespace against the receipt.
  5. With the previous receipt, check prev and seq.
  6. With the exported lines, check their count is calls.count and their root is calls.merkle_root.