Verify a signed attestation offline
Check an exported agent bill of materials or action-receipt checkpoint yourself, with no call to Anectico.
On this page
Anectico signs two kinds of artifact with a dedicated Ed25519 key:
- an agent bill of materials — the components an agent asset depends on at a point in time, and
- an action-receipt checkpoint — an assertion about how long one action-receipt chain was, and what its last entry's digest was, at a point in time.
Both are detached signatures. You can hand either artifact to an auditor, a regulator, or your own security team, and they can check it without an Anectico account, an API key, or a request to us. This page is the complete contract they need.
Online signature and receipt-chain checks keep their complete evidence even when invalid.
MCP returns status: error and a summary starting invalid: <reason>. The corresponding
CLI commands print the full verdict and exit 1 when invalid, including JSON output.
The manifest's online valid field checks its digest, body contract and Ed25519 signature
against a published key. Verify independently with the public key below when you need to
check the server's assertion. Receipt-chain verification authenticates a supplied retained
checkpoint before comparing its chain position.
What a signature proves — and what it does not
A valid signature proves exactly one thing: the holder of this Anectico private key asserted these exact bytes.
It does not prove:
- that the contents are accurate — signing attests to the bytes, not to the world they describe;
- that
generated_atis an independently witnessed time — it is the time we recorded, not a time anyone else vouched for; - that a compromised signer did not produce false or backdated artifacts.
Making the set of attestations append-only to a party we cannot influence is a different mechanism (a transparency log with an external witness) and Anectico does not offer one. Hardware-backed key custody is likewise not offered today. We would rather you know the shape of the claim than assume a larger one.
What the signature does give you is independence: verification never requires a secret. That is why the signature is asymmetric rather than a shared-secret MAC — a MAC can only be checked with the key that could also forge it, so handing you verification would hand you forgery.
Get the verification keys
GET https://<your Anectico host>/.well-known/anectico-signing-keys.json
No credential. The response is a JWK Set:
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"alg": "EdDSA",
"use": "sig",
"kid": "anectico-abom-2026-08",
"x": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg"
}
]
}
x is the raw 32-byte public key as unpadded base64url.
Retired keys stay in this document permanently. An artifact signed in 2027 is still verifiable in
2030, because the key it names is still published. Take the kid from inside the artifact — not
from any wrapper around it — and look it up here.
The byte contract
A bill of materials carries canonical_form: the exact bytes the signature covers. Verify
against that string, byte for byte. Its other fields are a convenience view over those bytes.
The public checkpoint object instead carries scope, chain, sequence, entry digest, generation
time, checkpoint digest, signature, signing key and algorithm. It has no canonical_form field.
Reconstruct its unsigned body using the fixed checkpoint contract below before checking its
digest and signature. Passing the complete object to receipt-chain verification does this for you.
The canonical form is RFC 8785 (JCS) JSON encoded as
UTF-8: object members sorted by key, no insignificant whitespace, minimal string escaping, integers
in shortest decimal form, and non-ASCII characters emitted as literal UTF-8 rather than \uXXXX
escapes. Beyond JCS, five rules are fixed so nothing is left to guess:
- Timestamps are UTC RFC 3339 with exactly nine fractional digits, trailing zeros kept:
2026-08-16T09:30:00.000000000Z. - Enumerated values enter as integers, not display names. A bill-of-materials component's
kindis the numeric value from the API'sAssetKindenumeration. - Every field is always present, including empty optional identifiers, which appear as
"". The key set never depends on the data. - Components are sorted by
(kind, ref, version, content_sha256, declared_by)before encoding. Every other array's order is significant and is encoded as given. releaseis the one member whose value may benull. The key is still always present — rule 3 holds — but its value distinguishes two facts that must never collapse into one:nullmeans the producer named no release, and""means the producer named the empty label. They are different bytes, a different digest and a different signature.
signing_key_id and signature_alg are inside the signed bytes, so neither can be swapped after
the fact. For a bill of materials, authority is inside them too — see below.
An agent bill of materials
{"asset_id":"7c9e6679-7425-40de-944b-e07fc1f90ae7","asset_version_id":"1b4e28ba-2fa1-11d2-883f-0016d3cca427","authority":"declared","components":[{"content_sha256":"bbbb…","declared_by":"telemetry","kind":1,"ref":"agent:triage","version":"4.0.1"}],"deployment_id":"","generated_at":"2026-08-16T09:30:00.000000000Z","manifest_id":"3f2504e0-4f89-41d3-9a0c-0305e82c3301","release":"2026.08.16","schema":"anectico.abom.v1","scope":{"environment":"production","org_id":"…","project_id":"…"},"signature_alg":"EdDSA","signing_key_id":"anectico-abom-2026-08"}
release says which release this bill of materials is for, using the same label the agent puts
on its spans as gen_ai.agent.version. It is inside the signed body so that "this is the
composition for the release that ran" is something you can check yourself: two releases of one
unchanged composition are two artifacts with two digests and two signatures, and a manifest signed
for one cannot be presented as the manifest for the other without breaking the signature. null
means the producer declared no release at all — not the same thing as "".
Read authority before you read anything else in a manifest. It has exactly two values and they
mean different things:
authority |
What the signature establishes |
|---|---|
declared |
The organization named in scope submitted this composition and Anectico notarized it. The signature proves what was declared and that it is unchanged since — not that the components are accurate. |
mediated |
Anectico itself supplied the composition. |
It is a member of the signed body, not a field beside it, precisely so that this distinction survives export: a manifest handed to you as a file carries its own authority, and no column, wrapper or covering email can change it without breaking the signature. A verifier that ignores it is reading a customer's declaration as a platform finding.
An action-receipt checkpoint
{"chain_id":"…","entry_sha256":"9f9f…","generated_at":"2026-08-16T09:30:00.000000000Z","schema":"anectico.receipt-checkpoint.v1","scope":{"environment":"production","org_id":"…","project_id":"…"},"seq":41,"signature_alg":"EdDSA","signing_key_id":"anectico-abom-2026-08"}
seq is the sequence number of the last entry covered, so a chain of 42 entries checkpoints at
seq 41.
For a public checkpoint, the body above has exactly these members. Set schema to
anectico.receipt-checkpoint.v1 and the signed body's signature_alg to EdDSA.
The public API's algorithm is SIGNATURE_ALGORITHM_ED25519. Preserve the original scope,
chain, sequence, entry digest and key id. Render the generation time in UTC with nine
fractional digits. The signature and checkpoint_sha256 are outside this unsigned body.
What is actually signed
Not the canonical form directly, and not the bare digest. Compute the raw 32-byte SHA-256 of the canonical form, then verify the Ed25519 signature over:
ASCII(<domain>) || 0x00 || <32-byte SHA-256 digest>
where <domain> is:
| Artifact | Domain string |
|---|---|
| Agent bill of materials | anectico.abom.signature.v1 |
| Action-receipt checkpoint | anectico.receipt-checkpoint.signature.v1 |
The trailing NUL byte is part of the input. The two domains are separate on purpose: a signature made for one artifact type will not verify as the other, so a checkpoint can never be presented as a bill of materials or the reverse.
The signature field is the Ed25519 signature as unpadded base64url (64 bytes decoded).
Verify it
import base64, hashlib, json, re
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
ABOM_DOMAIN = b"anectico.abom.signature.v1\x00"
CHECKPOINT_DOMAIN = b"anectico.receipt-checkpoint.signature.v1\x00"
def b64url(s): # unpadded base64url -> bytes
return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))
def checkpoint_canonical(artifact):
if artifact["signature_alg"] != "SIGNATURE_ALGORITHM_ED25519":
raise ValueError("unsupported checkpoint algorithm")
seq = artifact["seq"]
if (isinstance(seq, bool) or not isinstance(seq, (int, str))
or not re.fullmatch(r"0|[1-9]\d*", str(seq)) or int(seq) >= 2**64):
raise ValueError("checkpoint sequence must be an exact uint64")
# Checkpoint command timestamps are UTC; preserve their fractional precision.
match = re.fullmatch(r"(.+T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?Z", artifact["generated_at"])
if not match:
raise ValueError("checkpoint timestamp must be UTC RFC3339")
body = {"chain_id": artifact["chain_id"], "entry_sha256": artifact["entry_sha256"],
"generated_at": match[1] + "." + (match[2] or "").ljust(9, "0") + "Z",
"schema": "anectico.receipt-checkpoint.v1", "scope": artifact["scope"],
"seq": int(seq), "signature_alg": "EdDSA",
"signing_key_id": artifact["signing_key_id"]}
return json.dumps(body, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def verify(artifact, jwks, domain):
canonical = (artifact["canonical_form"].encode("utf-8") if domain == ABOM_DOMAIN
else checkpoint_canonical(artifact))
# The digest recorded beside the artifact must be the digest OF these bytes.
recorded = artifact.get("manifest_sha256") or artifact["checkpoint_sha256"]
if hashlib.sha256(canonical).hexdigest() != recorded:
return False
# The key id comes from INSIDE the signed bytes, never from a wrapper.
kid = json.loads(canonical)["signing_key_id"]
jwk = next((k for k in jwks["keys"] if k["kid"] == kid), None)
if jwk is None:
return False
pub = Ed25519PublicKey.from_public_bytes(b64url(jwk["x"]))
try:
pub.verify(b64url(artifact["signature"]), domain + hashlib.sha256(canonical).digest())
return True
except Exception:
return False
Test your implementation against these fixed vectors before trusting it. They are the same ones the platform's own tests are pinned to.
| Value | Meaning |
|---|---|
| Key id | anectico-abom-2026-08 |
Public key (x) |
A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg |
| Canonical-form SHA-256 | 55a4b06e4214bc956a8602d004c124d9f6132903af066cd33827aeee70125daa |
| Signature (bill of materials domain) | kryppUbmXxJMgwxs-ZBzeWwBxW6MZTLpxkdZz9hm63sMozjslk-LhZi7AXdKiYDIpMnceudaa2NG-NfxFVk1AQ |
A correct implementation also rejects that same signature when checked in the checkpoint domain. If yours accepts it, your domain separation is not wired up.
What a checkpoint is for
Action receipts are chained: each entry's digest covers the previous entry's digest. That makes the chain tamper-evident against modification and reordering — but not against truncation, since deleting the end of a chain leaves a shorter chain that is perfectly self-consistent.
A checkpoint you have retained yourself closes that. Your copy says the chain held n entries ending in a specific digest at a specific time, signed by a key whose private half never left Anectico. Nothing we do later can reach your copy.
That is only true of a copy you hold. Ask for a checkpoint on a schedule that matches how much history you need to be able to prove, store it outside Anectico, and keep the ones you have.
POST /api/v1/receipt-chains/{chainId}/checkpoints?project_id=...
Checkpoint signing requires agents:read, agents:write, mcp:read and mcp:write.
It is a write because it retains a signed attestation. The first request previews the complete
unsigned checkpoint and signing key. A faulty or empty chain is refused. Review that preview,
then repeat identical arguments with confirm_token and a stable idempotency_key.
anectico receipt-chains checkpoint actions --idempotency-key <key>
# After reviewing the returned preview:
anectico receipt-chains checkpoint actions --idempotency-key <key> --confirm-token <token>
Over MCP use sign_receipt_checkpoint through execute_internal_action. The confirmed mutation
receipt contains the complete signed checkpoint in its /checkpoint change. Retain that exact
checkpoint outside Anectico. Later pass it as retained_checkpoint to verify_receipt_chain, or
save the CLI response and use anectico receipt-chains verify actions --checkpoint-file <file>.
Verification authenticates its scope, signature, sequence and digest. matched means that exact
entry survives unchanged; contradicted makes verification invalid, including a consistently
deleted tail. outside_retained_range means retention removed the earlier assertion; it is not a match.
Record your own agents' actions
Actions Anectico performs or gates are entered in the chain as mediated. You can add your own
agents' actions to the same chain:
POST /api/v1/action-receipts/entries
Requires the agents:write scope. Send the draft — action_id (an id you mint for the action),
state, occurred_at, tool_name and the other fields you know — and omit chain_id and
receipt_authority, which are set for you — a draft that supplies either is refused. The entry is
recorded as reported, never mediated: it proves what you told us and when, not that the action happened. Sending
a byte-identical draft again within seven days returns the entry already recorded instead of adding
a second one, so a retry after a timeout is safe; after seven days the same draft is recorded as a
new entry. A reported entry does not add anything to the authority graph.
anectico receipt-chains append --environment production --file draft.json
anectico receipt-chains append --environment production --file draft.json --confirm-token "$TOKEN"
Over MCP this is the append_action_receipt write action. REST, MCP and the CLI
preview the complete current and proposed entry first, then require confirmation
with identical arguments. The preview needs both agents:read and agents:write
and stops if current state cannot be read. The applied answer carries the entry's
sequence number and digest. Recording the entry does not perform its reported action.
Ask your agent
Check that this project's action receipt chain is intact, and give me a signed checkpoint I can keep outside Anectico.
| Job | MCP tool or action | CLI command |
|---|---|---|
| Read one action's receipt with every ledger entry | get_action_receipt |
anectico action-receipts get |
| List receipts by action or chain | list_action_receipts |
anectico action-receipts list |
| Ask the platform to check a chain | verify_receipt_chain |
anectico receipt-chains verify |
| Get a signed checkpoint to keep | sign_receipt_checkpoint |
anectico receipt-chains checkpoint |
| Sign or read an agent bill of materials | sign_abom_manifest, get_abom_manifest |
anectico abom sign, anectico abom get |
Verification reads need mcp:read and agents:read; checkpoint signing additionally needs mcp:write and agents:write. A receipt has no proof page. The signed checkpoint
and the offline check below are the proof.
Ask the platform to check a chain
GET /api/v1/receipt-chains/{chainId}/verify?project_id=...
Requires the agents:read scope. The response reports whether the stored chain is internally
consistent:
| Field | Meaning |
|---|---|
valid |
Every entry's digest recomputes, every link matches its predecessor, and the sequence is gapless. |
entry_count |
How many entries were read. Reported even when valid is false. |
mediated_entry_count |
How many of them Anectico performed or gated. |
reported_entry_count |
How many of them your agents declared. Adds to mediated_entry_count to give entry_count. |
first_invalid_seq |
The lowest sequence number involved in a fault. Meaningful only when valid is false. |
fault_count |
Total number of faults found. Zero when valid is true. |
last_entry_sha256 |
The digest of the last entry read. |
detail |
A short lowercase code naming the kind of fault. Never free text and never your data. |
verified_from_seq |
The sequence number of the first entry the check covered. 0 until entries age out of the chain; see below. Entries before it are not verified: they no longer exist. |
anchor_entry_sha256 |
The digest of entry verified_from_seq - 1, the last one that aged out. Empty while verified_from_seq is 0. |
prune_anchor |
The signed statement that entries before verified_from_seq were removed for age, returned whole so you can check its signature yourself. Absent while verified_from_seq is 0. |
redacted_entry_count |
How many of the entries read had personal values erased. They are counted in entry_count. |
redaction_statement_count |
How many of the entries read are the ledger's own statements of which entries an erasure redacted. |
A chain that has never been opened — no action has been recorded in that project, by us or by you —
is a 404, not valid: false. Having done nothing is not evidence of tampering.
Read the two counts, not just valid. One chain holds both kinds of entry, and the split is what
tells you how much of a verified ledger is Anectico's own attestation and how much is your agents'
own account of themselves. mediated means the action ran through this platform. reported means an
agent told us it acted, and the record proves what we received and when — not that it happened. The
word is inside the bytes each entry's digest covers, so it survives export: an entry cannot be
relabelled without breaking its digest, which is exactly what the recipe above detects.
Entries older than 400 days leave the chain, under a signed prune anchor. A receipt is kept for
400 days. When the oldest entries pass that age they are removed from the front of the chain, never
from the middle, and in the same step a prune anchor is signed and stored. The chain then starts
at verified_from_seq and its first remaining entry links to anchor_entry_sha256.
A prune anchor is not a checkpoint. A checkpoint is something you can ask for at any time, so it cannot be what permits removing the start of a chain. The anchor is a separate signed statement, written only when entries are removed for age, and it says exactly what was done:
| Field | Meaning |
|---|---|
scope |
The organization, project and environment of the chain. |
chain_id |
The chain. |
base_seq |
The sequence number the chain now starts at. Equals verified_from_seq. |
base_prev_sha256 |
The digest of entry base_seq - 1, the last one removed. Equals anchor_entry_sha256. |
cutoff |
Every removed entry was recorded before this instant. |
retention_seconds |
The retention in force when the entries were removed. |
generated_at |
When the anchor was signed. |
anchor_sha256, signature, signing_key_id, signature_alg |
The digest of the anchor, its Ed25519 signature and the key that made it. |
What the check does with it, and what it leaves to you. The check refuses a chain that starts
after sequence 0 unless an anchor for exactly that start is present, states that start and that
digest, and carries a signature that verifies under one of our
published verification keys. A chain whose early entries are missing without such an
anchor does not verify (anchor_contradicted), and so does one whose anchor was altered or signed
by anything else. A checkpoint is never accepted in place of an anchor.
The check cannot verify the entries that were removed: they are gone, and nobody can recompute the
digest of an entry that no longer exists. valid: true on a chain with verified_from_seq above
0 is a statement about the entries from there on, plus a signed statement about where the chain
starts. It is not a statement that the removed entries were intact. If you need that, keep a
checkpoint of your own while the entries exist.
You do not have to take our word that the anchor's signature is good. Recompute it:
body = canonical JSON of
{"base_prev_sha256", "base_seq", "chain_id", "cutoff", "generated_at",
"retention_seconds", "schema": "anectico.receipt-prune-anchor.v1",
"scope": {"environment", "org_id", "project_id"},
"signature_alg": "EdDSA", "signing_key_id"}
digest = SHA-256(body) // equals anchor_sha256
signed = "anectico.receipt-prune-anchor.signature.v1" || 0x00 || digest
with keys sorted, no insignificant whitespace, integers in decimal, and both timestamps as UTC
RFC 3339 with exactly nine fractional digits (2027-09-20T00:00:00.000000000Z). Verify signature
(unpadded base64url) over signed with the Ed25519 public key whose id is signing_key_id. The
first string is different from the one a checkpoint is signed under, so a checkpoint's signature
never verifies as an anchor's.
entry_count and the two authority counts are of the entries that remain. A checkpoint you retained
over an entry that has since aged out is still a true, signed statement about the chain at that
time; it can no longer be compared against a stored entry.
An entry whose personal values were erased still verifies. A receipt can say who an action was
about. Those values — distinct_id, subject_refs, purpose, resource_uris, the session and
turn, and the three digests sent with the entry — are not inside an entry's digest in the clear. The
digest covers one commitment per value:
SHA-256("anectico.receipt-sealed.v1" || 0x00 || field name || 0x00 || salt || value)
where the salt is 32 random bytes drawn for that one entry and kept beside the values, outside the
chain. Erasing the person deletes the salt and the values together.
The commitments, and so every digest, link and checkpoint, are unchanged; and without the salt a
commitment cannot be matched to a value, even a guessed one. Such entries are counted in
redacted_entry_count. The people and credentials an entry names as having performed, requested or
approved the action are in the digest in the clear and are never erased.
An erasure is itself in the ledger. An entry is not treated as erased just because its values
are missing. When an erasure removes values from entries, it appends one or more statements to the same
chain in the same step: a mediated entry with tool redact_receipt_entries and an action_id of
the form redaction:<erasure operation id>:<n>, whose resource_uris list the affected entries
(anectico:receipt-chain/actions?redacted=3-5,9). It names the erasure operation and the entries,
and no person. An entry recorded after its subject was erased says so in its own digest
(born_redacted is true in its body) and needs no such statement. The check reports
redaction_unaccounted for an entry whose values are missing with neither, and for a statement
that does not match what it names. Nobody can submit an entry under the redaction: prefix.
This check does not replace a checkpoint you hold. It reads the same stored chain it is
reporting on, so it can tell you that a chain is internally inconsistent, but it cannot tell you that
entries were removed from the end. Compare last_entry_sha256 and entry_count against a checkpoint
you retained to answer that. The two together are the complete answer; either alone is not.
The same check is available where you are already working, and both default to your project's one action chain:
anectico receipt-chains verify # or: anectico receipt-chains verify <chain-id>
From an AI agent connected over MCP, ask it to run the verify_receipt_chain action — it is reached
through execute_read_action and needs the same agents:read scope. All three surfaces return the
same fields and the same verdict; a project that has recorded no action reports that it has no chain
rather than reporting a failure.
Fetch a bill of materials
GET /api/v1/abom-manifests/{manifestId}?project_id=...
Requires the agents:read scope. The response carries the manifest, its canonical form, the
canonical-form SHA-256 and the signature — everything the verification recipe above needs. Re-derive
the digest from the canonical bytes yourself rather than trusting the one in the response; a manifest
is immutable once signed, so the same id returns the same bytes and the same digest every time.
Read authority and release from the canonical form, not from the JSON fields beside it. They
agree today, and only one copy of each is covered by the signature.
The response also carries a verdict on the stored artifact: valid, and defect naming the reason
when valid is false. It checks the digest, current body contract, indexed authority and release and
signing key agreement, and the Ed25519 signature against the published key in the ABOM domain.
Retained published keys still verify after rotation. An unavailable key is an explicit defect.
Verify independently with the recipe above when you need to check the server's assertion.
From a terminal, anectico abom get <manifest-id> returns the same response. Over MCP,
get_abom_manifest returns the canonical form and signature byte-exact alongside the verdict.
Its canonical_form is an untrusted document: use its exact value, never its display label.
The read takes a manifest id and project; its environment is part of the signed body.
defect |
What it means |
|---|---|
| (empty) | The stored body matches this contract and its signature verifies under the published key. |
canonical_form_superseded |
The manifest was signed under an earlier version of this contract. Its signature is still a true statement about the bytes it covers; those bytes are simply no longer a body we would produce for that manifest. Sign a fresh manifest for the asset version and reference that one going forward. |
canonical_form_unreadable |
The stored body is not JSON. This should never happen; tell us. |
digest_mismatch |
The recorded manifest_sha256 is not the digest of the recorded canonical_form. This should never happen; tell us. |
column_body_mismatch |
An indexed authority, release or signing key disagrees with the signed body. |
signature_invalid |
The detached signature does not verify in the ABOM domain. |
signing_key_unavailable |
The signing key is absent from the current published set; signature validity cannot be established. |
signature_algorithm_unsupported |
The declared signature algorithm is unsupported. |
valid: false is reported with a normal 200 and the manifest is returned as stored. It is your
artifact, it is the only copy of it you can reach here, and withholding it would destroy a
historical attestation rather than protect anyone.
Declare a bill of materials
POST /api/v1/abom-manifests
Requires the agents:write scope, which is owner or admin only to mint. Send the scope, the asset (and
optionally the asset version and deployment) the manifest describes, generated_at, and the
component list — identifiers, versions and content hashes only. Send release as well, using the
same label your agents put on their spans as gen_ai.agent.version; it goes inside the signed bytes
so a holder can check that this is the composition for the release that actually ran. Omit it
entirely if you do not tag releases — the signed body then says "release": null, which is a
different statement from "release": "", and sending an empty string to mean "none" makes the
artifact say something you did not intend. The five derived fields
(canonical_form, manifest_sha256, signature, signing_key_id, authority) must be absent; a
supplied value is rejected rather than ignored, so nothing you sent can be silently discarded and
nothing you sent can choose what the signature means. The response is the complete signed artifact,
with authority: "declared". Two representations of the same fact coexist on that response: the
top-level REST field carries the prefixed enum name (ABOM_AUTHORITY_DECLARED), while the lowercase
declared shown above is the value inside canonical_form — the bytes the signature actually
covers.
To sign from a terminal, put the manifest in a file and run anectico abom sign --file manifest.json.
Over MCP, use the sign_abom_manifest write action. Both require manifest_id — generate a UUID
for each manifest you intend to sign. With it, retrying the same request returns the same signed
artifact, and a different manifest sent under an id already used is refused rather than signed
twice.
Checkpoint signing requires a durable idempotency_key (anectico receipt-chains checkpoint <chain-id> --idempotency-key <intent>). A retry returns the original signed artifact after chain growth; use a new key for a fresh tail. Preview and confirm with the same key. The chain supplies its actual environment. The preview may return an already-issued signed checkpoint unchanged; it creates no signature.
The applied or replayed structured receipt retains the complete detached checkpoint
as JSON in the target change whose path is /checkpoint. Decode that change's after
value and keep the exact object outside Anectico. Its target reference identifies the
artifact; it is not a browser proof link.