Sealed Calls
Commit now, reveal later, prove you called it. A sealed call is a piece of text whose existence at a point in time is what gets attested, not its content. You post the text; Sable signs a commitment to it right away and puts that commitment on a public chain within minutes. Later — by hand, or on a schedule you set — you reveal, and anyone can check that the text you revealed is the one you committed.
Alpha calls, research priority, a whistleblower's timestamp, prior art: the shape is the same every time. Say it sealed, prove it later, and every reveal page links back here so a reader can check the maths rather than take your word.
What a commitment proves
When you commit, the gateway:
- draws a 32-byte random salt,
- computes
commitment = sha256(salt || utf8(text))— the raw salt bytes immediately followed by the UTF-8 bytes of the text, no separator, - signs
{v, kind: "call", id, commitment, label?, committed_at, reveal_at?, trust_model}with the receipt signer (secp256k1 / EIP-191 — the same key and the same public verifier as every Sable receipt), and stores that receipt under the call's id, - queues the commitment for the anchor pass, which folds unanchored
commitments into a batch root and publishes
sable-calls:v1:<root>in a Solana memo transaction.
The text and the salt are AES-GCM-sealed at rest and never logged. Until you reveal, the public page shows the commitment, the signature, the timestamps, the anchor state, and your label — and nothing anyone could hash against.
When you reveal, the salt and the text become public. A reader recomputes the
hash and compares it to the commitment that was signed and anchored earlier.
If they match, this exact text existed at committed_at. That is the whole
claim.
Proven: that this exact text (byte for byte) existed no
later than the signed committed_at, attested by Sable's
signature over its commitment and — once the anchor pass has run — by a
public Solana transaction anyone can look up.
Not proven: anything about the call itself. Not that it was right, not that it was honest, not that it was original, not that it was made in good faith. Sable has not read it, judged it, or endorsed it. A sealed call is a timestamped commitment, not a track record and not a trust score.
Public before reveal: the optional label.
It is the one field you control that is readable from the moment you
commit. Never put the call itself in the label.
Why a salt
Predictions are short and the space of plausible ones is small. A commitment over the bare text would let anyone with a list of likely calls hash each one and unmask a sealed call before its reveal — “BTC closes above 100k this year” has a known sha256. The random salt, revealed together with the text, closes that: the commitment is unguessable until you choose to open it, and once opened it verifies exactly as before.
The salt is 32 bytes from the operating system's RNG, returned as 64 hex characters. The recomputation is over the raw bytes, not the hex string — the snippets below get that right, and the public page's in-browser check does too.
Timeline
| Moment | What exists |
|---|---|
| Commit | Signed receipt over the commitment. Verifiable through POST /v1/receipts/verify immediately. Public page live at /c/{id} showing the commitment and your label. |
| Anchor (within the anchor worker's cadence, about ten minutes, when a Solana keypair is configured on the deployment) | The commitment is in a batch whose root is in a public Solana memo. The public page shows the batch id, the root, and a Solscan link. Until then it says “awaiting anchor” — never a fabricated signature. |
Reveal (by hand, or automatically once reveal_at passes — the sweeper runs every minute) | Salt and text are public. Anyone recomputes and compares. |
| Withdraw (optional, any time) | Text and salt ciphertext destroyed on the spot. The commitment, the receipt and the anchor remain: they were public already, and a withdrawn commitment still proves something was committed at that time. |
Before the anchor pass runs, the proof rests on Sable's signature alone. After it, a reader can check the chain without trusting Sable at all. Both states are labeled honestly on the public page.
Quickstart
# An agent seals its own call under the account that owns the key.
curl -s https://api.buildsable.com/v1/calls/commit \
-H "Authorization: Bearer $SABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "ETH/BTC ratio prints above 0.06 before 2027-01-01.",
"label": "q3 macro call",
"reveal_at": "2027-01-02T00:00:00Z"
}'
# {
# "id": "call_9f3c…",
# "status": "sealed",
# "label": "q3 macro call",
# "commitment": "b1a4…", <- sha256(salt || text)
# "committed_at": "2026-09-06T12:00:00.123456+00:00",
# "reveal_at": "2027-01-02T00:00:00+00:00",
# "receipt": "eyJ2IjoxLCJraW5kIjoiY2FsbCIs…",
# "signature": "0x…", "signer": "0x…",
# "public_url": "https://buildsable.com/c/call_9f3c…",
# "anchor": null, <- until the anchor pass runs
# "trust_model": "A sealed call proves that this exact text existed…"
# }Verify it yourself
Two independent checks, neither of which trusts this site.
1. The commitment. Recompute sha256(salt_bytes || utf8(text)) from the
revealed material and compare with commitment. The pipeline form pulls the
public JSON and hashes in Python, so the text never passes through shell
quoting — a call containing quotes, newlines or $ verifies like any other.
curl -s https://api.buildsable.com/v1/calls/public/call_9f3c… | python3 -c '
import sys, json, hashlib
c = json.load(sys.stdin); r = c["revealed"]
h = hashlib.sha256(bytes.fromhex(r["salt_hex"]) + r["text"].encode("utf-8")).hexdigest()
print("recomputed", h); print("commitment", c["commitment"]); print("MATCH" if h == c["commitment"] else "MISMATCH")'2. The timestamp. Verify the receipt through the public endpoint — it
recovers the signer, which must equal GET /v1/receipts/pubkey — and read
committed_at and commitment out of the decoded payload. Then, if the call
is anchored, open the Solana signature on any explorer: the memo reads
sable-calls:v1:<root>, and GET /v1/vault/anchors/{anchor_id} lists the
ordered commitments that fold into that root, so you can recompute the root
too.
curl -s https://api.buildsable.com/v1/receipts/verify \
-H "Content-Type: application/json" \
-d '{"receipt":"<receipt>","signature":"<signature>"}'
# {"valid": true, "recovered_address": "0x…", "payload": {
# "v": 1, "kind": "call", "id": "call_9f3c…", "commitment": "b1a4…",
# "label": "q3 macro call", "committed_at": "2026-09-06T12:00:00.123456+00:00",
# "reveal_at": "2027-01-02T00:00:00+00:00", "trust_model": "…" }}The public page at /c/{id} runs both checks in the browser the moment it
loads and shows recomputed ✓ or a red mismatch — but the point of the
snippets is that you do not have to believe the page.
Scheduled reveals
Pass reveal_at (RFC 3339, in the future, at most five years out) and the
call reveals itself once that instant passes. The sweeper runs every minute,
so a scheduled reveal lands within about a minute of its time. You can still
reveal by hand before then, or withdraw, in which case the schedule is void.
The scheduled time is part of the signed receipt, so a reader can see you
committed to revealing on a date, not only to the text.
Withdrawal
POST /v1/calls/{id}/withdraw destroys the text and salt ciphertext in the
same statement that marks the call withdrawn, whether it was sealed or already
revealed. What it does not do is un-publish the commitment: that was
public from commit, it is in a signed receipt, and it may be on-chain. The
public page of a withdrawn call shows the commitment and withdrawn, and
nothing else. If the call had been revealed, anyone who already read it may
have kept it — withdrawal removes it from Sable, not from the world.
Endpoints
| Method | Path | Auth | Notes |
|---|---|---|---|
POST | /v1/calls | session (Member+) | Commit. Body {text, label?, reveal_at?}. Returns the signed receipt and public_url. |
GET | /v1/calls | session (Viewer+) | Your calls, content-free: id, label, commitment, status, timestamps, anchor state. |
POST | /v1/calls/{id}/reveal | session (Member+) | Reveal now; returns text + salt_hex. Idempotent. 409 if withdrawn. |
POST | /v1/calls/{id}/withdraw | session (Member+) | Destroy text + salt; commitment stays. Idempotent. |
POST | /v1/calls/commit | sk-sable_ key | Key-authed commit — an agent seals its own calls under the key's account. |
GET | /v1/calls/mine | sk-sable_ key | Key-authed list. |
POST | /v1/calls/commit/{id}/reveal | sk-sable_ key | Key-authed reveal. |
GET | /v1/calls/public/{id} | public | The artifact: {id, label, commitment, status, committed_at, reveal_at, receipt, signature, signer, anchor, revealed, trust_model}. 404 for an unknown id — another account's id looks identical. |
Limits: text 1–8192 UTF-8 bytes, label ≤120 characters (no control characters), 500 calls per account across every status. Calls are not metered against your credit balance in this version.
Privacy posture
The text and the salt are stored — you cannot reveal on a schedule what you did not keep — AES-GCM-sealed with the master key, the same envelope hosted agents, Relay and the Vault use. They are opened in-frame only to answer your own reveal or the public page after reveal, never logged, and nulled the instant you withdraw. The database holds the commitment, the label, a byte count, the receipt and the timestamps, and has no column that could hold the text in the clear. See the privacy contract for the general rules this follows.
Related
- Verifiable receipts — the signature scheme and the public verifier a sealed call reuses.
- Agent runs and Agent Passport — the other artifacts that anchor through the same worker.