Sealed mode — anchor a commitment whose hash itself stays private
Standard mode commits to naked hashes: anyone who independently has the file's hash can confirm an anchor exists for it (this is what makes standard proofs hash-discoverable via
/lookup_hash). If the existence of the hash itself is sensitive — sealed-bid auctions, eval scoreboards, rate-card tables, yes/no surveys, low-entropy data the auditor could brute-force — use sealed mode. Each commitment is an HMAC under a 32-byte salt; the chain reveals only that some commitment was anchored at time T. This guide covers both mirror mode (salt sent on the wire; the notary holds the salt-bearing bundle on disk — until you delete the proof by default, or for an explicit retain window — and returns a download URL) and blind mode (the salt never crosses the wire and the notary holds nothing).
Companion docs: API reference · OpenAPI spec · What to hash · Sealed-mode threat model · merkle-row schemes · Production checklist · Compatibility map
1. The 60-second framing
A sealed anchor publishes a 32-byte HMAC — not a SHA-256 — to the chain. The HMAC is keyed by a 32-byte master salt that the holder generates and (in mirror mode) sends once, or (in blind mode) never sends. The on-chain commitment is a salt-keyed function of the underlying bytes; a third party with the bytes alone can't verify, can't enumerate, can't grind.
Two submission protocols, same anchor format:
- Mirror mode. Client sends
salt_b64. Notary computes the HMAC, anchors, writes the salt-bearing.mbntbundle to disk, and returns abundle_urldownload link plusretain_until. Omittingretain_dayskeeps the bundle indefinitely — until you delete the proof (retain_until: nullin the response; the default). An explicitretain_days >= 1sets an auto-delete window instead, honored as given on every plan. A salt-bearingretain_days=0is rejected — to keep nothing, use blind mode. Higher trust assumption: the notary saw the salt in flight and holds the bundle for as long as it is retained. - Blind mode. Client omits
salt_b64. The notary returnscanonical_b64(the canonical-doc bytes whose hash was anchored) plus thedoc_hash. The client assembles the.mbntlocally, inserting the salt the client already holds. Lower trust assumption: the salt never crosses the wire.
Both produce bit-identical .mbnt bundles. The verifier accepts either transparently — bundle shape is independent of which assembly path ran.
2. Use this when
- The fact that any commitment exists at this hash leaks sensitive information (sealed-bid auctions, where seeing a hash mid-auction proves a participant has already submitted).
- Your payloads are low-entropy and a SHA-256 leaf could be brute-forced by an auditor with a small candidate set (yes/no votes, credit grades, eval scores 1-10, bid amounts in a bounded range).
- You're running a commit-then-reveal protocol and need the commitment chain-anchored before the reveal phase.
- You want selective disclosure on a tabular dataset — reveal one row to a counterparty without exposing the rest.
- The salt itself is the bearer secret: anyone with it can verify, no one without it can.
- You're publishing a policy snapshot or evidence bundle and the contents shouldn't be discoverable on chain.
Don't use this when:
- The hash being public is fine — standard mode is simpler, hash-discoverable, and supports the
/lookup_hashresolver. See Files. - You want anyone with the file alone to verify against your anchor without you sharing the salt — that's the standard-mode property; sealed gives you privacy at the cost of needing the salt at verify time.
- You can't safely hold a 32-byte secret indefinitely — losing the salt destroys the proof's verifiability.
3. What you send
Mirror mode (salt sent on submission)
{
"mode": "sealed",
"folder_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"
}
| field | type | required | meaning |
|---|---|---|---|
mode | string | yes | exactly "sealed". |
folder_slug | string | yes | folder slug. |
salt_b64 | string | mirror only | base64url of 32 random bytes (the master salt). |
byte_exact_commitment | string | yes | 64 lowercase hex chars = HMAC-SHA256(master_salt, canonical_bytes). |
file_size | integer | yes | byte length of the canonical artifact. Stays top-level; does NOT enter the on-chain envelope (sealed canonical docs strip leak fields). |
category | string | yes | one of the standard enum values. |
retain_days | integer | no | Mirror only (salt-bearing). Omit it (the default) and the notary keeps the bundle until you delete the proof — retain_until comes back null. Send >= 1 for an explicit auto-delete window, honored as given on every plan. An explicit 0 with a salt is rejected — to retain nothing, use blind mode (omit salt_b64). The server-side mirror is a recovery convenience, at the cost of letting the notary hold a copy of your salt-bearing bundle while it is retained. |
The notary refuses sha256 in a sealed body — sending it 400s before the wallet is touched (sending it is almost always a sign that the client built the body for the wrong mode). Algos in sealed bodies must be hmac-sha256 or merkle-hmac-sha256; plain sha256 is rejected at the same layer.
Blind mode (salt never sent)
{
"mode": "sealed",
"folder_slug": "agent-runs-prod",
"byte_exact_commitment": "<HMAC-SHA256 of the canonical bytes>",
"file_size": 208,
"category": "policy_snapshot"
}
Same body as mirror, minus salt_b64. Its absence IS the blind signal — there's no separate mode: "sealed-blind" flag. retain_days must be 0 or absent (nothing to retain — the notary never holds the salt and so can't build a bundle).
Multi-proof / selective disclosure (proof_set)
A sealed anchor can carry the same proof_set envelope as standard mode, with HMAC algos inside. proof_set.byte_exact is required; its commitment MUST equal the top-level byte_exact_commitment.
{
"mode": "sealed",
"folder_slug": "agent-runs-prod",
"salt_b64": "<32-byte salt, base64url>",
"byte_exact_commitment": "<HMAC-SHA256(salt, file)>",
"file_size": 208,
"proof_set": {
"byte_exact": {"algo": "hmac-sha256",
"commitment": "<same HMAC as above>"},
"content_canonical": {"algo": "hmac-sha256",
"scheme": "pdf-text-v1",
"commitment": "<HMAC of canonicalized text>"},
"chunk_merkle": {"algo": "merkle-hmac-sha256",
"scheme": "pdf-page-v1",
"leaf_count": 12,
"root": "<Merkle-HMAC root>"}
},
"proof_leaves": {"scheme": "pdf-page-v1",
"merkle_leaves": ["<leaf HMAC>", "..."],
"metadata": {"leaf_count": 12}}
}
Differences from standard-mode proof_set:
byte_exactcarriescommitment(HMAC), nothash/size.- Algos are
hmac-sha256/merkle-hmac-sha256. salt_versionis server-defaulted (salt_v1); the on-chain envelope binds it.- The chunk_merkle leaves ride OFF-chain in the bundle's
proofs.json; the canonical doc carries only{scheme, algo, leaf_count, root, salt_version}. Leaf recompute is structurally impossible at the notary's edge, so selective disclosure is the holder's decision later. - If
chunk_merkleis present,proof_leavesis required (otherwise the proof would be unverifiable). Orphanproof_leaveswithout aproof_set400s. - The
schemetag MUST be one of the registered tier-1 schemes:text-line-v1,csv-row-v1,json-keypath-v1,pdf-page-v1,image-tile-v1,zip-file-v1. Unregistered schemes 400 at the canonical-doc validator (the error lists the valid set). Full byte-level scheme rules:/spec-bundle §4.3.
There is no force_new 409 in sealed mode — sealed entries never index a naked file hash, so there's no default-dedup gate to escape.
Response
Mirror mode (here with an explicit retain_days; with the field omitted — the default — retain_until is null and the bundle is kept until you delete the proof):
{
"proof_id": "f83649e3846c4ea2",
"txid": "2e042a64...7a3db61b",
"mode": "sealed",
"category": "commitment",
"retain_until": "2026-07-06T00:00:00Z",
"folder_slug": "agent-runs-prod",
"proof_url": "https://app.satsignal.cloud/w/.../r/f83649e3846c4ea2",
"bundle_url": "https://app.satsignal.cloud/api/v1/anchors/f83649e3846c4ea2/bundle"
}
Download the bundle from bundle_url and persist it yourself: with an explicit window the server-side copy is reaped after retain_until; with the indefinite default it disappears when the proof is deleted. Either way your local copy is the durable one. (The legacy bundle_b64 field is permanently null and is never populated; the inline-bundle mode it served was removed.)
Blind mode:
{
"proof_id": "...",
"txid": "...",
"mode": "sealed",
"category": "commitment",
"folder_slug": "agent-runs-prod",
"proof_url": "...",
"canonical_b64": "<base64 of canonical.json bytes>",
"doc_hash": "<40 hex>"
}
Your client assembles .mbnt locally — see §6.
4. What you store
The salt is the bearer secret. Lose it and the proof is verifiable in chain-existence only; you can never re-prove what you anchored.
proof_id— your stable handle.txid— the BSV transaction.- The 32-byte master salt — under whatever secret-management practice you use for bearer credentials. NEVER log it. NEVER commit it. NEVER paste it into a screenshot or a support ticket.
- The
.mbntbundle — mirror mode returns abundle_url; download the bytes and persist them (do it promptly if you chose an explicit retain window). Blind mode means you assemble it locally; persist the result. - The canonical artifact — the bytes whose HMAC was anchored. Sealed mode hides the hash, not the artifact-bytes; the verifier still needs the original bytes at verify time, just as in standard mode.
- The proof's
salt_version(currently alwayssalt_v1). Future versions will introduce new derivation schemes; the bundle carries the version, but persist it in your store too for quick reference.
Salt-loss policy
A sealed proof without its salt:
- Still chain-verifies (the txid resolves, the OP_RETURN payload is parseable, the
doc_hashmatches the canonical doc). - Does NOT prove anything about a specific underlying artifact — the HMAC can't be recomputed without the salt.
If salt loss is a real risk for your use case, use the server-side mirror — by default the notary keeps the salt-bearing bundle until you delete the proof, so the salt is recoverable from bundle_url for as long as the proof lives. If you chose an explicit retain_days window instead, the copy is reaped when the window lapses (see the 10-minute sealed-TTL sweep below).
5. What verification needs later
Four things, every time:
- The
.mbntbundle — carries the manifest (withsalt_b64,salt_version,mode: "sealed"), the canonical doc (with HMAC commitments), and anyproofs.json. - The original artifact — the bytes whose HMAC was anchored.
- The 32-byte master salt — included in the manifest's
salt_b64field, so step 1 covers it. But the bundle itself IS the bearer secret here; treat the.mbntaccordingly. - A BSV node — any public one.
The verifier:
- Decodes
master_salt = base64url_decode(manifest.salt_b64). - Recomputes
HMAC-SHA256(master_salt, canonical_bytes), compares tosubject.proofs.byte_exact.commitment. - If
content_canonicalis present, re-canonicalizes per the scheme tag, recomputes the HMAC, compares. - If
chunk_merkleis present, derives per-leaf salts via HKDF (literal info string"satsignal-sealed-v1/per-leaf",info = b"chunk/" || u32_be(i)), computes leaf HMACs, builds the Merkle root, compares tochunk_merkle.root. - Re-encodes
canonical.jsonto SCJ-v1, sha256s, slices to 20 bytes, compares tomanifest.doc_hash_expected. - Resolves the txid, parses the OP_RETURN, compares the on-chain
doc_hashto the canonical doc's hash.
Full per-leaf HKDF construction: see bundle-v1 spec §5.
6. Copy-paste example
Mirror mode (Python, stdlib only)
import base64, hashlib, hmac, json, os, secrets, urllib.request
API = "https://app.satsignal.cloud"
KEY = os.environ["SATSIGNAL_API_KEY"]
FOLDER = "agent-runs-prod"
# 1. Build canonical bytes from your artifact.
artifact = b'{"event":"order.completed","id":"evt_demo_123"}'
canonical = artifact # or apply a canonicalizer; see What to hash.
# 2. Generate a 32-byte salt locally.
master_salt = secrets.token_bytes(32)
# 3. HMAC the canonical bytes under the salt.
mac = hmac.new(master_salt, canonical, hashlib.sha256).hexdigest()
# 4. Submit. Mirror mode: send salt_b64. Omitting retain_days keeps the
# mirror until you delete the proof (the default); send retain_days=1
# for minimal server-side exposure.
body = json.dumps({
"mode": "sealed",
"folder_slug": FOLDER,
"salt_b64": base64.urlsafe_b64encode(master_salt).decode().rstrip("="),
"byte_exact_commitment": mac,
"file_size": len(canonical),
"category": "commitment",
"retain_days": 1,
}).encode("utf-8")
req = urllib.request.Request(
f"{API}/api/v1/anchors",
data=body, method="POST",
headers={"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp:
out = json.load(resp)
# 5. Download the bundle from bundle_url and persist it locally — this
# example chose retain_days=1, so after retain_until the server-side
# copy is gone.
dl = urllib.request.Request(
out["bundle_url"], headers={"Authorization": f"Bearer {KEY}"})
with urllib.request.urlopen(dl) as bresp, open("proof.mbnt", "wb") as fh:
fh.write(bresp.read())
print(out["proof_id"], out["txid"])
Blind mode (salt never crosses the wire)
import base64, hashlib, hmac, io, json, os, secrets, urllib.request
import zipfile
API = "https://app.satsignal.cloud"
KEY = os.environ["SATSIGNAL_API_KEY"]
artifact = b'{"event":"order.completed","id":"evt_demo_123"}'
canonical = artifact
master_salt = secrets.token_bytes(32)
mac = hmac.new(master_salt, canonical, hashlib.sha256).hexdigest()
# Submit WITHOUT salt_b64.
body = json.dumps({
"mode": "sealed",
"folder_slug": "agent-runs-prod",
"byte_exact_commitment": mac,
"file_size": len(canonical),
"category": "commitment",
}).encode("utf-8")
req = urllib.request.Request(
f"{API}/api/v1/anchors",
data=body, method="POST",
headers={"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp:
out = json.load(resp)
# Notary returns canonical_b64 + doc_hash; assemble bundle locally.
canonical_json = base64.b64decode(out["canonical_b64"])
salt_b64 = base64.urlsafe_b64encode(master_salt).decode().rstrip("=")
manifest = {
"mbnt_version": "2.1",
"mode": "sealed",
"txid": out["txid"],
"network": "bsv-mainnet",
"doc_hash_expected": out["doc_hash"],
"salt_version": "salt_v1",
"salt_b64": salt_b64,
"bearer_secret": True,
}
with zipfile.ZipFile("proof.mbnt", "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("manifest.json", json.dumps(manifest))
z.writestr("canonical.json", canonical_json)
print(out["proof_id"], out["txid"])
The bundle is bit-identical-in-shape to a mirror-mode bundle. Verifiers don't know (and don't need to know) which assembly path produced it.
Bundle byte-canonicalization. Both
manifest.jsonandproofs.jsonSHOULD be SCJ-v1-compact (sorted keys, no whitespace, NFC, UTF-8 — deliberately NOT RFC 8785/JCS; see /spec-provenance §3). Usejson.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")in Python; the browser path uses the same shape (seetemplates.py:compactJson). The simplerjson.dumps(obj)in the example above happens to parse correctly but is NOT canonical bytes — fine for a verifier that parses then re-checks, fragile for any byte-level comparison.canonical.jsonMUST be the server-returned bytes verbatim; it was SCJ-v1-canonicalized server-side before the doc_hash was sliced.
Blind mode with multi-proof + selective disclosure
A blind anchor with byte_exact plus chunk_merkle (4 chunks of 16 bytes, one of the registered schemes — text-line-v1 here) is the same flow plus a per-leaf HMAC derivation and a Merkle root. The bearer can later strip proofs.json to selectively disclose only byte_exact while keeping the chunk leaves private; the canonical doc, the on-chain commit, and the byte_exact proof all still verify against the redacted bundle.
import base64, hashlib, hmac, json, os, secrets, struct, urllib.request, zipfile
API = "https://app.satsignal.cloud"
KEY = os.environ["SATSIGNAL_API_KEY"]
FOLDER = "agent-runs-prod"
SCHEME = "text-line-v1" # one of the registered tier-1 schemes
CHUNK_SIZE = 16
# 1. Artifact + master salt, both client-side only.
artifact = b"CHUNK-00-payloadCHUNK-01-payloadCHUNK-02-payloadCHUNK-03-payload"
assert len(artifact) % CHUNK_SIZE == 0
chunks = [artifact[i:i + CHUNK_SIZE]
for i in range(0, len(artifact), CHUNK_SIZE)]
master_salt = secrets.token_bytes(32)
# 2. byte_exact: HMAC over the full canonical bytes.
be_commitment = hmac.new(master_salt, artifact, hashlib.sha256).hexdigest()
# 3. chunk_merkle: per-leaf salt via HKDF, HMAC each chunk.
# info = b"chunk/" || u32_be(i). salt context label is a literal:
# b"satsignal-sealed-v1/per-leaf".
def hkdf_sha256(ikm, salt, info, length=32):
prk = hmac.new(salt, ikm, hashlib.sha256).digest()
return hmac.new(prk, info + b"\x01", hashlib.sha256).digest()[:length]
leaves_hex = []
for i, chunk in enumerate(chunks):
info = b"chunk/" + struct.pack(">I", i)
salt_i = hkdf_sha256(master_salt, b"satsignal-sealed-v1/per-leaf", info)
leaves_hex.append(hmac.new(salt_i, chunk, hashlib.sha256).hexdigest())
# 4. Merkle root: plain sha256 binary tree, duplicate-last on odd,
# single-leaf re-hashes with itself.
def merkle_root_sha256(leaves):
if len(leaves) == 1:
return hashlib.sha256(bytes.fromhex(leaves[0]) * 2).hexdigest()
nodes = [bytes.fromhex(l) for l in leaves]
while len(nodes) > 1:
if len(nodes) % 2 == 1:
nodes.append(nodes[-1])
nodes = [hashlib.sha256(nodes[i] + nodes[i + 1]).digest()
for i in range(0, len(nodes), 2)]
return nodes[0].hex()
root_hex = merkle_root_sha256(leaves_hex)
# 5. Submit. Blind: no salt_b64. Multi-proof: proof_set + proof_leaves.
body = json.dumps({
"mode": "sealed",
"folder_slug": FOLDER,
"byte_exact_commitment": be_commitment,
"file_size": len(artifact),
"category": "commitment",
"proof_set": {
"byte_exact": {"algo": "hmac-sha256", "commitment": be_commitment},
"chunk_merkle": {
"scheme": SCHEME,
"algo": "merkle-hmac-sha256",
"leaf_count": len(leaves_hex),
"root": root_hex,
},
},
"proof_leaves": {
"scheme": SCHEME,
"merkle_leaves": leaves_hex,
"metadata": {"leaf_count": len(leaves_hex)},
},
}).encode("utf-8")
req = urllib.request.Request(
f"{API}/api/v1/anchors", data=body, method="POST",
headers={"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp:
out = json.load(resp)
# 6. Assemble the full .mbnt locally — SCJ-v1-compact for manifest + proofs.
def compact_json(obj):
return json.dumps(obj, sort_keys=True, separators=(",", ":"),
ensure_ascii=False).encode("utf-8")
manifest = {
"mbnt_version": "2.1",
"mode": "sealed",
"txid": out["txid"],
"network": "bsv-mainnet",
"doc_hash_expected": out["doc_hash"],
"salt_version": "salt_v1",
"salt_b64": base64.urlsafe_b64encode(master_salt).rstrip(b"=").decode(),
"bearer_secret": True,
}
proofs = {
"scheme": SCHEME,
"salt_version": "salt_v1",
"merkle_leaves": leaves_hex,
"metadata": {"leaf_count": len(leaves_hex)},
}
with zipfile.ZipFile("proof-full.mbnt", "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("manifest.json", compact_json(manifest))
z.writestr("canonical.json", base64.b64decode(out["canonical_b64"]))
z.writestr("proofs.json", compact_json(proofs))
# 7. Selective-disclosure variant: same bundle, proofs.json stripped.
# byte_exact still verifies (salt + commitment + artifact all there).
# chunk_merkle's root remains anchored on-chain via the unchanged
# canonical doc, but a verifier without proofs.json cannot recompute
# the root — the chunk leaves are withheld.
with zipfile.ZipFile("proof-redacted.mbnt", "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("manifest.json", compact_json(manifest))
z.writestr("canonical.json", base64.b64decode(out["canonical_b64"]))
# NO proofs.json
print(out["proof_id"], out["txid"])
Both proof-full.mbnt and proof-redacted.mbnt are valid .mbnt files against the same on-chain anchor. The full bundle lets a verifier check both proofs; the redacted bundle lets them check byte_exact while honestly reporting chunk_merkle: leaves not disclosed. The canonical doc and on-chain commit are unchanged between the two — selective disclosure is a property of the bundle, not of the anchor.
Selective-disclosure / sealed rows (low-entropy data)
For tabular data where each row is low-entropy, use merkle-row-sealed-v1 (a scheme on top of chunk_merkle in sealed mode). The driver tool is at satsignal.cloud/merkle_row.py:
export SATSIGNAL_API_KEY=sk_...
curl -O https://satsignal.cloud/merkle_row.py
# 1. Seal the table.
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.
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.
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.
python3 merkle_row.py verify-sealed \
--reveal reveal-row-2.json \
--commit-doc commit_doc.json
Each leaf is HMAC-SHA256(salt_i, canonical_row_i) with a per- leaf salt derived from the master salt via HKDF. Disclosing salt_2 leaks no information about salt_0, salt_1, salt_3, .... The auditor can't enumerate the table — sibling commitments inside the inclusion path are 32-byte HMACs under unrevealed salts.
Full byte-level scheme — canonicalization, HKDF info string, leaf encoding, tree shape, reveal payloads, registered scheme strings — is the implementer spec at /spec-merkle-row.
7. Production notes
The 10-minute sealed-TTL sweep
The notary runs a background sweep every ~10 minutes. Bundles held under an explicit retain_days window are reaped at the second of server_retain_until_utc. Past that second the server-side copy is gone — your client-side copy is the only remaining one. Bundles anchored with retain_days omitted (the default) are never swept: their manifest carries server_retention: "indefinite" and they stay re-fetchable from bundle_url until you delete the proof.
If you chose an explicit window, plan for the sweep at integration time — it is a bounded recovery window, not storage.
Post-TTL graceful degradation
After the sweep:
- The on-chain anchor still verifies (txid resolves, OP_RETURN parseable, doc_hash matches).
bundle_urlreturns410 sealed_retain_expired./lookup_hashwas never resolvable for sealed entries (sealed proofs are excluded from/lookup_hashby design).- Your local bundle is the source of truth.
Idempotency
Sealed bodies don't dedup on the file hash (there is no naked hash — only an HMAC under a fresh salt). Replay-protection works via Idempotency-Key like any other anchor. A retry with the same Idempotency-Key and the same body returns the original anchor; a retry with the same key and a different body returns 409 idempotency_key_reuse_body_mismatch.
Mirror vs blind trade-off
| property | mirror | blind |
|---|---|---|
| salt-on-wire | yes (TLS-protected) | no |
| server-side bundle (while retained) | available | impossible |
| client-side assembly | not required | required |
| trust assumption on notary | notary sees salt in flight | notary never sees salt |
| operational simplicity | higher (notary returns bundle) | lower (client builds bundle) |
Pick mirror unless your threat model specifically excludes "the notary's process memory could leak the salt in flight". Blind is the right answer for ultra-sensitive workflows (regulatory compliance, true sealed-bid markets) where minimizing trust is the explicit goal.
Key rotation
salt_version is the rotation knob. Currently always salt_v1. A future salt_v2 will introduce a new HKDF derivation; the notary will accept both during the rotation window, and bundles will carry which version was used. Existing proofs verify indefinitely under whatever version they were anchored at.
Rate limits & quota
Same as standard mode. See Files §7. Sealed anchors count against the same monthly anchor quota.
For the full pre-flight checklist (key rotation, broadcast failure recovery, support flow), see Production checklist.
8. Errors you might see
| code | name | meaning | what to do |
|---|---|---|---|
400 | sealed_requires_commitment | sent sha256 instead of byte_exact_commitment | use the commitment field; algos must be HMAC |
400 | sealed_disallows_sha256 | sent sha256 in a sealed body | strip the field |
400 | bad_salt_b64 | salt_b64 isn't valid base64url, or decodes to ≠ 32 bytes | regenerate the salt |
400 | proof_leaves_orphan | proof_leaves present without proof_set.chunk_merkle | always pair them |
400 | proof_set_byte_exact_mismatch | proof_set.byte_exact.commitment ≠ top-level byte_exact_commitment | align them |
400 | retain_days_blind_must_be_zero | blind submission with retain_days > 0 | drop retain_days or set to 0 |
400 | retain_days: 0 requires blind mode — omit salt_b64 | mirror submission (salt-bearing) with an explicit retain_days == 0 | to keep nothing, go blind (omit salt_b64); to mirror, send retain_days >= 1 or omit it |
404 | folder_not_found | folder doesn't exist | create it |
410 | sealed_retain_expired | bundle_url requested after the retain window | use your local copy |
429 | quota_exceeded | monthly anchor count exhausted | back off; email hello@satsignal.cloud for a cap lift |
9. Legacy field aliases (if your code sends them)
Same as all other paths: responses emit the canonical proof / folder names only; requests written against the legacy spellings keep working forever.
Full canonical/legacy mapping across endpoints, fields, scopes, CLI flags, and error codes: Compatibility map.
10. Where this fits
- The sealed-mode threat model (why HMAC + per-leaf HKDF rather than per-tree salting, what a chain observer learns, the blind- submission rationale) is at proof.satsignal.cloud/spec.
- Selective row-reveal for low-entropy tabular data is merkle-row-sealed-v1; the §6 recipe above is the operator-facing usage.
- The full disclosure procedure (full unseal / selective disclosure / private adversarial verification) is in the Sealed-mode disclosure reference.
- For non-sealed (standard) anchors, use Files.
- For batched anchors, see Manifest. The
merkle-row-sealed-v1scheme is the documented combination of manifest batching with a Sealed proof — a sealed manifest. - For agent runs that include sealed commitments (commit-then- reveal pattern), see Agents.
- For the byte-selection details on sealed bodies (what "canonical bytes" means for an HMAC input), see What to hash.
- To produce a validated redacted copy + disclosure
.mbntoffline from an anchored file (CI / air-gapped, no upload, no re-anchor), see Headless redaction.
Questions about this specification? Email hello@satsignal.cloud.