Every year, I participate in a March Madness pool with a bunch of strangers. I was invited by a friend and just stuck around because I’ve been able to rank decently the past few years. One of the interesting things about the scoring is that you get 2-3X points for picking upsets.
I know next to nothing about basketball. I don’t even like the game, but I do know that building a bracket is a game of numbers and I can hang with that. I always do 2 brackets. Last year I came in 3rd and 5th out of 30. Only 1st and 2nd place win money so I wanted to get a leg up this year to try to increases my chances of getting at least 2nd place.
Here’s what I built: a tool that generates two brackets jointly optimized to finish high in this particular pool. Not just “smart” brackets, but mathematically paired brackets, tuned for my pool size and risk tolerance.
The Problem With Normal Bracket Tools
Most bracket tools just help you pick the better team, game by game. That’s fine, but most of the time you’re going to end up with a very chalky bracket (which worked well last year, but doesn’t usually).
How It Works
Three data sources, blended:
- Barttorvik efficiency metrics (a free KenPom equivalent)
- Historical upset rates from 10 years of tournament data
- Vegas championship odds scraped from DraftKings, BetMGM, and BetRivers
This data is scraped and static in the app so there aren’t any dynamic API calls. When the play in games are finished I’ll re-scrape everything with the latest data.
Monte Carlo-lite simulation: Simulate the tournament 500 times.
Simulated annealing: Start from a chalk bracket, flip picks at random, keep the ones that improve things, gradually become more selective. The objective function rewards both expected score and high variance.
Joint 2-bracket optimization: After Bracket A is locked, Bracket B gets a diversification penalty on overlapping picks — especially in high-leverage rounds like the Final Four and Championship. The algorithm finds the pair with the best joint probability of landing one bracket in the top two. You can tune how different you want them to be.
The Upset Problem
Here’s something we ran into: unconstrained, the optimizer absolutely loved to pick upsets.
The scoring system rewards them (2-3x multipliers), so if you let it run free it’ll happily pick 18, 20 upsets. Individually those picks have positive expected value. But the historical average in the past 5 years is about 9 upsets per tournament, and a bracket stuffed with longshots leaves a lot of chalk points on the table.
The fix is a soft quadratic penalty baked into the objective. We’re now basically targeting 9 upsets, but the user can tune it to prefer more or less upsets.
The Tuning Dashboard
Everything is a slider. Nothing requires math knowledge.
- Barttorvik / Historical / Vegas source weights
- Tournament chaos level (higher = more upsets expected)
- Upset target and enforcement strength
- Variance reward
- Diversification – how different the two brackets should be
- Pool size
Some Numbers Worth Knowing
- 5–10 seconds to fully optimize both brackets, running in your browser via Web Workers
- ~9 upsets per tournament by default
- 35 different point values depending on round, upset, and correct opponent. Every edge case handled.
- #12 seeds beat #5 seeds 35% of the time. More than most people think.
- #8 vs #9 in Round 1 is basically a coin flip.
- #1 seeds lose in the first round about 2% of the time. It happens.
Try It
The app is live at https://mm-swart-three.vercel.app/. Tune the sliders, click optimize. See if the brackets it outputs are something you’d actually submit.
It won’t guarantee a win – the tournament is random. But it gives you better odds and, at minimum, produces brackets that look like they were filled out by someone who knows what they’re doing.
For those wondering, yes, I vibe coded this with Claude Opus, Sonnet, and even Haiku.