Every draw freezes the holders first, then commits to a Solana block that doesn't exist yet. The snapshot's Merkle root goes into the seed, the winner is saved before the card moves, and a failed transfer retries to the same winner. Paste any draw below and your browser redoes the maths.
Paste a raffle id or pick a recent draw. Your browser downloads the published holder list, checks the whale cap against the strategy's locked policy, rebuilds the Merkle tree with SHA-256, recomputes the seed, the ticket and the winner, and compares every step with what we published. If anything differs, the step turns red. For a mainnet blockhash you can also ask a public Solana RPC for that block yourself; a flagged stand-in blockhash is recomputed from the commitment.
Every draw also has its own page with the card, the full holder list and the excluded wallets with their reasons: open any row on /raffles, or go to /raffles/<raffle id> directly.
// node verify-draw.mjs <raffle-id> (Node 20+, or paste into a browser console)
const BASE = "__ORIGIN__";
const id = globalThis.process?.argv[2] ?? prompt("raffle id");
const { raffle: r, holders } = await (await fetch(`${BASE}/api/raffles/${id}`)).json();
const sha = async (s) => [...new Uint8Array(await crypto.subtle.digest("SHA-256",
new TextEncoder().encode(s)))].map((b) => b.toString(16).padStart(2, "0")).join("");
// 1. holders sorted by owner (plain string compare), leaf = sha256(owner:weight)
const sorted = holders.slice().sort((a, b) => (a.owner < b.owner ? -1 : a.owner > b.owner ? 1 : 0));
let level = await Promise.all(sorted.map((h) => sha(`${h.owner}:${h.weight}`)));
// 2. parent = sha256(left + right), an odd level duplicates its last node
while (level.length > 1) {
if (level.length % 2) level.push(level.at(-1));
const next = [];
for (let i = 0; i < level.length; i += 2) next.push(await sha(level[i] + level[i + 1]));
level = next;
}
const root = level[0];
// 3. seed, ticket, winner
const seed = await sha(`${r.blockhash}:${r.id}:${root}`);
const total = sorted.reduce((s, h) => s + BigInt(h.weight), 0n);
const ticket = BigInt("0x" + seed) % total;
let run = 0n, winner = null;
for (const h of sorted) { run += BigInt(h.weight); if (run > ticket) { winner = h.owner; break; } }
// 4. a draw flagged "simulated" seeds from a stand-in derived from the commitment
const hex = await sha(`binder-simulated-blockhash:${r.id}:${root}:${r.target_slot}`);
const B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
let standIn = "";
for (let n = BigInt("0x" + hex); n > 0n; n /= 58n) standIn = B58[Number(n % 58n)] + standIn;
for (let i = 0; hex.startsWith("00", i); i += 2) standIn = "1" + standIn;
console.log({
target_ok: r.target_slot === r.snapshot_slot + 32,
root_ok: root === r.snapshot.root,
seed_ok: seed === r.seed,
ticket_ok: ticket.toString() === r.ticket,
winner_ok: winner === r.winner,
...(r.blockhash_source === "simulated" && { stand_in_ok: standIn === r.blockhash }),
}); No dependencies · uses only the public API · prints true for every check: five for a mainnet draw, six for a flagged simulated one.
The first card-strategy platform seeded its draws from a recent blockhash and nothing else. A blockhash is hard to predict, but on its own it leaves four doors open: the draw can be run again, the holder list can be read after the hash is known, nobody can prove which list was used, and a failed payout can end with a different winner. Here is how each one is shut.
Blockhash onlyWhen a prize transfer failed, the draw ran again on a newer blockhash: a new seed, and possibly a new winner.
BinderEach raffle has exactly one target slot, committed before it exists, and the raffle id is inside the seed. There is no "draw again" action anywhere.
Blockhash onlyThe seed came from a recent block, one that already existed when the draw ran, so whoever runs the draw can see that hash before the holder list is read.
BinderThe holder list is frozen at slot S and only then is the draw tied to slot S + 32. When the list was fixed, nobody could know the hash that decides it.
Blockhash onlyThe list of holders wasn't committed anywhere, so there was no way to prove which wallets and balances the winner was picked from.
BinderThe snapshot's Merkle root is published at commit time and goes into the seed. Add, drop or change one holder and the root, the seed and the winner all change.
Blockhash onlyThe winner wasn't pinned before the payout went out, so a failed send could end with someone else holding the card.
BinderThe winner is written to the record before anything is sent and can't be edited after. Failed transfers retry to that wallet; every draw shows its attempt count.