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

  1. 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.

  2. 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.

  3. 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.

Verify a round

Everything below is recomputed in your browser from the revealed seed โ€” the server only supplies the data.

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
Odds table
TierChanceu belowPrize k
No win70%0.70
Small hit20%0.91.5ร—
Nice hit7%0.975ร—
Big hit2.5%0.9958ร—
HUGE0.4%0.99925ร—
TIER-MAX0.1%150ร—

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.json was 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.md in 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.