Take the raw data from any draw and reproduce the result yourself with open scripts — completely independent of our servers.
Manual Verification
Total transparency: take the raw data from any draw and reproduce the result yourself, completely independent of our servers.
Step 1 — Get the Data
Open any completed draw on the verification page. The Verifiably Fair Data section lists: Server Seed (the Drand randomness), Client Seed (the timestamp), Round ID (the draw’s UUID), Static Salt, Tickets Sold, Max Tickets and Number of Winners.
Step 2 — Verify the Drand Source
Confirm the Server Seed really came from the Drand network and was not invented by us. Fetch the committed round from Cloudflare’s public relay (replace {ROUND_NUMBER}):
https://drand.cloudflare.com/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/{ROUND_NUMBER}
The randomness field must equal the Server Seed exactly. Cross-check the same URL on api.drand.sh, api2.drand.sh or api3.drand.sh — independent relays must agree.
Step 3 — Run the Calculation
We use HMAC-SHA256 to combine the seeds and rejection sampling to determine the winner — this guarantees uniform probability with no modulo bias. Run this PHP script in any sandbox (e.g. an online PHP runner):
<?php
// 1. INPUT YOUR DATA HERE
$serverSeed = 'REPLACE_WITH_SERVER_SEED'; // Drand randomness for the committed round
$clientSeed = 'REPLACE_WITH_CLIENT_SEED'; // Draw timestamp (ms)
$roundId = 'REPLACE_WITH_ROUND_ID'; // Draw UUID
$staticSalt = 'REPLACE_WITH_STATIC_SALT'; // Published static salt
$ticketsSold = 100; // Actual tickets sold
$maxTickets = 1000; // Max tickets (draw range)
$numWinners = 1; // Number of winners drawn
// 2. COMBINE SEEDS (HMAC-SHA256)
$data = "{$clientSeed}:{$roundId}:{$staticSalt}:{$ticketsSold}:{$maxTickets}";
$combinedHash = hash_hmac('sha256', $data, $serverSeed);
echo "Combined Hash: {$combinedHash}\n";
// 3. GENERATE RESULT (Rejection Sampling, uint32 little-endian)
$limit = 0xFFFFFFFF - (0xFFFFFFFF % $maxTickets);
$winners = array();
$block = 0;
$hashBin = hex2bin($combinedHash);
$i = 0;
while (count($winners) < $numWinners) {
if ($i + 4 > strlen($hashBin)) { // Extend deterministically
$block++;
$hashBin = hex2bin(hash_hmac('sha256', $data . ':extend:' . $block, $serverSeed));
$i = 0;
continue;
}
$value = unpack('V', substr($hashBin, $i, 4))[1]; // 'V' = uint32 LE
$i += 4;
if ($value >= $limit) continue; // Rejected: no modulo bias
$ticket = ($value % $maxTickets) + 1;
if (in_array($ticket, $winners, true)) continue; // Duplicate: skipped
$winners[] = $ticket;
}
echo "Winning Ticket(s): " . implode(', ', $winners) . "\n";
Or run this JavaScript directly in your browser console (Right click → Inspect → Console):
// 1. INPUT YOUR DATA HERE
const serverSeed = 'REPLACE_WITH_SERVER_SEED';
const clientSeed = 'REPLACE_WITH_CLIENT_SEED';
const roundId = 'REPLACE_WITH_ROUND_ID';
const staticSalt = 'REPLACE_WITH_STATIC_SALT';
const ticketsSold = 100;
const maxTickets = 1000;
const numWinners = 1;
async function hmac(message, key) {
const enc = new TextEncoder();
const k = await crypto.subtle.importKey('raw', enc.encode(key),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
return new Uint8Array(await crypto.subtle.sign('HMAC', k, enc.encode(message)));
}
(async () => {
const data = `${clientSeed}:${roundId}:${staticSalt}:${ticketsSold}:${maxTickets}`;
let bytes = await hmac(data, serverSeed);
console.log('Combined Hash:', [...bytes].map(b => b.toString(16).padStart(2, '0')).join(''));
const limit = 4294967295 - (4294967295 % maxTickets);
const winners = [];
let block = 0, i = 0;
while (winners.length < numWinners) {
if (i + 4 > bytes.length) { // Extend deterministically
block++;
bytes = await hmac(`${data}:extend:${block}`, serverSeed);
i = 0;
continue;
}
const value = new DataView(bytes.buffer, i, 4).getUint32(0, true); // uint32 LE
i += 4;
if (value >= limit) continue; // Rejected: no modulo bias
const ticket = (value % maxTickets) + 1;
if (winners.includes(ticket)) continue; // Duplicate: skipped
winners.push(ticket);
}
console.log('Winning Ticket(s):', winners.join(', '));
})();
Why can’t I use a standard hex converter?
Converting the whole hash to one giant number and taking a simple modulo introduces modulo bias — some tickets would be slightly more likely to win than others. Rejection sampling reads the hash 4 bytes at a time and discards values above a calculated limit, guaranteeing every ticket exactly the same probability. That is why you must use the scripts above.
Multiple winners
Winner #1 is always the first accepted value — identical to the single-winner calculation. Additional winners simply continue the same walk through the hash, skipping duplicate tickets. If the 32-byte hash is exhausted, the walk continues on deterministic extension blocks: HMAC-SHA256(data + ":extend:" + n, serverSeed) for n = 1, 2, 3…