·Demo

Five sealed bids, one anchor, one row revealed.

Five bidders sealed an amount each. The whole table was committed on chain in a single transaction (merkle-row-sealed-v1). Later, only one row was disclosed — the other four stay HMAC-sealed under salts the auditor never sees. Selective disclosure is the property a plain log can’t give you.

API examples below are against https://app.satsignal.cloud; we elide the host in body copy.

01Why this only works on chain

Cheaper infrastructure can’t do all three.

Signed logs, append-only DBs, transparency logs — any of those can give you tamper-evidence. None of them can give you tamper-evidence plus a public timestamp plus selective disclosure to a counterparty who doesn’t trust you. Anchoring one Merkle root binds all five bids to a moment in BSV chain order; per-leaf salts let you reveal one bidder without unsealing any other.

What this prevents.
  • Late edits to any bid. No bidder can change a bid after the anchor — every row is folded into the Merkle root, and the root’s SHA-256 is already in a Bitcoin SV block.
  • Pre-reveal copying. No bidder could have read another’s amount before the anchor — each leaf is HMAC-SHA256(salt_i, canonical_row_i) with a per-leaf salt the holder keeps private.
  • Operator collusion. Satsignal can’t forge an earlier commitment with a different table — a BSV miner independently signed the anchor, and anyone can verify the chain timestamp without us.
  • Sibling unsealing. Disclosing row 2 reveals row 2’s salt only. Per-leaf salts are derived via HKDF, so revealing one does not let anyone derive (or brute-force) any other bidder’s salt.

What this does not prevent: a holder choosing a smaller table (dropping a bidder before anchoring) or relabelling rows. If you need to bind the table’s scope first, anchor the bidder list with commit-reveal before sealing the bids. Then the holder is bound to disclose those rows.

02Phase 1 — Commit (one anchor for all five)

Five bids became one Merkle root.

Each row was a {bidder, bid_minor} JSON object, JCS-canonicalized to a fixed byte string. A 32-byte master salt fed an HKDF derivation to produce one salt_i per row; each leaf is HMAC-SHA256(salt_i, canonical_row_i). The five leaves were Merkle-rooted (binary tree, last-leaf-dup, SHA-256 inner nodes), and a tiny commit doc binding the root + leaf count was anchored on chain — one transaction, one OP_RETURN, the whole table sealed.

The commit doc (132 bytes canonical)

{
  "leaf_count": 5,
  "root": "a67956a2d0ca871b665da9d99fb1ac2dc3690d9484bcac23dd09c2c5ae477bf7",
  "scheme": "satsignal-merkle-row-sealed-v1"
}

SHA-256 of those canonical bytes is f3dd742039a4c287d23f735cb59833f669d1ff96dc50fab0c173a24608ef3584 — that is what hits the chain.

On-chain anchor

One transaction. One Merkle root. Five sealed leaves.

Scheme
merkle-row-sealed-v1
Bundle ID
007dff5816814594
Commit-doc SHA-256
f3dd7420…608ef3584
Merkle root
a67956a2…ae477bf7
Leaf count
5
Transaction
d623cb94…53a6c4dcc8
Anchored
2026-05-08 (BSV mainnet)
Download commit doc (JSON)

Public lookup, no auth

Resolve commit-doc SHA → bundle + txid.

Anyone can hit /lookup_hash on the public proof endpoint with the commit-doc SHA-256 and get back the bundle id, txid, and anchor time — no API key, no Satsignal account required.

curl 'https://proof.satsignal.cloud/lookup_hash?sha=f3dd742039a4c287d23f735cb59833f669d1ff96dc50fab0c173a24608ef3584'
Open the live lookup →

The full byte-level scheme is at /spec-merkle-row. Stdlib-only helpers reproduce the root from the rows byte-for-byte across runtimes: /merkle_row.py (Python), /merkle-row.js (browser + Node 18+).

03Phase 2 — Disclose row 2 only

One bidder disclosed. The other four stay sealed.

To answer one auditor question (“what was bidder gamma’s amount?”) the holder publishes a single reveal payload: the row, its canonical bytes, the per-leaf salt for index 2 only, the leaf’s commitment, and the Merkle inclusion path back to the root. Nothing about rows 0, 1, 3, or 4 is exposed — not their bidders, not their amounts, not their salts.

Single-row reveal payload (row index 2)

{
  "version":           "satsignal-merkle-row-sealed-v1",
  "leaf_index":        2,
  "leaf_count":        5,
  "label":             "row-2",
  "row":               { "bidder": "gamma", "bid_minor": 48 },
  "row_canonical_b64": "eyJiaWRfbWlub3IiOjQ4LCJiaWRkZXIiOiJnYW1tYSJ9",
  "salt_b64":          "2BempkijPVIw5QHTSqw4hSh34j15Xgx4fEONhjc1tRE=",
  "commitment_hex":    "fe1a6ecc7e0ecfecab0335e3f18aa189d8c9cce35b7fb642148d9ae551208586",
  "proof": [
    {"sib": "42b18e56...0587eb", "side": "R"},
    {"sib": "80119518...bce7f3", "side": "L"},
    {"sib": "97912b95...182d99", "side": "R"}
  ],
  "root_hex":          "a67956a2d0ca871b665da9d99fb1ac2dc3690d9484bcac23dd09c2c5ae477bf7"
}

Full reveal at /samples/merkle-row/sealed-reveal-row-2.json (proofs un-elided).

What the auditor learns — and what they don’t.
  • Bidder gamma: bid 48 minor units. Re-canonicalize the row, HMAC under the supplied salt, walk the Merkle path — the result is the on-chain root.
  • Bidders 0, 1, 3, 4: the auditor sees only the three sibling commitments inside the inclusion path. Each is a 32-byte HMAC under a salt the auditor has never seen. There is no candidate set to brute-force, because the salt space is 2256.
  • The master salt: stays with the holder. Per-leaf salts are HKDF-derived from it; revealing salt_2 doesn’t leak the master and doesn’t let anyone derive salt_0, salt_1, salt_3, salt_4.
04Phase 3 — Verify

Anyone can re-run the four binding checks.

Verification is fully client-side; nothing leaves the auditor’s machine except the one /lookup_hash call that resolves the commit-doc SHA-256 to the on-chain transaction. Three independent paths:

Option A

In a browser.

Open /verify and find the Sealed-row reveal card. Click Try the live sample to auto-load this demo’s commit doc + reveal, or drop your own pair of files. All four binding checks (root match, row binding, leaf commitment, Merkle path) run client-side; the on-chain commitment then resolves via the public /lookup_hash.

Option B

From the command line.

Stdlib only — no pip install, no Satsignal repo dependency:

curl -O https://satsignal.cloud/merkle_row.py
curl -O https://satsignal.cloud/samples/\
merkle-row/sealed-commit.json
curl -O https://satsignal.cloud/samples/\
merkle-row/sealed-reveal-row-2.json
python3 merkle_row.py verify-sealed \
  --reveal sealed-reveal-row-2.json \
  --commit-doc sealed-commit.json

Returns {"verified": true, "details": {"root_match": true, "row_binding": true, "leaf_commitment": true, "merkle_path": true}}.

Option C

By hand, in any language.

The byte-level scheme is at /spec-merkle-row. Four checks, in order: (1) JCS-canonicalize reveal.row and confirm it equals row_canonical_b64; (2) HMAC under salt_b64 matches commitment_hex; (3) walk the Merkle path back to root_hex; (4) the SHA-256 of canonicalize(commit_doc) resolves to the on-chain transaction via /lookup_hash. Reference helpers ride only on SHA-256, HMAC, and HKDF.

The selective-disclosure property. Phase 2 disclosed one row out of five. Re-run Option B with a hand-crafted reveal that lies about row while keeping row_canonical_b64 intact — check (1) fails. Lie about the salt — check (2) fails. Tamper with the proof — check (3) fails. Each check is independently protective; together they make the auditor’s confidence in row 2 not depend on access to the other four rows.

05Next

Build this in your own integration.

Everything on this page was produced by the helper below and the public anchor API. Plain Python, stdlib only, no Satsignal SDK to install. Drop it into a CI step, an agent runtime, or a one-off shell.

PY

merkle_row.py

The helper that built this demo’s sealed Merkle row, derived per-leaf salts via HKDF, and produces single-row reveals. Subcommands build-sealed / reveal / verify-sealed. Same byte-for-byte semantics as the in-browser verifier.

Source →  ·  Byte-level spec →

·

Quickstart

~30 lines of curl + python to anchor your own commit on chain — with an API key, a JSON body, and a receipt back. The same /api/v1/anchors endpoint that produced the transaction at the top of this page.

Open the quickstart →  ·  Other helpers →