Summary
A real-time live betting opportunity scanner that compares current in-game odds against fair opening lines to surface large line movements as they happen. Async FastAPI backend on Railway, vanilla JS frontend, deployed behind member auth. Operates inside a strict monthly API credit budget by adapting its polling cadence per league and per game state — fast when it matters, slow when it doesn't.
The Problem
Live betting moves fast. By the time a bettor manually checks if a line has shifted enough to be worth taking, the opportunity has usually already closed. Existing third-party tools either weren't real-time enough, weren't tied to fair opening lines (so the "movement" they showed was noise), or burned through API budgets faster than the underlying business could afford.
I needed a scanner that members of the existing platform could open mid-game, see a curated list of real line movements color-coded by magnitude, and act on within seconds. It also had to fit inside a 100K-credit-per-month budget across three sports (NBA, NCAAB, MLB) without me babysitting it.
The Approach
The scanner runs an async polling loop on a FastAPI backend deployed to Railway. Live odds come from a paid live-odds provider; fair opening lines are pulled once per day from a separate provider, with a pre-existing daily-data pipeline as the fallback path. Line movement is computed as the simple delta between current live line and fair opening line, then color-coded by magnitude (gold ≥ 10, orange ≥ 7, green ≥ 4, gray below).
Daily fair opening lines (1x/day) ─┐
├──→ Line movement = live − opening
Live odds (adaptive polling) ─┘ ↓
Color-coded scanner UI
(member-auth gated)
The architectural decision that made the math work was adaptive polling per league and per game state. NBA games refresh every 90s when live, NCAAB every 240s, MLB every 300s. Pre-game refreshes are slower; off-peak refreshes drop to 30 minutes. Alt lines (where bettors can "buy points" further from the current line) refresh on a stricter throttle and only when |LM| crosses a per-sport threshold. Net effect: the scanner stays inside ~80K credits/month across all three leagues with headroom for spikes.
What I Built
- Async FastAPI backend — non-blocking polling loops per league, in-memory line cache, REST endpoints for the frontend to consume
- Adaptive polling controller — per-league + per-game-state intervals, alt-line throttling tied to LM magnitude
- Fair opening line ingestion — 9 AM CT daily fetch with a fallback path to an existing daily-data pipeline if the primary source returns empty
- Single-page dashboard — vanilla JS, dark theme, color-coded by |LM|, no framework overhead
- Member-auth integration — frontend served by the existing platform behind JWT; backend enforces auth via Bearer headers and CORS pinned to the production domain
- Deploy + observability — Dockerfile, Railway deploy, structured logging, manual
force-scanandrefresh-openingsendpoints for ops
Engineering Highlights
- API budget as a first-class design constraint. Designed the polling cadence around a hard monthly credit ceiling, not around "what would feel fast." Every refresh interval was chosen with a credit-math justification documented in the project rules. The scanner has never blown budget.
- Frontend split from backend on purpose. Backend lives on Railway behind a custom domain; frontend is a static page served by the main platform on Vercel. Auth flows from the platform to the API via Bearer header. Lets each piece scale and deploy independently, and keeps the auth boundary clean.
- Sport-specific thresholds, not one-size-fits-all. MLB lines move in different units and at different cadences than NBA/NCAAB. Spread distance for "buy points" alts is 5-15 pts in basketball but 1-4 in baseball. Bake the sport asymmetry into the config rather than papering over it with averages.
- Operational guardrails baked in. Doc-only changes skip the Railway deploy.
2>&1is forbidden onrailway upbecause it eats streaming output and masks errors. Concrete operational footguns are documented in the project rules so they don't get re-discovered the hard way.
Outcome
Live to paying members. Scanner runs continuously through games; ops touches it only when the model itself changes. Daily API spend tracks within budget every month, freeing up headroom to add a fourth sport or expand alt-line coverage without renegotiating the API plan.
Tech footprint
- Frontend — single-page vanilla JS + HTML/CSS, dark theme dashboard
- Backend — Python FastAPI + httpx async client
- Data — daily fair opening lines from an external provider with a JSON-file fallback; in-memory cache for live state
- Auth — JWT Bearer headers, CORS pinned to production domain
- Deploy — Docker image on Railway, custom domain, structured logging
- Operational tooling — manual force-scan + refresh endpoints for ops; documented deploy rules