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

My role

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

An empty stadium under floodlights with lime accents.
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.

  1. stats.nba.com
  2. Ingestion worker → Supabase Postgres
  3. Rules projection + Random Forest (Ridge fallback)
  4. FastAPI /api/v1: jobs · quotas · auth
  5. Next.js product + saved history
Data

nba_api ingestion with normalized headers, validation, idempotent upserts, and stored provider snapshots.

Modeling

Python and scikit-learn: a seeded Random Forest blended with a rules engine, with Ridge as the regularized fallback.

Service

Typed FastAPI service with background jobs, ownership-scoped repositories, and per-user quotas.

Product

Next.js, TypeScript, Tailwind CSS, and React Query, with Clerk and Stripe for accounts and billing.

Contender homepage: See the numbers behind every pick.
The public homepage at contender.davishiggins.com, which introduces inspectable, saved analyses.

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

PythonFastAPIscikit-learnRandom ForestRidge regressionnba_apiNext.jsTypeScriptTailwind CSSReact QuerySupabasePostgresClerkStripeRenderVercel

More projects

All projects