Manual Verification

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…