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.
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
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
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
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
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.
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.
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
Get an API key.
Sign in at app.satsignal.cloud with a magic link. Mint a key with the
anchors:createscope at/w/<workspace>/keys. (See 02 for what the key authorizes.) -
2
Create a matter.
Matters are the namespace each receipt files under — one per integration is fine.
POST /api/v1/matterswith asluglikeagent-runs-prod. -
3
Anchor a commitment.
Hash your payload locally, then
POST /api/v1/anchorswith thematter_slug,sha256_hex,file_size, andcategory: "commitment". The response carriesbundle_id,txid, andreceipt_url. (See 04 for category choices.) -
4
Download the bundle.
The anchor response carries a
bundle_urlfield — that's the full URL, onapp.satsignal.cloud, with bearer auth.GET https://app.satsignal.cloud/bundle/<id>.mbntwith yourAuthorization: Bearer sk_...header returns the.mbntzip. It carries the canonical doc, the manifest, and the miner’s signed acceptance — everything an offline verifier needs. -
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.
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.
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
- An API key with the
anchors:createscope (mint one at/w/<workspace>/keysafter signing in). - A
matter_slugin 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.
/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"
}
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"
}
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.
}
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. Sendingdry_run: truewill 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 singlebyte_exactcommitment. Use the/notarizemultipart 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.
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>
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
}
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
}
}
Kinds
| Kind | When 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 }
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.
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.
| Category | Mode | What 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. |
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.
{"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}}
Selective-disclosure properties
- Per-leaf salts are derived, not stored.
salt_i = HKDF(master_salt, info=leaf_index_be). Disclosingsalt_2leaks no information aboutsalt_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.
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.
- All five samples in one document:
/samples/chain-anchor/RECEIPTS.md(or browse the directory listing). - Scheme spec:
/spec-chain-anchor. - Underlying wire format:
/spec-mbnt§5 (manifest mode — the leaf-hashing rulechain-anchor/v1rides on).
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.
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.
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.
policy_snapshot.py
Python 3 stdlib only. CLI subcommands
hash-component, build, and
verify. Selective-disclosure verify (one
field at a time) just works.
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.
/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.
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:
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.
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).
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.
.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.
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:
Get an API key, anchor your first commit.
Sign in to mint a key, create a matter, and anchor a receipt against your own runtime. The free plan covers a full integration and verify cycle — upgrade to Starter when 100 anchors a month stops being enough.
Mint an API key →
Sign in at app.satsignal.cloud, create a workspace, then
a matter. Mint an API key with the
anchors:create scope.
Read the agent reference →
The three outcomes in detail — commit-reveal, policy snapshots, and manifest receipts — with worked flows and links to the live agent demos.
Watch the sealed-bid demo →
Two agents committed before reveal. Both reveals match the on-chain commitments byte-for-byte. Bundles downloadable; chain order verifiable.
Need higher limits or a custom integration? →
We read every email. Tell us about your runtime, your threat model, and the buyer who’d use the receipt. We’ll figure out the rest.