The contract

At the end of every UTC day, Provable.io publishes a single 32-byte root that commits to every outcome generated that day. The leaves are sorted deterministically, hashed with a domain-separated SHA-256, and folded pairwise into a binary tree (RFC 6962 style: a single tag byte prefixes leaves vs. internal nodes so the two can't be confused).

Audit a day in one Node script

Save this as audit.mjs and run with node audit.mjs 2026-05-24 my-app:0:0. It needs Node 18+ — no third-party dependencies.

import crypto from "node:crypto";

const [, , date, outcomeId] = process.argv;
if (!date || !outcomeId) {
    console.error("usage: node audit.mjs YYYY-MM-DD clientSeed:cursor:nonce");
    process.exit(1);
}
const BASE = "https://api.provable.io";
const sha = (buf) => crypto.createHash("sha256").update(buf).digest();
const leafHash = (canonical) =>
    sha(Buffer.concat([Buffer.from([0x00]), Buffer.from(canonical, "utf8")]));
const nodeHash = (l, r) =>
    sha(Buffer.concat([Buffer.from([0x01]), l, r]));

// 1. Pull the published root and the inclusion proof for our outcome.
const root = await (await fetch(`${BASE}/api/merkle/${date}`)).json();
const proof = await (await fetch(`${BASE}/api/merkle/${date}/proof/${outcomeId}`)).json();
console.log("published root:", root.root);
console.log("leaves on", date, ":", root.leafCount);

// 2. Verify the leaf bytes hash to the leaf hash in the proof.
const recomputedLeaf = leafHash(proof.leaf.canonical).toString("hex");
console.assert(recomputedLeaf === proof.leaf.hash, "leaf hash mismatch");

// 3. Walk siblings up to the root.
let acc = Buffer.from(proof.leaf.hash, "hex");
for (const sib of proof.siblings) {
    const sibBuf = Buffer.from(sib.hash, "hex");
    acc = sib.position === "left" ? nodeHash(sibBuf, acc) : nodeHash(acc, sibBuf);
}
console.assert(acc.toString("hex") === root.root, "root mismatch");
console.log("✅ inclusion proof verifies against the published root");

Recompute the whole day's root from scratch

If you want to be extra-paranoid — proving not just that your outcome is in the tree but that the published root is the right tree over every outcome from that day — pull the full leaf set and rebuild the tree. The API doesn't expose a single "dump every outcome" endpoint (privacy: it would leak every account's client seeds), but you can rebuild the subset of leaves you care about and compare individual proofs.

For each clientSeed you've used:

const outcomes = await fetch(
    `${BASE}/api/listOutcomes?clientSeed=${encodeURIComponent(clientSeed)}`
).then((r) => r.json());
for (const o of outcomes) {
    const id = `${o.clientSeed}:${o.cursor}:${o.nonce}`;
    const day = new Date(o.created).toISOString().slice(0, 10);
    const proof = await fetch(`${BASE}/api/merkle/${day}/proof/${id}`).then((r) => r.json());
    // ...verify each proof as above
}

Any outcome of yours that doesn't appear in its day's tree — or whose proof doesn't reproduce the published root — is a critical fairness incident. Open one with us at support@provable.io with the failing outcomeId.

When the day's root isn't published yet

Today's root is computed and published at the end of the UTC day. Before then, /api/merkle/<today> returns 404 and there are no inclusion proofs to fetch. To audit today's outcomes, wait for the day to close (typically a few minutes after 00:00 UTC).

Common questions

Next steps