DEV Community

Cover image for How to run a provably fair giveaway in 10 lines of code
liju james
liju james

Posted on

How to run a provably fair giveaway in 10 lines of code

Every giveaway or raffle eventually gets the same comment: "this was rigged." Usually it wasn't — but there's no way for anyone outside the organizer to check. The list of entrants, the moment the winner was picked, and the randomness behind it all happen behind closed doors.

This post shows a different way: commit your entrant list first, then let the winner be decided by a value that doesn't exist yet — so nobody, including the organizer, can steer the outcome.

The idea: commit, then reveal

  1. You submit your list of entrants. The API hashes it and locks it in — that's the commitment. At this point nobody knows the winner, including the API.
  2. A public randomness beacon publishes its next value shortly after — a signed, hash-chained number nobody controlled at commitment time.
  3. The winner is derived from that value. Since the commitment happened before the random value existed, nobody could have picked it to favor a particular entrant.
  4. You get a signed certificate and a public verification page anyone can check.

What you're using

QBEACON publishes a public randomness beacon: a 4,096-bit value, signed with Ed25519 and hash-chained to the previous one, about every 2.5 minutes. Its /api/v1/draws endpoint wraps that beacon in the commit-then-reveal flow above.

It's on RapidAPI with a free tier (Basic: 10 draws a month, lists up to 100 entrants).

Base URL: https://qbeacon-verifiable-randomness.p.rapidapi.com
Headers on every call:

X-RapidAPI-Key: <your key>
X-RapidAPI-Host: qbeacon-verifiable-randomness.p.rapidapi.com
Enter fullscreen mode Exit fullscreen mode

Step 1: Create the draw

import requests

BASE = "https://qbeacon-verifiable-randomness.p.rapidapi.com"
HEADERS = {
    "X-RapidAPI-Key": "<your key>",
    "X-RapidAPI-Host": "qbeacon-verifiable-randomness.p.rapidapi.com",
}

entrants = ["Alice", "Bob", "Carol", "Dave"]

resp = requests.post(f"{BASE}/api/v1/draws", headers=HEADERS, json={
    "name": "Spring giveaway",
    "entrants": entrants,
    "n_winners": 1,
})
draw = resp.json()
print(draw["draw_id"], draw["status"])   # status: "committed"
Enter fullscreen mode Exit fullscreen mode

At this point your list is locked in — entrant_list_sha256 in the response is a hash of it, fixed before any random value exists.

Step 2: Wait for the pulse, then poll

The winner is decided by the next beacon pulse, which is usually published within about 2.5 minutes.

import time

draw_id = draw["draw_id"]
while True:
    r = requests.get(f"{BASE}/api/v1/draws/{draw_id}", headers=HEADERS).json()
    if r["status"] == "certified":
        break
    time.sleep(10)

winner_index = r["winners"][0]
print("Winner:", entrants[winner_index])
Enter fullscreen mode Exit fullscreen mode

Step 3: Hand over the proof

cert = requests.get(f"{BASE}/api/v1/draws/{draw_id}/certificate", headers=HEADERS).json()
Enter fullscreen mode Exit fullscreen mode

Or just share the verification page from the original response (draw["verify_url"]) — anyone can open it and check the draw without an API key.

Checking it yourself, without trusting the API

The certificate is signed, but you don't have to take that on faith either. QBEACON publishes a small verifier script (verify.py) that recomputes a pulse's signature and chain link independently:

curl https://qbeacon.ca/pulses/latest
python verify.py
Enter fullscreen mode Exit fullscreen mode

Being honest about the limits

This scheme proves two things: the entrant list was fixed before the pulse existed, and the pulse was signed and chained correctly. It does not prove the pulse's underlying entropy is "truly quantum" — that traces back to the ANU quantum random number generator, and you're trusting that relay the same way you'd trust any hardware RNG vendor. QBEACON states this openly rather than papering over it.

What you get instead of blind trust: a fixed commitment, a signature you can check, and a public page anyone can audit — which covers the actual complaint people have about giveaway winners ("how do I know you didn't just pick your favorite").

Wrapping up

Ten-ish lines to create the draw, poll, and read the winner — and the interesting part isn't the code, it's that the "trust me" step is gone. Full API reference: qbeacon.ca/docs.

Top comments (0)