Every settled game exposes a proof with everything needed to recompute the winner: the
commitment, the revealed server seed, the per-player client seeds and weights, and the nonce.
Nothing is hidden, and the check below uses only the Python standard library — no Luckie
code, no dependencies, no network.
1. Get the proof
Pull the proof JSON from the API:
curl -s https://api.luckie.bet/games/<game_id>/proof
Or open the human-readable proof page in the app: https://luckie.bet/games/<game_id>/proof.
It looks like this:
{
"game_id": "a1b2c3…",
"mode": "coinflip",
"nonce": "9f2ac8…",
"server_seed_hash": "sha256 of the server seed, published at game creation",
"server_seed": "revealed only after the game settles",
"winner": "<winner wallet>",
"players": [
{ "user_id": "<wallet A>", "client_seed": "…", "weight": 1000000 },
{ "user_id": "<wallet B>", "client_seed": "…", "weight": 1000000 }
]
}
server_seed is null until the game settles. Before settlement you can only see the
commitment server_seed_hash — which is the whole point: the outcome is sealed in advance.
2. The verifier
Save this as verify_fairness.py. It’s the exact algorithm Luckie uses, reimplemented with
stdlib only so you never have to trust our code:
#!/usr/bin/env python3
"""Luckie fairness verifier — recompute a settled game's winner from its public proof.
Standard library only: no dependencies, no network, no database.
Usage:
python verify_fairness.py proof.json
curl -s https://api.luckie.bet/games/<id>/proof | python verify_fairness.py -
"""
import hashlib
import hmac
import json
import sys
def canonical_message(game_id, nonce, players):
# Sort by user_id so the input is independent of join order — every verifier
# reconstructs the identical message regardless of who joined first.
parts = sorted(f"{p['user_id']}:{p['client_seed']}" for p in players)
return f"{game_id}|{nonce}|{'|'.join(parts)}".encode()
def outcome_int(server_seed, game_id, nonce, players):
# outcome = HMAC-SHA256(key=server_seed, msg=game_id|nonce|sorted player seeds)
digest = hmac.new(
server_seed.encode(), canonical_message(game_id, nonce, players), hashlib.sha256
).digest()
return int.from_bytes(digest, "big")
def pick_winner(mode, server_seed, game_id, nonce, players):
n = outcome_int(server_seed, game_id, nonce, players)
ordered = sorted(players, key=lambda p: p["user_id"])
if mode == "coinflip":
if len(players) != 2:
raise ValueError("coinflip requires exactly 2 players")
return ordered[n % 2]
if mode == "jackpot":
total = sum(int(p.get("weight", 1)) for p in players)
if total <= 0:
raise ValueError("total weight must be positive")
target = n % total # odds are weight-proportional
cumulative = 0
for p in ordered:
cumulative += int(p.get("weight", 1))
if target < cumulative:
return p
raise ValueError(f"unknown mode: {mode!r}")
def verify(proof):
# 1. The revealed seed must match the commitment published at game creation.
if hashlib.sha256(proof["server_seed"].encode()).hexdigest() != proof["server_seed_hash"]:
return False
# 2. Recomputing the outcome must reproduce the published winner.
winner = pick_winner(
proof["mode"], proof["server_seed"], proof["game_id"], proof["nonce"], proof["players"]
)
return winner["user_id"] == proof["winner"]
def main(argv):
if len(argv) != 2:
print(__doc__)
return 2
raw = sys.stdin.read() if argv[1] == "-" else open(argv[1], encoding="utf-8-sig").read()
proof = json.loads(raw)
ok = verify(proof)
print(f"{'PASS' if ok else 'FAIL'}: game {proof['game_id']} winner={proof['winner']!r}")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
3. Run it
# straight from the API
curl -s https://api.luckie.bet/games/<game_id>/proof | python verify_fairness.py -
# or from a saved file
python verify_fairness.py proof.json
A passing game prints:
PASS: game a1b2c3… winner='<winner wallet>'
If it ever prints FAIL, that’s cryptographic proof the result didn’t match its committed
seed — and we’d want to know immediately.
How it works
Commitment check
sha256(server_seed) must equal the server_seed_hash that was published before anyone
deposited. This proves the seed wasn’t swapped after the fact.
Deterministic outcome
The random integer is HMAC-SHA256(key=server_seed, msg=game_id | nonce | sorted player seeds). Players’ client seeds are mixed in after the server committed, so the server
can’t grind a seed to force a winner.
Winner selection
Coinflip: players sorted by wallet, winner = outcome % 2. Jackpot:
target = outcome % total_weight, then walk players (sorted by wallet) accumulating weight
until target falls in a player’s band — so odds are exactly proportional to your buy-in.
The server reveals the seed only after settlement. Before that the outcome is sealed —
neither the server nor any player can know or change it.