Recon tools
The recon tools capture what ESPN's own UI sends, so request shapes and selectors come from evidence instead of guesses. They are run by hand on a workstation and are not part of the production pipeline. To confirm a new request shape, start here.
There is only one real league, so every capture runs against it.
- Read-only first. Exhaust bundle reads,
GETs and write-guarded page loads before sending anything. - Every write needs explicit approval, including a write you expect ESPN to reject.
- A human clicks; the bot never does. Recon records traffic while you act in a headed browser.
- Every live capture is reverted. Snapshot first, undo the change, and re-read until the roster matches the snapshot.
- Never retry an ambiguous write before re-reading state.
- Cookies stay in memory. Never put
espn_s2,SWIDor a saved session in a capture, fixture, doc or commit.
Which tool do I use?
| Tool | Captures | Writes possible? | Output in recon-output/ |
|---|---|---|---|
lineup_recon.js | Team page DOM, selector probe, roster snapshot. With --watch-save, every non-GET ESPN XHR and its response. | Yes, if you click Save | lineup-dom.html, lineup-screenshot.png, lineup-selector-probe.json, lineup-roster-snapshot.json, lineup-xhr.jsonl |
waiver_recon.js | Free-agent page DOM and roster snapshot. With --watch-save, every non-GET ESPN XHR. | Yes, if you add or claim | waiver-dom.html, waiver-screenshot.png, waiver-roster-snapshot.json, waiver-xhr.jsonl |
trade_recon.js | Trade builder DOM, screenshots and network at each step, plus a per-row dump. | No. A write guard aborts every ESPN write. | trade-recon-latest.log, trade-recon-rows.jsonl, trade-recon-blocked-writes.jsonl |
draft_recon.js | Draft room WebSocket frames, HTTP traffic and numbered DOM snapshots, including one the moment you go on the clock. | Only if you pick | websockets.jsonl, http-requests.jsonl, http-responses.jsonl, dom-snapshot-N.html, draft-recon-latest.log |
debug_capture.js | The latest HTML and screenshot from any bot holding a page. | No | debug-<label>-latest.html, debug-<label>-latest.png |
recon-output/ and .espn-storage-state.json are gitignored. Playwright's request.headers() leaves out cookie headers, so *-xhr.jsonl files don't contain cookies. Still check a capture before turning it into a fixture.
Running a capture
- Node 20 and Playwright Chromium installed (
npm ci). ESPN_LEAGUE_IDset.ESPN_TEAM_IDdefaults to8andCURRENT_YEARto2026, so set both for your team.- Cookies in
ESPN_S2andSWID, or an authenticatedbaoCLI. Without either, the tool opens a browser for you to log in. - Approval for any write you plan to make.
ESPN_LEAGUE_ID={leagueId} ESPN_TEAM_ID={teamId} node src/bots/lineup_recon.js
ESPN_LEAGUE_ID={leagueId} ESPN_TEAM_ID={teamId} node src/bots/lineup_recon.js --watch-save
ESPN_LEAGUE_ID={leagueId} ESPN_TEAM_ID={teamId} TRADE_RECON_TO_TEAM_ID=3 node src/bots/trade_recon.js
node src/bots/draft_recon.js # start a fresh practice draft from the lobby
node src/bots/draft_recon.js <room-url> # observe a room that's still open
--watch-save always opens a visible browser. The tool waits for you to press Enter in the terminal once you've acted.
The capture-and-revert procedure
- Pre-check. Re-read the roster, settings and pending transactions. Confirm the preconditions still hold (for example the player is still a free agent and the roster is unlocked), then save the snapshot. Use the current
scoringPeriodId. - One change. Make a single change while recording, and keep the full response status and body.
- Revert and verify. Re-read even after a rejection. If the change landed, undo exactly that change. For a lineup or IR move, move the player back. For a pickup, send a DROP for that player only. Re-read until the snapshot matches, and investigate any queued claim before you conclude nothing changed.
- Record it. Turn the capture into a test fixture with SWIDs redacted. Run the captured response through the submit helper's classification with a mocked response; that needs no second live write. Update the section pages and any
*_VERIFIEDflag in the same change.
Pick a move with an exact undo. For an IR capture, use a bench player who is OUT or INJURY_RESERVE, so moving them back to the bench restores the roster. lineup-roster-snapshot.json lists each player's slot and injury status for this.
The write guard
installWriteGuard (in trade_builder.js) routes every request on a browser context. It aborts any non-GET, non-HEAD, non-OPTIONS request to an espn.com host and records it in guard.blocked. Install it before the first navigation.
trade_recon.js runs with the guard for the whole session and clicks Cancel Trade at the end. It never sends the final submit, so the final proposal body can't be captured this way. For bundle reads, add a stricter guard: abort every non-GET to any host, close every WebSocket with routeWebSocket, and stub serviceWorker.register.
How recon logs in
The tools reuse auth.js and share one saved login.
| Order | Path | When |
|---|---|---|
| 1 | Saved session in .espn-storage-state.json | draft_recon.js and trade_recon.js, when the file exists. The only local path that renders a working page. |
| 2 | Manual login in a visible browser, then save the session | FORCE_MANUAL_LOGIN=1, or no cookies anywhere |
| 3 | Cookie injection from env or OpenBao | Enough for REST and the WebSocket, but rendered pages show "Log in Required" |
lineup_recon.js and waiver_recon.js use cookies when they exist and fall back to a manual login. If the saved session has gone stale, delete the file and rerun. See Session and auth.
debug_capture.js
Any bot can call captureDebugSnapshot(page, label) to overwrite debug-<label>-latest.{html,png}. It answers "what did the page look like just now" without a recon harness. It's on by default.
| Variable | Effect |
|---|---|
DEBUG_CAPTURE | 0, false or no turns off debug snapshots and espn-write-log.jsonl. |
DEBUG_OUTPUT_DIR | Writes captures somewhere other than recon-output/. |
Pending captures
| Capture | Blocked on |
|---|---|
| Successful free-agent add and in-window waiver claim | Approval and a free agent worth adding. Use waiver_recon.js --watch-save. |
| Full-roster add-only rejection | The roster must be full (16 non-IR players) and the target a confirmed free agent. A pre-check on 10-10 found one IR slot filled, so an add would just succeed. One on 10-13 found the roster full again, but the target back on waivers until 10-14 07:00 UTC. Nothing was sent either time. |
| Healthy player blocking IR | The state must occur naturally. |
| Trade accept, decline and Decline & Counter | A real incoming offer. See Trades. |
| Draft board with picks on it | A real draft. Read the draft_dom_observer miss logs. |