A GM-perspective tool that assigns a draft dollar value to every NBA player for the Westlake 512 dynasty auction (12 teams · $400 · 24 rounds · custom points scoring). It blends real box-score production with a qualitative layer (youth, upside, injuries, role) and calibrates the dollar scale to how this league is actually bidding.
Data source: ESPN (current rosters, bios, and multi-season stats). One current source, so form is fresh through 2025-26 with a look ahead to 2026-27 via the age curve.
- Ranking table — sortable, filterable, generative gradient art per player, the youth
premium (
Age±) inline, and live model-tuning sliders. Drafted players dim out with the price paid. - Player deep-dive — gradient hero + headshot, 3 seasons of stats with FP/G trend, a transparent value breakdown, market verdict (bargain / reach vs price paid), bio, and the editable GM override panel.
Score = production(recent FP/G, or projection) · availability · age · role · injury
$Value = convex Value-Above-Replacement, normalized to the $4,800 pool
| Layer | Captures | How |
|---|---|---|
| Production | Exact fantasy output | Your 13-line points formula on ESPN per-game stats → FP/G |
| BPS | Recent, stable form | Recency-weighted over 2025-26 / 24-25 / 23-24 (0.62 / 0.28 / 0.10) |
| Availability | Injuries / missed games | Games-played rate; (1−λ)+λ·GP/82 |
| Age | Youth, upside, decline | NBA age curve × youth-tilt θ, with a star floor so elite vets resist decline |
| Qualitative | What stats miss | Prospect tier/projection, injury flag, role adj, manual pin, notes |
| $ conversion | Auction economics | VAR^convexity, budget-normalized — top-heavy like a real auction |
Scoring formula (all 13 lines, ESPN has tech/flagrant):
FP = 1·PTS + 1.2·REB + 1.8·AST + 3·STL + 3·BLK − 1.5·TOV
+ 1.6·FGM − 1.4·(FGA−FGM) + 0.75·FTM − 2.3·(FTA−FTM) + 1.3·3PM − 2·TECH − 2·FLAG
The convexity (top-heaviness) was fit to the observed auction. Veteran prices follow a clean
power law (R² = 0.98): the model reproduces SGA ($163 vs $161 paid), Jokić ($150 vs $175),
Banchero, Holmgren — and flags the pure upside premiums (Flagg $236, Harper $125) as reaches.
Total allocated ≈ the $4,800 pool. Tune convexity in the UI to taste.
- Age± — the youth premium/discount. Flagg +44%, Knueppel +34% (upside) vs Jokić −20% (age).
- Market verdict (deep-dive) —
bargain/reachvs the price paid, for drafted players. Early marquee nominations tend to read as reaches; value emerges mid-draft.
Requires Python 3.8+ (fastapi, uvicorn, requests) and Node 18+.
# 1. Ingest from ESPN (~15s, cached & resumable)
cd backend && python ingest.py
# 2. API (terminal 1, from backend/)
python -m uvicorn server:app --port 8000
# 3. Frontend (terminal 2, from frontend/)
npm install && npm run dev # → http://localhost:5173 (proxies /api to :8000)- Sort by $ Value for the board, Age± to hunt youth/upside, or Sleeper to compare against the dynasty-ADP consensus.
- Filter by position (roster-slot eligibility) and team; toggle Hide drafted.
- Tune model — teams, rounds, budget, youth-tilt (θ), availability (λ), top-heaviness. Live, and your settings persist in the browser.
- Click any player for the deep dive: the FP/G category breakdown, the value pipeline (production → age → availability → $), 10 years of stats, and the vs-consensus comparison.
api/index.py Vercel entrypoint (re-exports the FastAPI app)
backend/ server.py (FastAPI) scoring.py (model) db.py config.py
espn.py + sleeper.py (clients) ingest.py
frontend/ src/App.jsx components/{RankingTable,PlayerView}.jsx gradient.js styles.css
data/ dynasty.sqlite (committed; opened read-only in production)
vercel.json requirements.txt
Values recompute on every request from the bundled stats + the config the browser passes — tweak a slider and the whole board updates.
The board is wired to the league's Sleeper auction draft (config.DRAFT_ID, public — no
auth). The backend polls it server-side, so as picks happen players are marked drafted $X
with the owner and the $ curve re-calibrates on real prices. The Show filter switches the
board between All / Available / Drafted / Watched. Point it at another league via the
SLEEPER_DRAFT_ID env var.
Watched players (the ★ filter) come from your personal Sleeper watch list, which is
user-specific and needs a session token — set SLEEPER_TOKEN (never commit it). Without it the
Watched filter just shows nothing; the public draft feed is unaffected.
The backend is stateless: SQLite ships read-only and all tunable config lives in the browser (localStorage), passed per request — so it runs as a Vercel Python Function with no database.
- Push to GitHub (below), then Import the repo on vercel.com.
- Vercel reads
vercel.json: it builds the Vite frontend tofrontend/dist(served static) and deploysapi/index.pyas a Python function handling/api/*. Vercel'sVERCEL=1env puts the app in read-only mode automatically — no configuration needed. - Deploy.
Refresh prod data: run python ingest.py locally, commit the updated data/dynasty.sqlite,
and push — Vercel redeploys with the new data.
Re-run cd backend && python ingest.py (cheap; cached). Delete data/cache/ to pull fresh.