FAIRNESS · SNAPSHOT, COMMIT, RECOMPUTE

Built so nobody - including us - can rig it.

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.

…finished draws
…transfers retried, same winner
…seeded by a mainnet block
…flagged simulated
SIX STEPS - ONE DRAW

Snapshot > Commit > Root > Seed > Ticket > Pay

  1. Snapshot
    Every holder is recorded at slot S
    When the card's window ends, every holder of the strategy token is read at that Solana slot. The excluded wallets are dropped and each holder's weight is their balance, capped if the strategy set a whale cap.
    weight = min(balance, cap)
  2. Commit
    The draw is tied to a block 32 slots ahead
    Target slot S + 32 is fixed before that block exists, about 13 seconds out. Holdings are already frozen, and nobody knows that block's hash yet, including us.
    target_slot = snapshot_slot + 32
  3. Root
    The snapshot gets a fingerprint
    Holders sorted by address, one leaf per holder made from address and weight, hashed pairwise up to a Merkle root that is published with the commit. Change one balance and the root changes.
    leaf = sha256(owner + ":" + weight)
  4. Seed
    The block, the raffle and the root make the seed
    When the target slot is produced (or the first slot after it, if it was skipped), its blockhash, the raffle id and the Merkle root are hashed together.
    seed = sha256(blockhash:raffle_id:root)
  5. Ticket
    The seed picks a ticket, the ticket picks a wallet
    The seed read as a number, modulo the total weight, is the ticket. Walk the holders in address order adding up weights: the first whose running total passes the ticket wins. Odds = weight ÷ total.
    ticket = BigInt("0x" + seed) % total_weight
  6. Lock & pay
    The winner is saved before the card moves
    The winner is written to the record first and can't be changed after. If the card transfer fails, it is retried to the same wallet until it confirms. There is no path that draws again.
    store(winner) → transfer → retry(same winner)
SECTION 02

Verify any draw. Right here. In your browser.

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.

● IN-BROWSER VERIFIER
Paste a raffle id, or pick a recent draw.
Recent draws
Loading recent draws…

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.

REFERENCE IMPLEMENTATION

Copy-paste. Run it yourself. Same answer.

verify-draw.mjs JavaScript
// 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.

SECTION 03

Why not just a blockhash?

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.

SECTION 04

What we cannot do