How 90Picks Works
90Picks is an on-chain parimutuel betting platform for FIFA World Cup 2026, built on Base L2. This page covers the architecture, smart contracts, fee model, and security approach.
1. Parimutuel Model
90Picks uses a parimutuel model — the same system used by horse racing and tote betting for over 150 years. There are no fixed odds and no bookmaker. Instead:
- Pick a side — Home, Draw, or Away for any World Cup match
- Put dollars in — Your dollars go into the match pot (minimum $1, maximum $10,000)
- Winners split the pot — After the match, everyone who picked correctly splits the total pot proportionally
The less popular your pick, the bigger the payout if you win. All multipliers shown during the open phase are estimates (marked with ~). Final payouts are determined only when betting closes, 30 minutes before kickoff.
2. Architecture
The system has three components:
Users (Coinbase Smart Wallet)
│
├── deposit / claim
│
▼
Frontend (Next.js on Vercel)
│
├── reads pool data + sends transactions
│
▼
Base L2 Blockchain
│
├── PoolFactory → creates MatchPool per match
├── MatchPool #1 (Mexico vs Canada)
├── MatchPool #2 (USA vs Morocco)
└── MatchPool #N ...
│
├── resolve() ← CRE Resolver (polls match results)
└── cancel() ← CRE Resolver or OwnerBase L2 provides sub-cent gas costs and fast finality. All transactions are sponsored by the Coinbase paymaster — users never need to hold ETH or pay gas fees.
3. Smart Contracts
Two Solidity contracts, built with OpenZeppelin:
Deploys one MatchPool per match using CREATE2 (deterministic addresses from match ID). Owner-controlled pool creation.
Handles deposits, resolution, claims, fees, and cancellation for a single match. Uses ReentrancyGuard and SafeERC20.
Key Functions
deposit(outcome, amount, referrer)Place a bet during the open phaseresolve(outcome)CRE resolver sets the match winnercancel()CRE resolver or owner cancels — everyone gets full refundsclaim()Winners collect proportional payout, or full refund on cancelresolveManual(outcome)Owner safety valve — only after deadline + 48 hourscollectFees()Fee recipient withdraws 2.5% of losing pools after 7-day claim windowsweep()Owner cleans up remaining dust after 90 daysConstants
FEE_BPS = 2502.5% fee on losing poolsMIN_BET = $11,000,000 USDC units (6 decimals)MAX_BET = $10,00010,000,000,000 USDC units4. Pool Lifecycle
[CREATED] ──deposit()──▶ [OPEN] ──deadline──▶ [CLOSED]
│
resolve() or cancel()
│
┌───────────────┼───────────────┐
▼ ▼
[RESOLVED] [CANCELLED]
│ │
claim() claim()
(winners) (full refund)
│
collectFees()
(after 7 days)
│
sweep()
(after 90 days)- Open phase: Users can deposit until 30 minutes before kickoff
- No withdrawals: Bets lock at deposit — this prevents odds manipulation
- 90-minute result: Only the 90-minute score counts. Extra time and penalties don't affect the outcome — Draw is always a valid pick
- Safety valve: If the automated resolver fails, the owner can resolve manually after 48 hours
5. Fee Model
A 2.5% fee is charged on losing pools only. Winners' deposits are never taxed.
Example: $10,000 total pot Home: $6,000 (60%) ← wins Draw: $1,500 (15%) Away: $2,500 (25%) Losing pool = $4,000 (Draw + Away) Fee = $4,000 × 2.5% = $100 Distributable to winners = $9,900 A $100 bet on Home → payout = $9,900 × ($100 / $6,000) = $165
Referral Rebate
If a user was referred, they receive a 50% rebate on their proportional fee share. The rebate comes from the platform fee — it does not affect other users' payouts.
6. Security
- OpenZeppelin libraries — ReentrancyGuard on deposit/claim, SafeERC20 for all USDC transfers. Battle-tested, industry-standard.
- Key separation — Three distinct roles with different permissions: Factory Owner (creates pools, emergency controls), CRE Resolver (resolves/cancels matches), and Users (deposit/claim only). Compromise of one key doesn't give full control.
- Safety valves — Owner can cancel any pool for full refunds. Manual resolution available after 48-hour waiting period. Fee collection delayed 7 days after resolution for claim window.
- No admin withdrawal — There is no function for the owner to withdraw user deposits. The only outflows are claim() (to users), collectFees() (to fee recipient, after 7 days), and sweep() (dust cleanup after 90 days).
- Deterministic addresses — Pool addresses are derived from match IDs via CREATE2. Anyone can verify a pool's authenticity on-chain.
7. Test Suite
59 tests covering all contract functions, edge cases, and mathematical invariants. Built with Foundry (Forge).
Coverage
- Deposit flows — valid deposits, boundary amounts ($1 / $10,000), referral tracking, paused pool, closed pool, invalid outcomes
- Resolution — CRE resolve, manual resolve (48h delay), resolve with all three outcomes, resolution when one outcome has zero deposits
- Cancellation — CRE cancel, owner cancel, full refund verification
- Claims — winner payouts, proportional math, double-claim prevention, refunds on cancel
- Fee math — 2.5% calculation, referral rebate deduction, fee collection timing (7-day window), zero-fee edge cases
- Invariant testing — conservation of funds (payout + fees = totalPool), cancel = exact refund, fee bounds, double-claim prevention, referral rebate conservation
- Sweep — 90-day timing, dust recovery, unauthorized access
Source code: github.com/djehuty94/Golazo
8. Audit Status
No formal third-party audit has been completed yet.
The contracts use battle-tested OpenZeppelin libraries (ReentrancyGuard, SafeERC20) and have 59 tests including invariant testing. The source code is fully open-source. A formal security audit is planned before mainnet launch.
9. Contract Addresses
Contract addresses will be published here once mainnet deployment is complete.
Individual MatchPool addresses are deterministic — derived from the match ID via CREATE2. Once pools are created, each match page will link directly to its contract on BaseScan.