1. The outcome lifecycle
Every /api/floats, /api/ints, /api/dice, or /api/shuffle call walks the same path. The diagram below traces a single outcome from the moment you pick a clientSeed to the moment it lands inside a publicly published daily Merkle root.
2. HMAC-SHA512 client-seed mixing
The actual random bytes are produced by HMAC-SHA512, keyed by our private serverSeed and called with a message of clientSeed:nonce:cursor. SHA-512 gives 64 bytes per call; floats consume 4 bytes each, integers consume as many as needed for an unbiased range fit. Because the HMAC is keyed by a secret server seed, you cannot grind clientSeeds to predict outcomes; because we commit to the hash of that secret before revealing it, we cannot adapt the secret to your seed either.
How to verify it yourself. After an outcome, save the clientSeed and serverHash. Once we reveal the serverSeed (via the dashboard, the commit-reveal flow, or in any inclusion proof), confirm SHA256(serverSeed) === serverHash, then recompute HMAC-SHA512(serverSeed, "clientSeed:n:c") using the reference library:
npm install @provableio/provable-core
# then in Node:
const { Provable } = require("@provableio/provable-core");
const out = Provable(() => {})({ clientSeed, serverSeed }).floats(5);
3. Commit-reveal for stronger fairness
The one-shot endpoints publish a serverHash with every response — that is already a binding commitment, because we hand you the hash on the first call for a given client seed and can never silently change it afterward. For maximum-paranoia use cases (lotteries, raffles, on-chain settlement) we also expose an explicit two-step flow:
POST /api/commit— we generate a freshserverSeed, return itsSHA-256hash and acommitId, and lock that pair for up toCOMMIT_TTL_MS(10 minutes by default).POST /api/reveal— you pass thecommitIdplus yourclientSeed. We reveal the serverSeed, proveSHA256(serverSeed) === serverHash, and only then mix in your seed to produce the outcome.
This rules out the only remaining attack on the one-shot flow — a malicious server picking a serverSeed in response to a clientSeed it has already seen. The commit happens before the client seed exists.
4. Daily Merkle transparency roots
Every UTC night we hash every outcome from the previous day into a SHA-256 Merkle tree and publish the root at /transparency. The leaf encoding is fixed and documented:
- Leaf:
SHA256(0x00 || utf8("clientSeed:cursor:nonce|serverHash|clientSeed|timestampMs")) - Internal node:
SHA256(0x01 || left(32) || right(32)) - Odd level: last node is duplicated (Bitcoin-style).
The root for any past day is append-only: a single byte changed in any past outcome would shift the root and break every inclusion proof for that day. Anyone can fetch GET /api/merkle/:date for the root and GET /api/merkle/:date/proof/:outcomeId for an inclusion proof, then recompute the root in their own browser or script using only the leaf and the published siblings — no trust in our server required.
The verifier on /transparency does exactly that, in your browser, using the WebCrypto API.
5. Idempotency keys
The optional Idempotency-Key header lets a client safely retry a request — over a flaky network or after a 502 — and get back byte-identical results instead of accidentally consuming a second outcome. The server stores the original response keyed by (scope, key) for 24 hours; a retry with the same key replays the cached body and sets Idempotent-Replayed: true. A retry with the same key but different parameters returns 409 rather than silently doing the wrong thing. Two concurrent retries with the same key are single-flighted via a database sentinel row, so only one of them produces an outcome.
6. Signed webhooks
When you wire a webhook to your account, every delivery carries an X-Provable-Signature: sha256=<hex> header. The signature is HMAC-SHA256(webhook_secret, raw_request_body). Verify it on your side using a constant-time comparison:
// Node.js
const crypto = require("crypto");
const expected = crypto.createHmac("sha256", WEBHOOK_SECRET)
.update(rawBody).digest("hex");
const actual = req.get("X-Provable-Signature").replace(/^sha256=/, "");
const ok = crypto.timingSafeEqual(
Buffer.from(expected, "hex"), Buffer.from(actual, "hex"));
Webhook URLs are validated for SSRF before each delivery: private IP ranges, link-local addresses, and .local/.internal hostnames are blocked. Failed deliveries are retried with exponential backoff and surfaced in your dashboard's delivery history.
7. Operational hygiene
- Open source core. The cryptographic primitives live in provable-core on GitHub. The same library runs server-side and is shipped to verifiers.
- HTTPS-only. The production API and dashboard are TLS-terminated; HTTP redirects to HTTPS.
- Session cookies.
HttpOnly,SameSite=Lax, andSecurein production. CSRF tokens are required on every state-changing request from the dashboard. - API keys. Stored hashed in Postgres; only the prefix and a masked preview are ever returned after creation. Keys can be rotated or revoked from the dashboard at any time, with per-key IP and Referer allowlists.
- Test keys.
pk_test_*keys produce deterministic outcomes that never touch live seed state, never count against quota, and never fire webhooks — safe for fixtures, CI, and snapshots.
8. Responsible disclosure
If you find a security issue, please email security@provable.io with reproduction steps. We will acknowledge receipt within two business days and keep you in the loop while we fix and disclose. Please do not publicly disclose before we have had a reasonable chance to patch.
Verify a specific outcome → · Browse daily Merkle roots → · Who runs this →