Provably Fair
Every Flip and Pull outcome is provably fair — buyers can verify the operator didn't rig the result. The trust comes from a commit-reveal flow with on-chain anchoring: the random seed gets hashed and posted to Solana before the result is decided, so the operator can't change it after seeing the outcome.
The cryptographic guarantee
Three primitives:
- SHA-256 — hash function. Given a seed, the hash is deterministic. Without the seed, you can't reverse-engineer it.
- Solana memo transaction — public on-chain record. Once written, you can't change or delete it. Anyone can read it.
- Random number generator —
crypto.getRandomValues(32 bytes), server-side
The flow (Flip example)
// Phase 1: COMMIT (buyer doesn't know the seed yet)
serverSeed = randomBytes(32) // 32-byte random
seedHash = SHA-256(serverSeed.toHex()) // 64-char hex string
solanaTx = anchorOnSolana(seedHash) // memo tx, returns signature
// Server stores: { playId, seedHash, solanaTx, sealedSeed: encrypt(serverSeed) }
// Returns to buyer: { playId, seedHash, solanaTx }
// Buyer can now verify hash exists on Solana (live, on-chain proof)
// Phase 2: REVEAL (after buyer commits payment)
serverSeed = decrypt(sealedSeed) // unlock the original
outcome = serverSeed[31] % 2 === 0 ? 'WIN' : 'LOSE' // last byte parity
// Server returns: { playId, serverSeed, outcome }
// Buyer's UI verifies: SHA-256(serverSeed) === seedHash → matches → fair
The verification path (buyer-side)
A buyer who's suspicious can verify any past play:
- Get the play's
seedHash+solanaTx+serverSeed+outcomefrom/api/play/[id] - Open Solana Explorer for the tx signature → confirm the memo contains
seedHash - Check the timestamp — was the tx confirmed BEFORE the outcome was revealed?
- Run
SHA-256(serverSeed)locally → must equalseedHash - Compute the outcome from the seed using the same formula → must equal what the operator returned
If steps 3-5 all pass, the play was fair. If ANY fails, the operator cheated.
Pull-specific differences
Pull works the same way but uses a different selection formula:
winnerIndex = serverSeed[0] % stackSize
winningCard = sortedStack[winnerIndex]
The stack is sorted by card_id before selection so the order in the buyer's UI doesn't affect outcome.
What the buyer sees
On the play result page, ToT shows the verification panel:
- Pre-commit hash: copyable hex string + Solana Explorer link
- Server seed (revealed): copyable hex string
- Verify locally button: runs the SHA-256 in the browser, shows match ✓ or mismatch ✗
- Outcome: WIN/LOSE for Flip, winning card for Pull
Optional: client seed (extra paranoid mode)
For maximum trust, the buyer can supply their own clientSeed at commit time. The server combines:
combinedSeed = SHA-256(serverSeed + clientSeed)
This means even if the operator somehow chose a seed AFTER seeing the buyer's intent, they can't predict the combined output. ToT supports this but doesn't surface it in the default buyer flow (most buyers don't care; the on-chain anchor is enough).
Operator costs
Each Solana memo tx costs ~5,000 lamports = ~$0.00005 USD on mainnet. At 1,000 plays/month that's $0.05 in anchoring fees. Negligible.
Set SOLANA_PRIVATE_KEY + SOLANA_RPC_URL in ToT's env. Without these, anchoring silently disables and plays are SHA-256-only (still verifiable in-browser, but no on-chain proof).
Most card games (Whatnot mystery boxes, Fanatics flash sales, etc.) have NO buyer-side verification. The operator picks the outcome and the buyer trusts. Provably-fair is your differentiator — every play page links the buyer to the on-chain proof. Make this visible in the UI and on the buyer-facing manual.
Code references
- Flip math:
thisorthat/src/lib/games/flip.ts(computeFlipOutcome()) - Pull math:
thisorthat/src/lib/games/pull.ts(computePullWinner()) - Solana anchor:
thisorthat/src/lib/blockchain/solana.ts(anchorHash()) - Verification UI:
thisorthat/src/components/play/ProvablyFairPanel.tsx