
Fanpool
Founding Engineer NestJS, React, Next.js, MongoDB, Redis, BullMQ, Circle, AWS | 2023 to 2025
TL;DR
I built Fanpool, a platform where people run fantasy football prize pools with friends instead of a spreadsheet. I built the backend and the product on my own, from an empty repo to 2,000+ users.
The parts that mattered were the competition engine, the payout engine, the wallet, live scoring and the FPL sync. Thousands of dollars in entry fees and prizes moved through it and none was lost.
The problem
- The spreadsheet problem. FPL prize pools were run by hand. One person collects the money, screenshots a leaderboard, and pays out days later. Everyone has to trust that person.
- The border problem. Fiat rails made a pool with members in Nigeria, the UK and Saudi Arabia close to impossible.
- The data problem. The FPL API is undocumented, read-only, has no webhooks, and updates in bursts during matchdays. A 38-week competition is a lot of state to keep consistent.
What I built
The competition engine. A competition has a point system, an entry limit, a capacity, a kickoff time and a settlement time. Private competitions take a one-time entry code. Creating one sets up the competition, its USDC wallet and its platform-token wallet in a single database transaction, and moving it to upcoming schedules the background jobs that take it live and settle it.
The payout engine. Winners are ranked when the game week settles. Prize money leaves the competition's own Circle wallet as a transfer to each winner's address, and the platform token is credited through its own ledger so both balances agree. Every payout runs as a job, which is what stops a retry from paying a winner twice.
The wallet. Users fund a wallet with a bank transfer through Scalex or with USDC on Polygon, and withdraw back out. A withdrawal goes through a PIN step, checks the balance first, and is recorded as pending, so the screen never lies about money in flight.
The marketplace. Airtime, internet data, electricity and vouchers, with vendors behind them. Each order runs through a queue rather than inside the request, so a slow vendor does not hold up the app.
Quiz games. Live sessions with a leaderboard, joined and played inside database transactions so a double tap cannot score twice. Sessions are scheduled, and ending one freezes the ranking.
Deep dive: live scoring
Matchdays are the whole product, and they are the worst-case load: thousands of players' scores change at roughly the same time.
The first architecture was tightly coupled. Writing scores meant recursively updating every competition that contained the player. With 38-week competitions and 200+ entries per pool, that locked the database and the UI went stale in front of users.
I split Competitions from Scoreboards:
- A non-blocking update manager processes gameweek data in batches.
- ACID transactions aggregate total competition points independently of the individual gameweek writes.
- A cache serves live points instantly, so a leaderboard read never hits the primary database.
Points updated effectively instantly, and the reliability is what let brands host large sponsored competitions on the platform.
That rebuild took a week. It is also the clearest example of a decision I would make differently the first time: I designed for short-term pools, then the product became season-long.
Deep dive: the FPL sync
Every fantasy platform has the same drop-off: the user has to re-enter their team to see it, and they stop coming back.
I integrated the official FPL data API with credential-based account linking and a daily sync job, so a user's real squad appeared inside Fanpool. Churn fell by roughly 30%. It was the least glamorous integration in the codebase and the one that mattered most.
What I would do differently: Postgres over MongoDB
We chose MongoDB early for schema flexibility. As the product matured, two things hurt:
- Nested ranking data. We modelled rankings and per-gameweek snapshots as TypeScript types. Postgres
JSONBwith targeted indexes would have given us fast leaderboard queries without giving up the flexibility we wanted. - Nullable unique keys. MongoDB treats nulls as a single unique value, so I wrote native-driver index code to work around it. A partial unique index in Postgres expresses the same rule in one line.
Outcomes
- 2,000+ users and thousands of dollars in entry fees and prizes, with zero loss of funds.
- 99.9%+ uptime through peak game weeks, with BetterStack alerting and under two minutes to detect an incident.
- Prize money that settles itself, on the game week the competition ends.
- A product that replaced a spreadsheet for people who just wanted to play with their friends.
- Website: https://fanpool.gg
Fanpool was eventually sunset after a business pivot, but the competition, payout and live-data problems I solved there are the ones I still reach for first.