Mubin Attar

Production

CrownWager

+EV sports-betting analytics — a validated XGBoost model turned into fair-odds edge, Kelly staking, an arbitrage finder, and a graded model track-record.

  • Designed and built (solo)
  • 2025 – present
Silent tour of the live product — model probabilities turned into +EV picks (informational only, no real-money wagering). Visit the live app →

Context

Sports-betting "tip" sites have a credibility problem, and it's almost always the same three failures: unvalidated models (accuracy quoted from one lucky split), naive edge math (comparing a model probability to raw book odds, ignoring the vig), and no accountability (picks published, then quietly forgotten). CrownWager was built to be defensible on all three — a cross-validated model, explicit Decimal money math, and a track record graded against reality.

Constraints

It's a ground-up rebuild of a legacy monolith that scored 2.3/10 on a twelve-dimension assessment, so hardening was a first-class requirement, not a follow-up. And it's a responsible-gambling product, which constrains the assistant on purpose.

System architecture

Three decoupled services so heavy ML never blocks web requests: a Django/DRF backend, a separate FastAPI ML service in its own container, and a Next.js frontend. Django never imports XGBoost — all inference happens over HTTP, and a model registry serves only a model whose checksum is validated, otherwise a clearly-labeled baseline.

System map

Tap to inspect

  1. 01
  2. 02
  3. 03
  4. 04

Use the arrow keys, Home, or End to move between nodes. Press Enter or Space to pin a node's decision, and Escape to release it.

CrownWager — the ML service is isolated behind HTTP with a circuit breaker.

Data flow

A request hits Django, which calls the ML service's /predict for a home-win probability, turns it into fair-odds edge and expected value, sizes a stake with half-Kelly, and persists the pick. When the game is final, the pick is graded against the real score.

Key decisions

Cross-validated accuracy over a lucky split

Instead of: report a single train/test split, quote accuracy without measuring it

The XGBoost model is trained deterministically and reported from 5-fold stratified cross-validation, and kept intentionally shallow (max_depth 3) to resist overfitting — a real edge above the base home-win rate.

Trade-off accepted A shallow model leaves some accuracy on the table versus an aggressively-tuned one — in exchange for a number you can trust.

Explicit, Decimal-precise edge and staking math

Instead of: float arithmetic, full Kelly staking

Implied prob is 1/odds, edge is model − implied, EV is explicit, and stakes use half-Kelly — all in Decimal so real-money arithmetic doesn't drift.

Trade-off accepted Half-Kelly grows a bankroll slower than full Kelly, deliberately, to survive variance.

A verifiable, graded track record — not a wall of pending picks

Instead of: show pending picks and hope, backfill an impressive history

Every pick settles only once its game is FINAL with both scores; results aggregate per edge tier and are flagged 'insufficient' below 20 settled picks; unmappable picks are voided.

Trade-off accepted The record starts empty and grows slowly — the price of it being real.

Graceful degradation everywhere

Instead of: let a slow dependency hang the request

A circuit breaker on the ML client, plus fallbacks for the odds API and the assistant, keep the product running (in labeled demo mode) even if every external dependency is down.

Trade-off accepted More fallback code to maintain, for a system that never hard-fails in front of a user.

A grounded assistant over a persuasive one

Instead of: a salesy chatbot that pushes wagers

CrownBot is grounded strictly in the current best bets; its prompt forbids telling anyone to place a wager, forbids promising profit, and mandates responsible-gambling messaging.

Trade-off accepted A less 'salesy' assistant — which is the entire point for this domain.

What failed

A slow or dead ML service used to block web requests while their calls timed out. The fix was a circuit breaker: after three consecutive failures it opens and fast-fails for thirty seconds, falling back to clearly-labeled demo predictions instead of hanging.

v1hangsML dependency

Causea slow or dead ML service blocked web requests on timeouts

v2fast-failsML dependency

Fixcircuit breaker opens after 3 failures, fast-fails 30s, and drops to labeled demo predictions

Performance & cost

MetricValueMethod
accuracy (CV)65.2% ± 0.8%5-fold stratified cross-validation on 15,115 NBA games (2012–24), one fixed random split — no cherry-picking. The base home-win rate in the data is 57.5%, so this is a real ~8-point edge.
held-out test64.3%80/20 hold-out on 3,023 unseen games; ROC-AUC 0.685, log-loss 0.628 — the calibration signal that keeps Kelly staking from over-sizing.
model shape106 features · depth 3750 shallow trees (max_depth 3, learning rate 0.01) over 106 pre-game features; kept deliberately shallow to resist overfitting and stay interpretable.
track recordgraded vs finalsPicks settle only on FINAL scores; aggregated per edge tier; unmappable picks voided; flagged insufficient below 20 settled picks.
money mathDecimalEdge, EV, and half-Kelly (0.5) staking in Decimal; arbitrage flagged when Σ(1/odds) < 1, legs sized to equalize payout.
legacy → rebuild2.3/10 → hardenedA twelve-dimension assessment drove a rebuild with per-IP rate limiting, admin 2FA, CSP, and blocking bandit + gitleaks in CI.

Operations

Celery + Redis run the async jobs; the ML service runs in its own container behind the circuit breaker. Security is hardened by default: proxy-aware per-IP rate limiting, constant-time token comparison, optional TOTP 2FA, DSN-gated Sentry, and a tested backup/restore runbook.

What I'd do differently

The model is deliberately simple; with more graded history I'd add calibration reporting (are the 60% picks actually winning 60%?) and a closing-line-value check, which is a stronger signal of edge than raw win rate.

Evidence

The app is live and the backend, ML service, and grading logic are in the repository above.

Changelog

  1. A verifiable model track-record and prediction confidence grades; cut a cherry-picked split in a reality pass.

  2. Best-bets credibility layer — fair odds, confidence grades, and filters.

  3. Security hardening: admin 2FA, cookie-JWT auth, a circuit breaker on the AI client, and blocking CI gates.

  4. Card-free free-tier deploy (Hugging Face Space + Vercel), bankroll ROI/P&L tracking, and launch hardening.

  5. First build — a validated XGBoost NBA moneyline model served behind the analytics platform.