Contender
A full-stack sports-analytics platform that analyzes NBA player props and two- to four-leg parlays, keeping the projection, the historical evidence, and the probability estimate visibly separate.
Live public site · Jan 2026 – Present

Solo build: the analysis engine and FastAPI service, data ingestion, the Next.js product, and accounts and billing.

- Metrics preserved / corrected
- 68 / 18Parity registry from porting the original engine; every correction documented.
- Backfill jobs, zero failures
- 433Two NBA seasons of team and player logs, September 2026.
- Automated backend tests
- 580+Counted in the backend source, October 2026.
The problem
Most prop tools hand you stats, matchup notes, and a confident-looking number, but very few show where a probability came from or how much to trust it. A projection, a historical hit rate, and an estimated probability are three different things, and blending them into one figure hides the evidence a user needs.
Contender started as a Streamlit prototype with a hybrid prediction engine. The challenge was turning it into a real product without silently changing what the engine produced.
My contribution
I designed and built the whole platform: the ported analysis engine and typed FastAPI service, the NBA ingestion pipeline, the Next.js interface, saved history, and the account, quota, and billing workflows.
What I built
Each analysis combines a rules-based projection with a Random Forest model (Ridge regression as the fallback for limited history), recent form, and matchup context, then returns the projection, the observed hit rate, and the estimated probability side by side. Results are saved as immutable snapshots rather than being recomputed later.
Parlays of two to four legs state their independence assumption next to dependence bounds and flags for linked legs, instead of presenting an unvalidated joint probability.
- Ingestion from stats.nba.com into a normalized Supabase Postgres store, with raw snapshots, content hashes, and correction tracking.
- Analyses run as background jobs: cached results return immediately and new ones are queued and polled.
- Saved picks and parlays are stored as they were produced and settled against final box scores.
- Clerk authentication, Stripe subscriptions, and quota-enforced plans with an idempotent reservation ledger.
How it works
The engine sits behind a versioned API, so the product, the background workers, and the saved history all read the same analysis contract.
- stats.nba.com
- Ingestion worker → Supabase Postgres
- Rules projection + Random Forest (Ridge fallback)
- FastAPI /api/v1: jobs · quotas · auth
- Next.js product + saved history
nba_api ingestion with normalized headers, validation, idempotent upserts, and stored provider snapshots.
Python and scikit-learn: a seeded Random Forest blended with a rules engine, with Ridge as the regularized fallback.
Typed FastAPI service with background jobs, ownership-scoped repositories, and per-user quotas.
Next.js, TypeScript, Tailwind CSS, and React Query, with Clerk and Stripe for accounts and billing.

Key decisions
Port the engine before improving it.
I recorded real NBA responses as fixtures and wrote parity tests against the original prototype. The resulting registry lists 68 metrics preserved exactly and 18 corrected, each with a documented reason and example, so nothing changed silently.
Use Ridge regression as the fallback path.
For players with limited history, a regularized linear model gives steadier output than an undertrained Random Forest.
Label probabilities honestly.
Every probability carries its calibration status. The public example labels its estimate as heuristic and uncalibrated rather than presenting it as a calibrated forecast.
Keep evidence immutable.
A saved analysis is a snapshot of what the engine produced at that moment, including visible misses, so the history can be inspected rather than rewritten.
Tradeoffs
Speed vs. depth: Heavy feature work runs as a background job so it never blocks a request; the cost is a short polling step for new analyses.
Joint model vs. honesty: Parlay probabilities multiply leg estimates under a stated independence assumption, shown beside dependence bounds, instead of shipping an unvalidated joint model.
Cost vs. always-on: Running the worker in-process on Render keeps hosting simple and inexpensive, at the cost of pauses when the instance sleeps.
Outcomes & evidence
Contender is live at contender.davishiggins.com with a public NBA product, a methodology page, and a replayed historical analysis that includes a visible miss. The backend source contains more than 580 automated tests, and the September 2026 backfill of two NBA seasons completed 433 ingestion jobs with zero failures.
The project's original build write-up is preserved in Writing as a dated build story.
Scope & limits
NBA is the public product. MLB and NFL work exists on a shared multi-sport foundation but is not a public release. Contender does not promise accuracy, profit, or betting outcomes, and its probabilities are labeled by calibration status.
What I learned
Preserving the evidence behind a result makes an analytics product easier to inspect and improve after both hits and misses.
Built with
Related writing
More projects

Flowcast
Hourly taxi-demand forecasts for all 262 NYC zones, with a public model card.

Cade
A Claude-powered working system with structured memory and reusable Jobs.

DHLM
A private intelligence workspace grounded in approved Google data and notes.