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
- 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.
- A public randomness beacon publishes its next value shortly after — a signed, hash-chained number nobody controlled at commitment time.
- 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.
- 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
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"
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])
Step 3: Hand over the proof
cert = requests.get(f"{BASE}/api/v1/draws/{draw_id}/certificate", headers=HEADERS).json()
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
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)