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.
{
"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#
- Check
receipt.versionis 1. - Verify
machine_signatureoverastraeus.receipt.v1\n+ canonical receipt withpublic_key. - Optionally check the first certificate names the machine and was issued by your node CA.
- Verify the JWS with the JWKS key of its
kid; checkreceipt_digest,machine_key,run,nodeandnamespaceagainst the receipt. - With the previous receipt, check
prevandseq. - With the exported lines, check their count is
calls.countand their root iscalls.merkle_root.