Portal
Documentation: all sections

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:

  1. draws a 32-byte random salt,
  2. computes commitment = sha256(salt || utf8(text)) — the raw salt bytes immediately followed by the UTF-8 bytes of the text, no separator,
  3. 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,
  4. 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.

Honesty box

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

MomentWhat exists
CommitSigned 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

MethodPathAuthNotes
POST/v1/callssession (Member+)Commit. Body {text, label?, reveal_at?}. Returns the signed receipt and public_url.
GET/v1/callssession (Viewer+)Your calls, content-free: id, label, commitment, status, timestamps, anchor state.
POST/v1/calls/{id}/revealsession (Member+)Reveal now; returns text + salt_hex. Idempotent. 409 if withdrawn.
POST/v1/calls/{id}/withdrawsession (Member+)Destroy text + salt; commitment stays. Idempotent.
POST/v1/calls/commitsk-sable_ keyKey-authed commit — an agent seals its own calls under the key's account.
GET/v1/calls/minesk-sable_ keyKey-authed list.
POST/v1/calls/commit/{id}/revealsk-sable_ keyKey-authed reveal.
GET/v1/calls/public/{id}publicThe 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.