This site uses one functional cookie to keep feature rollouts consistent for you. Nothing is set until you choose. See the privacy notice.
Dev notes
A Firefox extension that rides along inside the ESPN fantasy draft room: it watches picks happen, tracks keepers, knows my league's exact scoring, and answers the three questions that matter on the clock — who to take, what survives to my next pick, and where the tier cliff is. Lives in its own repo, not this one.
Every draft tool I tried lives in another tab showing someone else's rankings under someone else's scoring. Draft Lab is the opposite: it knows this league — full PPR, the OP superflex slot, distance-tiered kicking, the D/ST points-allowed brackets — and it sits inside the actual ESPN draft room, reading picks as they happen. While everyone else eyeballs an ADP cheat sheet, it answers the only three questions that matter on the clock: who should I take, what will still be here next turn, and where is the cliff.

It's a Firefox extension — it uses Firefox's extension APIs, so it won't load in Chrome. Once you have the zip:
about:debugging#/runtime/this-firefox in Firefox.manifest.json inside the folder.In Setup, enter your League ID (the leagueId=… in your ESPN league URL) or just open your league's ESPN page once so it's picked up — then your real team names load automatically. Set keepers, per-team tendencies, and confirm the roster/scoring match your league. Practice against the AI, or run Companion mode inside your live ESPN draft room.
One caveat of an unsigned add-on: a temporary install unloads when Firefox restarts — reload it the same way. For a permanent install, use Firefox Developer Edition, set xpinstall.signatures.requiredto false in about:config, then install the zip from about:addons → Install Add-on From File.
Three layers, deliberately separated. At the bottom is a pure draft engine — snake order, keeper slotting, roster legality, scoring, tier math — plain functions over plain data, no DOM, no storage, fully unit-testable. Above it, extension storage acts as the message bus: the content script in the draft room and the board tab both read and write one shared record, so neither depends on the other being open. At the top are two thin render surfaces — a shadow-DOM overlay injected into ESPN (so their CSS can't touch it and vice versa) and a full-page board.
The scoring pipeline starts from raw stat projections, not points: fetch a projection feed (ESPN, or import a CSV), then apply this league's rules locally. That ordering matters — the same stat line is worth different points under different rules, and it means a new data source re-scores automatically. Tiers come from projection-gap breaks, override any player's by hand, and the overrides persist across every practice draft.

Vanilla ES modules loaded straight from the extension directory: no bundler, no framework, no dependencies at all. An extension I reload twenty times during a draft is the wrong place for a build step. The split that matters is engine versus surface — snake order, keeper slotting, positional maximums, tier and survival math are pure functions in one module, exercised by node --test cases including a full 180-pick simulated draft and a legality stress run. The UI just renders what the engine says.
Every assumption about ESPN's draft room broke at least once. The pick sidebar virtualizes, so history disappears from the DOM. Row text has no whitespace between cells unless you read innerText instead of textContent. The Players table changes its button text depending on whose turn it is. The approach that survived: treat the rendered text as the interface, anchor patterns on stable tokens (the on-the-clock banner, the QUEUE/DRAFT buttons, the "R1, P1" pick format), and cross-check against ESPN's unofficial league API where cookies allow. Every scraper failure taught the same lesson — make the failure visible. The overlay grew a version badge, a detection log, and a debug line showing candidate-row counts, because "it's not working" with a screenshot of self-reported diagnostics is a one-round fix instead of four.
The unit tests pin the league scoring line by line, but the bugs that mattered came from one integration test that plays an entire draft: keepers being draftable by other teams before their slot resolved, and AI rosters finishing without a kicker because the ADP window never surfaced one. Neither shows up in any single-function test — they only exist across 180 sequential decisions. That test earned its keep more than the rest combined.
Update — August 30, 2026
The first real draft found the two places the companion could lie. It only ever read the ESPN Board tab, which is a virtualized grid — it only keeps the rounds near your scroll position in the DOM — so late-round picks that had scrolled off were never synced, and the overlay flashed "12 picks missing" that flipping tabs couldn't fix. It now also polls ESPN's league draft API, which returns every pick regardless of what's on screen. The catch: the API gives numeric player ids, so it harvests id→name off the team rosters and feeds each pick through the same name-match path the scraper uses — which means it backfills into any pool, not just an ESPN one. The DOM scraping stays as the fallback; the API is the authoritative fill.
The subtler lie was keepers. You pre-configure keepers — yours, and your best guess for every other team from last year's recap — and the overlay had been reserving all of them and counting them toward rosters whether or not the league actually set them. Now a configured keeper only counts when ESPN confirms it: the sync rebuilds the keeper reservations from the ones ESPN flags as genuinely kept, so a keeper the room didn't set stops reserving a player, stops filling a position in the recommendations, and returns to the pool. It falls back to trusting the config only when ESPN exposes no keeper flags at all, so a real keeper whose late slot the draft hasn't reached is never wrongly dropped.
Update — August 26, 2026
Running the extension against back-to-back practice drafts grew it fast. The overlay now carries a tier system — players auto-tiered from projection gaps over the full pool so tier identity survives the draft, editable per player from a Tiers tab (overrides persist across drafts), rendered as a ruled grid of left/total per position with my own fill against the roster caps. A tier outlook replaced the useless "most teams still need an RB" trend with expected survivors per tier by my next pick.
The bugs were better than the features. ESPN's Players list and Pick History rows collapse to nearly identical text, so the pick parser ingested ranking rows as picks — rank numbers became pick numbers and the QUEUE button text overwrote real team names. The guard is embarrassingly simple: a "team" called QUEUE is a button, not a franchise. Separately, keepers stopped being respected after switching projection sources, because keeper assignments stored player ids and every source uses a different id scheme — the reservation pointed at nobody, so the AI happily drafted a keeper three rounds early. Keepers now carry names and re-resolve against whatever pool is loaded.
One honest limit surfaced too: an auto-keeper mode that makes League Manager picks in ESPN's own UI works when it gets the clock, but it cannot stop ESPN's autopick from sniping a keeper-designated player early — the reservation only exists in the extension. The reliable lock is ESPN's native Keepers tab; the overlay now says so, loudly, when a keeper gets taken before their slot.
Update — August 30, 2026
Every survival number — "72% he's still here next turn" — rests on a guess about how each opponent drafts, and right now that guess is a label you pick once at setup: Balanced, Zero-RB, reaches for QBs. Companion mode watches the whole room draft for real, so the obvious idea is to stop trusting the label and start trusting the picks: infer each team's lean from what they've actually taken, and feed that back into the odds. I built it — position lean versus the room, how tightly their picks tracked ADP, blended with the label by how many picks I'd seen.
Then I made it prove itself before it shipped, and it couldn't. The test measures the thing the feature actually changes — the survival numbers — not whether my auto-pick finished higher. Across 3,000 simulated drafts I scored each prediction three ways on the identical board: the static label, my inferred profile, and an oracle that gets told each opponent's true setting. Lower Brier score is better-calibrated:
3,000 drafts · 922,656 predictions static (fixed label) Brier 0.1928 ← the bar adaptive (inferred) Brier 0.1941 +0.65% worse oracle (true settings) Brier 0.1895 -1.72% better
Two things in that table killed the feature. My inference lands worse than doing nothing — a lean read off a handful of early picks is mostly noise, and the noise costs more than the signal is worth. And the oracle, with perfect knowledge, only buys 1.7%: even knowing every opponent's true style barely moves the odds, because ADP and roster need already explain most of who survives. A tiny ceiling with a noisy estimator underneath is a bad trade, so it's shelved — the extension keeps the static label. I'd rather ship the negative result than a feature that quietly makes the numbers you trust a little worse. The prototype and its benchmark live in the repo as the record of why.
Update — August 31, 2026
When a draft finishes, every one of your picks now gets a grade — and the grade isn't "did this player score well," which you can't know in August. It's "of everyone still on the board when you were on the clock, how close was this to the best move for your lineup." A pick is measured by how much it added to the startable lineup you could field at that moment, so a third quarterback scores near zero even if he projects well, and the receiver who filled a real hole grades out. The column next to each pick names the best alternative you passed, and the hover lists the others at that position. It's all tiers — the review reads a finished draft, it doesn't need the live model.
The whole thing lives or dies on two words: available then. The first cut got this subtly wrong — the alternatives hover reused the board's "other options at this position" popup, which answers who's left now. So a first-round pick's hover suggested the dregs — the quarterbacks nobody wanted — when the players who mattered, the ones actually on the board in round one, had all been drafted by the time you looked. The fix reconstructs the pool pick by pick: walk the draft in order, and at each of your selections the available set is everyone not yet taken and not a keeper — keepers were never draftable, so they can't be the answer either. Grade and alternatives both read from that point-in-time board. A review that judges you against players who weren't available is worse than no review, because it sounds authoritative while being wrong.
Update — August 31, 2026
The extension keeps everything in the browser's own storage, which is fast and private right up until the moment it isn't there — a reload, a reinstall, a second machine, and the league you spent twenty minutes configuring is gone. I learned that the unglamorous way. So two small round-trips now cover it, and neither touches a server.
Settings CSV, for everyone: one button in Setup writes your league — team names, keepers, per-manager tendencies, position caps — to a plain CSV, and another reads it back. It's config only, not the projection pool, so the file stays small and human-readable, and it restores in one click after a reset or onto a laptop that's never seen the extension. The importer refuses a file from the wrong sport rather than quietly loading a 12-team football setup over a basketball league.
Result files, for the free and pro tiers: when a draft finishes you can download the whole thing as JSON — every pick, who took it, the final projected-lineup standings — and load it back to review later or hand to a leaguemate. Each pick carries whether it was yours or the simulator's, so an imported result reads honestly: "fully simulated" versus "nine of your own picks, finished second." It's the same shape the tool records internally, just written to a file you own instead of a database you don't.