Don't trust ยท verify
Provably fair
Every ticket's outcome is locked in before you scratch it โ and after each round anyone can prove it. No trust required: this page recomputes the tickets right here in your browser.
How it works
Commit
Before a round starts, the server picks a secret 32-byte server seed and publishes its SHA-256 hash. The hash locks the seed in โ change one bit and the hash no longer matches.
Reveal
When the round ends, the seed itself is published. Anyone can hash it and check it against the commitment that was public the whole time.
Recompute
Every ticket is
HMAC-SHA256(seed, "wallet:round:ticket"). With the seed public, anyone can re-derive every ticket โ tier, jackpot roll and exact prize โ and compare it with what was paid.
So every outcome is fixed the moment tickets are issued. The scratch is theater โ exactly like a paper scratch card, scratching only reveals what was already printed.
The exact math
One shared implementation (packages/core) runs on the server when tickets are issued and in your browser on this page. All money math is integer math on raw PUMP units โ no floats.
# per ticket
digest = HMAC-SHA256(key = serverSeed, msg = "wallet:roundId:ticketIdx")
x = uint64(digest bytes 0โ7) >> 11 # 53-bit uniform, u = x / 2^53
tier = first row with u < cumulative odds
y = uint64(digest bytes 8โ15) >> 11 # separate jackpot roll
jackpot hit โ y / 2^53 < 1 / 50,000
# prize, from the round's own stored parameters
B = P ร payoutRate
prize = min( floor(k ร (1 โ jackpotShare) ร B ร mult / W), floor(P ร 5%) )
- P
- โ pool snapshot taken when the round starts
- B
- โ round budget (0.5% of P by default)
- jackpotShare
- โ 10% of B feeds the progressive jackpot
- W
- โ ฮฃ tickets ร mult over every entry
- mult
- โ your whale multiplier for the round
- k
- โ the tier's prize multiple below
| Tier | Chance | u below | Prize k |
|---|---|---|---|
| No win | 70% | 0.7 | 0 |
| Small hit | 20% | 0.9 | 1.5ร |
| Nice hit | 7% | 0.97 | 5ร |
| Big hit | 2.5% | 0.995 | 8ร |
| HUGE | 0.4% | 0.999 | 25ร |
| TIER-MAX | 0.1% | 1 | 50ร |
Boundaries are compared exactly as integers (x ร 1,000,000 < cumulative_ppm ร 2^53), so floating-point rounding can never nudge a ticket into a better or worse tier. The jackpot is 1-in-50,000 on every ticket, the same for everyone; its amount is the pot at reveal time. Each round stores its own parameters, so a later config change can never break verification of an old round.
Don't trust this page either
This verifier runs the same TypeScript the server runs (packages/core/src/fair.ts). If you'd rather not trust our code at all, the whole scheme is plain HMAC-SHA256 plus integer math โ reproduce it anywhere.
- Independent test vectors. The repo's
packages/core/test/fixtures/fair-vectors.jsonwas generated by a separate Python implementation that shares no code with ours. The TypeScript must reproduce every vector byte-for-byte โ including one ticket per tier and a real jackpot hit โ or the test suite fails. - Raw data, no UI needed.
GET https://api.scratchyfees.fun/api/fair/<round>returns the commitment, revealed seed, stored parameters and every entry;/api/fair/<round>/tickets?wallet=<wallet>returns what the server recorded for that wallet. - Full write-up.
docs/FAIRNESS.mdin the repo walks through the scheme step by step (it's what this page summarizes).
Python 3 ยท standard library only
# $SCRATCHY round check โ Python 3 standard library only (hmac + hashlib)
import hashlib, hmac
ROUND_ID = <roundId>
SERVER_SEED = "<serverSeed hex>"
SEED_HASH = "<seedHash hex>"
P = <poolRaw> # pool snapshot, raw PUMP (6 decimals)
PAYOUT_RATE_PPM = <payoutRatePpm>
JACKPOT_SHARE_PPM = <jackpotSharePpm>
SINGLE_PRIZE_CAP_PPM = <singlePrizeCapPpm>
JACKPOT_ODDS = <jackpotOdds>
W = <totalWeightedMicro> # ฮฃ tickets ร mult, micro-units
ENTRIES = { # wallet: (tickets, multMicro)
"<wallet>": (<tickets>, <multMicro>),
}
CUM = [700000, 900000, 970000, 995000, 999000, 1000000] # cumulative odds, ppm
K10 = [0, 15, 50, 80, 250, 500] # prize in tenths of e
NAMES = ["loser", "small", "nice", "big", "huge", "tiermax"]
seed = bytes.fromhex(SERVER_SEED)
assert hashlib.sha256(seed).hexdigest() == SEED_HASH, "seed does not match the commitment"
B = P * PAYOUT_RATE_PPM // 1_000_000
ticket_budget = B - B * JACKPOT_SHARE_PPM // 1_000_000
cap = P * SINGLE_PRIZE_CAP_PPM // 1_000_000
for wallet, (tickets, mult) in ENTRIES.items():
for idx in range(tickets):
d = hmac.new(seed, f"{wallet}:{ROUND_ID}:{idx}".encode(), hashlib.sha256).digest()
x = int.from_bytes(d[0:8], "big") >> 11
y = int.from_bytes(d[8:16], "big") >> 11
tier = next(i for i, c in enumerate(CUM) if x * 1_000_000 < c * 2**53)
jackpot = y * JACKPOT_ODDS < 2**53
prize = min(K10[tier] * ticket_budget * mult // (10 * W), cap) if W else 0
print(wallet, idx, NAMES[tier], "JACKPOT" if jackpot else "-", prize)
Fill in the values from the round data panel above โ or verify a round first and use Copy Python check to get this script prefilled with that round.