Raffle algorithm
Each draw selects one winner from the token holders, weighted by balance. The selection is seedless and reproducible: anyone can recompute it from public on-chain data, and the program pays exactly the holder the data selects.
Overview
A draw runs over a fixed window that ends at a close slot — a Solana slot number chosen when the draw opens, still in the future. Shortly before that slot, the holder snapshot is committed on-chain. After it, the program reads that slot's hash and derives a single random value — the roll — which selects one holder, weighted by stake. The result is determined entirely by public information and can be reproduced by anyone.
There is no operator seed and no commit-and-reveal of a secret. That is deliberate: a secret the operator controls is something the operator could grind or withhold. By removing it, the only inputs are values neither we nor anyone else can manipulate after the fact. Everything below runs in the PokeGacha program BrZB…mPzF ↗.
01The snapshot
Before the close slot, the operator records every eligible holder and their balance: token balances are summed per wallet, and accounts owned by programs rather than people — the pump.fun bonding curve, PumpSwap pools, program vaults — are excluded, along with project wallets. Holders are sorted by their raw 32-byte public key and assigned a contiguous range of the total weight:
holder[i]: [ cumBefore , cumBefore + weight ) cumBefore = sum of all earlier holders' weight weight = balance in raw token units
The snapshot is committed on-chain as a Merkle root (plus the total weight), whose leaves are keccak256( keccak256( pubkey ‖ u64le(cumBefore) ‖ u64le(weight) ) ) and whose nodes are sorted-pair keccak256. The program refuses a commit once the close slot is reached, so the holder set is fixed before any randomness exists — tokens bought in the last moments before close (about a minute) don't count toward that draw. The full holder list is published so anyone can rebuild the root.
02The roll
After the close slot, anyone can call reveal. The program reads Solana's SlotHashes sysvar and takes the hash of the first produced slot at or after the close slot (a skipped slot simply rolls forward to the next one). Because that slot didn't exist when the snapshot was committed, its hash could not be predicted:
roll = keccak256( slotHash ‖ u64le(drawId) ) offset = u128le( roll[0..16] ) mod totalWeight
The operator cannot choose a favourable hash (the slot was fixed before it existed) and cannot withhold anything (there is no secret to reveal, and reveal is open to anyone). SlotHashes keeps the last 512 slots (about 3.4 minutes), so reveal has to land within that window.
03Winner selection
The winner is the holder whose weight range contains the offset — a standard weighted draw where a holder's probability equals their share of the eligible supply:
winner = the holder where
cumBefore <= offset < cumBefore + weightBecause the ranges are contiguous and cover [0, totalWeight), exactly one holder matches any offset. A holder with 4% of the eligible supply wins ~4% of draws.
04On-chain proof
Settlement is permissionless. Anyone can submit the winner together with a Merkle proof of their snapshot leaf. The program recomputes the roll, checks that the claimed holder's range covers the offset, and verifies the proof against the committed root:
settle(drawId, winner, cumBefore, weight, proof): offset = u128le(keccak256(slotHash ‖ u64le(drawId))[0..16]) mod totalWeight require cumBefore <= offset < cumBefore + weight require verify(proof, snapshotRoot, leaf(winner, cumBefore, weight)) → records exactly this winner payout(drawId): // also permissionless vault → the recorded winner's wallet
A separate, permissionless payout then has the draw's vault send the card to that recorded winner (it's a second transaction only because a card transfer and a Merkle proof don't fit in one). Because the winner is proven against committed data, the operator cannot substitute a different address — settlement either matches the public roll or fails.
What it guarantees
An operator, even one whose hot key is compromised, cannot:
- Influence the roll — the close slot was committed before its hash existed; the program reads the hash from the SlotHashes sysvar itself.
- Tune the holder set to the roll — the snapshot root must be committed before the close slot, and the full list is published so it can be checked against real balances.
- Withhold or re-roll — there is no seed to reveal, anyone can reveal and settle, and the owner key can only cancel a draw before it closes or after the reveal window has expired — never while the roll is known.
- Pay the wrong winner — settlement verifies a Merkle proof of the selected interval, and payout only sends to the recorded winner.
Two residual assumptions remain. First, the snapshot itself is built off-chain by the operator — it is committed before the roll exists and published for anyone to check against real balances, but the program can't recompute it. Second, as with any slot-hash randomness, the validator producing the close slot has marginal influence over its hash (for example by skipping the slot). The effect is small and bounded; a verifiable randomness beacon would remove it entirely.
Verify it yourself
Every draw can be recomputed from its public inputs — the slot hash stored on the draw account and the published holder snapshot — using the same code the worker runs. The interactive verifier does this in your browser; pick a draw and watch each step reproduce on-screen.

