Live production app
Tourney 🏆
A March Madness pool that's run for 15+ years on paper and spreadsheets -
rebuilt as a real, live web application. You draft individual players and
coaches, not whole teams, and your score updates automatically as real
games happen.
1 AWS box
Flask + MySQL + nginx, containerized with Docker, one EC2 host
2 AI systems
Claude vs. a self-hosted model, run head-to-head on the same task
15+ years
the pool itself - paper, then spreadsheets, then this
Live ESPN data
polled continuously during games - not a nightly batch
5 CI gates
secrets · tests · lint · security-lint · dependency audit, before every deploy
Zero PII
in the AI worker's own database account - one table, no names or emails
01The idea
A normal bracket pool asks you to predict which teams win. This one asks you to draft individual players - 22 of them, across five groups, seeded 1-4 through 13-16, plus head coaches - and your score comes from what those specific people actually do in real games.
Two people can agree on every game's winner and still land on completely different scores, because they drafted different players off the same teams. It rewards knowing the players, not just the bracket - and it's why building a live scoring engine mattered more than building another prediction form.
02Architecture
One AWS EC2 box, three Docker containers: a reverse proxy handling HTTPS, the Flask app itself, and its MySQL database. Deliberately lean - no server fleet, no managed services to wire together - sized for a free pool with a modest number of players, not over-engineered for scale it doesn't need.
What it's built with
Python / Flask
MySQL
SQLAlchemy
Docker
Nginx
HTMX
Amazon EC2
AWS SES
DuckDB (analytics warehouse)
GitHub Actions
Deploy pipeline
A commit to main auto-rebuilds a staging environment the instant five automated gates go green - secrets scan, test suite, lint, security-lint (bandit), dependency audit. Promotion to production is a single manual click after that, fast-forward-only, never a force-push.
Separate from the live app entirely: a second, offline pipeline pulls a conformed copy of six completed seasons (2018-2025) into a DuckDB analytics warehouse, kept apart from production so analysis never touches live scoring.
03AI - two systems, compared honestly
Every day, the app writes a short AI-generated update on how the standings changed - generated two different ways, on two different models, so the comparison is real rather than a marketing claim.
Simple
The app looks up the standings itself and hands the model everything it needs in one message. The model just writes.
Agentic
The model gets no data upfront - it's given a short menu of questions it's allowed to ask (standings, today's results, biggest movers) and decides for itself what it needs before writing.
Claude
Hosted by Anthropic, called through a real agentic tool-calling loop - the prompt deliberately contains no data.
Self-hosted
An open-weight model running on a dedicated NVIDIA DGX Spark - physical GPU hardware, an agentic loop built from scratch, zero API cost.
Both the prompt sent and every tool call made are watchable live, in the running app - not a claim you have to take on faith.
04Built to be safe, not just built to work
A side project that handles real names, emails, and phone numbers doesn't get to skip security because it's small.
- Passwords hashed with scrypt - not stored, not recoverable, not even by an admin.
- The AI worker connects with its own least-privilege database account: one table, no PII, outbound-only, rate-limited, with hard caps on tool calls, timeout, and payload size.
- The self-hosted model's connection to the app is proven with mutual TLS - a certificate issued to that one worker and separately registered on an approved list, checked on every request.
- Every deploy runs through an automated test suite and CI/CD gate before it reaches production - nothing ships on faith.