·Docs & API

Build with Satsignal.

One bearer-auth API for anchoring an agent’s commitments, policy snapshots, or evidence manifests. Drop-in helpers in Python and JavaScript produce byte-identical canonical bytes across runtimes, so a Python commit reveals cleanly to a JavaScript verifier and vice versa. Verification runs in any browser or CI runner against any public block explorer — no Satsignal API call required at verify time.

·Start here

Minimum viable agent integration.

Four anchors that make any agent run independently auditable. Each is one POST /api/v1/anchors; the Quickstart below walks the mechanics.

  1. 1

    Before — policy snapshot.

    Hash the model, tools, budget, and instructions in force when the agent starts. category: "policy_snapshot". Drop-in helper: policy_snapshot.py.

  2. 2

    During — commit each output.

    Hash each decision, score, or intermediate output and anchor with category: "commitment". The reveal can happen later; the commit binds order and timing now.

  3. 3

    After — evidence manifest.

    Merkle-root up to 10,000 artifacts (logs, files, intermediate hashes) into one anchor. category: "evidence_bundle". Per-leaf inclusion proofs verify offline.

  4. 4

    Store — what the agent keeps.

    Per anchor, persist bundle_id, txid, the original payload, and the .mbnt. That is everything an auditor needs — no Satsignal call at verify time.

Worked example end-to-end: example_agent_snapshot.py runs against your SATSIGNAL_API_KEY and produces a real on-chain receipt.

·CLI

Fastest: the CLI.

For file-anchor workflows, the official satsignal-cli is the shortest path from source file to verified receipt. Pure Python plus requests; no other deps. Source at github.com/Steleet/satsignal-cli, on PyPI as satsignal-cli.

$ pip install satsignal-cli

$ satsignal login            # paste your sk_... API key
$ satsignal anchor report.pdf --broadcast
✓ anchored report.pdf
  txid:    9c2e…4d11
  receipt: report.pdf.mbnt

$ satsignal verify report.pdf
✓ verified  (chain-confirmed by default)

Six verbs: anchor, verify, show, log, login, matters. Implements the bundle-v1 spec for verification — chain-confirm by default; error classes mapped to exit codes. Sidecar convention is <file>.mbnt next to the source. Scripted flows can opt into --strict (exit code 7) to fail when the local sidecar can’t be written, even when the on-chain anchor itself succeeded.

Building a LangChain agent? The first-party langchain-satsignal package (pip install langchain-satsignal) drops the same primitives into a LangChain agent as components — anchor a policy snapshot at session start, commit-reveal each decision, finalize with a manifest. Available on PyPI.

Want direct HTTP control, a non-Python runtime, or one of the agent-specific flows (commitments, policy snapshots, evidence manifests)? The Quickstart below walks the underlying POST /api/v1/anchors step-by-step.

01Quickstart

From signed-in user to verified receipt in five steps.

The shortest path from zero to an on-chain receipt your auditor can verify without us. Each step deep-links into the Reference below for the full shape.

  1. 1

    Get an API key.

    Sign in at app.satsignal.cloud with a magic link. Mint a key with the anchors:create scope at /w/<workspace>/keys. (See 02 for what the key authorizes.)

    Multi-tenant deployments: the mint form has a per-matter scope picker. Leave it unchecked for an unscoped key (sees every matter, the default). Check one or more matters to restrict the key to that exact set — useful for handing a customer-specific key to an auditor or per-tenant integration. Out-of-scope reads return 404 indistinguishable from “slug doesn’t exist”, so a scoped key can’t enumerate other tenants. Matters created later are not auto-included.

  2. 2

    Create a matter.

    Matters are the namespace each receipt files under — one per integration is fine. POST /api/v1/matters with a slug like agent-runs-prod.

  3. 3

    Anchor a commitment.

    Hash your payload locally, then POST /api/v1/anchors with the matter_slug, sha256_hex, file_size, and category: "commitment". The response carries bundle_id, txid, and receipt_url. (See 04 for category choices.)

  4. 4

    Download the bundle.

    The anchor response carries a bundle_url field — that's the full URL, on app.satsignal.cloud, with bearer auth. GET https://app.satsignal.cloud/bundle/<id>.mbnt with your Authorization: Bearer sk_... header returns the .mbnt zip. It carries the canonical doc, the manifest, and the miner’s signed acceptance — everything an offline verifier needs.

  5. 5

    Verify without us.

    Drop the bundle into proof.satsignal.cloud/verify, or re-hash the revealed payload in any language and compare against any block explorer’s OP_RETURN. No Satsignal API call required at verify time.

Need it in three lines instead of five steps? Use the CLI above — pip install satsignal-cli then satsignal anchor file.pdf --broadcast. For Python scripts wiring the HTTP path directly, commit_reveal.py produces the canonical bytes + sha256 in one call; the worked example at example_agent_snapshot.py runs end-to-end against your SATSIGNAL_API_KEY.
02Reference — The endpoint

One endpoint, three modes.

Every anchor — commitment, policy snapshot, manifest, sealed envelope, or plain file proof — goes through the same call. Mode is inferred from the body shape; category is an explicit field that tags the receipt without changing the primitive.

POST https://app.satsignal.cloud/api/v1/anchors
Authorization: Bearer <your-api-key>
Content-Type: application/json
You’ll need.
  • An API key with the anchors:create scope (mint one at /w/<workspace>/keys after signing in).
  • A matter_slug in your workspace — the namespace each receipt files under.
  • A SHA-256 fingerprint, or a list of {label, sha256_hex} items, computed locally. The payload itself never leaves your environment.
Plans. The free plan ships every new account with 100 anchors per calendar month. Paid plans (Starter / Pro / Scale) lift the cap to 10K / 100K / custom and bill monthly. Verification stays free regardless. Self-serve upgrade lives at /w/<workspace>/billing; for an invoice-billed Scale plan, mail hello@satsignal.cloud.

Standard mode (single fingerprint)

{
  "matter_slug": "agent-runs-prod",
  "sha256_hex": "d2f658c562d7bccc...d0fecded",
  "file_size": 208,
  "category": "commitment",
  "label": "agent-alpha sealed bid",
  "filename": "alpha-bid.json",
  "session_id": "run-2026-05-09-001"
}

All fields except matter_slug + sha256_hex + file_size are optional. session_id is a freeform operator-supplied grouping key (1-128 chars [A-Za-z0-9_.-]) that lets you list every anchor in one agent run via GET /api/v1/anchors?session_id=…. Off-chain by design — for the cryptographic "these anchors belong together" binding use manifest mode at end of session. The agent_anchor.py helper bundles both patterns; see /agents.

Manifest mode (Merkle batch, up to 10,000 items)

{
  "matter_slug": "agent-runs-prod",
  "items": [
    {"label": "Q1", "sha256_hex": "10343a87...aa921669"},
    {"label": "Q2", "sha256_hex": "f68246b5...4b56894c"}
  ],
  "category": "evidence_bundle",
  "label": "math-eval verdicts 2026-05-07",
  "session_id": "run-2026-05-09-001"
}

Mode is detected from the presence of items; do not also send sha256_hex or file_size.

For tabular data (CSV rows, JSON records, eval results) the merkle-row-v1 scheme is a documented use of this mode — each item is a JCS-canonicalized row. If your rows are low-entropy (an auctioned price, a Yes/No vote, a credit grade), see selective disclosure of low-entropy rows below: merkle-row-sealed-v1 seals each leaf under a per-leaf HMAC salt so a plain SHA-256 leaf can’t be brute-forced.

Sealed mode (HMAC commitment, salt private)

{
  "mode": "sealed",
  "matter_slug": "agent-runs-prod",
  "salt_b64": "<32-byte salt, base64url>",
  "byte_exact_commitment": "<HMAC-SHA256 of the canonical bytes>",
  "file_size": 208,
  "category": "policy_snapshot"
  // retain_days defaults to 0 for the API: bundle returned
  // inline as bundle_b64, salt-bearing zip never lands on
  // our disk. Pass retain_days: 1..30 to opt into a
  // server-side mirror (recovery convenience).
}

Sealed mode — blind submission (salt never reaches us)

Omit salt_b64 to opt into the cryptographically-blind protocol: the salt never crosses the wire, we return the canonical-doc bytes, and your client assembles the .mbnt bundle locally. No server-side bundle is ever written. retain_days must be 0 or absent (nothing to retain).

{
  "mode": "sealed",
  "matter_slug": "agent-runs-prod",
  // no salt_b64 — its absence is the blind signal.
  "byte_exact_commitment": "<HMAC-SHA256 of the canonical bytes>",
  "file_size": 208,
  "category": "policy_snapshot"
}

The response carries canonical_b64 + doc_hash in place of bundle_b64; retain_until is omitted. Your client builds manifest.json (with the salt it already holds), drops in the verbatim canonical.json bytes, optionally adds proofs.json rebuilt from local commitments, and zips. The verifier accepts the result transparently — bundle shape is identical regardless of where it was assembled.

Response (all modes)

{
  "bundle_id": "f83649e3846c4ea2",
  "txid": "2e042a64...7a3db61b",
  "mode": "standard",
  "category": "commitment",
  "retain_until": "2026-06-06T00:00:00Z",
  "dry_run": false,
  "matter_slug": "agent-runs-prod",
  "receipt_url": "https://app.satsignal.cloud/w/.../r/f83649e3846c4ea2"
  // manifest mode also: "leaf_count", "root"
  // sealed + retain_days=0 (salt-bearing default): "bundle_b64"
  //   carries the .mbnt zip inline; "retain_until" is null.
  //   You MUST persist the bundle yourself — server has no copy.
  // sealed + salt_b64 omitted (blind): "canonical_b64" + "doc_hash"
  //   replace "bundle_b64"; "retain_until" is omitted entirely.
  //   Your client assembles the .mbnt locally.
}

Quota denials are 429; validation errors 400; missing matter 404; unknown category 400 invalid_category; unrecognized body field 400 unknown_field.

Reserved fields (will 400)

Two field names that look reasonable are deliberately rejected:

  • dry_run — the broadcast policy is fixed per deploy on this endpoint. Sending dry_run: true will not gate the anchor; we 400 so you notice rather than burning sats on a probe.
  • chunk_merkle — not yet supported on /api/v1/anchors; standard mode binds only the single byte_exact commitment. Use the /notarize multipart form (browser path) for chunk_merkle proofs in the meantime.

Matter-level audit (selective-reveal defense)

A holder disclosing one anchor to an auditor can’t say this is the only one I created on their own word. The matter (the namespace each receipt files under) is the unit the operator’s API key controls; an auditor with read scope on that key can list every anchor — current and soft-deleted — the matter has ever contained:

GET https://app.satsignal.cloud/api/v1/matters/<slug>/anchors
Authorization: Bearer <key with receipts:read>

Returns anchor_count plus an anchors[] array (bundle_id, txid, mode, category, label, anchored_at, retain_until, deleted, receipt_url, bundle_url). Soft-deleted rows are included — chain anchors are permanent and the workspace-side delete must not mask siblings from the audit. Single-receipt callers get the same number on GET /api/v1/receipts/<id> as matter_anchor_count.

Scope. The audit is matter-scoped. An operator who pre-anchors candidate outcomes under separate matters is not detected by this endpoint; use the workspace-wide listing below for cross-matter audits.

Workspace-wide listing (cross-matter audit)

For audit / compliance scripts asking what did I anchor last week? across every matter:

GET https://app.satsignal.cloud/api/v1/anchors?limit=100
Authorization: Bearer <key with receipts:read>

Most-recent-first. Each row carries the matter-scoped fields plus matter_id / matter_slug / matter_name so a caller can stitch matter context without a second round trip. Soft- deleted rows are included for the same reason as the matter- scoped listing — chain anchors are permanent and the workspace-side delete must not mask siblings.

Pagination. Cursor-based on anchored_at:

{
  "anchor_count": 100,                    // size of THIS page
  "workspace_total": 432,                 // across the whole workspace
  "limit": 100,
  "before": null,
  "next_cursor": 1778196000,              // unix ts; pass as ?before= for next page
  "anchors": [ ... ]
}

Loop until next_cursor is null. limit defaults to 100, capped at 1000.

Filter by session. If you tagged anchors with a session_id at POST time, the listing accepts the same key as a query param to scope an audit to one agent run:

GET https://app.satsignal.cloud/api/v1/anchors?session_id=run-2026-05-09-001
Authorization: Bearer <key with receipts:read>

workspace_total in the response narrows to match the filter, so a caller knows how many anchors are in this session vs the workspace as a whole. Each row carries its own session_id; rows without a session id stay null. Off-chain only — the chain doesn’t see this field. See /agents for the full pattern.

Quota visibility

Every POST /api/v1/anchors response (200 success and 429 quota denial) carries standard rate-limit headers so callers can back-pressure before hitting a hard stop:

X-RateLimit-Limit:     100        # anchors allowed per window
X-RateLimit-Remaining: 87         # post-anchor on success; pre-anchor on 429
X-RateLimit-Reset:     1780272000 # unix ts of next window reset
X-RateLimit-Window:    month      # 'day' (legacy) or 'month' (free/starter/pro/scale)

Or fetch the same numbers without burning an anchor:

GET https://app.satsignal.cloud/api/v1/usage
Authorization: Bearer <key with receipts:read>

{
  "plan": "free",
  "window": "month",
  "used": 13,
  "limit": 100,
  "remaining": 87,
  "period_end_utc": 1780272000,
  "anchors_allowed": true,
  "reason": null
}

New signups land on the free plan: 100 anchors per calendar month, monthly reset at the first of the next month UTC. Higher tiers (Starter $29 / Pro $99 / Scale custom) self-serve at /w/<workspace>/billing.

03Reference — Annotating an anchor

Off-chain “this was a typo” without rewriting history.

The chain is permanent. If you anchored the wrong payload, or a later run supersedes an earlier one, you can’t edit the transaction. What you can do is attach an operator annotation to the workspace receipt. The chain anchor stays untouched; the annotation surfaces on the dashboard receipt page and on any share link you mint, with explicit workspace metadata, not chain mutation framing so an auditor isn’t misled into thinking the original anchor was revoked.

Verifiers ignore annotations entirely — they consume the bundle, not the workspace’s customer DB.

Endpoint

PATCH https://app.satsignal.cloud/api/v1/anchors/<bundle_id>
Authorization: Bearer sk_...
Content-Type: application/json

{
  "annotation": {
    "kind": "typo",                       // typo | superseded | redacted | note
    "text": "off-by-one in the label"     // optional, ≤ 1024 chars
  }
}

Required scope: receipts:annotate. Off the default-mint set so a stolen anchor-only key cannot retroactively flag receipts; opt in explicitly when minting a key for an annotation tool.

Kinds

KindWhen to use it
typo You anchored a payload that has a mistake in it but is not sensitive. The annotation explains the error so an auditor reviewing the matter understands the anchor should not be relied on.
superseded A later anchor in the same workspace replaces this one. Pass superseded_by_bundle_id with the replacement bundle id; the receipt page links across. Same-workspace only; cross-workspace pointers are rejected.
redacted The bundle’s context (label, memo, filename) is now flagged as sensitive or wrong. The on-chain hash stays public; the workspace surfaces a redaction notice on the receipt page. (You cannot retract chain data; this is a workspace-side disclosure that the anchor should not be referenced.)
note Catch-all for non-status context (a citation, a cross-reference, an operator’s comment). No behavioral implication.

Supersede shape

PATCH /api/v1/anchors/<bundle_id>
{
  "annotation": {
    "kind": "superseded",
    "superseded_by_bundle_id": "<replacement bundle id>",
    "text": "replaced by run #42 after the metric revision"
  }
}

Clearing an annotation

PATCH /api/v1/anchors/<bundle_id>
{ "annotation": null }

Both set and clear write a row to audit_events (receipt.annotation_set / receipt.annotation_cleared) with the API key prefix, the prior kind, and (for set) text length. The dashboard activity feed surfaces the trail so a renamed-or-revoked annotation is not silent.

Response

Returns the same JSON shape as GET /api/v1/receipts/<bundle_id>, with an annotation field (the new state) when one is set. The field is omitted when the annotation has been cleared.

Errors: 401 missing_bearer / 403 insufficient_scope (key lacks receipts:annotate) / 404 anchor_not_found (no receipt with that bundle id in the key’s workspace) / 400 annotation_failed (unknown kind, oversize text, missing supersede target, self-supersede, superseded_by_bundle_id on a non-superseded kind).

04Reference — Categories

The three agent-shaped categories.

The category field is a tag, not a primitive switch — the chain anchor is the same shape regardless. Tagging the receipt lets a verifier say this is a commitment rather than this is some hash.

CategoryModeWhat gets hashed
commitment standard or sealed Canonicalized {nonce_hex, payload} wrapper. The nonce makes the hash unguessable from the payload alone, so anchoring before reveal is safe even if the payload space is small (a score, a vote, a category).
policy_snapshot standard or sealed Canonicalized snapshot JSON. Each component (system prompt, user instruction, tool permissions, budget caps, model config) is hashed independently so an auditor with one component can verify it without seeing the others.
evidence_bundle manifest (always) A list of {label, sha256_hex} leaves. The server combines them into a Merkle tree and anchors the root. Recipients later prove a single leaf with its inclusion path; the other 9,999 stay private.

Two more categories exist for adjacent uses — output (the default; an agent’s produced artifact) and memory_checkpoint (a long-running agent’s rolling state hash). Anything not specified above defaults to output.

05Reference — Selective disclosure of low-entropy rows

Reveal one row of a table; the others stay sealed.

The merkle-row-sealed-v1 scheme is the property most timestamping products can’t give you. It rides on category: "commitment" and replaces each Merkle leaf’s plain SHA-256 with an HMAC under a per-leaf, HKDF-derived salt. Anchor the root once. Disclose one row later, with its salt and its inclusion path. The other rows stay opaque even to the auditor reading the revealed one.

Why a plain manifest isn’t enough for low-entropy rows. If your row is {"bidder": "gamma", "bid_minor": 48} and the bid space is small (say, < 220), an auditor holding the leaf hash and knowing the bidder list can brute-force the amount in milliseconds. Sealed rows kill that: each leaf is HMAC-SHA256(salt_i, canonical_row_i) with a 32-byte salt the holder keeps private. The salt space is 2256. There is no candidate set to grind.

When to use it.

  • Sealed-bid auctions. Anchor all bids in one transaction; reveal the winner’s row to the counterparty without exposing the runners-up.
  • Eval scoreboards. Anchor a batch of model verdicts; disclose one item to a reviewer without handing over the full result set.
  • Rate-card or quote tables. Anchor every line item; reveal the row applicable to a single customer without leaking the rest of the table.
  • Yes/No or graded responses. Anchor a survey; disclose one respondent’s answer to an auditor without unsealing the others. (A plain manifest leaks every answer to anyone who knows the respondent list.)

Build, anchor, and reveal one row

export SATSIGNAL_API_KEY=sk_...   # mint at /w/<workspace>/keys
curl -O https://satsignal.cloud/merkle_row.py

# 1. Seal the table. Produces commit_doc.json (anchor body) +
#    holder_state.json (PRIVATE: master salt + per-leaf salts).
python3 merkle_row.py build-sealed \
  --rows-jsonl bids.jsonl \
  --out-commit-doc commit_doc.json \
  --out-holder-state holder_state.json

# 2. Anchor the commit doc on chain (one transaction, one OP_RETURN).
curl -H "Authorization: Bearer $SATSIGNAL_API_KEY" \
     -H "Content-Type: application/json" \
     -d @commit_doc.json \
     https://app.satsignal.cloud/api/v1/anchors

# 3. Later, reveal exactly one row. The output is self-contained:
#    the row, its salt, the leaf commitment, and the Merkle path.
python3 merkle_row.py reveal \
  --holder-state holder_state.json \
  --leaf-index 2 \
  --out reveal-row-2.json

# 4. The auditor verifies with stdlib only — no Satsignal API call.
python3 merkle_row.py verify-sealed \
  --reveal reveal-row-2.json \
  --commit-doc commit_doc.json
# {"verified": true, "details": {"root_match": true,
#  "row_binding": true, "leaf_commitment": true,
#  "merkle_path": true}}

The four checks bind independently: re-canonicalize the row, HMAC under the supplied salt, walk the Merkle path, and confirm the commit-doc SHA-256 resolves to the on-chain transaction (/lookup_hash).

Selective-disclosure properties

  • Per-leaf salts are derived, not stored. salt_i = HKDF(master_salt, info=leaf_index_be). Disclosing salt_2 leaks no information about salt_0, salt_1, salt_3, salt_4 — HKDF is one-way per info string.
  • The auditor can’t enumerate the table. Sibling commitments inside the inclusion path are 32-byte HMACs under unrevealed salts. There is no plaintext to grind.
  • Roots and counts are public; rows are not. The on-chain commit doc binds {leaf_count, root, scheme}. An observer learns the table size and its anchor time. They learn nothing about row contents until the holder reveals one.
  • The holder cannot lie about what was anchored. Tamper with any byte of any unrevealed row and the remaining inclusion paths stop matching the on-chain root. The holder has commit-then-disclose discretion; they don’t have rewrite discretion.

Live demo: /demos/sealed-bid.html anchors a five-bidder table and reveals one row. The full byte-level scheme — canonicalization rules, HKDF info string, leaf encoding, tree shape — is at /spec-merkle-row.

06Reference — Interop & foreign formats

Embed a BSV anchor inside an AAR, C2PA, RFC 3161, or TAP receipt.

The chain-anchor/v1 scheme is a cross-system / cross-domain Merkle-batching format. One BSV transaction commits to a Merkle root over N leaves; each leaf can be a receipt from a different system (an Agent Action Receipt, an RFC 3161-style timestamp request, a C2PA-credentialed image, a Visa Trusted Agent Protocol signed request). Each receipt carries its own leaf + inclusion proof in an evidenceRef entry, so an auditor with just one receipt can independently verify the on-chain commitment without seeing the others.

Five live BSV-mainnet samples cover the canonical interop shapes — AAR batch, dual-attest (RFC 3161 + BSV), cross-domain (Ethereum + HuggingFace + C2PA), C2PA-credentialed image, and Visa TAP — each with full reproduction recipes, real txids, and pure-stdlib verification snippets.

07Reference — Helpers

Drop-in libraries, byte-for-byte across runtimes.

Stdlib only. No Satsignal SDK to install. Each helper produces the same canonical bytes the in-browser verifier expects, so anything you commit on one side reveals cleanly on the other.

PY

commit_reveal.py

Python 3 stdlib only. CLI with commit / reveal / verify subcommands. Use it from an agent runtime, a CI step, or a one-off shell.

Download →

JS

commit-reveal.js

ES module for the browser and Node 18+. Exports makeCommit and verifyReveal. Drop into a frontend, an Edge Function, or a runtime guardrail.

Download →

PY

policy_snapshot.py

Python 3 stdlib only. CLI subcommands hash-component, build, and verify. Selective-disclosure verify (one field at a time) just works.

Download →

PY

agent_anchor.py

Stdlib-only Session() context manager that bundles the four-anchor pattern (policy + N commitments + evidence-bundle manifest) into a six-line integration. Writes a handoff.json on exit so an auditor can verify the run offline. Doc page at /agents.

Download →

Runnable example. /example_agent_snapshot.py is a 30-line agent that hashes its five policy components, optionally POSTs the snapshot to /api/v1/anchors when SATSIGNAL_API_KEY is set, then takes a (deterministic) action. Replace the decide() stub with a real agent loop and you have an audit-trail-clean configuration anchor in your runtime today. For a fuller pattern (policy + decisions + manifest + handoff JSON in one context manager), see /agent_anchor.py + the /agents doc.

Commit and anchor, end to end

export SATSIGNAL_API_KEY=sk_...   # mint at /w/<workspace>/keys
curl -O https://satsignal.cloud/commit_reveal.py
echo '{"agent_id": "alpha", "score": 73}' | \
  python3 commit_reveal.py commit \
    --payload-json - \
    --out alpha.json \
    --out-anchor anchor_body.json
# alpha.json       — KEEP PRIVATE until reveal (carries the nonce)
# anchor_body.json — ready-to-curl body for /api/v1/anchors

curl -H "Authorization: Bearer $SATSIGNAL_API_KEY" \
     -H "Content-Type: application/json" \
     -d @anchor_body.json \
     https://app.satsignal.cloud/api/v1/anchors
# Anchor sha256(canonical {nonce_hex, payload}). Do NOT sha256
# alpha.json itself — that file's sha is not the commitment.
08Reference — Verifying without us

From CI, from a browser, from any language.

Every receipt ships with everything a verifier needs — the canonical doc, the on-chain transaction id, the chain name, the miner’s acceptance signature, and (for manifest receipts) every leaf with its Merkle path. Verifying is a re-hash and a public block-explorer lookup. Three common shapes:

In a browser

Drop the bundle.

Open proof.satsignal.cloud/verify and drop the .mbnt. Three pills populate from the receipt’s acceptance block and a public block-explorer fetch — no Satsignal API call.

In CI/CD

Re-hash and curl.

Re-hash the revealed payload with the helper. Fetch the OP_RETURN of the receipt’s txid from any block explorer’s API. Compare to subject.sha256_hex (or subject.root for manifest receipts).

By hand

50 lines, any language.

JCS-canonicalize {nonce_hex, payload}. SHA-256 the canonical bytes. Compare to the on-chain hash. The JS helper does it in 50 lines for browser or Node 18+; the Python helper is the same algorithm. For the on-chain payload byte layout, see /spec-mbnt.

The verifier never calls our API. Receipts record which chain they live on and which miner co-attested. The browser verifier reads the acceptance block, fetches the transaction from a public explorer, and re-hashes locally. So a saved .mbnt from 2026 still verifies in 2030, even if Satsignal isn’t around — how you ensure you have a saved .mbnt varies by mode.
Durability across modes — what the auditor needs at verify time, by anchor mode.

"Verify forever" assumes the auditor has the .mbnt bytes in hand. How the holder gets those bytes differs across modes — and for one mode, that's a scheduled obligation, not an automatic guarantee:

mode how the auditor gets the bundle bytes verify works without Satsignal?
Standard
commitment / output / policy_snapshot / evidence_bundle / file proof
Anchor response carries bundle_url. Holder MUST GET it (with bearer auth) during the retention window (default ~1 year — see retain_until) and persist locally. After retention ends, the server-side copy is gone. Yes — if the holder downloaded during retention. Otherwise the chain anchor still exists, but binding txid back to a specific submitted hash needs Satsignal-hosted /lookup_hash.
Sealed (server-mirrored)
retain_days > 0
Same as Standard — bundle_url + bearer auth during retention. Bundle is salted; selective reveals happen via /unseal. Yes — if downloaded during retention.
Sealed blind
retain_days = 0
Anchor response carries the bundle inline (bundle_b64, or canonical_b64 for client-assembled blind). Server keeps NO copy. Holder MUST persist at anchor time. Yes, unconditionally — server has nothing to lose; the holder is sole custodian from anchor time onward.

The asymmetry: in Standard and Sealed-mirrored modes, forgetting to download during retention means the chain anchor is permanent but binding txid back to your payload’s hash relies on /lookup_hash (a Satsignal-hosted service). Persist the .mbnt at anchor time and that dependency goes away.

09Reference — Receipt schema

The bundle, in plain JSON.

Each receipt is a .mbnt file — a small bundle containing the canonical-doc JSON, the manifest (filename, file size, scheme fingerprints), the miner’s signed acceptance, and the on-chain anchor metadata. Download a sample to inspect:

Each bundle is a regular ZIP — rename .mbnt to .zip and inspect the contents. Field-by-field reference is in the in-browser verifier’s side panel; drop a bundle and toggle “Show raw JSON” to see every field rendered against its source.