From c48d65faf33c37976367ab6d45b0c0afd88f4ea6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 17:52:19 +0000 Subject: [PATCH 1/2] Add fantasy football auction draft tracker Firefox add-on A read-only Firefox sidebar add-on that observes a live auction draft and layers analytics on top: who was taken, for how much, what every team has left, what they still need, and what the player on the block is worth in the current market. Design decisions worth calling out: - Reads the DOM/WebSocket, not the screen. A content script already lives inside the page, so OCR is a last resort rather than the design. A page-world shim wraps window.WebSocket and mirrors frames (passively -- it never sends, blocks or alters one); a MutationObserver layer runs alongside it so the DOM silently covers whatever the WS mapping misses. - State is an append-only event log. Budgets, rosters and analytics are a pure function of that log, which is what makes replay, undo, and mid-draft page-refresh recovery work at all. Low-confidence observations are quarantined for review rather than applied. - The platform seam is data, not code. Site knowledge lives in selector profiles so repairing a redesign is a config change, not a rewrite. Analytics: market inflation (discretionary form, with the $1-per-slot floor removed from both sides), per-team max bid, positional scarcity against replacement level, tier cliffs, budget pressure, and bid advice capped by what you can legally bid. Valuations are imported from CSV (alias-matched headers, FantasyPros exports work as-is) and rescaled to the league's total money, so a sheet built for a different league size does not silently bias every suggestion. src/core has no browser dependency, so the whole analytical layer runs under node --test with no DOM shim: 57 tests, plus a deterministic draft simulator and a replay harness that rehearses the full pipeline against a recorded draft. The NFL.com and CBS selector profiles are provisional -- written without access to a live auction room -- and need calibration against a mock draft; tools/calibrate.js does that. The WS field mappings need a real capture, so the WS layer ships in record-only mode. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SjniQqz3FPpxjKzASrVNCR --- fantasy-auction-tracker/README.md | 175 + .../fixtures/sample-draft.json | 8487 +++++++++++++++++ .../fixtures/sample-values.csv | 209 + fantasy-auction-tracker/manifest.json | 56 + fantasy-auction-tracker/package.json | 16 + fantasy-auction-tracker/src/adapters/base.js | 102 + .../src/adapters/generic-dom.js | 164 + .../src/adapters/profiles.js | 99 + fantasy-auction-tracker/src/adapters/ws.js | 231 + .../src/background/background.js | 152 + .../src/content/content.js | 100 + fantasy-auction-tracker/src/content/inject.js | 86 + fantasy-auction-tracker/src/core/analytics.js | 302 + fantasy-auction-tracker/src/core/config.js | 82 + fantasy-auction-tracker/src/core/events.js | 132 + fantasy-auction-tracker/src/core/players.js | 122 + fantasy-auction-tracker/src/core/reducer.js | 238 + fantasy-auction-tracker/src/core/store.js | 98 + .../src/core/valuations.js | 165 + .../src/sidebar/sidebar.css | 151 + .../src/sidebar/sidebar.html | 110 + .../src/sidebar/sidebar.js | 327 + .../test/analytics.test.js | 237 + fantasy-auction-tracker/test/reducer.test.js | 214 + fantasy-auction-tracker/test/replay.test.js | 105 + .../test/valuations.test.js | 127 + fantasy-auction-tracker/tools/calibrate.js | 137 + fantasy-auction-tracker/tools/replay.js | 131 + fantasy-auction-tracker/tools/simulate.js | 179 + 29 files changed, 12734 insertions(+) create mode 100644 fantasy-auction-tracker/README.md create mode 100644 fantasy-auction-tracker/fixtures/sample-draft.json create mode 100644 fantasy-auction-tracker/fixtures/sample-values.csv create mode 100644 fantasy-auction-tracker/manifest.json create mode 100644 fantasy-auction-tracker/package.json create mode 100644 fantasy-auction-tracker/src/adapters/base.js create mode 100644 fantasy-auction-tracker/src/adapters/generic-dom.js create mode 100644 fantasy-auction-tracker/src/adapters/profiles.js create mode 100644 fantasy-auction-tracker/src/adapters/ws.js create mode 100644 fantasy-auction-tracker/src/background/background.js create mode 100644 fantasy-auction-tracker/src/content/content.js create mode 100644 fantasy-auction-tracker/src/content/inject.js create mode 100644 fantasy-auction-tracker/src/core/analytics.js create mode 100644 fantasy-auction-tracker/src/core/config.js create mode 100644 fantasy-auction-tracker/src/core/events.js create mode 100644 fantasy-auction-tracker/src/core/players.js create mode 100644 fantasy-auction-tracker/src/core/reducer.js create mode 100644 fantasy-auction-tracker/src/core/store.js create mode 100644 fantasy-auction-tracker/src/core/valuations.js create mode 100644 fantasy-auction-tracker/src/sidebar/sidebar.css create mode 100644 fantasy-auction-tracker/src/sidebar/sidebar.html create mode 100644 fantasy-auction-tracker/src/sidebar/sidebar.js create mode 100644 fantasy-auction-tracker/test/analytics.test.js create mode 100644 fantasy-auction-tracker/test/reducer.test.js create mode 100644 fantasy-auction-tracker/test/replay.test.js create mode 100644 fantasy-auction-tracker/test/valuations.test.js create mode 100644 fantasy-auction-tracker/tools/calibrate.js create mode 100644 fantasy-auction-tracker/tools/replay.js create mode 100644 fantasy-auction-tracker/tools/simulate.js diff --git a/fantasy-auction-tracker/README.md b/fantasy-auction-tracker/README.md new file mode 100644 index 0000000..07896fb --- /dev/null +++ b/fantasy-auction-tracker/README.md @@ -0,0 +1,175 @@ +# Auction Draft Tracker + +A Firefox add-on that watches a fantasy football **auction** draft room and +turns it into a live analytical layer: who was taken, for how much, what every +team has left, what they still need, and what the player on the block is +actually worth *in this market* rather than in the one your rankings assumed. + +Read-only by design. It observes and advises; it never bids. + +--- + +## Why it works this way + +**It reads the DOM, not the screen.** A browser extension already lives inside +the page, so pixel-scraping and OCR are a last resort, not the design. Three +detection layers, best first: + +| Layer | How | Confidence | Notes | +|---|---|---|---| +| `ws` | wraps `window.WebSocket` in the page world and mirrors frames | 1.00 | exact player ids, prices, teams; instant | +| `dom` | `MutationObserver` over the draft board | 0.85 | works anywhere, breaks on redesigns | +| `ocr` | canvas fallback (not yet implemented) | 0.55 | only if the room renders to `` | + +Both implemented layers run **at the same time**. The store deduplicates, so +the DOM layer silently covers whatever the WebSocket mapping misses instead of +leaving a hole you notice only after the draft. + +**State is an append-only event log.** Every observation is an event; all +budgets, rosters and analytics are a pure function of that log +(`src/core/reducer.js`). That buys four things that matter on draft day: + +- **Replay** — rehearse against a recorded draft (`npm run replay`) +- **Undo** — a bad parse is retracted or corrected, not surgically unwound +- **Recovery** — refresh the draft room mid-auction and lose nothing +- **Post-mortem** — export the log and study what the league actually paid + +**Low-confidence events are quarantined, not applied.** Anything the adapter +was unsure about lands in a review queue in the sidebar with Confirm/Discard +buttons. Silence never means "fine" — unresolved problems surface as alerts. + +--- + +## The analytics + +The tracking is table stakes. These are the numbers worth having: + +**Inflation** — `remaining money ÷ remaining par value`, after removing the +$1-per-open-slot floor that can never chase value (the "discretionary" form). +Above 1.0 you must overpay or go home; below 1.0 there are bargains ahead and +patience pays. This is the single most valuable live number in an auction and +nobody tracks it by hand. + +**Max bid per team** — `money left − $1 × (other open slots)`. The real ceiling +a rival can reach, which is usually far below their raw remaining budget. The +Teams tab sorts by it, so you can see at a glance who can actually fight you. + +**Positional scarcity** — startable players remaining versus starting slots the +league still has to fill, with replacement level derived from your roster +settings (flex demand split across eligible positions). Below 1.0 means a run +is coming. + +**Tier cliff** — the drop from the best available at a position to the next +one. A big cliff is what justifies paying over par: losing that player costs +you the whole gap, not a dollar. + +**Budget pressure** — which teams are nearly locked into $1 bids. Your cue to +nominate expensive players you *don't* want while the field can still pay. + +**Bid advice** — combines the above into `walk-away` (par × inflation, the +disciplined number) and `ceiling` (walk-away + a configurable slice of the tier +cliff), both hard-capped by your own max bid so it never advises a bid you +cannot legally make. + +--- + +## Install (temporary, for development) + +``` +about:debugging → This Firefox → Load Temporary Add-on → pick manifest.json +``` + +Opens as a **sidebar** (`Ctrl+Shift+Y` or View → Sidebar), which is what you +want next to a draft room — no overlay fighting the page for space. + +Requires Firefox 128+ (MV3 background modules). No build step, no bundler, no +`npm install` — it is plain ES modules that load directly. + +## Set up before draft day + +1. **Sidebar → Setup → League** — teams, budget, min bid, roster slots + (`QB,RB,RB,WR,WR,WR,TE,FLEX,K,DST,BN x6`), and **your team id exactly as the + draft room spells it**. +2. **Setup → Valuations** — import a CSV with player / position / auction value + columns. FantasyPros exports work as-is; headers are alias-matched, and + `RB1`-style positions are split into position + positional rank. Values are + **rescaled to your league's total money**, so a $200/12-team sheet is + corrected automatically for a $300/10-team league. +3. **Calibrate against a mock draft** — see below. Do not skip this. + +## Calibrating for your platform + +The selectors in `src/adapters/profiles.js` are **provisional guesses** written +without access to a live auction room, and every platform reskins between +seasons. Verify them in a mock draft: + +```js +// paste tools/calibrate.js into the draft room console +__auctionCalibrate() // survey candidate selectors +__auctionCalibrate('Bijan Robinson') // locate a known player name +__auctionWatch() // log the most-mutated nodes for 30s +``` + +Better still, get the WebSocket mapping right — it beats every selector: + +1. Run a mock draft with the add-on loaded. +2. **Setup → Export WS capture**. +3. Feed the capture to `guessMapping()` in `src/adapters/ws.js` for candidate + field paths, confirm them by eye, and save the mapping to + `browser.storage.local` under `mapping`. + +Until a mapping exists the WS layer stays in **RECORD** mode (observe and +capture only) and the DOM layer does the work. + +## Rehearsing + +```bash +npm test # 57 tests +npm run replay -- fixtures/sample-draft.json fixtures/sample-values.csv +npm run replay -- fixtures/sample-draft.json fixtures/sample-values.csv --at 300 +``` + +`tools/simulate.js` generates a deterministic, internally-consistent 12-team +$200 auction (192 sales, every team landing on exactly the cap) plus the +matching valuation CSV. The replay tests assert on it end to end: no player +drafted twice, no team overspent, every dollar accounted for, and reducing a +prefix of the log equals replaying to that point — the property page-refresh +recovery depends on. + +## Layout + +``` +src/core/ pure logic, no DOM, no browser APIs — everything tested + events.js event vocabulary + validation + reducer.js log -> state (pure, total, deterministic) + store.js append-only log + dedup + subscribe + analytics.js inflation, scarcity, cliffs, bid advice + valuations.js CSV import + league rescaling + players.js name normalization + fuzzy resolution + config.js roster slots, flex eligibility, replacement level +src/adapters/ platform seam — the only place site knowledge lives +src/content/ content script + page-world WebSocket tap +src/background/ owns the store, persists to storage +src/sidebar/ view layer only; computes nothing +tools/ simulate, replay, calibrate +``` + +`src/core/` has no browser dependency, which is why the whole analytical layer +runs under `node --test` with no DOM shim. + +## Status + +Working and tested: the core, analytics, valuation import, store/persistence, +sidebar, DOM adapter, WS tap and record mode, replay and simulation. + +Provisional: the NFL.com and CBS selector profiles, which need calibration +against a live room, and the WS field mappings, which need a real capture. The +OCR layer is designed for but not implemented — only needed if a platform +renders its draft board to canvas. + +## A note on scope + +This tool observes your own draft and gives you advice. It does not place bids, +automate any interaction, or send anything anywhere — all state stays in +`browser.storage.local`. Keep it that way: auto-bidding would put you crosswise +with every platform's terms of service. diff --git a/fantasy-auction-tracker/fixtures/sample-draft.json b/fantasy-auction-tracker/fixtures/sample-draft.json new file mode 100644 index 0000000..8b456c3 --- /dev/null +++ b/fantasy-auction-tracker/fixtures/sample-draft.json @@ -0,0 +1,8487 @@ +{ + "version": 1, + "config": { + "numTeams": 12, + "budget": 200, + "minBid": 1, + "rosterSlots": [ + "QB", + "RB", + "RB", + "WR", + "WR", + "WR", + "TE", + "FLEX", + "K", + "DST", + "BN", + "BN", + "BN", + "BN", + "BN", + "BN" + ], + "myTeamId": null, + "reviewThreshold": 0.8 + }, + "log": [ + { + "id": "e1", + "type": "LEAGUE_CONFIGURED", + "ts": 1700000001000, + "source": "manual", + "confidence": 1, + "payload": { + "numTeams": 12, + "budget": 200, + "minBid": 1, + "rosterSlots": [ + "QB", + "RB", + "RB", + "WR", + "WR", + "WR", + "TE", + "FLEX", + "K", + "DST", + "BN", + "BN", + "BN", + "BN", + "BN", + "BN" + ], + "myTeamId": null, + "reviewThreshold": 0.8 + } + }, + { + "id": "e2", + "type": "TEAM_REGISTERED", + "ts": 1700000002000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-1", + "teamName": "Team 1" + } + }, + { + "id": "e3", + "type": "TEAM_REGISTERED", + "ts": 1700000003000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-2", + "teamName": "Team 2" + } + }, + { + "id": "e4", + "type": "TEAM_REGISTERED", + "ts": 1700000004000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-3", + "teamName": "Team 3" + } + }, + { + "id": "e5", + "type": "TEAM_REGISTERED", + "ts": 1700000005000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-4", + "teamName": "Team 4" + } + }, + { + "id": "e6", + "type": "TEAM_REGISTERED", + "ts": 1700000006000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-5", + "teamName": "Team 5" + } + }, + { + "id": "e7", + "type": "TEAM_REGISTERED", + "ts": 1700000007000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-6", + "teamName": "Team 6" + } + }, + { + "id": "e8", + "type": "TEAM_REGISTERED", + "ts": 1700000008000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-7", + "teamName": "Team 7" + } + }, + { + "id": "e9", + "type": "TEAM_REGISTERED", + "ts": 1700000009000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-8", + "teamName": "Team 8" + } + }, + { + "id": "e10", + "type": "TEAM_REGISTERED", + "ts": 1700000010000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-9", + "teamName": "Team 9" + } + }, + { + "id": "e11", + "type": "TEAM_REGISTERED", + "ts": 1700000011000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-10", + "teamName": "Team 10" + } + }, + { + "id": "e12", + "type": "TEAM_REGISTERED", + "ts": 1700000012000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-11", + "teamName": "Team 11" + } + }, + { + "id": "e13", + "type": "TEAM_REGISTERED", + "ts": 1700000013000, + "source": "ws", + "confidence": 1, + "payload": { + "teamId": "team-12", + "teamName": "Team 12" + } + }, + { + "id": "e14", + "type": "NOMINATION", + "ts": 1700000014000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico Parsons RB1", + "position": "RB", + "nflTeam": "SF", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e15", + "type": "BID", + "ts": 1700000015000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 23, + "teamId": "team-7" + } + }, + { + "id": "e16", + "type": "BID", + "ts": 1700000016000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 38, + "teamId": "team-12" + } + }, + { + "id": "e17", + "type": "SOLD", + "ts": 1700000017000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico Parsons RB1", + "position": "RB", + "nflTeam": "SF", + "teamId": "team-10", + "teamName": "Team 10", + "price": 46 + } + }, + { + "id": "e18", + "type": "NOMINATION", + "ts": 1700000018000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Wilson WR1", + "position": "WR", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e19", + "type": "BID", + "ts": 1700000019000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 26, + "teamId": "team-2" + } + }, + { + "id": "e20", + "type": "BID", + "ts": 1700000020000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 43, + "teamId": "team-4" + } + }, + { + "id": "e21", + "type": "SOLD", + "ts": 1700000021000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Wilson WR1", + "position": "WR", + "nflTeam": "PHI", + "teamId": "team-6", + "teamName": "Team 6", + "price": 53 + } + }, + { + "id": "e22", + "type": "NOMINATION", + "ts": 1700000022000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Robinson WR2", + "position": "WR", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e23", + "type": "BID", + "ts": 1700000023000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 22, + "teamId": "team-3" + } + }, + { + "id": "e24", + "type": "BID", + "ts": 1700000024000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 37, + "teamId": "team-8" + } + }, + { + "id": "e25", + "type": "SOLD", + "ts": 1700000025000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Robinson WR2", + "position": "WR", + "nflTeam": "PHI", + "teamId": "team-9", + "teamName": "Team 9", + "price": 45 + } + }, + { + "id": "e26", + "type": "NOMINATION", + "ts": 1700000026000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Deebo Pacheco RB2", + "position": "RB", + "nflTeam": "BUF", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e27", + "type": "BID", + "ts": 1700000027000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 19, + "teamId": "team-5" + } + }, + { + "id": "e28", + "type": "BID", + "ts": 1700000028000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 31, + "teamId": "team-10" + } + }, + { + "id": "e29", + "type": "SOLD", + "ts": 1700000029000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Deebo Pacheco RB2", + "position": "RB", + "nflTeam": "BUF", + "teamId": "team-7", + "teamName": "Team 7", + "price": 38 + } + }, + { + "id": "e30", + "type": "NOMINATION", + "ts": 1700000030000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Gibbs WR8", + "position": "WR", + "nflTeam": "BAL", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e31", + "type": "BID", + "ts": 1700000031000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 20, + "teamId": "team-5" + } + }, + { + "id": "e32", + "type": "BID", + "ts": 1700000032000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 33, + "teamId": "team-3" + } + }, + { + "id": "e33", + "type": "SOLD", + "ts": 1700000033000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Gibbs WR8", + "position": "WR", + "nflTeam": "BAL", + "teamId": "team-3", + "teamName": "Team 3", + "price": 41 + } + }, + { + "id": "e34", + "type": "NOMINATION", + "ts": 1700000034000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Breece Odunze RB3", + "position": "RB", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e35", + "type": "BID", + "ts": 1700000035000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 29, + "teamId": "team-3" + } + }, + { + "id": "e36", + "type": "BID", + "ts": 1700000036000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 48, + "teamId": "team-6" + } + }, + { + "id": "e37", + "type": "SOLD", + "ts": 1700000037000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Breece Odunze RB3", + "position": "RB", + "nflTeam": "CIN", + "teamId": "team-3", + "teamName": "Team 3", + "price": 58 + } + }, + { + "id": "e38", + "type": "NOMINATION", + "ts": 1700000038000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Blake Rice WR4", + "position": "WR", + "nflTeam": "TB", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e39", + "type": "BID", + "ts": 1700000039000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 30, + "teamId": "team-4" + } + }, + { + "id": "e40", + "type": "BID", + "ts": 1700000040000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 50, + "teamId": "team-7" + } + }, + { + "id": "e41", + "type": "SOLD", + "ts": 1700000041000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Blake Rice WR4", + "position": "WR", + "nflTeam": "TB", + "teamId": "team-4", + "teamName": "Team 4", + "price": 60 + } + }, + { + "id": "e42", + "type": "NOMINATION", + "ts": 1700000042000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Johnston RB10", + "position": "RB", + "nflTeam": "NYG", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e43", + "type": "BID", + "ts": 1700000043000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-12" + } + }, + { + "id": "e44", + "type": "BID", + "ts": 1700000044000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 30, + "teamId": "team-9" + } + }, + { + "id": "e45", + "type": "SOLD", + "ts": 1700000045000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Johnston RB10", + "position": "RB", + "nflTeam": "NYG", + "teamId": "team-1", + "teamName": "Team 1", + "price": 36 + } + }, + { + "id": "e46", + "type": "NOMINATION", + "ts": 1700000046000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah McBride WR3", + "position": "WR", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e47", + "type": "BID", + "ts": 1700000047000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 17, + "teamId": "team-4" + } + }, + { + "id": "e48", + "type": "BID", + "ts": 1700000048000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 28, + "teamId": "team-12" + } + }, + { + "id": "e49", + "type": "SOLD", + "ts": 1700000049000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah McBride WR3", + "position": "WR", + "nflTeam": "NO", + "teamId": "team-7", + "teamName": "Team 7", + "price": 35 + } + }, + { + "id": "e50", + "type": "NOMINATION", + "ts": 1700000050000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Hall RB4", + "position": "RB", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-5" + } + }, + { + "id": "e51", + "type": "BID", + "ts": 1700000051000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 27, + "teamId": "team-10" + } + }, + { + "id": "e52", + "type": "BID", + "ts": 1700000052000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 45, + "teamId": "team-3" + } + }, + { + "id": "e53", + "type": "SOLD", + "ts": 1700000053000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Hall RB4", + "position": "RB", + "nflTeam": "NO", + "teamId": "team-2", + "teamName": "Team 2", + "price": 55 + } + }, + { + "id": "e54", + "type": "NOMINATION", + "ts": 1700000054000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Gibbs WR5", + "position": "WR", + "nflTeam": "SF", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e55", + "type": "BID", + "ts": 1700000055000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 21, + "teamId": "team-9" + } + }, + { + "id": "e56", + "type": "BID", + "ts": 1700000056000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 35, + "teamId": "team-11" + } + }, + { + "id": "e57", + "type": "SOLD", + "ts": 1700000057000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Gibbs WR5", + "position": "WR", + "nflTeam": "SF", + "teamId": "team-11", + "teamName": "Team 11", + "price": 42 + } + }, + { + "id": "e58", + "type": "NOMINATION", + "ts": 1700000058000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Shakir RB6", + "position": "RB", + "nflTeam": "DAL", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e59", + "type": "BID", + "ts": 1700000059000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 25, + "teamId": "team-10" + } + }, + { + "id": "e60", + "type": "BID", + "ts": 1700000060000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 42, + "teamId": "team-9" + } + }, + { + "id": "e61", + "type": "SOLD", + "ts": 1700000061000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Shakir RB6", + "position": "RB", + "nflTeam": "DAL", + "teamId": "team-2", + "teamName": "Team 2", + "price": 51 + } + }, + { + "id": "e62", + "type": "NOMINATION", + "ts": 1700000062000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Parsons WR6", + "position": "WR", + "nflTeam": "LV", + "openingBid": 1, + "nominatingTeamId": "team-5" + } + }, + { + "id": "e63", + "type": "BID", + "ts": 1700000063000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 26, + "teamId": "team-6" + } + }, + { + "id": "e64", + "type": "BID", + "ts": 1700000064000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 43, + "teamId": "team-3" + } + }, + { + "id": "e65", + "type": "SOLD", + "ts": 1700000065000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Parsons WR6", + "position": "WR", + "nflTeam": "LV", + "teamId": "team-8", + "teamName": "Team 8", + "price": 52 + } + }, + { + "id": "e66", + "type": "NOMINATION", + "ts": 1700000066000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Hall WR10", + "position": "WR", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e67", + "type": "BID", + "ts": 1700000067000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 17, + "teamId": "team-1" + } + }, + { + "id": "e68", + "type": "BID", + "ts": 1700000068000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 28, + "teamId": "team-7" + } + }, + { + "id": "e69", + "type": "SOLD", + "ts": 1700000069000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Hall WR10", + "position": "WR", + "nflTeam": "ARI", + "teamId": "team-1", + "teamName": "Team 1", + "price": 34 + } + }, + { + "id": "e70", + "type": "NOMINATION", + "ts": 1700000070000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Hall WR7", + "position": "WR", + "nflTeam": "TB", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e71", + "type": "BID", + "ts": 1700000071000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 27, + "teamId": "team-10" + } + }, + { + "id": "e72", + "type": "BID", + "ts": 1700000072000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 45, + "teamId": "team-9" + } + }, + { + "id": "e73", + "type": "SOLD", + "ts": 1700000073000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Hall WR7", + "position": "WR", + "nflTeam": "TB", + "teamId": "team-3", + "teamName": "Team 3", + "price": 55 + } + }, + { + "id": "e74", + "type": "NOMINATION", + "ts": 1700000074000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Quentin Rice RB5", + "position": "RB", + "nflTeam": "DAL", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e75", + "type": "BID", + "ts": 1700000075000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-1" + } + }, + { + "id": "e76", + "type": "BID", + "ts": 1700000076000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 30, + "teamId": "team-1" + } + }, + { + "id": "e77", + "type": "SOLD", + "ts": 1700000077000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Quentin Rice RB5", + "position": "RB", + "nflTeam": "DAL", + "teamId": "team-12", + "teamName": "Team 12", + "price": 36 + } + }, + { + "id": "e78", + "type": "NOMINATION", + "ts": 1700000078000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Parsons RB7", + "position": "RB", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e79", + "type": "BID", + "ts": 1700000079000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 25, + "teamId": "team-11" + } + }, + { + "id": "e80", + "type": "BID", + "ts": 1700000080000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 41, + "teamId": "team-12" + } + }, + { + "id": "e81", + "type": "SOLD", + "ts": 1700000081000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Parsons RB7", + "position": "RB", + "nflTeam": "CIN", + "teamId": "team-1", + "teamName": "Team 1", + "price": 50 + } + }, + { + "id": "e82", + "type": "NOMINATION", + "ts": 1700000082000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Collins RB8", + "position": "RB", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e83", + "type": "BID", + "ts": 1700000083000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 14, + "teamId": "team-9" + } + }, + { + "id": "e84", + "type": "BID", + "ts": 1700000084000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 23, + "teamId": "team-10" + } + }, + { + "id": "e85", + "type": "SOLD", + "ts": 1700000085000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Collins RB8", + "position": "RB", + "nflTeam": "NO", + "teamId": "team-1", + "teamName": "Team 1", + "price": 28 + } + }, + { + "id": "e86", + "type": "NOMINATION", + "ts": 1700000086000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Corum WR9", + "position": "WR", + "nflTeam": "TB", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e87", + "type": "BID", + "ts": 1700000087000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 16, + "teamId": "team-10" + } + }, + { + "id": "e88", + "type": "BID", + "ts": 1700000088000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 27, + "teamId": "team-1" + } + }, + { + "id": "e89", + "type": "SOLD", + "ts": 1700000089000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Corum WR9", + "position": "WR", + "nflTeam": "TB", + "teamId": "team-9", + "teamName": "Team 9", + "price": 33 + } + }, + { + "id": "e90", + "type": "NOMINATION", + "ts": 1700000090000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey LaPorta RB9", + "position": "RB", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e91", + "type": "BID", + "ts": 1700000091000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 19, + "teamId": "team-2" + } + }, + { + "id": "e92", + "type": "BID", + "ts": 1700000092000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 32, + "teamId": "team-8" + } + }, + { + "id": "e93", + "type": "SOLD", + "ts": 1700000093000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey LaPorta RB9", + "position": "RB", + "nflTeam": "PHI", + "teamId": "team-12", + "teamName": "Team 12", + "price": 39 + } + }, + { + "id": "e94", + "type": "NOMINATION", + "ts": 1700000094000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Dell RB11", + "position": "RB", + "nflTeam": "SEA", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e95", + "type": "BID", + "ts": 1700000095000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-5" + } + }, + { + "id": "e96", + "type": "BID", + "ts": 1700000096000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 22, + "teamId": "team-10" + } + }, + { + "id": "e97", + "type": "SOLD", + "ts": 1700000097000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Dell RB11", + "position": "RB", + "nflTeam": "SEA", + "teamId": "team-8", + "teamName": "Team 8", + "price": 27 + } + }, + { + "id": "e98", + "type": "NOMINATION", + "ts": 1700000098000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Blake Shakir WR12", + "position": "WR", + "nflTeam": "TB", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e99", + "type": "BID", + "ts": 1700000099000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 14, + "teamId": "team-8" + } + }, + { + "id": "e100", + "type": "BID", + "ts": 1700000100000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 23, + "teamId": "team-2" + } + }, + { + "id": "e101", + "type": "SOLD", + "ts": 1700000101000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Blake Shakir WR12", + "position": "WR", + "nflTeam": "TB", + "teamId": "team-11", + "teamName": "Team 11", + "price": 29 + } + }, + { + "id": "e102", + "type": "NOMINATION", + "ts": 1700000102000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Deebo Tracy WR13", + "position": "WR", + "nflTeam": "LAR", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e103", + "type": "BID", + "ts": 1700000103000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 16, + "teamId": "team-5" + } + }, + { + "id": "e104", + "type": "BID", + "ts": 1700000104000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 27, + "teamId": "team-5" + } + }, + { + "id": "e105", + "type": "SOLD", + "ts": 1700000105000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Deebo Tracy WR13", + "position": "WR", + "nflTeam": "LAR", + "teamId": "team-8", + "teamName": "Team 8", + "price": 33 + } + }, + { + "id": "e106", + "type": "NOMINATION", + "ts": 1700000106000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Kincaid WR11", + "position": "WR", + "nflTeam": "BAL", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e107", + "type": "BID", + "ts": 1700000107000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 15, + "teamId": "team-12" + } + }, + { + "id": "e108", + "type": "BID", + "ts": 1700000108000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 25, + "teamId": "team-8" + } + }, + { + "id": "e109", + "type": "SOLD", + "ts": 1700000109000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Kincaid WR11", + "position": "WR", + "nflTeam": "BAL", + "teamId": "team-8", + "teamName": "Team 8", + "price": 31 + } + }, + { + "id": "e110", + "type": "NOMINATION", + "ts": 1700000110000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Corum TE1", + "position": "TE", + "nflTeam": "BUF", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e111", + "type": "BID", + "ts": 1700000111000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-12" + } + }, + { + "id": "e112", + "type": "BID", + "ts": 1700000112000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 22, + "teamId": "team-5" + } + }, + { + "id": "e113", + "type": "SOLD", + "ts": 1700000113000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Corum TE1", + "position": "TE", + "nflTeam": "BUF", + "teamId": "team-12", + "teamName": "Team 12", + "price": 27 + } + }, + { + "id": "e114", + "type": "NOMINATION", + "ts": 1700000114000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Sam Kupp QB1", + "position": "QB", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e115", + "type": "BID", + "ts": 1700000115000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 17, + "teamId": "team-7" + } + }, + { + "id": "e116", + "type": "BID", + "ts": 1700000116000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 28, + "teamId": "team-4" + } + }, + { + "id": "e117", + "type": "SOLD", + "ts": 1700000117000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Sam Kupp QB1", + "position": "QB", + "nflTeam": "ARI", + "teamId": "team-12", + "teamName": "Team 12", + "price": 35 + } + }, + { + "id": "e118", + "type": "NOMINATION", + "ts": 1700000118000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jaxon Parsons RB12", + "position": "RB", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e119", + "type": "BID", + "ts": 1700000119000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 15, + "teamId": "team-4" + } + }, + { + "id": "e120", + "type": "BID", + "ts": 1700000120000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 25, + "teamId": "team-5" + } + }, + { + "id": "e121", + "type": "SOLD", + "ts": 1700000121000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jaxon Parsons RB12", + "position": "RB", + "nflTeam": "PHI", + "teamId": "team-7", + "teamName": "Team 7", + "price": 31 + } + }, + { + "id": "e122", + "type": "NOMINATION", + "ts": 1700000122000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Blake Nacua QB2", + "position": "QB", + "nflTeam": "LAR", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e123", + "type": "BID", + "ts": 1700000123000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 9, + "teamId": "team-7" + } + }, + { + "id": "e124", + "type": "BID", + "ts": 1700000124000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 15, + "teamId": "team-4" + } + }, + { + "id": "e125", + "type": "SOLD", + "ts": 1700000125000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Blake Nacua QB2", + "position": "QB", + "nflTeam": "LAR", + "teamId": "team-4", + "teamName": "Team 4", + "price": 18 + } + }, + { + "id": "e126", + "type": "NOMINATION", + "ts": 1700000126000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Odunze WR14", + "position": "WR", + "nflTeam": "NYG", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e127", + "type": "BID", + "ts": 1700000127000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-12" + } + }, + { + "id": "e128", + "type": "BID", + "ts": 1700000128000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 22, + "teamId": "team-5" + } + }, + { + "id": "e129", + "type": "SOLD", + "ts": 1700000129000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Odunze WR14", + "position": "WR", + "nflTeam": "NYG", + "teamId": "team-12", + "teamName": "Team 12", + "price": 27 + } + }, + { + "id": "e130", + "type": "NOMINATION", + "ts": 1700000130000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Justin Nacua WR16", + "position": "WR", + "nflTeam": "SEA", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e131", + "type": "BID", + "ts": 1700000131000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 11, + "teamId": "team-4" + } + }, + { + "id": "e132", + "type": "BID", + "ts": 1700000132000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-2" + } + }, + { + "id": "e133", + "type": "SOLD", + "ts": 1700000133000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Justin Nacua WR16", + "position": "WR", + "nflTeam": "SEA", + "teamId": "team-8", + "teamName": "Team 8", + "price": 23 + } + }, + { + "id": "e134", + "type": "NOMINATION", + "ts": 1700000134000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "CeeDee Brown RB13", + "position": "RB", + "nflTeam": "LAR", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e135", + "type": "BID", + "ts": 1700000135000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 11, + "teamId": "team-5" + } + }, + { + "id": "e136", + "type": "BID", + "ts": 1700000136000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-10" + } + }, + { + "id": "e137", + "type": "SOLD", + "ts": 1700000137000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "CeeDee Brown RB13", + "position": "RB", + "nflTeam": "LAR", + "teamId": "team-5", + "teamName": "Team 5", + "price": 22 + } + }, + { + "id": "e138", + "type": "NOMINATION", + "ts": 1700000138000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome London RB15", + "position": "RB", + "nflTeam": "LV", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e139", + "type": "BID", + "ts": 1700000139000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 11, + "teamId": "team-12" + } + }, + { + "id": "e140", + "type": "BID", + "ts": 1700000140000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-6" + } + }, + { + "id": "e141", + "type": "SOLD", + "ts": 1700000141000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome London RB15", + "position": "RB", + "nflTeam": "LV", + "teamId": "team-4", + "teamName": "Team 4", + "price": 23 + } + }, + { + "id": "e142", + "type": "NOMINATION", + "ts": 1700000142000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Nacua WR24", + "position": "WR", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e143", + "type": "BID", + "ts": 1700000143000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 9, + "teamId": "team-3" + } + }, + { + "id": "e144", + "type": "BID", + "ts": 1700000144000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 15, + "teamId": "team-2" + } + }, + { + "id": "e145", + "type": "SOLD", + "ts": 1700000145000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Nacua WR24", + "position": "WR", + "nflTeam": "PHI", + "teamId": "team-1", + "teamName": "Team 1", + "price": 19 + } + }, + { + "id": "e146", + "type": "NOMINATION", + "ts": 1700000146000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Deebo Bowers RB16", + "position": "RB", + "nflTeam": "TB", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e147", + "type": "BID", + "ts": 1700000147000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 11, + "teamId": "team-8" + } + }, + { + "id": "e148", + "type": "BID", + "ts": 1700000148000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-6" + } + }, + { + "id": "e149", + "type": "SOLD", + "ts": 1700000149000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Deebo Bowers RB16", + "position": "RB", + "nflTeam": "TB", + "teamId": "team-9", + "teamName": "Team 9", + "price": 23 + } + }, + { + "id": "e150", + "type": "NOMINATION", + "ts": 1700000150000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Justin Samuel WR17", + "position": "WR", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e151", + "type": "BID", + "ts": 1700000151000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 11, + "teamId": "team-2" + } + }, + { + "id": "e152", + "type": "BID", + "ts": 1700000152000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-9" + } + }, + { + "id": "e153", + "type": "SOLD", + "ts": 1700000153000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Justin Samuel WR17", + "position": "WR", + "nflTeam": "NO", + "teamId": "team-8", + "teamName": "Team 8", + "price": 23 + } + }, + { + "id": "e154", + "type": "NOMINATION", + "ts": 1700000154000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Rice TE2", + "position": "TE", + "nflTeam": "BUF", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e155", + "type": "BID", + "ts": 1700000155000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 11, + "teamId": "team-1" + } + }, + { + "id": "e156", + "type": "BID", + "ts": 1700000156000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-10" + } + }, + { + "id": "e157", + "type": "SOLD", + "ts": 1700000157000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Rice TE2", + "position": "TE", + "nflTeam": "BUF", + "teamId": "team-12", + "teamName": "Team 12", + "price": 23 + } + }, + { + "id": "e158", + "type": "NOMINATION", + "ts": 1700000158000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Breece Nabers RB14", + "position": "RB", + "nflTeam": "KC", + "openingBid": 1, + "nominatingTeamId": "team-5" + } + }, + { + "id": "e159", + "type": "BID", + "ts": 1700000159000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-12" + } + }, + { + "id": "e160", + "type": "BID", + "ts": 1700000160000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 17, + "teamId": "team-2" + } + }, + { + "id": "e161", + "type": "SOLD", + "ts": 1700000161000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Breece Nabers RB14", + "position": "RB", + "nflTeam": "KC", + "teamId": "team-6", + "teamName": "Team 6", + "price": 21 + } + }, + { + "id": "e162", + "type": "NOMINATION", + "ts": 1700000162000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Bowers WR15", + "position": "WR", + "nflTeam": "ATL", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e163", + "type": "BID", + "ts": 1700000163000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-6" + } + }, + { + "id": "e164", + "type": "BID", + "ts": 1700000164000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 16, + "teamId": "team-6" + } + }, + { + "id": "e165", + "type": "SOLD", + "ts": 1700000165000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Bowers WR15", + "position": "WR", + "nflTeam": "ATL", + "teamId": "team-9", + "teamName": "Team 9", + "price": 20 + } + }, + { + "id": "e166", + "type": "NOMINATION", + "ts": 1700000166000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Lamb WR18", + "position": "WR", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e167", + "type": "BID", + "ts": 1700000167000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 11, + "teamId": "team-8" + } + }, + { + "id": "e168", + "type": "BID", + "ts": 1700000168000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-10" + } + }, + { + "id": "e169", + "type": "SOLD", + "ts": 1700000169000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Lamb WR18", + "position": "WR", + "nflTeam": "PHI", + "teamId": "team-7", + "teamName": "Team 7", + "price": 23 + } + }, + { + "id": "e170", + "type": "NOMINATION", + "ts": 1700000170000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Johnston WR26", + "position": "WR", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e171", + "type": "BID", + "ts": 1700000171000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-1" + } + }, + { + "id": "e172", + "type": "BID", + "ts": 1700000172000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 16, + "teamId": "team-8" + } + }, + { + "id": "e173", + "type": "SOLD", + "ts": 1700000173000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Johnston WR26", + "position": "WR", + "nflTeam": "CIN", + "teamId": "team-10", + "teamName": "Team 10", + "price": 20 + } + }, + { + "id": "e174", + "type": "NOMINATION", + "ts": 1700000174000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Nacua TE3", + "position": "TE", + "nflTeam": "HOU", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e175", + "type": "BID", + "ts": 1700000175000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 9, + "teamId": "team-4" + } + }, + { + "id": "e176", + "type": "BID", + "ts": 1700000176000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 15, + "teamId": "team-12" + } + }, + { + "id": "e177", + "type": "SOLD", + "ts": 1700000177000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Nacua TE3", + "position": "TE", + "nflTeam": "HOU", + "teamId": "team-7", + "teamName": "Team 7", + "price": 19 + } + }, + { + "id": "e178", + "type": "NOMINATION", + "ts": 1700000178000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil Harrison WR19", + "position": "WR", + "nflTeam": "DAL", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e179", + "type": "BID", + "ts": 1700000179000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-6" + } + }, + { + "id": "e180", + "type": "BID", + "ts": 1700000180000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 21, + "teamId": "team-9" + } + }, + { + "id": "e181", + "type": "SOLD", + "ts": 1700000181000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil Harrison WR19", + "position": "WR", + "nflTeam": "DAL", + "teamId": "team-6", + "teamName": "Team 6", + "price": 26 + } + }, + { + "id": "e182", + "type": "NOMINATION", + "ts": 1700000182000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Drake Kincaid RB17", + "position": "RB", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e183", + "type": "BID", + "ts": 1700000183000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 11, + "teamId": "team-9" + } + }, + { + "id": "e184", + "type": "BID", + "ts": 1700000184000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-3" + } + }, + { + "id": "e185", + "type": "SOLD", + "ts": 1700000185000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Drake Kincaid RB17", + "position": "RB", + "nflTeam": "CHI", + "teamId": "team-6", + "teamName": "Team 6", + "price": 22 + } + }, + { + "id": "e186", + "type": "NOMINATION", + "ts": 1700000186000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Wilson RB18", + "position": "RB", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e187", + "type": "BID", + "ts": 1700000187000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 9, + "teamId": "team-1" + } + }, + { + "id": "e188", + "type": "BID", + "ts": 1700000188000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 15, + "teamId": "team-6" + } + }, + { + "id": "e189", + "type": "SOLD", + "ts": 1700000189000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Wilson RB18", + "position": "RB", + "nflTeam": "CHI", + "teamId": "team-4", + "teamName": "Team 4", + "price": 18 + } + }, + { + "id": "e190", + "type": "NOMINATION", + "ts": 1700000190000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Blake Samuel WR21", + "position": "WR", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-5" + } + }, + { + "id": "e191", + "type": "BID", + "ts": 1700000191000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-7" + } + }, + { + "id": "e192", + "type": "BID", + "ts": 1700000192000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 17, + "teamId": "team-7" + } + }, + { + "id": "e193", + "type": "SOLD", + "ts": 1700000193000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Blake Samuel WR21", + "position": "WR", + "nflTeam": "DET", + "teamId": "team-5", + "teamName": "Team 5", + "price": 21 + } + }, + { + "id": "e194", + "type": "NOMINATION", + "ts": 1700000194000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Rice QB3", + "position": "QB", + "nflTeam": "NYG", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e195", + "type": "BID", + "ts": 1700000195000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-9" + } + }, + { + "id": "e196", + "type": "BID", + "ts": 1700000196000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-2" + } + }, + { + "id": "e197", + "type": "SOLD", + "ts": 1700000197000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Rice QB3", + "position": "QB", + "nflTeam": "NYG", + "teamId": "team-10", + "teamName": "Team 10", + "price": 17 + } + }, + { + "id": "e198", + "type": "NOMINATION", + "ts": 1700000198000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan McBride WR22", + "position": "WR", + "nflTeam": "HOU", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e199", + "type": "BID", + "ts": 1700000199000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-9" + } + }, + { + "id": "e200", + "type": "BID", + "ts": 1700000200000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-10" + } + }, + { + "id": "e201", + "type": "SOLD", + "ts": 1700000201000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan McBride WR22", + "position": "WR", + "nflTeam": "HOU", + "teamId": "team-7", + "teamName": "Team 7", + "price": 17 + } + }, + { + "id": "e202", + "type": "NOMINATION", + "ts": 1700000202000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Corum WR23", + "position": "WR", + "nflTeam": "ATL", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e203", + "type": "BID", + "ts": 1700000203000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 9, + "teamId": "team-3" + } + }, + { + "id": "e204", + "type": "BID", + "ts": 1700000204000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 15, + "teamId": "team-6" + } + }, + { + "id": "e205", + "type": "SOLD", + "ts": 1700000205000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Corum WR23", + "position": "WR", + "nflTeam": "ATL", + "teamId": "team-6", + "teamName": "Team 6", + "price": 19 + } + }, + { + "id": "e206", + "type": "NOMINATION", + "ts": 1700000206000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Quentin Corum TE4", + "position": "TE", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-5" + } + }, + { + "id": "e207", + "type": "BID", + "ts": 1700000207000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 11, + "teamId": "team-9" + } + }, + { + "id": "e208", + "type": "BID", + "ts": 1700000208000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 18, + "teamId": "team-8" + } + }, + { + "id": "e209", + "type": "SOLD", + "ts": 1700000209000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Quentin Corum TE4", + "position": "TE", + "nflTeam": "CHI", + "teamId": "team-7", + "teamName": "Team 7", + "price": 23 + } + }, + { + "id": "e210", + "type": "NOMINATION", + "ts": 1700000210000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Tracy WR20", + "position": "WR", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e211", + "type": "BID", + "ts": 1700000211000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-5" + } + }, + { + "id": "e212", + "type": "BID", + "ts": 1700000212000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-1" + } + }, + { + "id": "e213", + "type": "SOLD", + "ts": 1700000213000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Tracy WR20", + "position": "WR", + "nflTeam": "CHI", + "teamId": "team-3", + "teamName": "Team 3", + "price": 17 + } + }, + { + "id": "e214", + "type": "NOMINATION", + "ts": 1700000214000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Jefferson QB4", + "position": "QB", + "nflTeam": "HOU", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e215", + "type": "BID", + "ts": 1700000215000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 9, + "teamId": "team-1" + } + }, + { + "id": "e216", + "type": "BID", + "ts": 1700000216000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 15, + "teamId": "team-2" + } + }, + { + "id": "e217", + "type": "SOLD", + "ts": 1700000217000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Jefferson QB4", + "position": "QB", + "nflTeam": "HOU", + "teamId": "team-6", + "teamName": "Team 6", + "price": 18 + } + }, + { + "id": "e218", + "type": "NOMINATION", + "ts": 1700000218000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Kupp RB19", + "position": "RB", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e219", + "type": "BID", + "ts": 1700000219000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-10" + } + }, + { + "id": "e220", + "type": "BID", + "ts": 1700000220000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 16, + "teamId": "team-3" + } + }, + { + "id": "e221", + "type": "SOLD", + "ts": 1700000221000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Kupp RB19", + "position": "RB", + "nflTeam": "DET", + "teamId": "team-4", + "teamName": "Team 4", + "price": 20 + } + }, + { + "id": "e222", + "type": "NOMINATION", + "ts": 1700000222000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Nacua RB24", + "position": "RB", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e223", + "type": "BID", + "ts": 1700000223000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 7, + "teamId": "team-6" + } + }, + { + "id": "e224", + "type": "BID", + "ts": 1700000224000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 11, + "teamId": "team-4" + } + }, + { + "id": "e225", + "type": "SOLD", + "ts": 1700000225000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Nacua RB24", + "position": "RB", + "nflTeam": "ARI", + "teamId": "team-4", + "teamName": "Team 4", + "price": 14 + } + }, + { + "id": "e226", + "type": "NOMINATION", + "ts": 1700000226000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil McBride WR25", + "position": "WR", + "nflTeam": "ATL", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e227", + "type": "BID", + "ts": 1700000227000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-4" + } + }, + { + "id": "e228", + "type": "BID", + "ts": 1700000228000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-12" + } + }, + { + "id": "e229", + "type": "SOLD", + "ts": 1700000229000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil McBride WR25", + "position": "WR", + "nflTeam": "ATL", + "teamId": "team-5", + "teamName": "Team 5", + "price": 16 + } + }, + { + "id": "e230", + "type": "NOMINATION", + "ts": 1700000230000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Collins QB5", + "position": "QB", + "nflTeam": "DAL", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e231", + "type": "BID", + "ts": 1700000231000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-9" + } + }, + { + "id": "e232", + "type": "BID", + "ts": 1700000232000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-7" + } + }, + { + "id": "e233", + "type": "SOLD", + "ts": 1700000233000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Collins QB5", + "position": "QB", + "nflTeam": "DAL", + "teamId": "team-5", + "teamName": "Team 5", + "price": 16 + } + }, + { + "id": "e234", + "type": "NOMINATION", + "ts": 1700000234000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jaxon Samuel WR27", + "position": "WR", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e235", + "type": "BID", + "ts": 1700000235000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-6" + } + }, + { + "id": "e236", + "type": "BID", + "ts": 1700000236000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-8" + } + }, + { + "id": "e237", + "type": "SOLD", + "ts": 1700000237000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jaxon Samuel WR27", + "position": "WR", + "nflTeam": "NO", + "teamId": "team-5", + "teamName": "Team 5", + "price": 17 + } + }, + { + "id": "e238", + "type": "NOMINATION", + "ts": 1700000238000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Deebo McBride RB20", + "position": "RB", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e239", + "type": "BID", + "ts": 1700000239000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-5" + } + }, + { + "id": "e240", + "type": "BID", + "ts": 1700000240000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-9" + } + }, + { + "id": "e241", + "type": "SOLD", + "ts": 1700000241000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Deebo McBride RB20", + "position": "RB", + "nflTeam": "ARI", + "teamId": "team-2", + "teamName": "Team 2", + "price": 17 + } + }, + { + "id": "e242", + "type": "NOMINATION", + "ts": 1700000242000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Puka Odunze QB6", + "position": "QB", + "nflTeam": "BAL", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e243", + "type": "BID", + "ts": 1700000243000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-7" + } + }, + { + "id": "e244", + "type": "BID", + "ts": 1700000244000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-4" + } + }, + { + "id": "e245", + "type": "SOLD", + "ts": 1700000245000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Puka Odunze QB6", + "position": "QB", + "nflTeam": "BAL", + "teamId": "team-10", + "teamName": "Team 10", + "price": 10 + } + }, + { + "id": "e246", + "type": "NOMINATION", + "ts": 1700000246000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Pacheco TE5", + "position": "TE", + "nflTeam": "SF", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e247", + "type": "BID", + "ts": 1700000247000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-7" + } + }, + { + "id": "e248", + "type": "BID", + "ts": 1700000248000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 16, + "teamId": "team-9" + } + }, + { + "id": "e249", + "type": "SOLD", + "ts": 1700000249000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Pacheco TE5", + "position": "TE", + "nflTeam": "SF", + "teamId": "team-9", + "teamName": "Team 9", + "price": 20 + } + }, + { + "id": "e250", + "type": "NOMINATION", + "ts": 1700000250000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay London TE7", + "position": "TE", + "nflTeam": "ATL", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e251", + "type": "BID", + "ts": 1700000251000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-10" + } + }, + { + "id": "e252", + "type": "BID", + "ts": 1700000252000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-9" + } + }, + { + "id": "e253", + "type": "SOLD", + "ts": 1700000253000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay London TE7", + "position": "TE", + "nflTeam": "ATL", + "teamId": "team-10", + "teamName": "Team 10", + "price": 10 + } + }, + { + "id": "e254", + "type": "NOMINATION", + "ts": 1700000254000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Kupp RB21", + "position": "RB", + "nflTeam": "LAR", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e255", + "type": "BID", + "ts": 1700000255000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-9" + } + }, + { + "id": "e256", + "type": "BID", + "ts": 1700000256000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-8" + } + }, + { + "id": "e257", + "type": "SOLD", + "ts": 1700000257000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Kupp RB21", + "position": "RB", + "nflTeam": "LAR", + "teamId": "team-6", + "teamName": "Team 6", + "price": 16 + } + }, + { + "id": "e258", + "type": "NOMINATION", + "ts": 1700000258000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Justin Nacua QB7", + "position": "QB", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e259", + "type": "BID", + "ts": 1700000259000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-3" + } + }, + { + "id": "e260", + "type": "BID", + "ts": 1700000260000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-10" + } + }, + { + "id": "e261", + "type": "SOLD", + "ts": 1700000261000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Justin Nacua QB7", + "position": "QB", + "nflTeam": "PHI", + "teamId": "team-3", + "teamName": "Team 3", + "price": 13 + } + }, + { + "id": "e262", + "type": "NOMINATION", + "ts": 1700000262000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Shakir RB22", + "position": "RB", + "nflTeam": "SF", + "openingBid": 1, + "nominatingTeamId": "team-5" + } + }, + { + "id": "e263", + "type": "BID", + "ts": 1700000263000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-7" + } + }, + { + "id": "e264", + "type": "BID", + "ts": 1700000264000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 13, + "teamId": "team-10" + } + }, + { + "id": "e265", + "type": "SOLD", + "ts": 1700000265000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Shakir RB22", + "position": "RB", + "nflTeam": "SF", + "teamId": "team-5", + "teamName": "Team 5", + "price": 16 + } + }, + { + "id": "e266", + "type": "NOMINATION", + "ts": 1700000266000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Brown WR28", + "position": "WR", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e267", + "type": "BID", + "ts": 1700000267000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-10" + } + }, + { + "id": "e268", + "type": "BID", + "ts": 1700000268000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-5" + } + }, + { + "id": "e269", + "type": "SOLD", + "ts": 1700000269000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Brown WR28", + "position": "WR", + "nflTeam": "CHI", + "teamId": "team-1", + "teamName": "Team 1", + "price": 12 + } + }, + { + "id": "e270", + "type": "NOMINATION", + "ts": 1700000270000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Dell RB23", + "position": "RB", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-5" + } + }, + { + "id": "e271", + "type": "BID", + "ts": 1700000271000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 9, + "teamId": "team-7" + } + }, + { + "id": "e272", + "type": "BID", + "ts": 1700000272000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 15, + "teamId": "team-11" + } + }, + { + "id": "e273", + "type": "SOLD", + "ts": 1700000273000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Dell RB23", + "position": "RB", + "nflTeam": "ARI", + "teamId": "team-10", + "teamName": "Team 10", + "price": 18 + } + }, + { + "id": "e274", + "type": "NOMINATION", + "ts": 1700000274000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra London WR35", + "position": "WR", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-5" + } + }, + { + "id": "e275", + "type": "BID", + "ts": 1700000275000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-2" + } + }, + { + "id": "e276", + "type": "BID", + "ts": 1700000276000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-11" + } + }, + { + "id": "e277", + "type": "SOLD", + "ts": 1700000277000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra London WR35", + "position": "WR", + "nflTeam": "CIN", + "teamId": "team-4", + "teamName": "Team 4", + "price": 13 + } + }, + { + "id": "e278", + "type": "NOMINATION", + "ts": 1700000278000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Tracy RB26", + "position": "RB", + "nflTeam": "BAL", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e279", + "type": "BID", + "ts": 1700000279000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-10" + } + }, + { + "id": "e280", + "type": "BID", + "ts": 1700000280000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-7" + } + }, + { + "id": "e281", + "type": "SOLD", + "ts": 1700000281000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Tracy RB26", + "position": "RB", + "nflTeam": "BAL", + "teamId": "team-4", + "teamName": "Team 4", + "price": 13 + } + }, + { + "id": "e282", + "type": "NOMINATION", + "ts": 1700000282000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Parsons WR37", + "position": "WR", + "nflTeam": "LAR", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e283", + "type": "BID", + "ts": 1700000283000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-3" + } + }, + { + "id": "e284", + "type": "BID", + "ts": 1700000284000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-8" + } + }, + { + "id": "e285", + "type": "SOLD", + "ts": 1700000285000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Parsons WR37", + "position": "WR", + "nflTeam": "LAR", + "teamId": "team-5", + "teamName": "Team 5", + "price": 11 + } + }, + { + "id": "e286", + "type": "NOMINATION", + "ts": 1700000286000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Bowers TE6", + "position": "TE", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e287", + "type": "BID", + "ts": 1700000287000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-2" + } + }, + { + "id": "e288", + "type": "BID", + "ts": 1700000288000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-10" + } + }, + { + "id": "e289", + "type": "SOLD", + "ts": 1700000289000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Bowers TE6", + "position": "TE", + "nflTeam": "DET", + "teamId": "team-4", + "teamName": "Team 4", + "price": 13 + } + }, + { + "id": "e290", + "type": "NOMINATION", + "ts": 1700000290000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone McBride RB31", + "position": "RB", + "nflTeam": "BAL", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e291", + "type": "BID", + "ts": 1700000291000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-3" + } + }, + { + "id": "e292", + "type": "BID", + "ts": 1700000292000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-5" + } + }, + { + "id": "e293", + "type": "SOLD", + "ts": 1700000293000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone McBride RB31", + "position": "RB", + "nflTeam": "BAL", + "teamId": "team-2", + "teamName": "Team 2", + "price": 11 + } + }, + { + "id": "e294", + "type": "NOMINATION", + "ts": 1700000294000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Hall RB25", + "position": "RB", + "nflTeam": "NYG", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e295", + "type": "BID", + "ts": 1700000295000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 7, + "teamId": "team-3" + } + }, + { + "id": "e296", + "type": "BID", + "ts": 1700000296000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 12, + "teamId": "team-7" + } + }, + { + "id": "e297", + "type": "SOLD", + "ts": 1700000297000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Hall RB25", + "position": "RB", + "nflTeam": "NYG", + "teamId": "team-10", + "teamName": "Team 10", + "price": 15 + } + }, + { + "id": "e298", + "type": "NOMINATION", + "ts": 1700000298000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Odunze WR30", + "position": "WR", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e299", + "type": "BID", + "ts": 1700000299000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-7" + } + }, + { + "id": "e300", + "type": "BID", + "ts": 1700000300000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-7" + } + }, + { + "id": "e301", + "type": "SOLD", + "ts": 1700000301000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Odunze WR30", + "position": "WR", + "nflTeam": "CHI", + "teamId": "team-5", + "teamName": "Team 5", + "price": 10 + } + }, + { + "id": "e302", + "type": "NOMINATION", + "ts": 1700000302000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin London RB34", + "position": "RB", + "nflTeam": "BUF", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e303", + "type": "BID", + "ts": 1700000303000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-3" + } + }, + { + "id": "e304", + "type": "BID", + "ts": 1700000304000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-11" + } + }, + { + "id": "e305", + "type": "SOLD", + "ts": 1700000305000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin London RB34", + "position": "RB", + "nflTeam": "BUF", + "teamId": "team-9", + "teamName": "Team 9", + "price": 8 + } + }, + { + "id": "e306", + "type": "NOMINATION", + "ts": 1700000306000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Kincaid WR29", + "position": "WR", + "nflTeam": "NYJ", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e307", + "type": "BID", + "ts": 1700000307000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-12" + } + }, + { + "id": "e308", + "type": "BID", + "ts": 1700000308000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-4" + } + }, + { + "id": "e309", + "type": "SOLD", + "ts": 1700000309000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Kincaid WR29", + "position": "WR", + "nflTeam": "NYJ", + "teamId": "team-10", + "teamName": "Team 10", + "price": 12 + } + }, + { + "id": "e310", + "type": "NOMINATION", + "ts": 1700000310000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Corum WR31", + "position": "WR", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e311", + "type": "BID", + "ts": 1700000311000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-11" + } + }, + { + "id": "e312", + "type": "BID", + "ts": 1700000312000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-1" + } + }, + { + "id": "e313", + "type": "SOLD", + "ts": 1700000313000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Corum WR31", + "position": "WR", + "nflTeam": "DET", + "teamId": "team-1", + "teamName": "Team 1", + "price": 10 + } + }, + { + "id": "e314", + "type": "NOMINATION", + "ts": 1700000314000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico McBride RB27", + "position": "RB", + "nflTeam": "SEA", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e315", + "type": "BID", + "ts": 1700000315000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-9" + } + }, + { + "id": "e316", + "type": "BID", + "ts": 1700000316000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-8" + } + }, + { + "id": "e317", + "type": "SOLD", + "ts": 1700000317000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico McBride RB27", + "position": "RB", + "nflTeam": "SEA", + "teamId": "team-9", + "teamName": "Team 9", + "price": 12 + } + }, + { + "id": "e318", + "type": "NOMINATION", + "ts": 1700000318000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rashee Samuel WR32", + "position": "WR", + "nflTeam": "KC", + "openingBid": 1, + "nominatingTeamId": "team-5" + } + }, + { + "id": "e319", + "type": "BID", + "ts": 1700000319000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-7" + } + }, + { + "id": "e320", + "type": "BID", + "ts": 1700000320000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-1" + } + }, + { + "id": "e321", + "type": "SOLD", + "ts": 1700000321000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rashee Samuel WR32", + "position": "WR", + "nflTeam": "KC", + "teamId": "team-10", + "teamName": "Team 10", + "price": 13 + } + }, + { + "id": "e322", + "type": "NOMINATION", + "ts": 1700000322000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rashee Gibbs WR33", + "position": "WR", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e323", + "type": "BID", + "ts": 1700000323000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-10" + } + }, + { + "id": "e324", + "type": "BID", + "ts": 1700000324000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 7, + "teamId": "team-6" + } + }, + { + "id": "e325", + "type": "SOLD", + "ts": 1700000325000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rashee Gibbs WR33", + "position": "WR", + "nflTeam": "NO", + "teamId": "team-5", + "teamName": "Team 5", + "price": 9 + } + }, + { + "id": "e326", + "type": "NOMINATION", + "ts": 1700000326000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Breece Johnston WR34", + "position": "WR", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e327", + "type": "BID", + "ts": 1700000327000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-7" + } + }, + { + "id": "e328", + "type": "BID", + "ts": 1700000328000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 7, + "teamId": "team-8" + } + }, + { + "id": "e329", + "type": "SOLD", + "ts": 1700000329000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Breece Johnston WR34", + "position": "WR", + "nflTeam": "PHI", + "teamId": "team-10", + "teamName": "Team 10", + "price": 9 + } + }, + { + "id": "e330", + "type": "NOMINATION", + "ts": 1700000330000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Deebo Smith-Njigba QB8", + "position": "QB", + "nflTeam": "SEA", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e331", + "type": "BID", + "ts": 1700000331000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-5" + } + }, + { + "id": "e332", + "type": "BID", + "ts": 1700000332000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 7, + "teamId": "team-6" + } + }, + { + "id": "e333", + "type": "SOLD", + "ts": 1700000333000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Deebo Smith-Njigba QB8", + "position": "QB", + "nflTeam": "SEA", + "teamId": "team-9", + "teamName": "Team 9", + "price": 9 + } + }, + { + "id": "e334", + "type": "NOMINATION", + "ts": 1700000334000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Nacua RB28", + "position": "RB", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e335", + "type": "BID", + "ts": 1700000335000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-6" + } + }, + { + "id": "e336", + "type": "BID", + "ts": 1700000336000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-5" + } + }, + { + "id": "e337", + "type": "SOLD", + "ts": 1700000337000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Nacua RB28", + "position": "RB", + "nflTeam": "PHI", + "teamId": "team-9", + "teamName": "Team 9", + "price": 10 + } + }, + { + "id": "e338", + "type": "NOMINATION", + "ts": 1700000338000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Hall RB32", + "position": "RB", + "nflTeam": "NYJ", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e339", + "type": "BID", + "ts": 1700000339000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-9" + } + }, + { + "id": "e340", + "type": "BID", + "ts": 1700000340000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-3" + } + }, + { + "id": "e341", + "type": "SOLD", + "ts": 1700000341000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Hall RB32", + "position": "RB", + "nflTeam": "NYJ", + "teamId": "team-9", + "teamName": "Team 9", + "price": 10 + } + }, + { + "id": "e342", + "type": "NOMINATION", + "ts": 1700000342000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Dell TE10", + "position": "TE", + "nflTeam": "MIN", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e343", + "type": "BID", + "ts": 1700000343000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-2" + } + }, + { + "id": "e344", + "type": "BID", + "ts": 1700000344000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-8" + } + }, + { + "id": "e345", + "type": "SOLD", + "ts": 1700000345000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Dell TE10", + "position": "TE", + "nflTeam": "MIN", + "teamId": "team-2", + "teamName": "Team 2", + "price": 7 + } + }, + { + "id": "e346", + "type": "NOMINATION", + "ts": 1700000346000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome LaPorta WR38", + "position": "WR", + "nflTeam": "DAL", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e347", + "type": "BID", + "ts": 1700000347000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-9" + } + }, + { + "id": "e348", + "type": "BID", + "ts": 1700000348000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 7, + "teamId": "team-3" + } + }, + { + "id": "e349", + "type": "SOLD", + "ts": 1700000349000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome LaPorta WR38", + "position": "WR", + "nflTeam": "DAL", + "teamId": "team-10", + "teamName": "Team 10", + "price": 9 + } + }, + { + "id": "e350", + "type": "NOMINATION", + "ts": 1700000350000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Isiah Shakir RB29", + "position": "RB", + "nflTeam": "NYG", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e351", + "type": "BID", + "ts": 1700000351000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-12" + } + }, + { + "id": "e352", + "type": "BID", + "ts": 1700000352000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 7, + "teamId": "team-8" + } + }, + { + "id": "e353", + "type": "SOLD", + "ts": 1700000353000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Isiah Shakir RB29", + "position": "RB", + "nflTeam": "NYG", + "teamId": "team-5", + "teamName": "Team 5", + "price": 9 + } + }, + { + "id": "e354", + "type": "NOMINATION", + "ts": 1700000354000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Johnston WR36", + "position": "WR", + "nflTeam": "SF", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e355", + "type": "BID", + "ts": 1700000355000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-3" + } + }, + { + "id": "e356", + "type": "BID", + "ts": 1700000356000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 10, + "teamId": "team-12" + } + }, + { + "id": "e357", + "type": "SOLD", + "ts": 1700000357000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Johnston WR36", + "position": "WR", + "nflTeam": "SF", + "teamId": "team-2", + "teamName": "Team 2", + "price": 12 + } + }, + { + "id": "e358", + "type": "NOMINATION", + "ts": 1700000358000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Brock Hall TE8", + "position": "TE", + "nflTeam": "LAR", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e359", + "type": "BID", + "ts": 1700000359000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-3" + } + }, + { + "id": "e360", + "type": "BID", + "ts": 1700000360000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-6" + } + }, + { + "id": "e361", + "type": "SOLD", + "ts": 1700000361000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Brock Hall TE8", + "position": "TE", + "nflTeam": "LAR", + "teamId": "team-11", + "teamName": "Team 11", + "price": 10 + } + }, + { + "id": "e362", + "type": "NOMINATION", + "ts": 1700000362000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Pacheco RB30", + "position": "RB", + "nflTeam": "KC", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e363", + "type": "BID", + "ts": 1700000363000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-11" + } + }, + { + "id": "e364", + "type": "BID", + "ts": 1700000364000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-9" + } + }, + { + "id": "e365", + "type": "SOLD", + "ts": 1700000365000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Pacheco RB30", + "position": "RB", + "nflTeam": "KC", + "teamId": "team-10", + "teamName": "Team 10", + "price": 8 + } + }, + { + "id": "e366", + "type": "NOMINATION", + "ts": 1700000366000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil Nacua QB9", + "position": "QB", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e367", + "type": "BID", + "ts": 1700000367000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-1" + } + }, + { + "id": "e368", + "type": "BID", + "ts": 1700000368000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 7, + "teamId": "team-9" + } + }, + { + "id": "e369", + "type": "SOLD", + "ts": 1700000369000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil Nacua QB9", + "position": "QB", + "nflTeam": "DET", + "teamId": "team-5", + "teamName": "Team 5", + "price": 9 + } + }, + { + "id": "e370", + "type": "NOMINATION", + "ts": 1700000370000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Justin Lamb RB33", + "position": "RB", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e371", + "type": "BID", + "ts": 1700000371000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-2" + } + }, + { + "id": "e372", + "type": "BID", + "ts": 1700000372000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-6" + } + }, + { + "id": "e373", + "type": "SOLD", + "ts": 1700000373000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Justin Lamb RB33", + "position": "RB", + "nflTeam": "DET", + "teamId": "team-5", + "teamName": "Team 5", + "price": 7 + } + }, + { + "id": "e374", + "type": "NOMINATION", + "ts": 1700000374000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Puka Gibbs WR39", + "position": "WR", + "nflTeam": "ATL", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e375", + "type": "BID", + "ts": 1700000375000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-4" + } + }, + { + "id": "e376", + "type": "BID", + "ts": 1700000376000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 8, + "teamId": "team-6" + } + }, + { + "id": "e377", + "type": "SOLD", + "ts": 1700000377000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Puka Gibbs WR39", + "position": "WR", + "nflTeam": "ATL", + "teamId": "team-11", + "teamName": "Team 11", + "price": 10 + } + }, + { + "id": "e378", + "type": "NOMINATION", + "ts": 1700000378000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Wilson WR40", + "position": "WR", + "nflTeam": "DAL", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e379", + "type": "BID", + "ts": 1700000379000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-1" + } + }, + { + "id": "e380", + "type": "BID", + "ts": 1700000380000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-3" + } + }, + { + "id": "e381", + "type": "SOLD", + "ts": 1700000381000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Wilson WR40", + "position": "WR", + "nflTeam": "DAL", + "teamId": "team-6", + "teamName": "Team 6", + "price": 7 + } + }, + { + "id": "e382", + "type": "NOMINATION", + "ts": 1700000382000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Pacheco TE9", + "position": "TE", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e383", + "type": "BID", + "ts": 1700000383000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-10" + } + }, + { + "id": "e384", + "type": "BID", + "ts": 1700000384000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 7, + "teamId": "team-9" + } + }, + { + "id": "e385", + "type": "SOLD", + "ts": 1700000385000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Pacheco TE9", + "position": "TE", + "nflTeam": "PHI", + "teamId": "team-2", + "teamName": "Team 2", + "price": 9 + } + }, + { + "id": "e386", + "type": "NOMINATION", + "ts": 1700000386000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Sam McBride WR45", + "position": "WR", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e387", + "type": "BID", + "ts": 1700000387000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-3" + } + }, + { + "id": "e388", + "type": "BID", + "ts": 1700000388000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-3" + } + }, + { + "id": "e389", + "type": "SOLD", + "ts": 1700000389000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Sam McBride WR45", + "position": "WR", + "nflTeam": "DET", + "teamId": "team-11", + "teamName": "Team 11", + "price": 8 + } + }, + { + "id": "e390", + "type": "NOMINATION", + "ts": 1700000390000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Flowers RB38", + "position": "RB", + "nflTeam": "ATL", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e391", + "type": "BID", + "ts": 1700000391000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-8" + } + }, + { + "id": "e392", + "type": "BID", + "ts": 1700000392000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-1" + } + }, + { + "id": "e393", + "type": "SOLD", + "ts": 1700000393000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Flowers RB38", + "position": "RB", + "nflTeam": "ATL", + "teamId": "team-6", + "teamName": "Team 6", + "price": 8 + } + }, + { + "id": "e394", + "type": "NOMINATION", + "ts": 1700000394000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil Kincaid QB10", + "position": "QB", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e395", + "type": "BID", + "ts": 1700000395000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-6" + } + }, + { + "id": "e396", + "type": "BID", + "ts": 1700000396000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-3" + } + }, + { + "id": "e397", + "type": "SOLD", + "ts": 1700000397000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil Kincaid QB10", + "position": "QB", + "nflTeam": "PHI", + "teamId": "team-11", + "teamName": "Team 11", + "price": 8 + } + }, + { + "id": "e398", + "type": "NOMINATION", + "ts": 1700000398000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Johnston RB35", + "position": "RB", + "nflTeam": "DAL", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e399", + "type": "BID", + "ts": 1700000399000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-6" + } + }, + { + "id": "e400", + "type": "BID", + "ts": 1700000400000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-9" + } + }, + { + "id": "e401", + "type": "SOLD", + "ts": 1700000401000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Johnston RB35", + "position": "RB", + "nflTeam": "DAL", + "teamId": "team-10", + "teamName": "Team 10", + "price": 8 + } + }, + { + "id": "e402", + "type": "NOMINATION", + "ts": 1700000402000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Drake Tracy WR44", + "position": "WR", + "nflTeam": "LAR", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e403", + "type": "BID", + "ts": 1700000403000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-12" + } + }, + { + "id": "e404", + "type": "BID", + "ts": 1700000404000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-3" + } + }, + { + "id": "e405", + "type": "SOLD", + "ts": 1700000405000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Drake Tracy WR44", + "position": "WR", + "nflTeam": "LAR", + "teamId": "team-11", + "teamName": "Team 11", + "price": 7 + } + }, + { + "id": "e406", + "type": "NOMINATION", + "ts": 1700000406000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico Johnston TE12", + "position": "TE", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e407", + "type": "BID", + "ts": 1700000407000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-1" + } + }, + { + "id": "e408", + "type": "BID", + "ts": 1700000408000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-8" + } + }, + { + "id": "e409", + "type": "SOLD", + "ts": 1700000409000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico Johnston TE12", + "position": "TE", + "nflTeam": "CIN", + "teamId": "team-2", + "teamName": "Team 2", + "price": 6 + } + }, + { + "id": "e410", + "type": "NOMINATION", + "ts": 1700000410000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Samuel WR47", + "position": "WR", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e411", + "type": "BID", + "ts": 1700000411000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-11" + } + }, + { + "id": "e412", + "type": "BID", + "ts": 1700000412000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-3" + } + }, + { + "id": "e413", + "type": "SOLD", + "ts": 1700000413000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Samuel WR47", + "position": "WR", + "nflTeam": "CHI", + "teamId": "team-5", + "teamName": "Team 5", + "price": 6 + } + }, + { + "id": "e414", + "type": "NOMINATION", + "ts": 1700000414000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Brock Lamb RB37", + "position": "RB", + "nflTeam": "NYJ", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e415", + "type": "BID", + "ts": 1700000415000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-11" + } + }, + { + "id": "e416", + "type": "BID", + "ts": 1700000416000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-2" + } + }, + { + "id": "e417", + "type": "SOLD", + "ts": 1700000417000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Brock Lamb RB37", + "position": "RB", + "nflTeam": "NYJ", + "teamId": "team-11", + "teamName": "Team 11", + "price": 7 + } + }, + { + "id": "e418", + "type": "NOMINATION", + "ts": 1700000418000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Brock Dell WR41", + "position": "WR", + "nflTeam": "MIN", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e419", + "type": "BID", + "ts": 1700000419000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-11" + } + }, + { + "id": "e420", + "type": "BID", + "ts": 1700000420000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-10" + } + }, + { + "id": "e421", + "type": "SOLD", + "ts": 1700000421000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Brock Dell WR41", + "position": "WR", + "nflTeam": "MIN", + "teamId": "team-7", + "teamName": "Team 7", + "price": 6 + } + }, + { + "id": "e422", + "type": "NOMINATION", + "ts": 1700000422000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Nabers TE11", + "position": "TE", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e423", + "type": "BID", + "ts": 1700000423000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-2" + } + }, + { + "id": "e424", + "type": "BID", + "ts": 1700000424000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-2" + } + }, + { + "id": "e425", + "type": "SOLD", + "ts": 1700000425000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Nabers TE11", + "position": "TE", + "nflTeam": "CIN", + "teamId": "team-2", + "teamName": "Team 2", + "price": 6 + } + }, + { + "id": "e426", + "type": "NOMINATION", + "ts": 1700000426000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Samuel QB11", + "position": "QB", + "nflTeam": "LAR", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e427", + "type": "BID", + "ts": 1700000427000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-12" + } + }, + { + "id": "e428", + "type": "BID", + "ts": 1700000428000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-4" + } + }, + { + "id": "e429", + "type": "SOLD", + "ts": 1700000429000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Samuel QB11", + "position": "QB", + "nflTeam": "LAR", + "teamId": "team-2", + "teamName": "Team 2", + "price": 7 + } + }, + { + "id": "e430", + "type": "NOMINATION", + "ts": 1700000430000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Hall RB36", + "position": "RB", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e431", + "type": "BID", + "ts": 1700000431000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-7" + } + }, + { + "id": "e432", + "type": "BID", + "ts": 1700000432000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-6" + } + }, + { + "id": "e433", + "type": "SOLD", + "ts": 1700000433000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Hall RB36", + "position": "RB", + "nflTeam": "CIN", + "teamId": "team-11", + "teamName": "Team 11", + "price": 7 + } + }, + { + "id": "e434", + "type": "NOMINATION", + "ts": 1700000434000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Jefferson RB39", + "position": "RB", + "nflTeam": "LV", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e435", + "type": "BID", + "ts": 1700000435000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-8" + } + }, + { + "id": "e436", + "type": "BID", + "ts": 1700000436000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-4" + } + }, + { + "id": "e437", + "type": "SOLD", + "ts": 1700000437000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Jefferson RB39", + "position": "RB", + "nflTeam": "LV", + "teamId": "team-2", + "teamName": "Team 2", + "price": 7 + } + }, + { + "id": "e438", + "type": "NOMINATION", + "ts": 1700000438000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Williams RB40", + "position": "RB", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e439", + "type": "BID", + "ts": 1700000439000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-9" + } + }, + { + "id": "e440", + "type": "BID", + "ts": 1700000440000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 6, + "teamId": "team-6" + } + }, + { + "id": "e441", + "type": "SOLD", + "ts": 1700000441000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Williams RB40", + "position": "RB", + "nflTeam": "CIN", + "teamId": "team-11", + "teamName": "Team 11", + "price": 8 + } + }, + { + "id": "e442", + "type": "NOMINATION", + "ts": 1700000442000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Parsons RB46", + "position": "RB", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e443", + "type": "BID", + "ts": 1700000443000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 1, + "teamId": "team-4" + } + }, + { + "id": "e444", + "type": "BID", + "ts": 1700000444000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 2, + "teamId": "team-7" + } + }, + { + "id": "e445", + "type": "SOLD", + "ts": 1700000445000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Parsons RB46", + "position": "RB", + "nflTeam": "ARI", + "teamId": "team-10", + "teamName": "Team 10", + "price": 3 + } + }, + { + "id": "e446", + "type": "NOMINATION", + "ts": 1700000446000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Puka Shakir WR42", + "position": "WR", + "nflTeam": "SF", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e447", + "type": "BID", + "ts": 1700000447000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-12" + } + }, + { + "id": "e448", + "type": "BID", + "ts": 1700000448000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-2" + } + }, + { + "id": "e449", + "type": "SOLD", + "ts": 1700000449000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Puka Shakir WR42", + "position": "WR", + "nflTeam": "SF", + "teamId": "team-3", + "teamName": "Team 3", + "price": 6 + } + }, + { + "id": "e450", + "type": "NOMINATION", + "ts": 1700000450000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Tracy WR49", + "position": "WR", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e451", + "type": "BID", + "ts": 1700000451000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-3" + } + }, + { + "id": "e452", + "type": "BID", + "ts": 1700000452000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-9" + } + }, + { + "id": "e453", + "type": "SOLD", + "ts": 1700000453000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Tracy WR49", + "position": "WR", + "nflTeam": "ARI", + "teamId": "team-2", + "teamName": "Team 2", + "price": 6 + } + }, + { + "id": "e454", + "type": "NOMINATION", + "ts": 1700000454000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Brown WR43", + "position": "WR", + "nflTeam": "SEA", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e455", + "type": "BID", + "ts": 1700000455000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-1" + } + }, + { + "id": "e456", + "type": "BID", + "ts": 1700000456000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-3" + } + }, + { + "id": "e457", + "type": "SOLD", + "ts": 1700000457000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Brown WR43", + "position": "WR", + "nflTeam": "SEA", + "teamId": "team-11", + "teamName": "Team 11", + "price": 6 + } + }, + { + "id": "e458", + "type": "NOMINATION", + "ts": 1700000458000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Kincaid QB12", + "position": "QB", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e459", + "type": "BID", + "ts": 1700000459000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-7" + } + }, + { + "id": "e460", + "type": "BID", + "ts": 1700000460000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-12" + } + }, + { + "id": "e461", + "type": "SOLD", + "ts": 1700000461000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Kincaid QB12", + "position": "QB", + "nflTeam": "NO", + "teamId": "team-11", + "teamName": "Team 11", + "price": 6 + } + }, + { + "id": "e462", + "type": "NOMINATION", + "ts": 1700000462000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Gibbs RB48", + "position": "RB", + "nflTeam": "NYJ", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e463", + "type": "BID", + "ts": 1700000463000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 1, + "teamId": "team-11" + } + }, + { + "id": "e464", + "type": "BID", + "ts": 1700000464000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 2, + "teamId": "team-8" + } + }, + { + "id": "e465", + "type": "SOLD", + "ts": 1700000465000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Marvin Gibbs RB48", + "position": "RB", + "nflTeam": "NYJ", + "teamId": "team-12", + "teamName": "Team 12", + "price": 3 + } + }, + { + "id": "e466", + "type": "NOMINATION", + "ts": 1700000466000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Harrison RB41", + "position": "RB", + "nflTeam": "SEA", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e467", + "type": "BID", + "ts": 1700000467000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-2" + } + }, + { + "id": "e468", + "type": "BID", + "ts": 1700000468000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-8" + } + }, + { + "id": "e469", + "type": "SOLD", + "ts": 1700000469000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Harrison RB41", + "position": "RB", + "nflTeam": "SEA", + "teamId": "team-11", + "teamName": "Team 11", + "price": 6 + } + }, + { + "id": "e470", + "type": "NOMINATION", + "ts": 1700000470000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Tracy RB47", + "position": "RB", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e471", + "type": "BID", + "ts": 1700000471000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 1, + "teamId": "team-7" + } + }, + { + "id": "e472", + "type": "BID", + "ts": 1700000472000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 2, + "teamId": "team-11" + } + }, + { + "id": "e473", + "type": "SOLD", + "ts": 1700000473000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Tracy RB47", + "position": "RB", + "nflTeam": "CHI", + "teamId": "team-2", + "teamName": "Team 2", + "price": 3 + } + }, + { + "id": "e474", + "type": "NOMINATION", + "ts": 1700000474000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Parsons RB42", + "position": "RB", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e475", + "type": "BID", + "ts": 1700000475000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-11" + } + }, + { + "id": "e476", + "type": "BID", + "ts": 1700000476000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-1" + } + }, + { + "id": "e477", + "type": "SOLD", + "ts": 1700000477000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Parsons RB42", + "position": "RB", + "nflTeam": "NO", + "teamId": "team-11", + "teamName": "Team 11", + "price": 6 + } + }, + { + "id": "e478", + "type": "NOMINATION", + "ts": 1700000478000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Pacheco RB44", + "position": "RB", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e479", + "type": "BID", + "ts": 1700000479000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 2, + "teamId": "team-6" + } + }, + { + "id": "e480", + "type": "BID", + "ts": 1700000480000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-8" + } + }, + { + "id": "e481", + "type": "BID", + "ts": 1700000481000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 4, + "teamId": "team-3" + } + }, + { + "id": "e482", + "type": "SOLD", + "ts": 1700000482000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Pacheco RB44", + "position": "RB", + "nflTeam": "NO", + "teamId": "team-9", + "teamName": "Team 9", + "price": 5 + } + }, + { + "id": "e483", + "type": "NOMINATION", + "ts": 1700000483000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Harrison WR46", + "position": "WR", + "nflTeam": "KC", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e484", + "type": "BID", + "ts": 1700000484000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 2, + "teamId": "team-12" + } + }, + { + "id": "e485", + "type": "BID", + "ts": 1700000485000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-8" + } + }, + { + "id": "e486", + "type": "SOLD", + "ts": 1700000486000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Harrison WR46", + "position": "WR", + "nflTeam": "KC", + "teamId": "team-6", + "teamName": "Team 6", + "price": 4 + } + }, + { + "id": "e487", + "type": "NOMINATION", + "ts": 1700000487000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "CeeDee Jefferson WR48", + "position": "WR", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e488", + "type": "BID", + "ts": 1700000488000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 1, + "teamId": "team-9" + } + }, + { + "id": "e489", + "type": "BID", + "ts": 1700000489000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 2, + "teamId": "team-12" + } + }, + { + "id": "e490", + "type": "SOLD", + "ts": 1700000490000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "CeeDee Jefferson WR48", + "position": "WR", + "nflTeam": "ARI", + "teamId": "team-1", + "teamName": "Team 1", + "price": 3 + } + }, + { + "id": "e491", + "type": "NOMINATION", + "ts": 1700000491000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Breece Nacua TE13", + "position": "TE", + "nflTeam": "DAL", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e492", + "type": "BID", + "ts": 1700000492000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 3, + "teamId": "team-7" + } + }, + { + "id": "e493", + "type": "BID", + "ts": 1700000493000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 5, + "teamId": "team-4" + } + }, + { + "id": "e494", + "type": "SOLD", + "ts": 1700000494000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Breece Nacua TE13", + "position": "TE", + "nflTeam": "DAL", + "teamId": "team-5", + "teamName": "Team 5", + "price": 6 + } + }, + { + "id": "e495", + "type": "NOMINATION", + "ts": 1700000495000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil Collins QB13", + "position": "QB", + "nflTeam": "KC", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e496", + "type": "BID", + "ts": 1700000496000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 1, + "teamId": "team-12" + } + }, + { + "id": "e497", + "type": "SOLD", + "ts": 1700000497000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil Collins QB13", + "position": "QB", + "nflTeam": "KC", + "teamId": "team-4", + "teamName": "Team 4", + "price": 2 + } + }, + { + "id": "e498", + "type": "NOMINATION", + "ts": 1700000498000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico Hall RB43", + "position": "RB", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e499", + "type": "BID", + "ts": 1700000499000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 1, + "teamId": "team-8" + } + }, + { + "id": "e500", + "type": "SOLD", + "ts": 1700000500000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico Hall RB43", + "position": "RB", + "nflTeam": "NO", + "teamId": "team-8", + "teamName": "Team 8", + "price": 2 + } + }, + { + "id": "e501", + "type": "NOMINATION", + "ts": 1700000501000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "CeeDee Rice RB49", + "position": "RB", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e502", + "type": "BID", + "ts": 1700000502000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 1, + "teamId": "team-3" + } + }, + { + "id": "e503", + "type": "SOLD", + "ts": 1700000503000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "CeeDee Rice RB49", + "position": "RB", + "nflTeam": "ARI", + "teamId": "team-12", + "teamName": "Team 12", + "price": 2 + } + }, + { + "id": "e504", + "type": "NOMINATION", + "ts": 1700000504000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Odunze WR50", + "position": "WR", + "nflTeam": "KC", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e505", + "type": "SOLD", + "ts": 1700000505000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Odunze WR50", + "position": "WR", + "nflTeam": "KC", + "teamId": "team-12", + "teamName": "Team 12", + "price": 1 + } + }, + { + "id": "e506", + "type": "NOMINATION", + "ts": 1700000506000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Johnston WR51", + "position": "WR", + "nflTeam": "BUF", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e507", + "type": "SOLD", + "ts": 1700000507000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Johnston WR51", + "position": "WR", + "nflTeam": "BUF", + "teamId": "team-7", + "teamName": "Team 7", + "price": 1 + } + }, + { + "id": "e508", + "type": "NOMINATION", + "ts": 1700000508000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Tracy WR52", + "position": "WR", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e509", + "type": "SOLD", + "ts": 1700000509000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Tracy WR52", + "position": "WR", + "nflTeam": "PHI", + "teamId": "team-8", + "teamName": "Team 8", + "price": 1 + } + }, + { + "id": "e510", + "type": "NOMINATION", + "ts": 1700000510000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Brown TE14", + "position": "TE", + "nflTeam": "HOU", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e511", + "type": "SOLD", + "ts": 1700000511000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Brown TE14", + "position": "TE", + "nflTeam": "HOU", + "teamId": "team-7", + "teamName": "Team 7", + "price": 1 + } + }, + { + "id": "e512", + "type": "NOMINATION", + "ts": 1700000512000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "CeeDee London QB14", + "position": "QB", + "nflTeam": "HOU", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e513", + "type": "SOLD", + "ts": 1700000513000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "CeeDee London QB14", + "position": "QB", + "nflTeam": "HOU", + "teamId": "team-8", + "teamName": "Team 8", + "price": 1 + } + }, + { + "id": "e514", + "type": "NOMINATION", + "ts": 1700000514000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rashee Robinson QB15", + "position": "QB", + "nflTeam": "SEA", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e515", + "type": "SOLD", + "ts": 1700000515000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rashee Robinson QB15", + "position": "QB", + "nflTeam": "SEA", + "teamId": "team-3", + "teamName": "Team 3", + "price": 1 + } + }, + { + "id": "e516", + "type": "NOMINATION", + "ts": 1700000516000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Pacheco WR57", + "position": "WR", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e517", + "type": "SOLD", + "ts": 1700000517000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Pacheco WR57", + "position": "WR", + "nflTeam": "CHI", + "teamId": "team-3", + "teamName": "Team 3", + "price": 1 + } + }, + { + "id": "e518", + "type": "NOMINATION", + "ts": 1700000518000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Brown RB45", + "position": "RB", + "nflTeam": "NYJ", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e519", + "type": "SOLD", + "ts": 1700000519000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Brown RB45", + "position": "RB", + "nflTeam": "NYJ", + "teamId": "team-3", + "teamName": "Team 3", + "price": 1 + } + }, + { + "id": "e520", + "type": "NOMINATION", + "ts": 1700000520000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Pacheco RB50", + "position": "RB", + "nflTeam": "NYJ", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e521", + "type": "SOLD", + "ts": 1700000521000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Pacheco RB50", + "position": "RB", + "nflTeam": "NYJ", + "teamId": "team-3", + "teamName": "Team 3", + "price": 1 + } + }, + { + "id": "e522", + "type": "NOMINATION", + "ts": 1700000522000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Wilson WR53", + "position": "WR", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e523", + "type": "SOLD", + "ts": 1700000523000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Wilson WR53", + "position": "WR", + "nflTeam": "NO", + "teamId": "team-2", + "teamName": "Team 2", + "price": 1 + } + }, + { + "id": "e524", + "type": "NOMINATION", + "ts": 1700000524000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Williams RB51", + "position": "RB", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e525", + "type": "SOLD", + "ts": 1700000525000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Williams RB51", + "position": "RB", + "nflTeam": "DET", + "teamId": "team-1", + "teamName": "Team 1", + "price": 1 + } + }, + { + "id": "e526", + "type": "NOMINATION", + "ts": 1700000526000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey London QB18", + "position": "QB", + "nflTeam": "TB", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e527", + "type": "SOLD", + "ts": 1700000527000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey London QB18", + "position": "QB", + "nflTeam": "TB", + "teamId": "team-3", + "teamName": "Team 3", + "price": 1 + } + }, + { + "id": "e528", + "type": "NOMINATION", + "ts": 1700000528000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "CeeDee Flowers WR54", + "position": "WR", + "nflTeam": "NYJ", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e529", + "type": "SOLD", + "ts": 1700000529000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "CeeDee Flowers WR54", + "position": "WR", + "nflTeam": "NYJ", + "teamId": "team-8", + "teamName": "Team 8", + "price": 1 + } + }, + { + "id": "e530", + "type": "NOMINATION", + "ts": 1700000530000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Rice WR55", + "position": "WR", + "nflTeam": "NYJ", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e531", + "type": "SOLD", + "ts": 1700000531000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Rice WR55", + "position": "WR", + "nflTeam": "NYJ", + "teamId": "team-1", + "teamName": "Team 1", + "price": 1 + } + }, + { + "id": "e532", + "type": "NOMINATION", + "ts": 1700000532000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Dalton Addison WR56", + "position": "WR", + "nflTeam": "BAL", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e533", + "type": "SOLD", + "ts": 1700000533000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Dalton Addison WR56", + "position": "WR", + "nflTeam": "BAL", + "teamId": "team-9", + "teamName": "Team 9", + "price": 1 + } + }, + { + "id": "e534", + "type": "NOMINATION", + "ts": 1700000534000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Kupp WR59", + "position": "WR", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e535", + "type": "SOLD", + "ts": 1700000535000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Kupp WR59", + "position": "WR", + "nflTeam": "DET", + "teamId": "team-8", + "teamName": "Team 8", + "price": 1 + } + }, + { + "id": "e536", + "type": "NOMINATION", + "ts": 1700000536000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Dell WR58", + "position": "WR", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e537", + "type": "SOLD", + "ts": 1700000537000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Dell WR58", + "position": "WR", + "nflTeam": "CIN", + "teamId": "team-9", + "teamName": "Team 9", + "price": 1 + } + }, + { + "id": "e538", + "type": "NOMINATION", + "ts": 1700000538000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Sam Shakir RB52", + "position": "RB", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e539", + "type": "SOLD", + "ts": 1700000539000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Sam Shakir RB52", + "position": "RB", + "nflTeam": "CHI", + "teamId": "team-3", + "teamName": "Team 3", + "price": 1 + } + }, + { + "id": "e540", + "type": "NOMINATION", + "ts": 1700000540000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Addison TE15", + "position": "TE", + "nflTeam": "ATL", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e541", + "type": "SOLD", + "ts": 1700000541000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Addison TE15", + "position": "TE", + "nflTeam": "ATL", + "teamId": "team-1", + "teamName": "Team 1", + "price": 1 + } + }, + { + "id": "e542", + "type": "NOMINATION", + "ts": 1700000542000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Collins TE16", + "position": "TE", + "nflTeam": "MIN", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e543", + "type": "SOLD", + "ts": 1700000543000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Collins TE16", + "position": "TE", + "nflTeam": "MIN", + "teamId": "team-1", + "teamName": "Team 1", + "price": 1 + } + }, + { + "id": "e544", + "type": "NOMINATION", + "ts": 1700000544000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Pacheco QB16", + "position": "QB", + "nflTeam": "BAL", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e545", + "type": "SOLD", + "ts": 1700000545000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Pacheco QB16", + "position": "QB", + "nflTeam": "BAL", + "teamId": "team-6", + "teamName": "Team 6", + "price": 1 + } + }, + { + "id": "e546", + "type": "NOMINATION", + "ts": 1700000546000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil Jefferson QB17", + "position": "QB", + "nflTeam": "BUF", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e547", + "type": "SOLD", + "ts": 1700000547000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Khalil Jefferson QB17", + "position": "QB", + "nflTeam": "BUF", + "teamId": "team-8", + "teamName": "Team 8", + "price": 1 + } + }, + { + "id": "e548", + "type": "NOMINATION", + "ts": 1700000548000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris London QB20", + "position": "QB", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e549", + "type": "SOLD", + "ts": 1700000549000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris London QB20", + "position": "QB", + "nflTeam": "NO", + "teamId": "team-3", + "teamName": "Team 3", + "price": 1 + } + }, + { + "id": "e550", + "type": "NOMINATION", + "ts": 1700000550000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Johnston RB53", + "position": "RB", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e551", + "type": "SOLD", + "ts": 1700000551000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Johnston RB53", + "position": "RB", + "nflTeam": "CIN", + "teamId": "team-1", + "teamName": "Team 1", + "price": 1 + } + }, + { + "id": "e552", + "type": "NOMINATION", + "ts": 1700000552000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Samuel RB55", + "position": "RB", + "nflTeam": "DAL", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e553", + "type": "SOLD", + "ts": 1700000553000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Samuel RB55", + "position": "RB", + "nflTeam": "DAL", + "teamId": "team-8", + "teamName": "Team 8", + "price": 1 + } + }, + { + "id": "e554", + "type": "NOMINATION", + "ts": 1700000554000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Isiah Jefferson WR60", + "position": "WR", + "nflTeam": "NO", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e555", + "type": "SOLD", + "ts": 1700000555000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Isiah Jefferson WR60", + "position": "WR", + "nflTeam": "NO", + "teamId": "team-6", + "teamName": "Team 6", + "price": 1 + } + }, + { + "id": "e556", + "type": "NOMINATION", + "ts": 1700000556000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Parsons WR61", + "position": "WR", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e557", + "type": "SOLD", + "ts": 1700000557000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Parsons WR61", + "position": "WR", + "nflTeam": "DET", + "teamId": "team-6", + "teamName": "Team 6", + "price": 1 + } + }, + { + "id": "e558", + "type": "NOMINATION", + "ts": 1700000558000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Flowers WR62", + "position": "WR", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e559", + "type": "SOLD", + "ts": 1700000559000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Flowers WR62", + "position": "WR", + "nflTeam": "DET", + "teamId": "team-12", + "teamName": "Team 12", + "price": 1 + } + }, + { + "id": "e560", + "type": "NOMINATION", + "ts": 1700000560000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Kupp RB56", + "position": "RB", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e561", + "type": "SOLD", + "ts": 1700000561000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Kupp RB56", + "position": "RB", + "nflTeam": "ARI", + "teamId": "team-9", + "teamName": "Team 9", + "price": 1 + } + }, + { + "id": "e562", + "type": "NOMINATION", + "ts": 1700000562000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Rice WR63", + "position": "WR", + "nflTeam": "MIN", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e563", + "type": "SOLD", + "ts": 1700000563000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Rice WR63", + "position": "WR", + "nflTeam": "MIN", + "teamId": "team-4", + "teamName": "Team 4", + "price": 1 + } + }, + { + "id": "e564", + "type": "NOMINATION", + "ts": 1700000564000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Robinson TE19", + "position": "TE", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e565", + "type": "SOLD", + "ts": 1700000565000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Robinson TE19", + "position": "TE", + "nflTeam": "CIN", + "teamId": "team-12", + "teamName": "Team 12", + "price": 1 + } + }, + { + "id": "e566", + "type": "NOMINATION", + "ts": 1700000566000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Nabers WR64", + "position": "WR", + "nflTeam": "LAR", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e567", + "type": "SOLD", + "ts": 1700000567000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Nabers WR64", + "position": "WR", + "nflTeam": "LAR", + "teamId": "team-4", + "teamName": "Team 4", + "price": 1 + } + }, + { + "id": "e568", + "type": "NOMINATION", + "ts": 1700000568000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Williams TE17", + "position": "TE", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e569", + "type": "SOLD", + "ts": 1700000569000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Williams TE17", + "position": "TE", + "nflTeam": "ARI", + "teamId": "team-6", + "teamName": "Team 6", + "price": 1 + } + }, + { + "id": "e570", + "type": "NOMINATION", + "ts": 1700000570000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico Odunze TE20", + "position": "TE", + "nflTeam": "MIN", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e571", + "type": "SOLD", + "ts": 1700000571000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico Odunze TE20", + "position": "TE", + "nflTeam": "MIN", + "teamId": "team-4", + "teamName": "Team 4", + "price": 1 + } + }, + { + "id": "e572", + "type": "NOMINATION", + "ts": 1700000572000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Collins RB60", + "position": "RB", + "nflTeam": "ARI", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e573", + "type": "SOLD", + "ts": 1700000573000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Collins RB60", + "position": "RB", + "nflTeam": "ARI", + "teamId": "team-12", + "teamName": "Team 12", + "price": 1 + } + }, + { + "id": "e574", + "type": "NOMINATION", + "ts": 1700000574000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Hall DST1", + "position": "DST", + "nflTeam": "ATL", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e575", + "type": "BID", + "ts": 1700000575000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 1, + "teamId": "team-7" + } + }, + { + "id": "e576", + "type": "SOLD", + "ts": 1700000576000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Trey Hall DST1", + "position": "DST", + "nflTeam": "ATL", + "teamId": "team-11", + "teamName": "Team 11", + "price": 2 + } + }, + { + "id": "e577", + "type": "NOMINATION", + "ts": 1700000577000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Dalton Tracy QB19", + "position": "QB", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e578", + "type": "SOLD", + "ts": 1700000578000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Dalton Tracy QB19", + "position": "QB", + "nflTeam": "DET", + "teamId": "team-12", + "teamName": "Team 12", + "price": 1 + } + }, + { + "id": "e579", + "type": "NOMINATION", + "ts": 1700000579000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Puka Samuel RB54", + "position": "RB", + "nflTeam": "BAL", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e580", + "type": "SOLD", + "ts": 1700000580000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Puka Samuel RB54", + "position": "RB", + "nflTeam": "BAL", + "teamId": "team-4", + "teamName": "Team 4", + "price": 1 + } + }, + { + "id": "e581", + "type": "NOMINATION", + "ts": 1700000581000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Rice RB57", + "position": "RB", + "nflTeam": "DAL", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e582", + "type": "SOLD", + "ts": 1700000582000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Rice RB57", + "position": "RB", + "nflTeam": "DAL", + "teamId": "team-7", + "teamName": "Team 7", + "price": 1 + } + }, + { + "id": "e583", + "type": "NOMINATION", + "ts": 1700000583000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Brock Samuel RB58", + "position": "RB", + "nflTeam": "NYG", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e584", + "type": "SOLD", + "ts": 1700000584000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Brock Samuel RB58", + "position": "RB", + "nflTeam": "NYG", + "teamId": "team-12", + "teamName": "Team 12", + "price": 1 + } + }, + { + "id": "e585", + "type": "NOMINATION", + "ts": 1700000585000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rashee McBride RB59", + "position": "RB", + "nflTeam": "KC", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e586", + "type": "SOLD", + "ts": 1700000586000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rashee McBride RB59", + "position": "RB", + "nflTeam": "KC", + "teamId": "team-7", + "teamName": "Team 7", + "price": 1 + } + }, + { + "id": "e587", + "type": "NOMINATION", + "ts": 1700000587000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Olave WR65", + "position": "WR", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e588", + "type": "SOLD", + "ts": 1700000588000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Olave WR65", + "position": "WR", + "nflTeam": "CHI", + "teamId": "team-7", + "teamName": "Team 7", + "price": 1 + } + }, + { + "id": "e589", + "type": "NOMINATION", + "ts": 1700000589000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Smith-Njigba DST2", + "position": "DST", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e590", + "type": "BID", + "ts": 1700000590000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 1, + "teamId": "team-5" + } + }, + { + "id": "e591", + "type": "SOLD", + "ts": 1700000591000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Smith-Njigba DST2", + "position": "DST", + "nflTeam": "CIN", + "teamId": "team-5", + "teamName": "Team 5", + "price": 2 + } + }, + { + "id": "e592", + "type": "NOMINATION", + "ts": 1700000592000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Flowers K3", + "position": "K", + "nflTeam": "ATL", + "openingBid": 1, + "nominatingTeamId": "team-11" + } + }, + { + "id": "e593", + "type": "SOLD", + "ts": 1700000593000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Flowers K3", + "position": "K", + "nflTeam": "ATL", + "teamId": "team-3", + "teamName": "Team 3", + "price": 1 + } + }, + { + "id": "e594", + "type": "NOMINATION", + "ts": 1700000594000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Sam Nacua TE18", + "position": "TE", + "nflTeam": "SF", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e595", + "type": "SOLD", + "ts": 1700000595000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Sam Nacua TE18", + "position": "TE", + "nflTeam": "SF", + "teamId": "team-3", + "teamName": "Team 3", + "price": 1 + } + }, + { + "id": "e596", + "type": "NOMINATION", + "ts": 1700000596000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Hall TE21", + "position": "TE", + "nflTeam": "LV", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e597", + "type": "SOLD", + "ts": 1700000597000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Hall TE21", + "position": "TE", + "nflTeam": "LV", + "teamId": "team-8", + "teamName": "Team 8", + "price": 1 + } + }, + { + "id": "e598", + "type": "NOMINATION", + "ts": 1700000598000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Brown K1", + "position": "K", + "nflTeam": "BUF", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e599", + "type": "BID", + "ts": 1700000599000, + "source": "ws", + "confidence": 1, + "payload": { + "amount": 1, + "teamId": "team-4" + } + }, + { + "id": "e600", + "type": "SOLD", + "ts": 1700000600000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Brown K1", + "position": "K", + "nflTeam": "BUF", + "teamId": "team-11", + "teamName": "Team 11", + "price": 2 + } + }, + { + "id": "e601", + "type": "NOMINATION", + "ts": 1700000601000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jaxon Lamb K2", + "position": "K", + "nflTeam": "DET", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e602", + "type": "SOLD", + "ts": 1700000602000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jaxon Lamb K2", + "position": "K", + "nflTeam": "DET", + "teamId": "team-5", + "teamName": "Team 5", + "price": 1 + } + }, + { + "id": "e603", + "type": "NOMINATION", + "ts": 1700000603000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Olave DST3", + "position": "DST", + "nflTeam": "LV", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e604", + "type": "SOLD", + "ts": 1700000604000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Olave DST3", + "position": "DST", + "nflTeam": "LV", + "teamId": "team-8", + "teamName": "Team 8", + "price": 1 + } + }, + { + "id": "e605", + "type": "NOMINATION", + "ts": 1700000605000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Odunze QB21", + "position": "QB", + "nflTeam": "KC", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e606", + "type": "SOLD", + "ts": 1700000606000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Bijan Odunze QB21", + "position": "QB", + "nflTeam": "KC", + "teamId": "team-1", + "teamName": "Team 1", + "price": 1 + } + }, + { + "id": "e607", + "type": "NOMINATION", + "ts": 1700000607000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Rice QB22", + "position": "QB", + "nflTeam": "SF", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e608", + "type": "SOLD", + "ts": 1700000608000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Malik Rice QB22", + "position": "QB", + "nflTeam": "SF", + "teamId": "team-7", + "teamName": "Team 7", + "price": 1 + } + }, + { + "id": "e609", + "type": "NOMINATION", + "ts": 1700000609000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Jefferson K4", + "position": "K", + "nflTeam": "LAR", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e610", + "type": "SOLD", + "ts": 1700000610000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Jefferson K4", + "position": "K", + "nflTeam": "LAR", + "teamId": "team-12", + "teamName": "Team 12", + "price": 1 + } + }, + { + "id": "e611", + "type": "NOMINATION", + "ts": 1700000611000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico Addison K5", + "position": "K", + "nflTeam": "KC", + "openingBid": 1, + "nominatingTeamId": "team-1" + } + }, + { + "id": "e612", + "type": "SOLD", + "ts": 1700000612000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Nico Addison K5", + "position": "K", + "nflTeam": "KC", + "teamId": "team-9", + "teamName": "Team 9", + "price": 1 + } + }, + { + "id": "e613", + "type": "NOMINATION", + "ts": 1700000613000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Samuel K6", + "position": "K", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e614", + "type": "SOLD", + "ts": 1700000614000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Micah Samuel K6", + "position": "K", + "nflTeam": "CIN", + "teamId": "team-1", + "teamName": "Team 1", + "price": 1 + } + }, + { + "id": "e615", + "type": "NOMINATION", + "ts": 1700000615000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Pacheco K8", + "position": "K", + "nflTeam": "NYJ", + "openingBid": 1, + "nominatingTeamId": "team-4" + } + }, + { + "id": "e616", + "type": "SOLD", + "ts": 1700000616000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Pacheco K8", + "position": "K", + "nflTeam": "NYJ", + "teamId": "team-2", + "teamName": "Team 2", + "price": 1 + } + }, + { + "id": "e617", + "type": "NOMINATION", + "ts": 1700000617000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Odunze K7", + "position": "K", + "nflTeam": "SF", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e618", + "type": "SOLD", + "ts": 1700000618000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tyrone Odunze K7", + "position": "K", + "nflTeam": "SF", + "teamId": "team-4", + "teamName": "Team 4", + "price": 1 + } + }, + { + "id": "e619", + "type": "NOMINATION", + "ts": 1700000619000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Robinson DST14", + "position": "DST", + "nflTeam": "PHI", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e620", + "type": "SOLD", + "ts": 1700000620000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Kyren Robinson DST14", + "position": "DST", + "nflTeam": "PHI", + "teamId": "team-7", + "teamName": "Team 7", + "price": 1 + } + }, + { + "id": "e621", + "type": "NOMINATION", + "ts": 1700000621000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Corum DST10", + "position": "DST", + "nflTeam": "MIN", + "openingBid": 1, + "nominatingTeamId": "team-2" + } + }, + { + "id": "e622", + "type": "SOLD", + "ts": 1700000622000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Corum DST10", + "position": "DST", + "nflTeam": "MIN", + "teamId": "team-2", + "teamName": "Team 2", + "price": 1 + } + }, + { + "id": "e623", + "type": "NOMINATION", + "ts": 1700000623000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Odunze K9", + "position": "K", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e624", + "type": "SOLD", + "ts": 1700000624000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Amon-Ra Odunze K9", + "position": "K", + "nflTeam": "CHI", + "teamId": "team-10", + "teamName": "Team 10", + "price": 1 + } + }, + { + "id": "e625", + "type": "NOMINATION", + "ts": 1700000625000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Hall K10", + "position": "K", + "nflTeam": "CIN", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e626", + "type": "SOLD", + "ts": 1700000626000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Zay Hall K10", + "position": "K", + "nflTeam": "CIN", + "teamId": "team-6", + "teamName": "Team 6", + "price": 1 + } + }, + { + "id": "e627", + "type": "NOMINATION", + "ts": 1700000627000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Brock LaPorta K11", + "position": "K", + "nflTeam": "TB", + "openingBid": 1, + "nominatingTeamId": "team-7" + } + }, + { + "id": "e628", + "type": "SOLD", + "ts": 1700000628000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Brock LaPorta K11", + "position": "K", + "nflTeam": "TB", + "teamId": "team-7", + "teamName": "Team 7", + "price": 1 + } + }, + { + "id": "e629", + "type": "NOMINATION", + "ts": 1700000629000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Harrison K12", + "position": "K", + "nflTeam": "BUF", + "openingBid": 1, + "nominatingTeamId": "team-8" + } + }, + { + "id": "e630", + "type": "SOLD", + "ts": 1700000630000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Cooper Harrison K12", + "position": "K", + "nflTeam": "BUF", + "teamId": "team-8", + "teamName": "Team 8", + "price": 1 + } + }, + { + "id": "e631", + "type": "NOMINATION", + "ts": 1700000631000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Brown DST13", + "position": "DST", + "nflTeam": "CHI", + "openingBid": 1, + "nominatingTeamId": "team-12" + } + }, + { + "id": "e632", + "type": "SOLD", + "ts": 1700000632000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Chris Brown DST13", + "position": "DST", + "nflTeam": "CHI", + "teamId": "team-1", + "teamName": "Team 1", + "price": 1 + } + }, + { + "id": "e633", + "type": "NOMINATION", + "ts": 1700000633000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Addison DST4", + "position": "DST", + "nflTeam": "BUF", + "openingBid": 1, + "nominatingTeamId": "team-10" + } + }, + { + "id": "e634", + "type": "SOLD", + "ts": 1700000634000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jahmyr Addison DST4", + "position": "DST", + "nflTeam": "BUF", + "teamId": "team-4", + "teamName": "Team 4", + "price": 1 + } + }, + { + "id": "e635", + "type": "NOMINATION", + "ts": 1700000635000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Flowers DST5", + "position": "DST", + "nflTeam": "ATL", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e636", + "type": "SOLD", + "ts": 1700000636000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Tank Flowers DST5", + "position": "DST", + "nflTeam": "ATL", + "teamId": "team-10", + "teamName": "Team 10", + "price": 1 + } + }, + { + "id": "e637", + "type": "NOMINATION", + "ts": 1700000637000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Justin Smith-Njigba DST6", + "position": "DST", + "nflTeam": "SEA", + "openingBid": 1, + "nominatingTeamId": "team-6" + } + }, + { + "id": "e638", + "type": "SOLD", + "ts": 1700000638000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Justin Smith-Njigba DST6", + "position": "DST", + "nflTeam": "SEA", + "teamId": "team-6", + "teamName": "Team 6", + "price": 1 + } + }, + { + "id": "e639", + "type": "NOMINATION", + "ts": 1700000639000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Lamb DST9", + "position": "DST", + "nflTeam": "TB", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e640", + "type": "SOLD", + "ts": 1700000640000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Jordan Lamb DST9", + "position": "DST", + "nflTeam": "TB", + "teamId": "team-12", + "teamName": "Team 12", + "price": 1 + } + }, + { + "id": "e641", + "type": "NOMINATION", + "ts": 1700000641000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Corum DST7", + "position": "DST", + "nflTeam": "SEA", + "openingBid": 1, + "nominatingTeamId": "team-3" + } + }, + { + "id": "e642", + "type": "SOLD", + "ts": 1700000642000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Garrett Corum DST7", + "position": "DST", + "nflTeam": "SEA", + "teamId": "team-3", + "teamName": "Team 3", + "price": 1 + } + }, + { + "id": "e643", + "type": "NOMINATION", + "ts": 1700000643000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Kupp DST8", + "position": "DST", + "nflTeam": "TB", + "openingBid": 1, + "nominatingTeamId": "team-9" + } + }, + { + "id": "e644", + "type": "SOLD", + "ts": 1700000644000, + "source": "ws", + "confidence": 1, + "payload": { + "playerName": "Rome Kupp DST8", + "position": "DST", + "nflTeam": "TB", + "teamId": "team-9", + "teamName": "Team 9", + "price": 1 + } + }, + { + "id": "e645", + "type": "DRAFT_COMPLETE", + "ts": 1700000645000, + "source": "ws", + "confidence": 1, + "payload": {} + } + ] +} \ No newline at end of file diff --git a/fantasy-auction-tracker/fixtures/sample-values.csv b/fantasy-auction-tracker/fixtures/sample-values.csv new file mode 100644 index 0000000..14223f9 --- /dev/null +++ b/fantasy-auction-tracker/fixtures/sample-values.csv @@ -0,0 +1,209 @@ +Rank,Player,Team,Position,Auction Value +1,Nico Parsons RB1,SF,RB,$54.2 +2,Amon-Ra Wilson WR1,PHI,WR,$52.4 +3,Kyren Robinson WR2,PHI,WR,$52.4 +4,Deebo Pacheco RB2,BUF,RB,$49.8 +5,Breece Odunze RB3,CIN,RB,$48.9 +6,Blake Rice WR4,TB,WR,$47.2 +7,Micah McBride WR3,NO,WR,$46.3 +8,Cooper Hall RB4,NO,RB,$44.6 +9,Chris Gibbs WR5,SF,WR,$44.6 +10,Jordan Shakir RB6,DAL,RB,$43.7 +11,Jahmyr Parsons WR6,LV,WR,$43.7 +12,Jordan Hall WR7,TB,WR,$42.8 +13,Quentin Rice RB5,DAL,RB,$41.1 +14,Zay Parsons RB7,CIN,RB,$40.2 +15,Tank Gibbs WR8,BAL,WR,$40.2 +16,Rome Collins RB8,NO,RB,$37.6 +17,Zay Hall WR10,ARI,WR,$35.9 +18,Garrett Corum WR9,TB,WR,$34.1 +19,Trey LaPorta RB9,PHI,RB,$32.4 +20,Garrett Johnston RB10,NYG,RB,$32.4 +21,Cooper Dell RB11,SEA,RB,$30.6 +22,Blake Shakir WR12,TB,WR,$30.6 +23,Deebo Tracy WR13,LAR,WR,$30.6 +24,Bijan Kincaid WR11,BAL,WR,$29.8 +25,Kyren Corum TE1,BUF,TE,$29.8 +26,Sam Kupp QB1,ARI,QB,$28.9 +27,Jaxon Parsons RB12,PHI,RB,$28.9 +28,Marvin Odunze WR14,NYG,WR,$28 +29,CeeDee Brown RB13,LAR,RB,$25.4 +30,Rome London RB15,LV,RB,$25.4 +31,Deebo Bowers RB16,TB,RB,$24.5 +32,Justin Samuel WR17,NO,WR,$24.5 +33,Kyren Rice TE2,BUF,TE,$24.5 +34,Blake Nacua QB2,LAR,QB,$23.7 +35,Breece Nabers RB14,KC,RB,$23.7 +36,Marvin Bowers WR15,ATL,WR,$23.7 +37,Trey Lamb WR18,PHI,WR,$23.7 +38,Justin Nacua WR16,SEA,WR,$22.8 +39,Chris Nacua TE3,HOU,TE,$21.9 +40,Khalil Harrison WR19,DAL,WR,$21 +41,Drake Kincaid RB17,CHI,RB,$20.2 +42,Marvin Wilson RB18,CHI,RB,$20.2 +43,Blake Samuel WR21,DET,WR,$20.2 +44,Chris Rice QB3,NYG,QB,$19.3 +45,Tyrone Corum WR23,ATL,WR,$19.3 +46,Quentin Corum TE4,CHI,TE,$19.3 +47,Rome Tracy WR20,CHI,WR,$18.4 +48,Jordan McBride WR22,HOU,WR,$17.6 +49,Chris Jefferson QB4,HOU,QB,$16.7 +50,Malik Kupp RB19,DET,RB,$16.7 +51,Bijan Nacua WR24,PHI,WR,$16.7 +52,Khalil McBride WR25,ATL,WR,$16.7 +53,Marvin Johnston WR26,CIN,WR,$16.7 +54,Amon-Ra Collins QB5,DAL,QB,$15.8 +55,Deebo McBride RB20,ARI,RB,$15.8 +56,Tyrone Pacheco TE5,SF,TE,$15.8 +57,Micah Kupp RB21,LAR,RB,$14.9 +58,Kyren Shakir RB22,SF,RB,$14.9 +59,Zay Brown WR28,CHI,WR,$14.9 +60,Trey Dell RB23,ARI,RB,$14.1 +61,Bijan Nacua RB24,ARI,RB,$14.1 +62,Jaxon Samuel WR27,NO,WR,$13.2 +63,Chris Bowers TE6,DET,TE,$13.2 +64,Puka Odunze QB6,BAL,QB,$12.3 +65,Justin Nacua QB7,PHI,QB,$12.3 +66,Micah Hall RB25,NYG,RB,$12.3 +67,Marvin Kincaid WR29,NYJ,WR,$12.3 +68,Malik Odunze WR30,CHI,WR,$12.3 +69,Rome Corum WR31,DET,WR,$12.3 +70,Jahmyr Tracy RB26,BAL,RB,$11.5 +71,Nico McBride RB27,SEA,RB,$11.5 +72,Rashee Samuel WR32,KC,WR,$11.5 +73,Rashee Gibbs WR33,NO,WR,$11.5 +74,Zay London TE7,ATL,TE,$11.5 +75,Breece Johnston WR34,PHI,WR,$10.6 +76,Deebo Smith-Njigba QB8,SEA,QB,$9.7 +77,Micah Nacua RB28,PHI,RB,$9.7 +78,Isiah Shakir RB29,NYG,RB,$9.7 +79,Tyrone McBride RB31,BAL,RB,$9.7 +80,Amon-Ra London WR35,CIN,WR,$9.7 +81,Trey Johnston WR36,SF,WR,$9.7 +82,Brock Hall TE8,LAR,TE,$9.7 +83,Cooper Pacheco RB30,KC,RB,$8.8 +84,Micah Hall RB32,NYJ,RB,$8.8 +85,Zay Parsons WR37,LAR,WR,$8.8 +86,Khalil Nacua QB9,DET,QB,$8 +87,Justin Lamb RB33,DET,RB,$8 +88,Marvin London RB34,BUF,RB,$8 +89,Rome LaPorta WR38,DAL,WR,$8 +90,Puka Gibbs WR39,ATL,WR,$8 +91,Amon-Ra Wilson WR40,DAL,WR,$8 +92,Trey Pacheco TE9,PHI,TE,$8 +93,Amon-Ra Dell TE10,MIN,TE,$8 +94,Khalil Kincaid QB10,PHI,QB,$7.1 +95,Cooper Johnston RB35,DAL,RB,$7.1 +96,Brock Lamb RB37,NYJ,RB,$7.1 +97,Brock Dell WR41,MIN,WR,$7.1 +98,Micah Nabers TE11,CIN,TE,$7.1 +99,Jordan Samuel QB11,LAR,QB,$6.2 +100,Jordan Hall RB36,CIN,RB,$6.2 +101,Tyrone Flowers RB38,ATL,RB,$6.2 +102,Bijan Jefferson RB39,LV,RB,$6.2 +103,Micah Williams RB40,CIN,RB,$6.2 +104,Puka Shakir WR42,SF,WR,$6.2 +105,Rome Brown WR43,SEA,WR,$6.2 +106,Drake Tracy WR44,LAR,WR,$6.2 +107,Sam McBride WR45,DET,WR,$6.2 +108,Nico Johnston TE12,CIN,TE,$6.2 +109,Bijan Kincaid QB12,NO,QB,$5.4 +110,Tank Harrison RB41,SEA,RB,$5.4 +111,Tank Parsons RB42,NO,RB,$5.4 +112,Garrett Harrison WR46,KC,WR,$5.4 +113,Tyrone Samuel WR47,CHI,WR,$5.4 +114,CeeDee Jefferson WR48,ARI,WR,$5.4 +115,Tank Tracy WR49,ARI,WR,$5.4 +116,Breece Nacua TE13,DAL,TE,$5.4 +117,Khalil Collins QB13,KC,QB,$4.5 +118,Nico Hall RB43,NO,RB,$4.5 +119,Garrett Pacheco RB44,NO,RB,$4.5 +120,Malik Odunze WR50,KC,WR,$4.5 +121,Jahmyr Johnston WR51,BUF,WR,$4.5 +122,Kyren Tracy WR52,PHI,WR,$4.5 +123,Malik Brown TE14,HOU,TE,$4.5 +124,CeeDee London QB14,HOU,QB,$3.6 +125,Rashee Robinson QB15,SEA,QB,$3.6 +126,Trey Brown RB45,NYJ,RB,$3.6 +127,Malik Parsons RB46,ARI,RB,$3.6 +128,Malik Tracy RB47,CHI,RB,$3.6 +129,Marvin Gibbs RB48,NYJ,RB,$3.6 +130,CeeDee Rice RB49,ARI,RB,$3.6 +131,Tyrone Pacheco RB50,NYJ,RB,$3.6 +132,Cooper Wilson WR53,NO,WR,$3.6 +133,CeeDee Flowers WR54,NYJ,WR,$3.6 +134,Malik Rice WR55,NYJ,WR,$3.6 +135,Dalton Addison WR56,BAL,WR,$3.6 +136,Tank Pacheco WR57,CHI,WR,$3.6 +137,Tyrone Dell WR58,CIN,WR,$3.6 +138,Bijan Addison TE15,ATL,TE,$3.6 +139,Micah Collins TE16,MIN,TE,$3.6 +140,Tyrone Pacheco QB16,BAL,QB,$2.7 +141,Khalil Jefferson QB17,BUF,QB,$2.7 +142,Trey London QB18,TB,QB,$2.7 +143,Kyren Williams RB51,DET,RB,$2.7 +144,Sam Shakir RB52,CHI,RB,$2.7 +145,Cooper Johnston RB53,CIN,RB,$2.7 +146,Kyren Samuel RB55,DAL,RB,$2.7 +147,Jordan Kupp WR59,DET,WR,$2.7 +148,Isiah Jefferson WR60,NO,WR,$2.7 +149,Micah Parsons WR61,DET,WR,$2.7 +150,Rome Flowers WR62,DET,WR,$2.7 +151,Jahmyr Rice WR63,MIN,WR,$2.7 +152,Jahmyr Nabers WR64,LAR,WR,$2.7 +153,Bijan Williams TE17,ARI,TE,$2.7 +154,Trey Hall DST1,ATL,DST,$2.7 +155,Dalton Tracy QB19,DET,QB,$1.9 +156,Chris London QB20,NO,QB,$1.9 +157,Puka Samuel RB54,BAL,RB,$1.9 +158,Amon-Ra Kupp RB56,ARI,RB,$1.9 +159,Kyren Rice RB57,DAL,RB,$1.9 +160,Brock Samuel RB58,NYG,RB,$1.9 +161,Rashee McBride RB59,KC,RB,$1.9 +162,Amon-Ra Collins RB60,ARI,RB,$1.9 +163,Bijan Olave WR65,CHI,WR,$1.9 +164,Blake Pacheco WR66,NO,WR,$1.9 +165,CeeDee London WR67,SF,WR,$1.9 +166,Quentin Dell WR68,LAR,WR,$1.9 +167,Jahmyr Lamb WR69,LAR,WR,$1.9 +168,Quentin Hall WR70,ARI,WR,$1.9 +169,Quentin Kincaid WR71,HOU,WR,$1.9 +170,Kyren Smith-Njigba WR72,LAR,WR,$1.9 +171,Sam Nacua TE18,SF,TE,$1.9 +172,Tyrone Robinson TE19,CIN,TE,$1.9 +173,Nico Odunze TE20,MIN,TE,$1.9 +174,Garrett Hall TE21,LV,TE,$1.9 +175,Tyrone Brown K1,BUF,K,$1.9 +176,Jordan Smith-Njigba DST2,CIN,DST,$1.9 +177,Micah Olave DST3,LV,DST,$1.9 +178,Bijan Odunze QB21,KC,QB,$1 +179,Malik Rice QB22,SF,QB,$1 +180,Amon-Ra Pacheco QB23,MIN,QB,$1 +181,Jaxon Gibbs QB24,CHI,QB,$1 +182,Malik Olave TE22,PHI,TE,$1 +183,Quentin LaPorta TE23,BUF,TE,$1 +184,Quentin Flowers TE24,NYJ,TE,$1 +185,Jaxon Lamb K2,DET,K,$1 +186,Chris Flowers K3,ATL,K,$1 +187,Tank Jefferson K4,LAR,K,$1 +188,Nico Addison K5,KC,K,$1 +189,Micah Samuel K6,CIN,K,$1 +190,Tyrone Odunze K7,SF,K,$1 +191,Zay Pacheco K8,NYJ,K,$1 +192,Amon-Ra Odunze K9,CHI,K,$1 +193,Zay Hall K10,CIN,K,$0 +194,Brock LaPorta K11,TB,K,$0 +195,Cooper Harrison K12,BUF,K,$0 +196,Malik Collins K13,TB,K,$0 +197,Puka Johnston K14,KC,K,$0 +198,Jahmyr Addison DST4,BUF,DST,$0 +199,Tank Flowers DST5,ATL,DST,$0 +200,Justin Smith-Njigba DST6,SEA,DST,$0 +201,Garrett Corum DST7,SEA,DST,$0 +202,Rome Kupp DST8,TB,DST,$0 +203,Jordan Lamb DST9,TB,DST,$0 +204,Zay Corum DST10,MIN,DST,$0 +205,Trey Flowers DST11,MIN,DST,$0 +206,Drake Lamb DST12,LV,DST,$0 +207,Chris Brown DST13,CHI,DST,$0 +208,Kyren Robinson DST14,PHI,DST,$0 \ No newline at end of file diff --git a/fantasy-auction-tracker/manifest.json b/fantasy-auction-tracker/manifest.json new file mode 100644 index 0000000..cdd89d4 --- /dev/null +++ b/fantasy-auction-tracker/manifest.json @@ -0,0 +1,56 @@ +{ + "manifest_version": 3, + "name": "Auction Draft Tracker", + "version": "0.1.0", + "description": "Real-time tracking and analytics for fantasy football auction drafts: prices, budgets, roster needs, inflation and bid guidance.", + + "browser_specific_settings": { + "gecko": { + "id": "auction-tracker@localhost", + "strict_min_version": "128.0" + } + }, + + "permissions": ["storage", "activeTab"], + + "host_permissions": [ + "*://*.fantasy.nfl.com/*", + "*://*.cbssports.com/*" + ], + + "background": { + "scripts": ["src/background/background.js"], + "type": "module" + }, + + "sidebar_action": { + "default_title": "Auction Tracker", + "default_panel": "src/sidebar/sidebar.html" + }, + + "content_scripts": [ + { + "matches": [ + "*://*.fantasy.nfl.com/*", + "*://*.cbssports.com/*" + ], + "js": ["src/content/content.js"], + "run_at": "document_idle", + "all_frames": false + } + ], + + "web_accessible_resources": [ + { + "resources": [ + "src/content/inject.js", + "src/adapters/*.js", + "src/core/*.js" + ], + "matches": [ + "*://*.fantasy.nfl.com/*", + "*://*.cbssports.com/*" + ] + } + ] +} diff --git a/fantasy-auction-tracker/package.json b/fantasy-auction-tracker/package.json new file mode 100644 index 0000000..a22a39f --- /dev/null +++ b/fantasy-auction-tracker/package.json @@ -0,0 +1,16 @@ +{ + "name": "fantasy-auction-tracker", + "version": "0.1.0", + "private": true, + "description": "Firefox add-on that tracks a fantasy football auction draft in real time and layers analytics on top.", + "type": "module", + "scripts": { + "test": "node --test test/*.test.js", + "replay": "node tools/replay.js", + "package": "cd . && zip -r -FS ../auction-tracker.zip manifest.json src -x '*.DS_Store'" + }, + "license": "MIT", + "engines": { + "node": ">=20" + } +} diff --git a/fantasy-auction-tracker/src/adapters/base.js b/fantasy-auction-tracker/src/adapters/base.js new file mode 100644 index 0000000..0e27441 --- /dev/null +++ b/fantasy-auction-tracker/src/adapters/base.js @@ -0,0 +1,102 @@ +/** + * Adapter contract. + * + * An adapter's only job is to turn "whatever this draft site does" into the + * event vocabulary in core/events.js. It must never compute analytics, and it + * must never assume it saw everything -- a missed bid is survivable, a missed + * or wrong SOLD is not, so sales carry an explicit confidence. + * + * Detection strategy, best first: + * 'ws' - intercepted WebSocket frames. Structured, exact, instant. + * 'dom' - MutationObserver over the rendered draft board. Reliable enough, + * breaks whenever the site reskins. + * 'ocr' - last resort for canvas-rendered rooms. Always low confidence. + */ + +export const Strategy = { WS: 'ws', DOM: 'dom', OCR: 'ocr' }; + +/** Confidence floors by strategy; adapters may lower but should not raise. */ +export const STRATEGY_CONFIDENCE = { + [Strategy.WS]: 1.0, + [Strategy.DOM]: 0.85, + [Strategy.OCR]: 0.55, +}; + +export class Adapter { + /** + * @param {(event: object) => void} emit - hand a built event to the pipeline + * @param {object} [options] + */ + constructor(emit, options = {}) { + this.emit = emit; + this.options = options; + this.strategy = null; + this.disposers = []; + } + + /** Human-readable id, e.g. 'nfl'. Overridden by subclasses. */ + static get id() { return 'base'; } + + /** Does this adapter handle the given location? */ + static matches(_url) { return false; } + + /** Begin observing. Subclasses override. */ + async start() { throw new Error('not implemented'); } + + stop() { + for (const dispose of this.disposers.splice(0)) { + try { dispose(); } catch { /* teardown must not throw */ } + } + } + + track(dispose) { this.disposers.push(dispose); } + + confidenceFor(strategy, penalty = 0) { + return Math.max(0, (STRATEGY_CONFIDENCE[strategy] ?? 0.5) - penalty); + } +} + +/** Pull the first integer out of a string like "$47" or "Sold for 47". */ +export function parseMoney(text) { + if (text == null) return null; + const m = /-?\d+(?:\.\d+)?/.exec(String(text).replace(/,/g, '')); + return m ? Number(m[0]) : null; +} + +/** Debounce noisy DOM callbacks; auctions fire dozens of mutations per bid. */ +export function debounce(fn, ms = 120) { + let timer = null; + const wrapped = (...args) => { + clearTimeout(timer); + timer = setTimeout(() => fn(...args), ms); + }; + wrapped.cancel = () => clearTimeout(timer); + return wrapped; +} + +/** + * Observe a subtree and call back on any change. Returns a disposer. + * Waits for the node to exist, since draft rooms mount asynchronously. + */ +export function observe(root, callback, options = {}) { + const observer = new MutationObserver(callback); + observer.observe(root, { + childList: true, subtree: true, characterData: true, ...options, + }); + return () => observer.disconnect(); +} + +/** Poll for a selector to appear, resolving with the node or null on timeout. */ +export function waitFor(selector, { timeout = 30000, interval = 250, root = document } = {}) { + return new Promise((resolve) => { + const existing = root.querySelector(selector); + if (existing) return resolve(existing); + const started = Date.now(); + const timer = setInterval(() => { + const node = root.querySelector(selector); + if (node) { clearInterval(timer); resolve(node); } + else if (Date.now() - started > timeout) { clearInterval(timer); resolve(null); } + }, interval); + return undefined; + }); +} diff --git a/fantasy-auction-tracker/src/adapters/generic-dom.js b/fantasy-auction-tracker/src/adapters/generic-dom.js new file mode 100644 index 0000000..bc4069a --- /dev/null +++ b/fantasy-auction-tracker/src/adapters/generic-dom.js @@ -0,0 +1,164 @@ +/** + * Configuration-driven DOM adapter. + * + * Rather than hand-writing a class per site, most draft rooms can be described + * by a selector map. That means retargeting to a new platform -- or repairing + * one after a site redesign -- is a data change, not a code change, which + * matters when the site can change the week before your draft. + * + * A profile looks like: + * + * { + * id: 'example', + * match: /example\.com\/draft/, + * selectors: { + * nomination: '.auction-player', // container for the live player + * nomName: '.player-name', + * nomPosition: '.player-pos', + * nomTeam: '.player-nfl-team', + * highBid: '.current-bid', + * highBidder: '.high-bidder', + * resultRow: '.draft-results tr', // one row per completed sale + * rowPlayer: '.name', + * rowPosition: '.pos', + * rowPrice: '.price', + * rowTeam: '.owner', + * teamRow: '.team-budgets .team', + * teamName: '.team-name', + * teamBudget: '.budget-left', + * }, + * } + */ + +import { EventType, makeEvent } from '../core/events.js'; +import { Adapter, Strategy, parseMoney, debounce, observe, waitFor } from './base.js'; + +const text = (root, selector) => { + if (!selector) return null; + const node = root.querySelector(selector); + return node ? node.textContent.trim().replace(/\s+/g, ' ') : null; +}; + +export class GenericDomAdapter extends Adapter { + constructor(emit, options = {}) { + super(emit, options); + this.profile = options.profile; + this.strategy = Strategy.DOM; + this.lastNomination = null; + this.lastBid = null; + this.seenRows = new Set(); + } + + static get id() { return 'generic-dom'; } + + async start() { + const { selectors } = this.profile; + const anchor = await waitFor(selectors.root ?? 'body'); + if (!anchor) { + this.emit(makeEvent(EventType.CORRECTION, { + targetId: 'n/a', + patch: {}, + note: 'draft room never mounted; adapter idle', + }, { source: 'dom' })); + return; + } + + const scan = debounce(() => this.scan(), this.options.debounceMs ?? 120); + this.track(observe(document.body, scan)); + this.track(() => scan.cancel()); + this.scan(); + } + + scan() { + try { + this.scanNomination(); + this.scanResults(); + this.scanTeams(); + } catch (err) { + // A selector drift must not kill the observer -- degrade, don't die. + console.warn('[auction-tracker] scan failed', err); + } + } + + scanNomination() { + const s = this.profile.selectors; + const root = s.nomination ? document.querySelector(s.nomination) : null; + if (!root) { + if (this.lastNomination) { + this.lastNomination = null; + this.lastBid = null; + } + return; + } + + const name = text(root, s.nomName); + const position = text(root, s.nomPosition); + if (!name || !position) return; + + const signature = `${name}|${position}`; + if (signature !== this.lastNomination) { + this.lastNomination = signature; + this.lastBid = null; + this.emit(makeEvent(EventType.NOMINATION, { + playerName: name, + position, + nflTeam: text(root, s.nomTeam), + openingBid: parseMoney(text(root, s.highBid)) ?? undefined, + nominatingTeamId: text(root, s.highBidder) ?? undefined, + }, { source: 'dom', confidence: this.confidenceFor(Strategy.DOM) })); + } + + const amount = parseMoney(text(root, s.highBid)); + const bidder = text(root, s.highBidder); + if (amount != null && bidder && `${amount}|${bidder}` !== this.lastBid) { + this.lastBid = `${amount}|${bidder}`; + this.emit(makeEvent(EventType.BID, { + amount, teamId: bidder, + }, { source: 'dom', confidence: this.confidenceFor(Strategy.DOM) })); + } + } + + scanResults() { + const s = this.profile.selectors; + if (!s.resultRow) return; + + for (const row of document.querySelectorAll(s.resultRow)) { + const name = text(row, s.rowPlayer); + const price = parseMoney(text(row, s.rowPrice)); + const teamId = text(row, s.rowTeam); + if (!name || price == null || !teamId) continue; + + const signature = `${name}|${teamId}|${price}`; + if (this.seenRows.has(signature)) continue; + this.seenRows.add(signature); + + // A results row lacking a position is still a sale worth recording, but + // it cannot be matched to a valuation, so drop confidence to flag it. + const position = text(row, s.rowPosition); + this.emit(makeEvent(EventType.SOLD, { + playerName: name, + position: position ?? 'UNK', + nflTeam: text(row, s.rowNflTeam), + teamId, + teamName: teamId, + price, + }, { + source: 'dom', + confidence: this.confidenceFor(Strategy.DOM, position ? 0 : 0.2), + })); + } + } + + scanTeams() { + const s = this.profile.selectors; + if (!s.teamRow) return; + for (const row of document.querySelectorAll(s.teamRow)) { + const name = text(row, s.teamName); + if (!name) continue; + this.emit(makeEvent(EventType.TEAM_REGISTERED, { + teamId: name, + teamName: name, + }, { source: 'dom', confidence: this.confidenceFor(Strategy.DOM) })); + } + } +} diff --git a/fantasy-auction-tracker/src/adapters/profiles.js b/fantasy-auction-tracker/src/adapters/profiles.js new file mode 100644 index 0000000..b2fb4b4 --- /dev/null +++ b/fantasy-auction-tracker/src/adapters/profiles.js @@ -0,0 +1,99 @@ +/** + * Site profiles for the generic DOM adapter. + * + * IMPORTANT -- these selectors are PROVISIONAL. They were written without + * access to a live auction draft room, and every fantasy platform reskins its + * draft app between seasons. Do not trust them on draft day. + * + * Calibrate before you rely on any of this: + * 1. open the draft room (a mock draft is fine, and is the only safe way to + * test this) + * 2. run `tools/calibrate.js` in the page console -- it dumps candidate + * selectors for player names, prices and team rows + * 3. paste the corrected selectors here, or set them at runtime from the + * sidebar's Advanced panel, which writes an override to storage + * + * `wsHints` lists substrings that identify the draft WebSocket, and the JSON + * paths the tap should look for. Getting these right is worth far more than + * perfecting the selectors -- the WS layer is exact. + */ + +export const PROFILES = [ + { + id: 'nfl', + label: 'NFL.com Fantasy', + match: /(^|\.)fantasy\.nfl\.com$/, + urlHint: /draft/i, + selectors: { + root: '#draftBoard, .draftContainer, body', + nomination: '.currentPlayer, .nominatedPlayer, .auctionNomination', + nomName: '.playerName, .playerNameFull, a.playerName', + nomPosition: '.playerPosition, em', + nomTeam: '.playerTeam', + highBid: '.currentBid, .bidAmount, .auctionCurrentBid', + highBidder: '.highBidder, .currentBidder, .bidTeam', + resultRow: '#draftResults tr, .draftResultsTable tr', + rowPlayer: '.playerName, td.player a', + rowPosition: '.playerPosition, em', + rowPrice: '.auctionPrice, td.price', + rowTeam: '.teamName, td.team', + teamRow: '.auctionBudgets tr, .teamBudgetRow', + teamName: '.teamName', + teamBudget: '.budgetRemaining, .remaining', + }, + wsHints: { + urlContains: ['fantasy.nfl.com', '/draft', 'socket'], + messageKeys: ['playerId', 'auctionAmount', 'teamId', 'eventType'], + }, + }, + { + id: 'cbs', + label: 'CBS Sports Fantasy', + match: /(^|\.)cbssports\.com$/, + urlHint: /draft|auction/i, + selectors: { + root: '.draft-room, #draftRoom, body', + nomination: '.auction-nomination, .nominated-player, .current-player', + nomName: '.player-name, .playerLink', + nomPosition: '.player-position, .position', + nomTeam: '.player-team, .proTeam', + highBid: '.current-bid, .high-bid, .bid-amount', + highBidder: '.high-bidder, .bidding-team', + resultRow: '.draft-results tr, .results-row', + rowPlayer: '.player-name', + rowPosition: '.player-position, .position', + rowPrice: '.bid-amount, .price, .salary', + rowTeam: '.team-name, .owner', + teamRow: '.team-budgets .team-row, .budget-row', + teamName: '.team-name', + teamBudget: '.budget-remaining, .remaining-salary', + }, + wsHints: { + urlContains: ['cbssports.com', 'draft', 'socket', 'stream'], + messageKeys: ['playerId', 'amount', 'teamId', 'type'], + }, + }, + { + // Fallback: matches nothing automatically. Point it at any site by setting + // selectors from the sidebar's Advanced panel. + id: 'custom', + label: 'Custom (user-configured)', + match: /$^/, + selectors: {}, + wsHints: { urlContains: [], messageKeys: [] }, + }, +]; + +export function profileFor(url, overrides = {}) { + let host; + try { host = new URL(url).hostname; } catch { return null; } + + const base = PROFILES.find((p) => p.match.test(host)); + if (!base) return overrides.selectors ? { ...PROFILES.at(-1), ...overrides } : null; + + return { + ...base, + selectors: { ...base.selectors, ...(overrides.selectors ?? {}) }, + wsHints: { ...base.wsHints, ...(overrides.wsHints ?? {}) }, + }; +} diff --git a/fantasy-auction-tracker/src/adapters/ws.js b/fantasy-auction-tracker/src/adapters/ws.js new file mode 100644 index 0000000..33e1508 --- /dev/null +++ b/fantasy-auction-tracker/src/adapters/ws.js @@ -0,0 +1,231 @@ +/** + * WebSocket adapter. + * + * Consumes frames mirrored out of the page by content/inject.js. Because the + * on-the-wire schema differs per platform (and is undocumented), this works in + * two modes: + * + * RECORD - no mapping configured yet. Frames are stored raw so you can run + * a mock draft, export the capture, and derive an exact mapping. + * This is the intended first run against any new platform. + * MAP - a mapping is configured. Frames are translated into draft events + * with full confidence. + * + * The heuristic sniffer in `guessMapping` is a convenience for building that + * mapping from a capture; it is not trusted to run unattended, because a + * wrongly-guessed price field is worse than no data at all. + */ + +import { EventType, makeEvent } from '../core/events.js'; +import { Adapter, Strategy } from './base.js'; + +/** Read a dotted path, tolerating arrays: 'payload.player.name'. */ +export function get(obj, path) { + if (!path) return undefined; + return path.split('.').reduce((acc, key) => { + if (acc == null) return undefined; + return Array.isArray(acc) && /^\d+$/.test(key) ? acc[Number(key)] : acc[key]; + }, obj); +} + +/** + * A mapping describes how to read one platform's frames. + * + * { + * typePath: 'type', + * types: { nomination: ['NOMINATE'], bid: ['BID'], sold: ['SOLD','WON'] }, + * paths: { + * playerName: 'player.fullName', + * position: 'player.position', + * nflTeam: 'player.proTeam', + * teamId: 'teamId', + * amount: 'amount', + * }, + * } + */ +export class WebSocketAdapter extends Adapter { + constructor(emit, options = {}) { + super(emit, options); + this.strategy = Strategy.WS; + this.mapping = options.mapping ?? null; + this.hints = options.wsHints ?? { urlContains: [] }; + /** Raw capture, used in RECORD mode and for post-draft debugging. */ + this.capture = []; + this.maxCapture = options.maxCapture ?? 5000; + this.onFrame = options.onFrame ?? null; + } + + static get id() { return 'ws'; } + + get mode() { return this.mapping ? 'MAP' : 'RECORD'; } + + /** Is this socket plausibly the draft feed rather than ads/telemetry? */ + relevant(url) { + const hints = this.hints.urlContains ?? []; + if (!hints.length) return true; + return hints.some((h) => url.includes(h)); + } + + handleFrame({ url, body }) { + if (!this.relevant(url)) return; + + let parsed; + try { parsed = JSON.parse(body); } catch { return; } + + if (this.capture.length < this.maxCapture) { + this.capture.push({ url, body }); + } + this.onFrame?.({ url, parsed }); + + if (!this.mapping) return; // RECORD mode: observe only. + + // Frames often batch several updates. + const items = Array.isArray(parsed) ? parsed : [parsed]; + for (const item of items) this.translate(item); + } + + translate(msg) { + const { typePath, types, paths } = this.mapping; + const rawType = String(get(msg, typePath) ?? '').toUpperCase(); + if (!rawType) return; + + const matches = (names) => (names ?? []).some((n) => rawType.includes(n.toUpperCase())); + const conf = this.confidenceFor(Strategy.WS); + + const playerName = get(msg, paths.playerName); + const position = get(msg, paths.position); + const teamId = get(msg, paths.teamId); + const amount = Number(get(msg, paths.amount)); + + if (matches(types.sold)) { + if (!playerName || !teamId || !Number.isFinite(amount)) return; + this.emit(makeEvent(EventType.SOLD, { + playerName: String(playerName), + position: String(position ?? 'UNK'), + nflTeam: get(msg, paths.nflTeam) ?? null, + teamId: String(teamId), + teamName: String(get(msg, paths.teamName) ?? teamId), + price: amount, + }, { source: 'ws', confidence: conf })); + return; + } + + if (matches(types.nomination)) { + if (!playerName) return; + this.emit(makeEvent(EventType.NOMINATION, { + playerName: String(playerName), + position: String(position ?? 'UNK'), + nflTeam: get(msg, paths.nflTeam) ?? null, + openingBid: Number.isFinite(amount) ? amount : undefined, + nominatingTeamId: teamId != null ? String(teamId) : undefined, + }, { source: 'ws', confidence: conf })); + return; + } + + if (matches(types.bid)) { + if (!Number.isFinite(amount) || teamId == null) return; + this.emit(makeEvent(EventType.BID, { + amount, teamId: String(teamId), + }, { source: 'ws', confidence: conf })); + } + } + + exportCapture() { + return { version: 1, frames: this.capture }; + } + + async start() { + // Frames are pushed in by content.js; nothing to poll. + } +} + +/** + * Suggest a mapping from a recorded capture. + * + * Scores candidate paths by how well they behave like the field in question: + * a price field is a small positive integer that changes often; a player name + * is a two-word string; a type field is a short repeated enum. Output is a + * starting point for a human to confirm, never something to ship unreviewed. + */ +export function guessMapping(frames) { + const paths = new Map(); // path -> { values:Set, samples:[] } + + const walk = (obj, prefix = '') => { + if (obj == null || typeof obj !== 'object') return; + for (const [key, value] of Object.entries(obj)) { + const path = prefix ? `${prefix}.${key}` : key; + if (value && typeof value === 'object') { + walk(value, path); + } else { + if (!paths.has(path)) paths.set(path, { values: new Set(), samples: [] }); + const entry = paths.get(path); + entry.values.add(value); + if (entry.samples.length < 20) entry.samples.push(value); + } + } + }; + + for (const frame of frames) { + try { walk(JSON.parse(frame.body ?? frame)); } catch { /* skip */ } + } + + const score = (predicate) => [...paths.entries()] + .map(([path, entry]) => ({ path, entry, score: predicate(entry, path) })) + .filter((c) => c.score > 0) + .sort((a, b) => b.score - a.score); + + const isEnum = (e, path) => { + const strings = [...e.values].filter((v) => typeof v === 'string'); + if (strings.length !== e.values.size || e.values.size === 0) return 0; + const short = strings.every((s) => s.length <= 32 && !s.includes(' ')); + const repeated = e.samples.length > e.values.size; + return (short ? 2 : 0) + (repeated ? 2 : 0) + (/type|event|action|kind/i.test(path) ? 3 : 0); + }; + + const isName = (e, path) => { + const strings = [...e.values].filter((v) => typeof v === 'string'); + if (!strings.length) return 0; + const nameish = strings.filter((s) => /^[A-Z][a-z'’.-]+ [A-Z]/.test(s)).length / strings.length; + return nameish * 5 + (/name|player|full/i.test(path) ? 3 : 0); + }; + + const isMoney = (e, path) => { + const nums = [...e.values].filter((v) => typeof v === 'number'); + if (nums.length !== e.values.size || !nums.length) return 0; + const plausible = nums.filter((n) => Number.isInteger(n) && n >= 0 && n <= 400).length / nums.length; + return plausible * 4 + (/amount|bid|price|cost|value|salary/i.test(path) ? 3 : 0); + }; + + const isPosition = (e) => { + const strings = [...e.values].filter((v) => typeof v === 'string'); + if (!strings.length) return 0; + const posish = strings.filter((s) => /^(QB|RB|WR|TE|K|DEF|DST|D\/ST)$/i.test(s.trim())).length; + return (posish / strings.length) * 10; + }; + + const isTeamId = (e, path) => (/team|owner|franchise|roster/i.test(path) && e.values.size <= 32 ? 4 : 0); + + const top = (list) => list[0]?.path ?? null; + + return { + typePath: top(score(isEnum)), + types: { + nomination: ['NOMINAT'], + bid: ['BID'], + sold: ['SOLD', 'WON', 'COMPLETE'], + }, + paths: { + playerName: top(score(isName)), + position: top(score(isPosition)), + teamId: top(score(isTeamId)), + amount: top(score(isMoney)), + }, + candidates: { + type: score(isEnum).slice(0, 5).map((c) => c.path), + playerName: score(isName).slice(0, 5).map((c) => c.path), + position: score(isPosition).slice(0, 5).map((c) => c.path), + teamId: score(isTeamId).slice(0, 5).map((c) => c.path), + amount: score(isMoney).slice(0, 5).map((c) => c.path), + }, + }; +} diff --git a/fantasy-auction-tracker/src/background/background.js b/fantasy-auction-tracker/src/background/background.js new file mode 100644 index 0000000..f3b7ed9 --- /dev/null +++ b/fantasy-auction-tracker/src/background/background.js @@ -0,0 +1,152 @@ +/** + * Background event page. + * + * Owns the single source of truth: the draft store and the loaded valuations. + * The content script feeds it events; the sidebar reads snapshots from it. + * Keeping state here (not in the sidebar or the page) means a draft-room + * refresh, a sidebar close, or a tab crash loses nothing. + */ + +import { DraftStore } from '../core/store.js'; +import { EventType } from '../core/events.js'; +import { snapshot } from '../core/analytics.js'; +import { loadValuations, rescaleToLeague } from '../core/valuations.js'; +import { DEFAULT_CONFIG } from '../core/config.js'; + +const STORAGE_KEY = 'draft:current'; + +let store = new DraftStore({ config: DEFAULT_CONFIG }); +let valuations = []; +let status = { profileId: null, tap: false, lastEventAt: null, source: null }; + +/** Persist on a timer rather than per-event; a hot auction fires in bursts. */ +let saveTimer = null; +function schedulePersist() { + clearTimeout(saveTimer); + saveTimer = setTimeout(async () => { + try { + await browser.storage.local.set({ [STORAGE_KEY]: store.serialize() }); + } catch (err) { + console.error('[auction-tracker] persist failed', err); + } + }, 500); +} + +async function restore() { + const saved = await browser.storage.local.get([STORAGE_KEY, 'valuationsCsv', 'config']); + const config = { ...DEFAULT_CONFIG, ...(saved.config ?? {}) }; + + if (saved[STORAGE_KEY]?.log?.length) { + store = DraftStore.deserialize({ config, log: saved[STORAGE_KEY].log }); + } else { + store = new DraftStore({ config }); + } + + if (saved.valuationsCsv) applyValuations(saved.valuationsCsv, config); + store.subscribe(() => schedulePersist()); +} + +function applyValuations(csv, config = store.baseConfig) { + const { players, problems } = loadValuations(csv); + valuations = rescaleToLeague(players, config); + store.emit(EventType.VALUATIONS_LOADED, { source: 'csv', count: players.length }); + return { count: players.length, problems }; +} + +function currentSnapshot() { + return { + ...snapshot(store.state, valuations, { + aggressiveness: store.baseConfig.aggressiveness ?? 0.5, + }), + config: store.baseConfig, + valuationCount: valuations.length, + logLength: store.log.length, + status, + }; +} + +browser.runtime.onMessage.addListener((msg, sender) => { + switch (msg?.kind) { + case 'draft-event': { + const result = store.append(msg.event); + if (result.ok) { + status.lastEventAt = Date.now(); + status.source = msg.event.source; + broadcast(); + } + return Promise.resolve(result); + } + + case 'ws-seen': + status.tap = true; + return Promise.resolve({ ok: true }); + + case 'adapter-status': + status.profileId = msg.status; + status.tabId = sender?.tab?.id ?? null; + broadcast(); + return Promise.resolve({ ok: true }); + + case 'get-snapshot': + return Promise.resolve(currentSnapshot()); + + case 'get-log': + return Promise.resolve(store.serialize()); + + case 'load-valuations': { + const result = applyValuations(msg.csv); + browser.storage.local.set({ valuationsCsv: msg.csv }); + broadcast(); + return Promise.resolve(result); + } + + case 'set-config': { + const config = { ...store.baseConfig, ...msg.config }; + store.baseConfig = config; + store.emit(EventType.LEAGUE_CONFIGURED, config, { source: 'manual' }); + // Values are scaled to league size, so a config change invalidates them. + browser.storage.local.get('valuationsCsv').then(({ valuationsCsv }) => { + if (valuationsCsv) applyValuations(valuationsCsv, config); + browser.storage.local.set({ config }); + broadcast(); + }); + return Promise.resolve({ ok: true }); + } + + case 'manual-event': { + const result = store.emit(msg.type, msg.payload, { source: 'manual' }); + broadcast(); + return Promise.resolve(result); + } + + case 'retract': { + const result = store.retract(msg.targetId, msg.reason); + broadcast(); + return Promise.resolve(result); + } + + case 'correct': { + const result = store.correct(msg.targetId, msg.patch); + broadcast(); + return Promise.resolve(result); + } + + case 'reset': + store = new DraftStore({ config: store.baseConfig }); + store.subscribe(() => schedulePersist()); + browser.storage.local.remove(STORAGE_KEY); + broadcast(); + return Promise.resolve({ ok: true }); + + default: + return undefined; + } +}); + +/** Push a fresh snapshot to any open sidebar. */ +function broadcast() { + browser.runtime.sendMessage({ kind: 'snapshot', snapshot: currentSnapshot() }) + .catch(() => { /* no sidebar open */ }); +} + +restore().catch((err) => console.error('[auction-tracker] restore failed', err)); diff --git a/fantasy-auction-tracker/src/content/content.js b/fantasy-auction-tracker/src/content/content.js new file mode 100644 index 0000000..9c1957f --- /dev/null +++ b/fantasy-auction-tracker/src/content/content.js @@ -0,0 +1,100 @@ +/** + * Content script. + * + * Runs in the isolated world on the draft page. Responsibilities: + * - inject the page-world WebSocket tap + * - relay tapped frames to the WS adapter + * - run the DOM adapter as a parallel fallback + * - forward every produced event to the background page + * + * Both adapters run at once on purpose. The store deduplicates, so the DOM + * layer silently covers anything the WS mapping misses instead of leaving a + * hole you only notice after the draft. + */ + +(async () => { + const CHANNEL = '__auction_tracker__'; + const url = (path) => browser.runtime.getURL(path); + + const [{ profileFor }, { GenericDomAdapter }, { WebSocketAdapter }] = await Promise.all([ + import(url('src/adapters/profiles.js')), + import(url('src/adapters/generic-dom.js')), + import(url('src/adapters/ws.js')), + ]); + + const { overrides = {}, mapping = null, enabled = true } = + await browser.storage.local.get(['overrides', 'mapping', 'enabled']); + + if (enabled === false) return; + + const profile = profileFor(window.location.href, overrides); + if (!profile) { + browser.runtime.sendMessage({ + kind: 'adapter-status', + status: 'no-profile', + href: window.location.href, + }); + return; + } + + const send = (event) => { + browser.runtime.sendMessage({ kind: 'draft-event', event }).catch(() => { + // Background may be asleep between events; the next send wakes it. + }); + }; + + // --- WebSocket layer ----------------------------------------------------- + const wsAdapter = new WebSocketAdapter(send, { + mapping, + wsHints: profile.wsHints, + onFrame: ({ url: frameUrl }) => { + browser.runtime.sendMessage({ kind: 'ws-seen', url: frameUrl }).catch(() => {}); + }, + }); + await wsAdapter.start(); + + window.addEventListener('message', (ev) => { + if (ev.source !== window) return; + const data = ev.data; + if (!data || data.channel !== CHANNEL) return; + + if (data.kind === 'ws-message' || data.kind === 'http-response') { + wsAdapter.handleFrame(data.detail); + } else if (data.kind === 'tap-ready') { + browser.runtime.sendMessage({ kind: 'adapter-status', status: 'tap-ready' }).catch(() => {}); + } + }); + + // Inject into the page world. A + + diff --git a/fantasy-auction-tracker/src/sidebar/sidebar.js b/fantasy-auction-tracker/src/sidebar/sidebar.js new file mode 100644 index 0000000..bd92fe0 --- /dev/null +++ b/fantasy-auction-tracker/src/sidebar/sidebar.js @@ -0,0 +1,327 @@ +/** + * Sidebar UI. + * + * Pure view layer: it renders snapshots pushed by the background page and + * sends user intent back. No analytics live here, so the numbers on screen are + * always the same numbers the tests cover. + */ + +import { EventType } from '../core/events.js'; + +const $ = (sel) => document.querySelector(sel); +const el = (tag, props = {}, children = []) => { + const node = Object.assign(document.createElement(tag), props); + for (const child of [].concat(children)) { + node.append(child instanceof Node ? child : document.createTextNode(String(child))); + } + return node; +}; +const money = (n) => (n == null || Number.isNaN(n) ? '—' : `$${Math.round(n)}`); + +let latest = null; + +// --- tabs ------------------------------------------------------------------- +for (const tab of document.querySelectorAll('.tab')) { + tab.addEventListener('click', () => { + for (const t of document.querySelectorAll('.tab')) t.classList.toggle('active', t === tab); + for (const p of document.querySelectorAll('.panel')) { + p.classList.toggle('active', p.id === `panel-${tab.dataset.panel}`); + } + }); +} + +// --- render ----------------------------------------------------------------- +function renderStatus(snap) { + const node = $('#status'); + const age = snap.status.lastEventAt ? Date.now() - snap.status.lastEventAt : null; + if (!snap.logLength) { + node.textContent = 'waiting for draft'; + node.className = 'status'; + } else if (snap.status.source === 'ws') { + node.textContent = 'live (websocket)'; + node.className = 'status live'; + } else { + node.textContent = `live (${snap.status.source ?? 'dom'})`; + node.className = 'status degraded'; + } + node.title = age != null ? `last event ${Math.round(age / 1000)}s ago` : 'no events yet'; +} + +function renderAdvice(snap) { + const box = $('#advice'); + box.textContent = ''; + const a = snap.advice; + + if (!a) { + box.append(el('p', { className: 'empty' }, 'Nothing on the block.')); + return; + } + + const explain = { + bid: 'Below your inflation-adjusted value — this is profit.', + stretch: 'Above par but inside your cliff allowance. Only if you want the player.', + pass: 'At or past your ceiling. Let it go.', + unvalued: 'No valuation for this player. Bid on your own read.', + 'no-slot': 'No open roster slot accepts this position.', + }[a.verdict]; + + box.append( + el('div', {}, [ + el('span', { className: 'player' }, a.player), + ' ', + el('span', { className: 'pos' }, a.position), + ' ', + el('span', { className: `verdict ${a.verdict}` }, a.verdict), + ]), + el('div', { className: 'numbers' }, [ + el('div', {}, [el('span', { className: 'k' }, 'On board'), el('span', { className: 'v' }, money(a.currentBid))]), + el('div', {}, [el('span', { className: 'k' }, 'Walk away'), el('span', { className: 'v' }, money(a.walkAway))]), + el('div', {}, [el('span', { className: 'k' }, 'Ceiling'), el('span', { className: 'v' }, money(a.ceiling))]), + ]), + el('p', { className: 'hint' }, explain ?? ''), + el('p', { className: 'hint' }, + `par ${money(a.parValue)} × inflation ${a.inflation} · cliff ${money(a.tierCliff)} to ${a.nextBest ?? 'nobody'} · ` + + `${a.threats} rival${a.threats === 1 ? '' : 's'} can outbid` + + (a.topRival ? ` (max ${money(a.topRival.maxBid)}, ${a.topRival.teamName})` : '') + + (a.myMaxBid != null ? ` · your max ${money(a.myMaxBid)}` : '')), + ); +} + +function renderHistory(snap) { + const body = $('#history tbody'); + body.textContent = ''; + for (const sale of snap.history) { + const par = sale.parValue; + const delta = par != null ? sale.price - par : null; + body.append(el('tr', {}, [ + el('td', { title: sale.name }, sale.name), + el('td', {}, sale.position ?? '—'), + el('td', { title: sale.teamId }, sale.teamId), + el('td', { className: 'num' }, money(sale.price)), + el('td', { className: `num ${delta > 0 ? 'over' : 'under'}` }, + delta == null ? '—' : `${delta > 0 ? '+' : ''}${Math.round(delta)}`), + ])); + } +} + +function renderTeams(snap) { + const body = $('#teams tbody'); + body.textContent = ''; + const sorted = [...snap.teams].sort((a, b) => b.maxBid - a.maxBid); + for (const t of sorted) { + const needs = Object.entries(t.needs) + .filter(([slot]) => slot !== 'BN') + .map(([slot, n]) => (n > 1 ? `${slot}×${n}` : slot)) + .join(' '); + body.append(el('tr', { + className: [ + t.teamId === snap.config.myTeamId ? 'me' : '', + t.maxBid <= 1 ? 'broke' : '', + ].filter(Boolean).join(' '), + }, [ + el('td', { title: t.teamName }, t.teamName), + el('td', { className: 'num' }, money(t.remaining)), + el('td', { className: 'num' }, String(t.openSlots)), + el('td', { className: 'num' }, money(t.maxBid)), + el('td', { title: needs }, needs || '—'), + ])); + } +} + +function renderMarket(snap) { + const inf = snap.inflation; + const box = $('#inflation'); + box.textContent = ''; + const rate = inf.discretionary; + const reading = rate > 1.05 + ? 'Money is chasing fewer players — expect to overpay from here.' + : rate < 0.95 + ? 'Value is outrunning money — bargains ahead, stay patient.' + : 'Market is near par.'; + box.append( + el('div', { className: 'numbers' }, [ + el('div', {}, [el('span', { className: 'k' }, 'Inflation'), el('span', { className: 'v' }, rate.toFixed(2))]), + el('div', {}, [el('span', { className: 'k' }, '$ left'), el('span', { className: 'v' }, money(inf.remainingMoney))]), + el('div', {}, [el('span', { className: 'k' }, 'Spots left'), el('span', { className: 'v' }, String(inf.openSpots))]), + ]), + el('p', { className: 'hint' }, reading), + el('p', { className: 'hint' }, + `${money(Math.abs(inf.surplusSpent))} ${inf.surplusSpent >= 0 ? 'over' : 'under'} par spent so far.`), + ); + + const scarcityBody = $('#scarcity tbody'); + scarcityBody.textContent = ''; + for (const [pos, s] of Object.entries(snap.scarcity)) { + if (!s.available && !s.openStarterSlots) continue; + scarcityBody.append(el('tr', {}, [ + el('td', {}, pos), + el('td', { className: 'num' }, String(s.startable)), + el('td', { className: 'num' }, String(s.openStarterSlots)), + el('td', { className: `num ${s.ratio < 1 ? 'over' : ''}` }, + Number.isFinite(s.ratio) ? s.ratio.toFixed(2) : '∞'), + el('td', { title: s.topAvailable.map((p) => p.name).join(', ') }, + s.topAvailable[0]?.name ?? '—'), + ])); + } + + const pressureBody = $('#pressure tbody'); + pressureBody.textContent = ''; + for (const t of snap.pressure) { + pressureBody.append(el('tr', {}, [ + el('td', {}, t.teamName), + el('td', { className: 'num' }, money(t.remaining)), + el('td', { className: 'num' }, String(t.openSlots)), + el('td', { className: `num ${t.locked > 0.8 ? 'over' : ''}` }, `${Math.round(t.locked * 100)}%`), + ])); + } +} + +function renderAlerts(snap) { + const box = $('#alerts'); + box.textContent = ''; + + for (const item of snap.needsReview.slice(-5)) { + const p = item.payload; + box.append(el('div', { className: 'alert review' }, [ + `Unsure: ${p.playerName ?? item.type} ` + + (p.price != null ? `for ${money(p.price)} to ${p.teamId} ` : ''), + el('button', { + onclick: () => send({ kind: 'correct', targetId: item.id, patch: {} }), + }, 'Confirm'), + el('button', { + onclick: () => send({ kind: 'retract', targetId: item.id, reason: 'rejected in review' }), + }, 'Discard'), + ])); + } + + for (const w of snap.warnings.slice(-5)) { + box.append(el('div', { className: 'alert' }, w.message)); + } + + if (!snap.valuationCount) { + box.append(el('div', { className: 'alert' }, + 'No valuations loaded — inflation and bid advice are off. Import a CSV in Setup.')); + } +} + +function render(snap) { + latest = snap; + renderStatus(snap); + renderAdvice(snap); + renderHistory(snap); + renderTeams(snap); + renderMarket(snap); + renderAlerts(snap); +} + +// --- messaging -------------------------------------------------------------- +const send = (msg) => browser.runtime.sendMessage(msg); + +browser.runtime.onMessage.addListener((msg) => { + if (msg?.kind === 'snapshot') render(msg.snapshot); + return undefined; +}); + +// --- setup handlers --------------------------------------------------------- +const form = $('#config-form'); + +form.addEventListener('submit', (ev) => { + ev.preventDefault(); + const data = new FormData(form); + const slotText = String(data.get('rosterSlots') ?? '').trim(); + + // Accepts "QB,RB,RB,BN x6" as well as a bare comma list. + let rosterSlots; + if (slotText) { + rosterSlots = slotText.split(',').flatMap((chunk) => { + const m = /^\s*([A-Za-z]+)\s*(?:x\s*(\d+))?\s*$/.exec(chunk); + if (!m) return []; + return Array.from({ length: Number(m[2] ?? 1) }, () => m[1].toUpperCase()); + }); + } + + send({ + kind: 'set-config', + config: { + numTeams: Number(data.get('numTeams')), + budget: Number(data.get('budget')), + minBid: Number(data.get('minBid')), + myTeamId: String(data.get('myTeamId') ?? '').trim() || null, + aggressiveness: Number(data.get('aggressiveness')), + ...(rosterSlots?.length ? { rosterSlots } : {}), + }, + }); +}); + +form.aggressiveness.addEventListener('input', (ev) => { + $('#aggr-value').textContent = ev.target.value; +}); + +$('#csv-file').addEventListener('change', async (ev) => { + const file = ev.target.files?.[0]; + if (!file) return; + const csv = await file.text(); + const result = await send({ kind: 'load-valuations', csv }); + $('#csv-status').textContent = `${result.count} players loaded` + + (result.problems.length ? ` — ${result.problems.join('; ')}` : ''); +}); + +function download(name, data) { + const url = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })); + const a = el('a', { href: url, download: name }); + document.body.append(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} + +$('#export-log').addEventListener('click', async () => { + download('draft-log.json', await send({ kind: 'get-log' })); +}); + +$('#export-capture').addEventListener('click', async () => { + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + if (!tab) return; + const capture = await browser.tabs.sendMessage(tab.id, { kind: 'export-capture' }); + download('ws-capture.json', capture); +}); + +$('#reset').addEventListener('click', () => { + if (confirm('Discard the current draft log? This cannot be undone.')) { + send({ kind: 'reset' }); + } +}); + +$('#manual-form').addEventListener('submit', (ev) => { + ev.preventDefault(); + const data = new FormData(ev.target); + send({ + kind: 'manual-event', + type: EventType.SOLD, + payload: { + playerName: String(data.get('playerName')).trim(), + position: String(data.get('position')).trim().toUpperCase(), + teamId: String(data.get('teamId')).trim(), + teamName: String(data.get('teamId')).trim(), + price: Number(data.get('price')), + }, + }); + ev.target.reset(); +}); + +// --- boot ------------------------------------------------------------------- +(async () => { + const snap = await send({ kind: 'get-snapshot' }); + const c = snap.config; + form.numTeams.value = c.numTeams; + form.budget.value = c.budget; + form.minBid.value = c.minBid; + form.myTeamId.value = c.myTeamId ?? ''; + form.aggressiveness.value = c.aggressiveness ?? 0.5; + form.rosterSlots.value = c.rosterSlots.join(','); + $('#aggr-value').textContent = form.aggressiveness.value; + render(snap); +})(); + +// Keeps the "last event Ns ago" tooltip honest while the draft is quiet. +setInterval(() => { if (latest) renderStatus(latest); }, 5000); diff --git a/fantasy-auction-tracker/test/analytics.test.js b/fantasy-auction-tracker/test/analytics.test.js new file mode 100644 index 0000000..d2dab2a --- /dev/null +++ b/fantasy-auction-tracker/test/analytics.test.js @@ -0,0 +1,237 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; + +import { EventType, makeEvent } from '../src/core/events.js'; +import { reduce } from '../src/core/reducer.js'; +import { DEFAULT_CONFIG, expandSlots } from '../src/core/config.js'; +import { + inflation, positionalInflation, scarcity, tierCliff, + liveBidders, bidAdvice, budgetPressure, availablePlayers, +} from '../src/core/analytics.js'; + +const config = { + ...DEFAULT_CONFIG, + numTeams: 2, + budget: 100, + minBid: 1, + myTeamId: 'me', + rosterSlots: expandSlots({ RB: 2, WR: 2 }), +}; + +/** 8 players at $25 par each: 2 teams x 4 slots, $200 total, exactly at par. */ +const valuations = [ + { name: 'RB A', position: 'RB', value: 25 }, + { name: 'RB B', position: 'RB', value: 25 }, + { name: 'RB C', position: 'RB', value: 25 }, + { name: 'RB D', position: 'RB', value: 25 }, + { name: 'WR A', position: 'WR', value: 25 }, + { name: 'WR B', position: 'WR', value: 25 }, + { name: 'WR C', position: 'WR', value: 25 }, + { name: 'WR D', position: 'WR', value: 25 }, +]; + +const sold = (name, pos, teamId, price, id) => + makeEvent(EventType.SOLD, { + playerName: name, position: pos, teamId, teamName: teamId, price, + }, { id, source: 'ws' }); + +const register = (teamId) => + makeEvent(EventType.TEAM_REGISTERED, { teamId, teamName: teamId }, { id: `r-${teamId}` }); + +const nominate = (name, pos, bid, bidder) => makeEvent(EventType.NOMINATION, { + playerName: name, position: pos, openingBid: bid, nominatingTeamId: bidder, +}, { id: `n-${name}` }); + +test('a perfectly par market has inflation 1.0', () => { + const state = reduce([register('me'), register('you')], { config }); + const inf = inflation(state, valuations); + + assert.equal(inf.remainingMoney, 200); + assert.equal(inf.remainingValue, 200); + assert.equal(inf.raw, 1); + assert.equal(inf.discretionary, 1); +}); + +test('overpaying early inflates the remaining market', () => { + // $25 player goes for $45: $20 of value left the pool but $45 of money did. + const state = reduce([register('me'), register('you'), sold('RB A', 'RB', 'you', 45, 'e1')], { config }); + const inf = inflation(state, valuations); + + assert.equal(inf.remainingMoney, 155); + assert.equal(inf.remainingValue, 175); + assert.equal(inf.surplusSpent, 20); + assert.ok(inf.discretionary < 1, 'money left the pool faster than value'); +}); + +test('bargains early deflate the remaining market', () => { + const state = reduce([register('me'), register('you'), sold('RB A', 'RB', 'you', 5, 'e1')], { config }); + const inf = inflation(state, valuations); + + assert.equal(inf.surplusSpent, -20); + assert.ok(inf.discretionary > 1, 'money outlasted value, so prices rise'); +}); + +test('discretionary inflation strips the $1-per-slot floor from both sides', () => { + const state = reduce([register('me'), register('you')], { config }); + const inf = inflation(state, valuations); + + // 4 open spots per team x 2 = 8 spots, so $8 is pinned and cannot chase value. + assert.equal(inf.openSpots, 8); + // Raw and discretionary agree only because this market is exactly at par. + assert.equal(inf.raw, inf.discretionary); +}); + +test('max bid caps what a rival can actually pay', () => { + const state = reduce([ + register('me'), register('you'), + sold('RB A', 'RB', 'you', 97, 'e1'), + ], { config }); + + const rivals = liveBidders(state, 'WR', { excludeTeamId: 'me' }); + // $3 left, 3 open slots -> can bid $1 and still fill the rest at $1. + assert.equal(rivals[0].maxBid, 1); +}); + +test('a team with no eligible open slot is not a live bidder', () => { + const state = reduce([ + register('me'), register('you'), + sold('RB A', 'RB', 'you', 10, 'e1'), + sold('RB B', 'RB', 'you', 10, 'e2'), + ], { config }); + + const rivals = liveBidders(state, 'RB', { excludeTeamId: 'me' }); + assert.equal(rivals.length, 0, 'both RB slots are full and there is no flex'); +}); + +test('scarcity counts startable players against unfilled starting slots', () => { + const state = reduce([register('me'), register('you')], { config }); + const s = scarcity(state, valuations); + + assert.equal(s.RB.available, 4); + assert.equal(s.RB.openStarterSlots, 4); + assert.ok(Number.isFinite(s.RB.ratio)); +}); + +test('tier cliff measures the drop to the next player at the position', () => { + const uneven = [ + { name: 'Elite RB', position: 'RB', value: 60 }, + { name: 'Meh RB', position: 'RB', value: 12 }, + ]; + const state = reduce([register('me')], { config }); + const cliff = tierCliff(state, uneven, 'RB'); + + assert.equal(cliff.cliff, 48); + assert.equal(cliff.next.name, 'Meh RB'); +}); + +test('drafted players leave the available pool', () => { + const state = reduce([sold('RB A', 'RB', 'you', 25, 'e1')], { config }); + const avail = availablePlayers(state, valuations); + + assert.equal(avail.length, 7); + assert.ok(!avail.some((p) => p.name === 'RB A')); +}); + +test('advice says bid when the board is under your adjusted value', () => { + const state = reduce([register('me'), register('you'), nominate('RB A', 'RB', 10, 'you')], { config }); + const a = bidAdvice(state, valuations, { aggressiveness: 0 }); + + assert.equal(a.verdict, 'bid'); + assert.equal(a.parValue, 25); + assert.equal(a.currentBid, 10); + assert.ok(a.surplus > 0); +}); + +test('advice says pass once the board passes your ceiling', () => { + const state = reduce([ + register('me'), register('you'), + makeEvent(EventType.NOMINATION, { playerName: 'RB A', position: 'RB', openingBid: 40 }, { id: 'n1' }), + ], { config }); + + const a = bidAdvice(state, valuations, { aggressiveness: 0 }); + assert.equal(a.verdict, 'pass'); +}); + +test('aggressiveness raises the ceiling by a share of the tier cliff', () => { + // Scaled to the league (8 players, $200 total) so inflation is exactly 1.0 + // and the only thing separating the two ceilings is the cliff allowance. + const cliffy = [ + { name: 'Elite RB', position: 'RB', value: 60 }, + { name: 'Meh RB', position: 'RB', value: 12 }, + { name: 'RB C', position: 'RB', value: 12 }, + { name: 'RB D', position: 'RB', value: 12 }, + { name: 'WR A', position: 'WR', value: 30 }, + { name: 'WR B', position: 'WR', value: 26 }, + { name: 'WR C', position: 'WR', value: 26 }, + { name: 'WR D', position: 'WR', value: 22 }, + ]; + const log = [register('me'), register('you'), nominate('Elite RB', 'RB', 60)]; + const state = reduce(log, { config }); + + const timid = bidAdvice(state, cliffy, { aggressiveness: 0 }); + const bold = bidAdvice(state, cliffy, { aggressiveness: 1 }); + + assert.ok(bold.ceiling > timid.ceiling); + assert.equal(bold.verdict, 'stretch'); + assert.equal(timid.verdict, 'pass'); +}); + +test('your own max bid hard-caps the ceiling', () => { + const state = reduce([ + register('me'), register('you'), + sold('WR A', 'WR', 'me', 97, 'e1'), // me: $3 left, 3 slots open + nominate('RB A', 'RB', 1), + ], { config }); + + const a = bidAdvice(state, valuations, { aggressiveness: 1 }); + assert.equal(a.myMaxBid, 1); + assert.ok(a.ceiling <= 1, 'never advise a bid you cannot legally make'); +}); + +test('advice flags a player with no open slot on your roster', () => { + const state = reduce([ + register('me'), register('you'), + sold('RB A', 'RB', 'me', 10, 'e1'), + sold('RB B', 'RB', 'me', 10, 'e2'), + nominate('RB C', 'RB', 5), + ], { config }); + + assert.equal(bidAdvice(state, valuations).verdict, 'no-slot'); +}); + +test('advice flags a player missing from your valuations', () => { + const state = reduce([register('me'), nominate('Undrafted Guy', 'RB', 3)], { config }); + assert.equal(bidAdvice(state, valuations).verdict, 'unvalued'); +}); + +test('no nomination means no advice', () => { + const state = reduce([register('me')], { config }); + assert.equal(bidAdvice(state, valuations), null); +}); + +test('positional inflation reflects money chasing one position', () => { + const state = reduce([register('me'), register('you')], { config }); + const rb = positionalInflation(state, valuations, 'RB'); + + assert.equal(rb.demand, 4, 'two teams x two RB slots'); + assert.ok(rb.rate > 0); +}); + +test('budget pressure ranks teams closest to forced $1 bids', () => { + const state = reduce([ + register('me'), register('you'), + sold('RB A', 'RB', 'you', 97, 'e1'), + ], { config }); + + const pressure = budgetPressure(state); + assert.equal(pressure[0].teamId, 'you'); + assert.ok(pressure[0].locked > pressure[1].locked); +}); + +test('analytics tolerate an empty valuation set', () => { + const state = reduce([register('me'), nominate('Whoever', 'RB', 5)], { config }); + + assert.doesNotThrow(() => inflation(state, [])); + assert.doesNotThrow(() => scarcity(state, [])); + assert.equal(bidAdvice(state, []).verdict, 'unvalued'); +}); diff --git a/fantasy-auction-tracker/test/reducer.test.js b/fantasy-auction-tracker/test/reducer.test.js new file mode 100644 index 0000000..89478b3 --- /dev/null +++ b/fantasy-auction-tracker/test/reducer.test.js @@ -0,0 +1,214 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; + +import { EventType, makeEvent, validate } from '../src/core/events.js'; +import { reduce, teamSummary, allTeamSummaries } from '../src/core/reducer.js'; +import { DraftStore } from '../src/core/store.js'; +import { DEFAULT_CONFIG, expandSlots, startersByPosition } from '../src/core/config.js'; +import { normalizeName, playerKey, resolvePlayer, buildIndex } from '../src/core/players.js'; + +const config = { + ...DEFAULT_CONFIG, + numTeams: 4, + budget: 100, + rosterSlots: expandSlots({ QB: 1, RB: 2, WR: 2, FLEX: 1, BN: 2 }), +}; + +const sold = (name, pos, teamId, price, id) => + makeEvent(EventType.SOLD, { + playerName: name, position: pos, teamId, teamName: teamId, price, + }, { id, source: 'ws' }); + +test('sale updates budget, roster and drafted set', () => { + const state = reduce([sold('Bijan Robinson', 'RB', 'alpha', 55)], { config }); + const alpha = teamSummary(state.teams.get('alpha'), config); + + assert.equal(alpha.spent, 55); + assert.equal(alpha.remaining, 45); + assert.equal(alpha.roster.length, 1); + assert.ok(state.drafted.has(playerKey('Bijan Robinson', 'RB'))); +}); + +test('max bid reserves $1 for every other open slot', () => { + // 8 slots, $100 budget, nothing spent -> 7 slots must keep $1 each. + const state = reduce([makeEvent(EventType.TEAM_REGISTERED, { + teamId: 'alpha', teamName: 'alpha', + })], { config }); + const alpha = teamSummary(state.teams.get('alpha'), config); + + assert.equal(alpha.openSlots, 8); + assert.equal(alpha.maxBid, 93); +}); + +test('max bid is zero when the roster is full', () => { + const log = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'].map((n, i) => + sold(`Player ${n}`, i < 3 ? 'RB' : 'WR', 'alpha', 1, `e${i}`)); + const state = reduce(log, { config }); + const alpha = teamSummary(state.teams.get('alpha'), config); + + assert.equal(alpha.openSlots, 0); + assert.equal(alpha.maxBid, 0); +}); + +test('players fill dedicated slots before flex, and flex before bench', () => { + const state = reduce([ + sold('RB One', 'RB', 'alpha', 10, 'e1'), + sold('RB Two', 'RB', 'alpha', 10, 'e2'), + sold('RB Three', 'RB', 'alpha', 10, 'e3'), + ], { config }); + + const slots = state.teams.get('alpha').roster.map((r) => r.slot); + assert.deepEqual(slots, ['RB', 'RB', 'FLEX']); +}); + +test('a duplicate sale is ignored and warned about, not double-counted', () => { + const state = reduce([ + sold('Bijan Robinson', 'RB', 'alpha', 55, 'e1'), + sold('Bijan Robinson', 'RB', 'beta', 55, 'e2'), + ], { config }); + + assert.equal(state.drafted.get(playerKey('Bijan Robinson', 'RB')).teamId, 'alpha'); + assert.equal(state.warnings.length, 1); + assert.match(state.warnings[0].message, /duplicate/); +}); + +test('retraction removes a sale and restores the budget', () => { + const log = [ + sold('Bijan Robinson', 'RB', 'alpha', 55, 'e1'), + makeEvent(EventType.RETRACTION, { targetId: 'e1' }, { id: 'e2' }), + ]; + const state = reduce(log, { config }); + + assert.equal(state.drafted.size, 0); + assert.equal(teamSummary(state.teams.get('alpha') ?? { + spent: 0, roster: [], slots: config.rosterSlots.map((t) => ({ type: t, filled: null })), + }, config).spent, 0); +}); + +test('correction patches an earlier payload in place', () => { + const log = [ + sold('Bijan Robinson', 'RB', 'alpha', 5, 'e1'), + makeEvent(EventType.CORRECTION, { targetId: 'e1', patch: { price: 55 } }, { id: 'e2' }), + ]; + const state = reduce(log, { config }); + assert.equal(state.teams.get('alpha').spent, 55); +}); + +test('low-confidence events are quarantined rather than applied', () => { + const shaky = makeEvent(EventType.SOLD, { + playerName: 'Blurry Name', position: 'WR', teamId: 'alpha', price: 30, + }, { id: 'e1', source: 'ocr', confidence: 0.4 }); + + const state = reduce([shaky], { config }); + assert.equal(state.drafted.size, 0); + assert.equal(state.needsReview.length, 1); +}); + +test('confirming a quarantined event applies it', () => { + const shaky = makeEvent(EventType.SOLD, { + playerName: 'Blurry Name', position: 'WR', teamId: 'alpha', price: 30, + }, { id: 'e1', source: 'ocr', confidence: 0.4 }); + + const state = reduce([ + shaky, + makeEvent(EventType.CORRECTION, { targetId: 'e1', patch: {} }, { id: 'e2' }), + ], { config }); + + assert.equal(state.drafted.size, 1); + assert.equal(state.needsReview.length, 0); +}); + +test('bids never move the high bid backwards', () => { + const state = reduce([ + makeEvent(EventType.NOMINATION, { playerName: 'CeeDee Lamb', position: 'WR' }, { id: 'e1' }), + makeEvent(EventType.BID, { amount: 40, teamId: 'alpha' }, { id: 'e2' }), + makeEvent(EventType.BID, { amount: 12, teamId: 'beta' }, { id: 'e3' }), // stale frame + ], { config }); + + assert.equal(state.nomination.highBid, 40); + assert.equal(state.nomination.highBidder, 'alpha'); +}); + +test('a sale clears the matching nomination', () => { + const state = reduce([ + makeEvent(EventType.NOMINATION, { playerName: 'CeeDee Lamb', position: 'WR' }, { id: 'e1' }), + sold('CeeDee Lamb', 'WR', 'alpha', 52, 'e2'), + ], { config }); + + assert.equal(state.nomination, null); +}); + +test('reduce is deterministic and side-effect free', () => { + const log = [sold('A B', 'RB', 'alpha', 10, 'e1'), sold('C D', 'WR', 'beta', 20, 'e2')]; + const a = JSON.stringify(allTeamSummaries(reduce(log, { config }))); + const b = JSON.stringify(allTeamSummaries(reduce(log, { config }))); + assert.equal(a, b); +}); + +test('store rejects malformed events instead of logging them', () => { + const store = new DraftStore({ config }); + const bad = makeEvent(EventType.SOLD, { + playerName: 'No Price', position: 'RB', teamId: 'alpha', + }); + + const result = store.append(bad); + assert.equal(result.ok, false); + assert.equal(store.log.length, 0); + assert.match(result.problems[0], /price/); +}); + +test('store suppresses a repeated sale from a re-rendering DOM', () => { + const store = new DraftStore({ config }); + assert.equal(store.append(sold('Bijan Robinson', 'RB', 'alpha', 55, 'e1')).ok, true); + assert.equal(store.append(sold('Bijan Robinson', 'RB', 'alpha', 55, 'e2')).ok, false); + assert.equal(store.log.length, 1); +}); + +test('store round-trips through serialize/deserialize', () => { + const store = new DraftStore({ config }); + store.append(sold('Bijan Robinson', 'RB', 'alpha', 55, 'e1')); + const restored = DraftStore.deserialize(store.serialize()); + + assert.equal(restored.log.length, 1); + assert.equal(restored.state.teams.get('alpha').spent, 55); +}); + +test('name normalization collapses suffixes, punctuation and inversion', () => { + assert.equal(normalizeName('Marvin Harrison Jr.'), 'marvin harrison'); + assert.equal(normalizeName('Harrison Jr., Marvin'), 'marvin harrison'); + assert.equal(normalizeName("Ja'Marr Chase"), 'jamarr chase'); + assert.equal(normalizeName('Amon-Ra St. Brown'), 'amon-ra st brown'); +}); + +test('player resolution falls back through name and last-name tiers', () => { + const index = buildIndex([ + { name: 'Marvin Harrison Jr.', position: 'WR', nflTeam: 'ARI', value: 30 }, + { name: 'Josh Allen', position: 'QB', nflTeam: 'BUF', value: 25 }, + ]); + + assert.equal( + resolvePlayer(index, { name: 'Marvin Harrison', position: 'WR' }).tier, + 'name+pos', + ); + assert.equal( + resolvePlayer(index, { name: 'Josh Allen', position: 'RB' }).confidence, + 0.9, + ); + assert.equal(resolvePlayer(index, { name: 'Nobody Here', position: 'TE' }), null); +}); + +test('starter counts split flex demand across eligible positions', () => { + const starters = startersByPosition(config); + // 4 teams: 2 dedicated RB each = 8, plus a third of each team's FLEX. + assert.equal(starters.RB, 8 + (4 * (1 / 3))); + assert.equal(starters.QB, 4); +}); + +test('event validation catches every required field', () => { + assert.deepEqual(validate(makeEvent(EventType.SOLD, { + playerName: 'A B', position: 'RB', teamId: 'alpha', price: 10, + })), []); + + const problems = validate(makeEvent(EventType.SOLD, { price: -1 })); + assert.ok(problems.length >= 4); +}); diff --git a/fantasy-auction-tracker/test/replay.test.js b/fantasy-auction-tracker/test/replay.test.js new file mode 100644 index 0000000..f70c6af --- /dev/null +++ b/fantasy-auction-tracker/test/replay.test.js @@ -0,0 +1,105 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +import { replay } from '../tools/replay.js'; +import { loadValuations, rescaleToLeague } from '../src/core/valuations.js'; +import { allTeamSummaries, reduce } from '../src/core/reducer.js'; +import { inflation, scarcity, budgetPressure } from '../src/core/analytics.js'; +import { rosterSize } from '../src/core/config.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const fixtures = join(here, '..', 'fixtures'); + +const logData = JSON.parse(readFileSync(join(fixtures, 'sample-draft.json'), 'utf8')); +const { players } = loadValuations(readFileSync(join(fixtures, 'sample-values.csv'), 'utf8')); +const valuations = rescaleToLeague(players, logData.config); + +test('a full draft replays to a complete, consistent league', () => { + const { store } = replay(logData, valuations); + const state = store.state; + const teams = allTeamSummaries(state); + const size = rosterSize(state.config); + + assert.equal(teams.length, state.config.numTeams); + assert.equal(state.history.length, state.config.numTeams * size); + assert.ok(state.complete, 'DRAFT_COMPLETE was seen'); + + for (const t of teams) { + assert.equal(t.roster.length, size, `${t.teamName} filled every slot`); + assert.equal(t.openSlots, 0); + assert.equal(t.maxBid, 0); + assert.ok(t.spent <= state.config.budget, `${t.teamName} never overspent`); + assert.ok(t.remaining >= 0); + } +}); + +test('no player is drafted twice across a full draft', () => { + const { store } = replay(logData, valuations); + const keys = store.state.history.map((s) => s.playerKey); + assert.equal(new Set(keys).size, keys.length); + assert.equal(store.state.warnings.length, 0, 'a clean log produces no warnings'); +}); + +test('every dollar is accounted for', () => { + const { store } = replay(logData, valuations); + const spent = allTeamSummaries(store.state).reduce((s, t) => s + t.spent, 0); + const sales = store.state.history.reduce((s, r) => s + r.price, 0); + assert.equal(spent, sales); +}); + +test('replay is deterministic', () => { + const a = replay(logData, valuations).store.state.history.map((s) => `${s.playerKey}:${s.price}`); + const b = replay(logData, valuations).store.state.history.map((s) => `${s.playerKey}:${s.price}`); + assert.deepEqual(a, b); +}); + +test('replaying a prefix matches replaying the whole log then rewinding', () => { + // The reducer is pure, so a prefix of the log must give the same state as + // reducing that prefix directly. This is the property page-refresh recovery + // depends on. + const cut = 250; + const viaReplay = replay(logData, valuations, { until: cut }).store.state; + const viaReduce = reduce(logData.log.slice(0, cut), { config: logData.config }); + + assert.equal(viaReplay.history.length, viaReduce.history.length); + assert.deepEqual( + allTeamSummaries(viaReplay).map((t) => [t.teamId, t.spent, t.openSlots]).sort(), + allTeamSummaries(viaReduce).map((t) => [t.teamId, t.spent, t.openSlots]).sort(), + ); +}); + +test('mid-draft analytics stay in a sane range', () => { + const { store } = replay(logData, valuations, { until: 300 }); + const state = store.state; + const inf = inflation(state, valuations); + + assert.ok(inf.remainingMoney > 0 && inf.remainingMoney < state.config.numTeams * state.config.budget); + assert.ok(inf.openSpots > 0); + assert.ok(inf.discretionary > 0.2 && inf.discretionary < 3, `inflation ${inf.discretionary} is plausible`); + + for (const [pos, s] of Object.entries(scarcity(state, valuations))) { + assert.ok(s.startable >= 0, `${pos} startable is non-negative`); + assert.ok(s.available >= s.startable, `${pos} startable never exceeds available`); + } +}); + +test('budget pressure rises monotonically as the draft drains money', () => { + const early = budgetPressure(replay(logData, valuations, { until: 100 }).store.state); + const late = budgetPressure(replay(logData, valuations, { until: 450 }).store.state); + + const avg = (rows) => rows.reduce((s, t) => s + t.locked, 0) / rows.length; + assert.ok(avg(late) > avg(early), 'teams are more constrained later'); +}); + +test('the final state has no money left unspent beyond the $1 floor', () => { + const { store } = replay(logData, valuations); + // Every team filled every slot, so remaining money is pure surplus. The + // simulator spends to the cap, so this should be near zero; the assertion + // guards against the reducer double-counting or dropping a sale. + for (const t of allTeamSummaries(store.state)) { + assert.ok(t.remaining >= 0 && t.remaining <= store.state.config.budget); + } +}); diff --git a/fantasy-auction-tracker/test/valuations.test.js b/fantasy-auction-tracker/test/valuations.test.js new file mode 100644 index 0000000..2e29f33 --- /dev/null +++ b/fantasy-auction-tracker/test/valuations.test.js @@ -0,0 +1,127 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; + +import { parseCsv, loadValuations, rescaleToLeague } from '../src/core/valuations.js'; +import { DEFAULT_CONFIG, expandSlots } from '../src/core/config.js'; + +test('csv parser handles quotes, embedded commas and CRLF', () => { + const rows = parseCsv('a,b\r\n"Smith, John",7\r\n"say ""hi""",8\r\n'); + assert.deepEqual(rows, [['a', 'b'], ['Smith, John', '7'], ['say "hi"', '8']]); +}); + +test('csv parser skips blank lines', () => { + assert.equal(parseCsv('a,b\n\n1,2\n\n').length, 2); +}); + +test('loads a FantasyPros-shaped export', () => { + const csv = [ + 'Rank,Player,Team,Position,Auction Value,Tier,Bye', + '1,Ja\'Marr Chase,CIN,WR1,$62,1,10', + '2,Bijan Robinson,ATL,RB1,"$58",1,5', + '3,Justin Jefferson,MIN,WR2,$55,1,6', + ].join('\n'); + + const { players, problems } = loadValuations(csv); + + assert.equal(players.length, 3); + assert.equal(problems.length, 0); + assert.deepEqual( + players[0], + { + name: "Ja'Marr Chase", + position: 'WR', + nflTeam: 'CIN', + value: 62, + hasValue: true, + rank: 1, + posRank: 1, + tier: 1, + projection: null, + bye: 10, + source: 'csv', + }, + ); + assert.equal(players[1].value, 58, 'quoted dollar amounts parse'); + assert.equal(players[1].posRank, 1, 'RB1 splits into position + positional rank'); +}); + +test('alternative header names are recognized', () => { + const { players } = loadValuations('Name,Pos,$\nJosh Allen,QB,40'); + assert.equal(players[0].name, 'Josh Allen'); + assert.equal(players[0].position, 'QB'); + assert.equal(players[0].value, 40); +}); + +test('defense position aliases normalize to DST', () => { + const { players } = loadValuations('Player,Position,Value\n49ers,D/ST,4\nRavens,DEF,3'); + assert.deepEqual(players.map((p) => p.position), ['DST', 'DST']); +}); + +test('missing values are reported rather than silently zeroed', () => { + const { players, problems } = loadValuations('Player,Position,Value\nA B,RB,\nC D,WR,10'); + assert.equal(players[0].value, 0); + assert.equal(players[0].hasValue, false); + assert.ok(problems.some((p) => /no auction value/.test(p))); +}); + +test('a CSV with no value column is flagged, not accepted quietly', () => { + const { problems } = loadValuations('Player,Position\nA B,RB'); + assert.ok(problems.some((p) => /auction-value column/.test(p))); +}); + +test('an empty CSV degrades to an empty set with a problem', () => { + const { players, problems } = loadValuations('Player,Position,Value'); + assert.equal(players.length, 0); + assert.equal(problems[0], 'CSV has no data rows'); +}); + +test('rescaling makes values sum to the league total money', () => { + const config = { + ...DEFAULT_CONFIG, + numTeams: 2, + budget: 100, + minBid: 1, + rosterSlots: expandSlots({ RB: 2, WR: 2 }), + }; + + // Values from a different-sized league: total $100, not $200. + const players = [ + { name: 'A B', position: 'RB', value: 40 }, + { name: 'C D', position: 'RB', value: 30 }, + { name: 'E F', position: 'WR', value: 20 }, + { name: 'G H', position: 'WR', value: 10 }, + ]; + + const scaled = rescaleToLeague(players, config); + const total = scaled.reduce((s, p) => s + p.value, 0); + + assert.ok(Math.abs(total - 200) < 0.5, `expected ~200, got ${total}`); + assert.ok(scaled.every((p) => p.value >= config.minBid)); + assert.equal(scaled[0].rawValue, 40, 'the original value is preserved'); + assert.ok(scaled[0].value > scaled[1].value, 'ordering is preserved'); +}); + +test('players beyond the rosterable pool are zeroed and marked', () => { + const config = { + ...DEFAULT_CONFIG, + numTeams: 1, + budget: 100, + rosterSlots: expandSlots({ RB: 2 }), + }; + const players = [ + { name: 'A B', position: 'RB', value: 50 }, + { name: 'C D', position: 'RB', value: 40 }, + { name: 'E F', position: 'RB', value: 1 }, + ]; + + const scaled = rescaleToLeague(players, config); + assert.equal(scaled[2].rosterable, false); + assert.equal(scaled[2].value, 0); + assert.equal(scaled[0].rosterable, true); +}); + +test('rescaling is a no-op when there is no surplus to distribute', () => { + const config = { ...DEFAULT_CONFIG, numTeams: 1, budget: 10, rosterSlots: ['RB'] }; + const players = [{ name: 'A B', position: 'RB', value: 0 }]; + assert.deepEqual(rescaleToLeague(players, config), players); +}); diff --git a/fantasy-auction-tracker/tools/calibrate.js b/fantasy-auction-tracker/tools/calibrate.js new file mode 100644 index 0000000..c3768ed --- /dev/null +++ b/fantasy-auction-tracker/tools/calibrate.js @@ -0,0 +1,137 @@ +/** + * Selector calibration helper. + * + * The site profiles in src/adapters/profiles.js are guesses. This finds the + * real selectors. Open your draft room (a MOCK draft -- never calibrate during + * the real thing), paste this whole file into the browser console, and run: + * + * __auctionCalibrate() // survey the page + * __auctionCalibrate('Bijan Robinson') // find where a known name lives + * __auctionWatch() // log DOM changes for 30s + * + * Copy the reported selectors into the profile for your platform, or paste + * them into the sidebar's Advanced panel. + */ + +(() => { + /** Shortest reasonably stable CSS path to a node. */ + function pathFor(node) { + const parts = []; + let el = node; + while (el && el.nodeType === 1 && parts.length < 5) { + let part = el.tagName.toLowerCase(); + const classes = [...el.classList] + // Skip hashed/utility classes -- they change on every deploy. + .filter((c) => !/^(css-|sc-|jsx-|_)/.test(c) && !/\d{4,}/.test(c)) + .slice(0, 2); + if (el.id && !/\d{4,}/.test(el.id)) { parts.unshift(`#${el.id}`); break; } + if (classes.length) part += `.${classes.join('.')}`; + parts.unshift(part); + el = el.parentElement; + } + return parts.join(' > '); + } + + function textNodes(root = document.body) { + const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + const out = []; + let node; + while ((node = walker.nextNode())) { + const text = node.textContent.trim(); + if (text && node.parentElement) out.push({ text, el: node.parentElement }); + } + return out; + } + + window.__auctionCalibrate = (knownName = null) => { + const nodes = textNodes(); + + const report = (label, matches) => { + const grouped = new Map(); + for (const m of matches) { + const path = pathFor(m.el); + if (!grouped.has(path)) grouped.set(path, []); + grouped.get(path).push(m.text); + } + const rows = [...grouped.entries()] + .sort((a, b) => b[1].length - a[1].length) + .slice(0, 5) + .map(([selector, samples]) => ({ + selector, + count: samples.length, + samples: samples.slice(0, 3).join(' | '), + })); + console.group(`%c${label} (${matches.length} candidates)`, 'font-weight:bold'); + if (rows.length) console.table(rows); else console.log('none found'); + console.groupEnd(); + return rows[0]?.selector ?? null; + }; + + const found = {}; + + if (knownName) { + found.player = report( + `nodes containing "${knownName}"`, + nodes.filter((n) => n.text.includes(knownName)), + ); + } + + found.playerName = report( + 'player-name shaped text ("First Last")', + nodes.filter((n) => /^[A-Z][a-z'’.-]+ [A-Z][a-zA-Z'’.-]+/.test(n.text) && n.text.length < 40), + ); + + found.position = report( + 'position labels (QB/RB/WR/TE/K/DST)', + nodes.filter((n) => /^(QB|RB|WR|TE|K|DEF|DST|D\/ST)$/i.test(n.text)), + ); + + found.money = report( + 'dollar amounts', + nodes.filter((n) => /^\$\s?\d{1,3}$/.test(n.text)), + ); + + console.group('%cWebSocket / fetch endpoints seen', 'font-weight:bold'); + console.log('Reload the page with the extension installed, then use'); + console.log(' Setup -> Export WS capture'); + console.log('to get the real frames. That beats every selector below.'); + console.groupEnd(); + + console.log('%cSuggested profile fragment:', 'font-weight:bold'); + console.log(JSON.stringify({ + selectors: { + nomName: found.playerName, + nomPosition: found.position, + highBid: found.money, + }, + }, null, 2)); + + return found; + }; + + window.__auctionWatch = (seconds = 30) => { + const seen = new Map(); + const observer = new MutationObserver((records) => { + for (const r of records) { + const target = r.target.nodeType === 1 ? r.target : r.target.parentElement; + if (!target) continue; + const path = pathFor(target); + seen.set(path, (seen.get(path) ?? 0) + 1); + } + }); + observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + + console.log(`watching for ${seconds}s -- run a few bids now`); + setTimeout(() => { + observer.disconnect(); + const rows = [...seen.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 15) + .map(([selector, changes]) => ({ selector, changes })); + console.log('%cMost-mutated nodes (the live bid area is usually near the top):', 'font-weight:bold'); + console.table(rows); + }, seconds * 1000); + }; + + console.log('calibration loaded: __auctionCalibrate() / __auctionWatch()'); +})(); diff --git a/fantasy-auction-tracker/tools/replay.js b/fantasy-auction-tracker/tools/replay.js new file mode 100644 index 0000000..c5f8fc7 --- /dev/null +++ b/fantasy-auction-tracker/tools/replay.js @@ -0,0 +1,131 @@ +#!/usr/bin/env node +/** + * Replay a recorded draft log through the real store and analytics. + * + * This is the rehearsal you cannot otherwise have. Point it at a log exported + * from the sidebar (or the synthetic fixture) and watch the same numbers the + * sidebar would have shown, sale by sale. + * + * node tools/replay.js fixtures/sample-draft.json fixtures/sample-values.csv + * node tools/replay.js fixtures/sample-draft.json fixtures/sample-values.csv --at 100 + */ + +import { readFileSync } from 'node:fs'; + +import { DraftStore } from '../src/core/store.js'; +import { EventType } from '../src/core/events.js'; +import { loadValuations, rescaleToLeague } from '../src/core/valuations.js'; +import { inflation, scarcity, budgetPressure, bidAdvice } from '../src/core/analytics.js'; +import { allTeamSummaries } from '../src/core/reducer.js'; + +const money = (n) => `$${Math.round(n)}`; +const pad = (s, n) => String(s).padEnd(n).slice(0, n); +const padNum = (s, n) => String(s).padStart(n); + +export function replay(logData, valuations, { until = Infinity } = {}) { + const store = new DraftStore({ config: logData.config, log: [] }); + const timeline = []; + + for (const event of logData.log) { + if (store.log.length >= until) break; + // Bypass the dedup guard: a recorded log is already deduplicated, and + // re-checking it would drop legitimate repeat prices. + store.log.push(event); + + if (event.type === EventType.SOLD) { + store.recompute(); + timeline.push({ + index: timeline.length + 1, + sale: store.state.history.at(-1), + inflation: inflation(store.state, valuations).discretionary, + }); + } + } + store.recompute(); + return { store, timeline }; +} + +function main() { + const [logPath, csvPath] = process.argv.slice(2).filter((a) => !a.startsWith('--')); + const atFlag = process.argv.indexOf('--at'); + const until = atFlag !== -1 ? Number(process.argv[atFlag + 1]) : Infinity; + + if (!logPath) { + console.error('usage: node tools/replay.js [values.csv] [--at N]'); + process.exit(1); + } + + const logData = JSON.parse(readFileSync(logPath, 'utf8')); + let valuations = []; + if (csvPath) { + const { players, problems } = loadValuations(readFileSync(csvPath, 'utf8')); + valuations = rescaleToLeague(players, logData.config); + if (problems.length) console.error(`csv notes: ${problems.join('; ')}`); + } + + const { store, timeline } = replay(logData, valuations, { until }); + const state = store.state; + + console.log(`\nreplayed ${store.log.length} events -> ${state.history.length} sales\n`); + + console.log('last 10 sales'); + console.log(' ' + pad('player', 26) + pad('pos', 5) + pad('team', 10) + padNum('price', 6) + padNum('par', 6) + padNum('infl', 7)); + for (const row of timeline.slice(-10)) { + const val = valuations.find((p) => `${p.name}|${p.position}` === `${row.sale.name}|${row.sale.position}`); + console.log(' ' + + pad(row.sale.name, 26) + + pad(row.sale.position, 5) + + pad(row.sale.teamId, 10) + + padNum(money(row.sale.price), 6) + + padNum(val ? money(val.value) : '—', 6) + + padNum(row.inflation.toFixed(2), 7)); + } + + console.log('\nteams'); + console.log(' ' + pad('team', 12) + padNum('spent', 7) + padNum('left', 6) + padNum('slots', 6) + padNum('max', 6) + ' needs'); + for (const t of allTeamSummaries(state).sort((a, b) => b.maxBid - a.maxBid)) { + const needs = Object.entries(t.needs).filter(([s]) => s !== 'BN').map(([s, n]) => (n > 1 ? `${s}x${n}` : s)).join(' '); + console.log(' ' + + pad(t.teamName, 12) + + padNum(money(t.spent), 7) + + padNum(money(t.remaining), 6) + + padNum(t.openSlots, 6) + + padNum(money(t.maxBid), 6) + + ' ' + needs); + } + + if (valuations.length) { + const inf = inflation(state, valuations); + console.log(`\ninflation ${inf.discretionary.toFixed(2)} ` + + `(${money(inf.remainingMoney)} chasing ${money(inf.remainingValue)} over ${inf.openSpots} spots, ` + + `${money(inf.surplusSpent)} spent above par)`); + + console.log('\nscarcity'); + for (const [pos, s] of Object.entries(scarcity(state, valuations))) { + if (!s.available && !s.openStarterSlots) continue; + console.log(` ${pad(pos, 5)}${padNum(s.startable, 4)} startable / ${padNum(s.openStarterSlots, 3)} slots` + + ` ratio ${Number.isFinite(s.ratio) ? s.ratio.toFixed(2) : 'inf'}` + + ` best: ${s.topAvailable[0]?.name ?? '—'}`); + } + + const advice = bidAdvice(state, valuations); + if (advice) { + console.log(`\non the block: ${advice.player} (${advice.position}) at ${money(advice.currentBid)}` + + ` -> ${advice.verdict.toUpperCase()} walk-away ${money(advice.walkAway)}, ceiling ${money(advice.ceiling)}`); + } + } + + const pressure = budgetPressure(state).filter((t) => t.openSlots > 0).slice(0, 3); + if (pressure.length) { + console.log('\nmost budget-constrained: ' + + pressure.map((t) => `${t.teamName} (${Math.round(t.locked * 100)}%)`).join(', ')); + } + + if (state.warnings.length) { + console.log(`\n${state.warnings.length} warning(s):`); + for (const w of state.warnings.slice(0, 5)) console.log(` - ${w.message}`); + } + console.log(); +} + +if (import.meta.url === `file://${process.argv[1]}`) main(); diff --git a/fantasy-auction-tracker/tools/simulate.js b/fantasy-auction-tracker/tools/simulate.js new file mode 100644 index 0000000..f21a590 --- /dev/null +++ b/fantasy-auction-tracker/tools/simulate.js @@ -0,0 +1,179 @@ +#!/usr/bin/env node +/** + * Generate a synthetic auction draft. + * + * Draft day happens once and cannot be rehearsed, so the whole pipeline needs + * something to run against. This produces a full, internally-consistent draft + * (every team ends at exactly the roster limit and never overspends) plus the + * matching valuation CSV, which the replay test then asserts on. + * + * Deterministic: a fixed seed means the fixture is reproducible and diffs are + * meaningful. + * + * node tools/simulate.js > fixtures/sample-draft.json + */ + +import { EventType, makeEvent } from '../src/core/events.js'; +import { DEFAULT_CONFIG, expandSlots, slotAccepts } from '../src/core/config.js'; +import { rescaleToLeague } from '../src/core/valuations.js'; + +/** mulberry32 — small, seeded, good enough for fixtures. */ +function rng(seed) { + return () => { + seed |= 0; seed = (seed + 0x6D2B79F5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const FIRST = ['Bijan', 'CeeDee', 'Justin', 'Amon-Ra', 'Marvin', 'Breece', 'Puka', 'Garrett', + 'Jahmyr', 'Nico', 'Malik', 'Brock', 'Kyren', 'Rome', 'Trey', 'Chris', 'Tank', 'Zay', + 'Drake', 'Jaxon', 'Tyrone', 'Deebo', 'Rashee', 'Jordan', 'Isiah', 'Quentin', 'Khalil', + 'Blake', 'Sam', 'Dalton', 'Cooper', 'Micah']; +const LAST = ['Robinson', 'Lamb', 'Jefferson', 'Brown', 'Harrison', 'Hall', 'Nacua', 'Wilson', + 'Gibbs', 'Collins', 'Nabers', 'Bowers', 'Williams', 'Odunze', 'McBride', 'Olave', 'Dell', + 'Flowers', 'London', 'Smith-Njigba', 'Tracy', 'Samuel', 'Rice', 'Addison', 'Pacheco', + 'Johnston', 'Shakir', 'Corum', 'LaPorta', 'Kincaid', 'Kupp', 'Parsons']; +const NFL = ['ATL', 'DAL', 'MIN', 'DET', 'ARI', 'NYJ', 'LAR', 'SEA', 'BUF', 'HOU', 'NYG', + 'LV', 'CHI', 'PHI', 'KC', 'NO', 'CIN', 'BAL', 'SF', 'TB']; + +const config = { + ...DEFAULT_CONFIG, + numTeams: 12, + budget: 200, + minBid: 1, + rosterSlots: expandSlots({ QB: 1, RB: 2, WR: 3, TE: 1, FLEX: 1, K: 1, DST: 1, BN: 6 }), +}; + +const rosterSize = config.rosterSlots.length; +const totalSpots = config.numTeams * rosterSize; + +// --- player pool ------------------------------------------------------------- +// Positional supply roughly mirrors a real player pool, and values follow a +// steep power curve so tier cliffs are real rather than uniform. +const POOL = { QB: 24, RB: 60, WR: 72, TE: 24, K: 14, DST: 14 }; +const POS_TOP = { QB: 30, RB: 62, WR: 60, TE: 34, K: 2, DST: 3 }; + +function buildPlayers(rand) { + const players = []; + for (const [pos, count] of Object.entries(POOL)) { + for (let i = 0; i < count; i += 1) { + const decay = Math.exp(-i / (count * 0.28)); + const noise = 0.9 + rand() * 0.2; + const value = Math.max(1, Math.round(POS_TOP[pos] * decay * noise)); + players.push({ + name: `${FIRST[Math.floor(rand() * FIRST.length)]} ${LAST[Math.floor(rand() * LAST.length)]} ${pos}${i + 1}`, + position: pos, + nflTeam: NFL[Math.floor(rand() * NFL.length)], + value, + }); + } + } + return players; +} + +// --- draft ------------------------------------------------------------------- +function simulate(seed = 20260817) { + const rand = rng(seed); + // Scale the pool so total par equals total league money. Without this the + // simulated teams pay par for early studs and run dry halfway through, + // producing a fixture that exercises only the broke end of the market. + const players = rescaleToLeague(buildPlayers(rand), config) + .filter((p) => p.value > 0 || !p.rosterable); + const log = []; + let eventId = 0; + const next = (type, payload, meta = {}) => + log.push(makeEvent(type, payload, { id: `e${++eventId}`, source: 'ws', ts: 1_700_000_000_000 + eventId * 1000, ...meta })); + + next(EventType.LEAGUE_CONFIGURED, config, { source: 'manual' }); + + const teams = Array.from({ length: config.numTeams }, (_, i) => ({ + teamId: `team-${i + 1}`, + teamName: `Team ${i + 1}`, + spent: 0, + slots: config.rosterSlots.map((type) => ({ type, filled: false })), + })); + for (const t of teams) next(EventType.TEAM_REGISTERED, { teamId: t.teamId, teamName: t.teamName }); + + const openSlots = (t) => t.slots.filter((s) => !s.filled).length; + const maxBid = (t) => { + const open = openSlots(t); + return open === 0 ? 0 : (config.budget - t.spent) - (open - 1) * config.minBid; + }; + const canTake = (t, pos) => t.slots.some((s) => !s.filled && slotAccepts(s.type, pos)); + + const available = [...players].sort((a, b) => b.value - a.value); + let sold = 0; + + while (sold < totalSpots && available.length) { + // Nomination: mostly the best player left, sometimes a random one -- real + // rooms mix value nominations with price enforcement. + const idx = rand() < 0.7 ? 0 : Math.floor(rand() * Math.min(20, available.length)); + const player = available.splice(idx, 1)[0]; + + const bidders = teams.filter((t) => canTake(t, player.position) && maxBid(t) >= config.minBid); + if (!bidders.length) continue; + + next(EventType.NOMINATION, { + playerName: player.name, + position: player.position, + nflTeam: player.nflTeam, + openingBid: config.minBid, + nominatingTeamId: bidders[Math.floor(rand() * bidders.length)].teamId, + }); + + // Price forms around par with noise, then gets clipped by what the field + // can actually afford -- this is what produces realistic late-draft + // bargains and the inflation swings the analytics are meant to catch. + const ceiling = Math.max(...bidders.map(maxBid)); + const wanted = Math.max(config.minBid, Math.round(player.value * (0.75 + rand() * 0.55))); + const price = Math.max(config.minBid, Math.min(wanted, ceiling)); + + const affordable = bidders.filter((t) => maxBid(t) >= price); + const winner = affordable[Math.floor(rand() * affordable.length)] ?? bidders[0]; + + // A couple of intermediate bids so the BID path gets exercised too. + for (let step = Math.max(1, Math.floor(price / 2)); step < price; step += Math.max(1, Math.floor(price / 3))) { + const other = bidders[Math.floor(rand() * bidders.length)]; + next(EventType.BID, { amount: step, teamId: other.teamId }); + } + + next(EventType.SOLD, { + playerName: player.name, + position: player.position, + nflTeam: player.nflTeam, + teamId: winner.teamId, + teamName: winner.teamName, + price, + }); + + const slot = winner.slots.find((s) => !s.filled && slotAccepts(s.type, player.position)); + slot.filled = true; + winner.spent += price; + sold += 1; + } + + next(EventType.DRAFT_COMPLETE, {}); + return { config, log, players }; +} + +function toCsv(players) { + const rows = [['Rank', 'Player', 'Team', 'Position', 'Auction Value']]; + [...players] + .sort((a, b) => b.value - a.value) + .forEach((p, i) => rows.push([i + 1, p.name, p.nflTeam, p.position, `$${p.value}`])); + return rows.map((r) => r.map((c) => (String(c).includes(',') ? `"${c}"` : c)).join(',')).join('\n'); +} + +export { simulate, toCsv, config as simConfig }; + +if (import.meta.url === `file://${process.argv[1]}`) { + const { config: cfg, log, players } = simulate(); + const which = process.argv[2] ?? 'draft'; + if (which === 'csv') { + process.stdout.write(toCsv(players)); + } else { + process.stdout.write(JSON.stringify({ version: 1, config: cfg, log }, null, 2)); + } +} From 188c87b10191867f54c7dbef8bffa6233860cce3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 18:05:24 +0000 Subject: [PATCH 2/2] Add tiered target board with adjustable price targets Valuations say what a player is worth to the market; targets say what he is worth to you, which is the number you actually bid off. Adds a Targets tab and the core logic behind it. - Tiers group players who are interchangeable to you, so losing one only matters when the tier runs dry. Tiers are flagged healthy, critical (<=2 left) or exhausted. - Price targets are per-player and override the market-derived ceiling entirely -- set $42 and the Live tab says PASS at $43. Every price and tier cell is editable in place and saves on change (not on keystroke, so a half-typed number never briefly becomes the live ceiling). - Optional inflation adjustment scales targets with the market, since a $40 target in a 1.2x market is really a $48 target. Off by default; the adjusted figure is shown beside the typed one so it is never a silent override. - Plan feasibility fits open targets into remaining roster slots and compares the cost against money in hand, reporting the exact shortfall. Walking into "$12 left, plan needed $60" is the classic auction death and this makes it impossible. - Seeding from the CSV uses the export's own tier column when present, otherwise bands by value, and merges rather than replaces so tuned prices survive. Testing, toward the CBS Salary Cap target: - The DOM adapter is now driven through a real DOM (linkedom, dev-only) against a CBS-shaped fixture built deliberately messy: a header row, a suffixed name, a D/ST alias, three price formats, and a row with no position. The full chain from DOM mutation to league state is asserted. - tools/selftest.js verifies the profile inside a live draft room and is itself tested. It separates FAIL (container present, selector wrong) from EMPTY (nothing has happened yet) and NO-SCOPE (parent absent), because conflating those sends you chasing correct selectors. Two bugs found and fixed along the way: - LEAGUE_CONFIGURED merged its whole payload, so a config event carrying a null myTeamId silently erased your own team identity -- disabling the max-bid cap, the no-slot verdict and the entire target plan. Config merges now ignore null/undefined. - The self-test scoped row-level selectors to the first matching row, which is usually the table header, and so reported correct selectors as broken while letting a genuinely wrong one pass as "waiting for a sale". It now scopes across every matching row. 124 tests pass. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SjniQqz3FPpxjKzASrVNCR --- fantasy-auction-tracker/.gitignore | 2 + fantasy-auction-tracker/README.md | 134 ++++++- .../fixtures/cbs-draft-room.html | 84 ++++ .../fixtures/sample-targets.json | 110 ++++++ fantasy-auction-tracker/package-lock.json | 300 +++++++++++++++ fantasy-auction-tracker/package.json | 3 + .../src/background/background.js | 38 +- fantasy-auction-tracker/src/core/analytics.js | 69 +++- fantasy-auction-tracker/src/core/reducer.js | 8 +- fantasy-auction-tracker/src/core/targets.js | 244 ++++++++++++ .../src/sidebar/sidebar.css | 56 +++ .../src/sidebar/sidebar.html | 20 + .../src/sidebar/sidebar.js | 182 +++++++++ .../test/dom-adapter.test.js | 337 ++++++++++++++++ fantasy-auction-tracker/test/reducer.test.js | 21 + fantasy-auction-tracker/test/selftest.test.js | 204 ++++++++++ fantasy-auction-tracker/test/targets.test.js | 364 ++++++++++++++++++ fantasy-auction-tracker/tools/replay.js | 41 +- fantasy-auction-tracker/tools/selftest.js | 300 +++++++++++++++ 19 files changed, 2493 insertions(+), 24 deletions(-) create mode 100644 fantasy-auction-tracker/.gitignore create mode 100644 fantasy-auction-tracker/fixtures/cbs-draft-room.html create mode 100644 fantasy-auction-tracker/fixtures/sample-targets.json create mode 100644 fantasy-auction-tracker/package-lock.json create mode 100644 fantasy-auction-tracker/src/core/targets.js create mode 100644 fantasy-auction-tracker/test/dom-adapter.test.js create mode 100644 fantasy-auction-tracker/test/selftest.test.js create mode 100644 fantasy-auction-tracker/test/targets.test.js create mode 100644 fantasy-auction-tracker/tools/selftest.js diff --git a/fantasy-auction-tracker/.gitignore b/fantasy-auction-tracker/.gitignore new file mode 100644 index 0000000..552f221 --- /dev/null +++ b/fantasy-auction-tracker/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.log diff --git a/fantasy-auction-tracker/README.md b/fantasy-auction-tracker/README.md index 07896fb..31c37ac 100644 --- a/fantasy-auction-tracker/README.md +++ b/fantasy-auction-tracker/README.md @@ -69,7 +69,47 @@ nominate expensive players you *don't* want while the field can still pay. **Bid advice** — combines the above into `walk-away` (par × inflation, the disciplined number) and `ceiling` (walk-away + a configurable slice of the tier cliff), both hard-capped by your own max bid so it never advises a bid you -cannot legally make. +cannot legally make. An explicit price target on your board replaces the +market-derived number entirely — see below. + +--- + +## The target board + +Valuations say what a player is worth to the market. Targets say what he is +worth **to you** — a different question, and the one you actually bid off. + +**Tiers** group players who are interchangeable *to you*. The point is that you +need N players from a tier, not one specific name, so losing one only matters +when the tier runs dry. The board flags each tier as healthy, **critical** (two +or fewer names left) or **exhausted**. + +**Price targets** are yours and override the market-derived ceiling. Set $42 on +a player and the Live tab will say PASS at $43 no matter what the CSV thinks. +Every price and tier on the board is an editable cell — retuning mid-draft is +one click, and it saves immediately. Leave a price blank to fall back to par. + +Optional **adjust for inflation** scales your targets by the live market rate, +because a $40 target in a market running 1.2× is really a $48 target — holding +the nominal number while everything inflates means quietly targeting a worse +player. Off by default; when on, the adjusted figure is shown next to your +typed one so the override is visible rather than surprising. + +**Plan feasibility** is the number that earns the tab. It walks your open +targets in tier order, fits each into a remaining roster slot, and compares the +total against the money you actually have: + +``` +plan: $127 of targets for 6 of 13 open slots, $99 available -- SHORT BY $35 +``` + +Discovering at $12 remaining that your plan needed $60 is the classic auction +death. This makes it impossible to walk into. + +Seed a starting board from your CSV in one click (it uses the export's own tier +column when there is one, otherwise bands by value), then edit. Seeding merges +rather than replaces, so it never wipes prices you have already tuned. + --- @@ -97,14 +137,51 @@ Requires Firefox 128+ (MV3 background modules). No build step, no bundler, no corrected automatically for a $300/10-team league. 3. **Calibrate against a mock draft** — see below. Do not skip this. -## Calibrating for your platform +## Calibrating on a CBS Salary Cap mock draft The selectors in `src/adapters/profiles.js` are **provisional guesses** written without access to a live auction room, and every platform reskins between -seasons. Verify them in a mock draft: +seasons. This takes about two minutes and turns them into verified ones. + +Open a **CBS Salary Cap Mock Draft** — never calibrate in your real league — +and paste `tools/selftest.js` into the browser console: + +```js +__auctionSelfTest() // check every selector against the live page +``` + +You get a table with one row per selector and a verdict: + +| Status | Meaning | +|---|---| +| `OK` | resolved, with a sample of what it matched | +| `FAIL` | the container exists but this selector matched nothing — **wrong, fix it** | +| `INVALID` | not valid CSS | +| `EMPTY` | nothing to match yet; it tells you which draft action would populate it | +| `NO-SCOPE` | its parent container is absent, so it had nowhere to look — not broken | + +That `FAIL` / `EMPTY` distinction is the point: a mid-draft lull with no player +on the block is not the same as a broken selector, and conflating them sends you +chasing selectors that are already correct. + +Then nominate a player, run the bidding up, let one sale complete, and re-run. +It prints the exact events the adapter would emit, warns about sales parsed +without a position (those never auto-apply), and ends with a pasteable selector +block for the sidebar's Advanced panel. + +Also confirm the room is observable at all: + +```js +__auctionSelfTestLive() // watch 45s of live bidding +``` + +If it reports fewer than two distinct states while bids are visibly moving, the +room renders through canvas or shadow DOM and the DOM layer cannot see it — the +WebSocket capture is then the only viable path. + +To hunt for replacements for anything that failed, `tools/calibrate.js`: ```js -// paste tools/calibrate.js into the draft room console __auctionCalibrate() // survey candidate selectors __auctionCalibrate('Bijan Robinson') // locate a known player name __auctionWatch() // log the most-mutated nodes for 30s @@ -124,9 +201,27 @@ capture only) and the DOM layer does the work. ## Rehearsing ```bash -npm test # 57 tests +npm install # linkedom, for the DOM tests only — the add-on ships dependency-free +npm test # 124 tests + +# rehearse a full draft, or freeze it mid-auction npm run replay -- fixtures/sample-draft.json fixtures/sample-values.csv npm run replay -- fixtures/sample-draft.json fixtures/sample-values.csv --at 300 + +# rehearse your target board against it +npm run replay -- fixtures/sample-draft.json fixtures/sample-values.csv \ + --at 150 --targets fixtures/sample-targets.json --me team-4 +``` + +The last one prints exactly what the Targets tab would show: + +``` +target board + tier 1: 0/6 open won 0 ($0) lost 6 EXHAUSTED + tier 2: 1/6 open won 2 ($41) lost 3 CRITICAL + tier 3: 5/6 open won 0 ($0) lost 1 + +plan: $127 of targets for 6 of 13 open slots, $99 available -- SHORT BY $35 ``` `tools/simulate.js` generates a deterministic, internally-consistent 12-team @@ -145,13 +240,14 @@ src/core/ pure logic, no DOM, no browser APIs — everything tested store.js append-only log + dedup + subscribe analytics.js inflation, scarcity, cliffs, bid advice valuations.js CSV import + league rescaling + targets.js tiers, price targets, plan feasibility players.js name normalization + fuzzy resolution config.js roster slots, flex eligibility, replacement level src/adapters/ platform seam — the only place site knowledge lives src/content/ content script + page-world WebSocket tap src/background/ owns the store, persists to storage src/sidebar/ view layer only; computes nothing -tools/ simulate, replay, calibrate +tools/ simulate, replay, calibrate, selftest ``` `src/core/` has no browser dependency, which is why the whole analytical layer @@ -159,13 +255,25 @@ runs under `node --test` with no DOM shim. ## Status -Working and tested: the core, analytics, valuation import, store/persistence, -sidebar, DOM adapter, WS tap and record mode, replay and simulation. - -Provisional: the NFL.com and CBS selector profiles, which need calibration -against a live room, and the WS field mappings, which need a real capture. The -OCR layer is designed for but not implemented — only needed if a platform -renders its draft board to canvas. +**Working and tested (124 tests):** the core, analytics, the target board, +valuation import, store/persistence, sidebar, DOM adapter, WS tap and record +mode, the self-test tool, replay and simulation. + +The DOM adapter is driven through a real DOM (linkedom) against +`fixtures/cbs-draft-room.html` — a CBS-shaped room deliberately built messy +(header rows, `$58` / `58` / ` $7 ` price formats, a suffixed name, a `D/ST` +alias, a row with no position) — and the full chain from DOM mutation to league +state is asserted end to end. + +**Not verified: that the CBS selectors match the live site.** The fixture is a +reconstruction, not a capture; it proves the adapter handles a room of that +*shape*, not that CBS uses those class names. Nothing runnable offline can prove +that — `__auctionSelfTest()` in a real mock draft is what closes the gap, and it +is built to say so loudly rather than pass quietly. + +Also provisional: the WS field mappings, which need a real capture, so the +WebSocket layer ships in record-only mode. The OCR layer is designed for but not +implemented — only needed if a platform renders its draft board to canvas. ## A note on scope diff --git a/fantasy-auction-tracker/fixtures/cbs-draft-room.html b/fantasy-auction-tracker/fixtures/cbs-draft-room.html new file mode 100644 index 0000000..8c91603 --- /dev/null +++ b/fantasy-auction-tracker/fixtures/cbs-draft-room.html @@ -0,0 +1,84 @@ + +
+ +
+
+ Ja'Marr Chase + WR + CIN +
+
+ $47 + Gridiron Gurus +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PlayerPosPriceTeam
Bijan RobinsonRB$58Team Chaos
Marvin Harrison Jr.WR44Waiver Wire Warriors
San Francisco 49ersD/ST $7 Team Chaos
Tucker Kraft$3Gridiron Gurus
+ +
+
+ Team Chaos + $135 +
+
+ Gridiron Gurus + $197 +
+
+ Waiver Wire Warriors + $156 +
+
+ +
diff --git a/fantasy-auction-tracker/fixtures/sample-targets.json b/fantasy-auction-tracker/fixtures/sample-targets.json new file mode 100644 index 0000000..1ee24fc --- /dev/null +++ b/fantasy-auction-tracker/fixtures/sample-targets.json @@ -0,0 +1,110 @@ +[ + { + "name": "Jordan Hall WR7", + "position": "WR", + "tier": 1, + "maxPrice": 43 + }, + { + "name": "Zay Parsons RB7", + "position": "RB", + "tier": 1, + "maxPrice": 40 + }, + { + "name": "Rome Collins RB8", + "position": "RB", + "tier": 1, + "maxPrice": 38 + }, + { + "name": "Garrett Corum WR9", + "position": "WR", + "tier": 1, + "maxPrice": 34 + }, + { + "name": "Garrett Johnston RB10", + "position": "RB", + "tier": 1, + "maxPrice": 32 + }, + { + "name": "Blake Shakir WR12", + "position": "WR", + "tier": 1, + "maxPrice": 31 + }, + { + "name": "Bijan Kincaid WR11", + "position": "WR", + "tier": 2, + "maxPrice": 30 + }, + { + "name": "Sam Kupp QB1", + "position": "QB", + "tier": 2, + "maxPrice": 29 + }, + { + "name": "Marvin Odunze WR14", + "position": "WR", + "tier": 2, + "maxPrice": 28 + }, + { + "name": "Rome London RB15", + "position": "RB", + "tier": 2, + "maxPrice": 25 + }, + { + "name": "Justin Samuel WR17", + "position": "WR", + "tier": 2, + "maxPrice": 24 + }, + { + "name": "Blake Nacua QB2", + "position": "QB", + "tier": 2, + "maxPrice": 24 + }, + { + "name": "Marvin Bowers WR15", + "position": "WR", + "tier": 3, + "maxPrice": 24 + }, + { + "name": "Justin Nacua WR16", + "position": "WR", + "tier": 3, + "maxPrice": 23 + }, + { + "name": "Khalil Harrison WR19", + "position": "WR", + "tier": 3, + "maxPrice": 21 + }, + { + "name": "Marvin Wilson RB18", + "position": "RB", + "tier": 3, + "maxPrice": 20 + }, + { + "name": "Chris Rice QB3", + "position": "QB", + "tier": 3, + "maxPrice": 19 + }, + { + "name": "Quentin Corum TE4", + "position": "TE", + "tier": 3, + "maxPrice": 19 + } +] \ No newline at end of file diff --git a/fantasy-auction-tracker/package-lock.json b/fantasy-auction-tracker/package-lock.json new file mode 100644 index 0000000..c7c6ca2 --- /dev/null +++ b/fantasy-auction-tracker/package-lock.json @@ -0,0 +1,300 @@ +{ + "name": "fantasy-auction-tracker", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "fantasy-auction-tracker", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "linkedom": "^0.18.13" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/boolbase": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz", + "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==", + "license": "ISC", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz", + "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^2.0.0", + "css-what": "^8.0.0", + "domhandler": "^6.0.1", + "domutils": "^4.0.2", + "nth-check": "^3.0.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz", + "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", + "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/domhandler": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/linkedom": { + "version": "0.18.13", + "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.13.tgz", + "integrity": "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw==", + "license": "ISC", + "dependencies": { + "css-select": "^7.0.0", + "cssom": "^0.5.0", + "html-escaper": "^3.0.3", + "htmlparser2": "^10.1.0", + "uhyphen": "^0.2.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": ">= 2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/nth-check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz", + "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^2.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/uhyphen": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", + "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", + "license": "ISC" + } + } +} diff --git a/fantasy-auction-tracker/package.json b/fantasy-auction-tracker/package.json index a22a39f..7b798a9 100644 --- a/fantasy-auction-tracker/package.json +++ b/fantasy-auction-tracker/package.json @@ -12,5 +12,8 @@ "license": "MIT", "engines": { "node": ">=20" + }, + "devDependencies": { + "linkedom": "^0.18.13" } } diff --git a/fantasy-auction-tracker/src/background/background.js b/fantasy-auction-tracker/src/background/background.js index f3b7ed9..07aebd9 100644 --- a/fantasy-auction-tracker/src/background/background.js +++ b/fantasy-auction-tracker/src/background/background.js @@ -11,12 +11,15 @@ import { DraftStore } from '../core/store.js'; import { EventType } from '../core/events.js'; import { snapshot } from '../core/analytics.js'; import { loadValuations, rescaleToLeague } from '../core/valuations.js'; +import { buildTargets, seedFromValuations } from '../core/targets.js'; import { DEFAULT_CONFIG } from '../core/config.js'; const STORAGE_KEY = 'draft:current'; let store = new DraftStore({ config: DEFAULT_CONFIG }); let valuations = []; +let targets = []; +let autoAdjustTargets = false; let status = { profileId: null, tap: false, lastEventAt: null, source: null }; /** Persist on a timer rather than per-event; a hot auction fires in bursts. */ @@ -33,7 +36,9 @@ function schedulePersist() { } async function restore() { - const saved = await browser.storage.local.get([STORAGE_KEY, 'valuationsCsv', 'config']); + const saved = await browser.storage.local.get([ + STORAGE_KEY, 'valuationsCsv', 'config', 'targets', 'autoAdjustTargets', + ]); const config = { ...DEFAULT_CONFIG, ...(saved.config ?? {}) }; if (saved[STORAGE_KEY]?.log?.length) { @@ -43,6 +48,10 @@ async function restore() { } if (saved.valuationsCsv) applyValuations(saved.valuationsCsv, config); + // Targets are the user's own work, so they are restored verbatim rather + // than re-seeded from the CSV, which would silently discard hand edits. + targets = buildTargets(saved.targets ?? []); + autoAdjustTargets = saved.autoAdjustTargets ?? false; store.subscribe(() => schedulePersist()); } @@ -57,6 +66,8 @@ function currentSnapshot() { return { ...snapshot(store.state, valuations, { aggressiveness: store.baseConfig.aggressiveness ?? 0.5, + targets, + autoAdjustTargets, }), config: store.baseConfig, valuationCount: valuations.length, @@ -131,6 +142,31 @@ browser.runtime.onMessage.addListener((msg, sender) => { return Promise.resolve(result); } + case 'set-targets': { + targets = buildTargets(msg.targets ?? []); + browser.storage.local.set({ targets }); + broadcast(); + return Promise.resolve({ ok: true, count: targets.length }); + } + + case 'seed-targets': { + // Merge rather than replace: seeding is meant to fill an empty board or + // top it up, never to wipe prices you have already tuned. + const seeded = seedFromValuations(valuations, msg.options ?? {}); + const existing = new Map(targets.map((t) => [t.key, t])); + targets = buildTargets([...seeded, ...existing.values()]); + browser.storage.local.set({ targets }); + broadcast(); + return Promise.resolve({ ok: true, count: targets.length }); + } + + case 'set-auto-adjust': { + autoAdjustTargets = Boolean(msg.value); + browser.storage.local.set({ autoAdjustTargets }); + broadcast(); + return Promise.resolve({ ok: true }); + } + case 'reset': store = new DraftStore({ config: store.baseConfig }); store.subscribe(() => schedulePersist()); diff --git a/fantasy-auction-tracker/src/core/analytics.js b/fantasy-auction-tracker/src/core/analytics.js index f44444b..40e4a17 100644 --- a/fantasy-auction-tracker/src/core/analytics.js +++ b/fantasy-auction-tracker/src/core/analytics.js @@ -16,6 +16,9 @@ import { allTeamSummaries } from './reducer.js'; import { startersByPosition, slotAccepts, rosterSize } from './config.js'; import { POSITIONS, playerKey } from './players.js'; +import { + targetFor, effectivePrice, tierPressure, planFeasibility, targetStatus, +} from './targets.js'; /** * playerKey -> valuation, cached per valuation array. @@ -200,7 +203,11 @@ export function liveBidders(state, position, { excludeTeamId = null } = {}) { * alternative to overpaying is a materially worse roster. Both are hard-capped * by what you can legally bid. */ -export function bidAdvice(state, valuations, { aggressiveness = 0.5 } = {}) { +export function bidAdvice(state, valuations, { + aggressiveness = 0.5, + targets = [], + autoAdjustTargets = false, +} = {}) { const nom = state.nomination; if (!nom) return null; @@ -213,9 +220,28 @@ export function bidAdvice(state, valuations, { aggressiveness = 0.5 } = {}) { const rivals = liveBidders(state, nom.position, { excludeTeamId: config.myTeamId }); const parValue = val?.value ?? 0; - const adjusted = parValue * inf.discretionary; - const walkAway = Math.round(adjusted); - const rawCeiling = adjusted + aggressiveness * Math.max(0, cliff.cliff); + const marketValue = parValue * inf.discretionary; + + // An explicit price target is the user's own number and outranks anything + // derived from the CSV -- that is the whole point of setting one. The market + // figure is still reported alongside so divergence is visible rather than + // silently overridden. + const target = targetFor(targets, nom.playerKey); + const targetPrice = target + ? effectivePrice(target, { + parValue, inflation: inf.discretionary, autoAdjust: autoAdjustTargets, + }) + : null; + + const basis = targetPrice != null ? targetPrice : marketValue; + const walkAway = Math.round(basis); + + // A target's price is a ceiling you chose, so the cliff allowance does not + // get to push past it. Without a target, the cliff is what justifies going + // over par on a thin position. + const rawCeiling = targetPrice != null + ? targetPrice + : basis + aggressiveness * Math.max(0, cliff.cliff); const iCanFit = me ? me.slots.some((s) => !s.filled && slotAccepts(s.type, nom.position)) @@ -224,7 +250,7 @@ export function bidAdvice(state, valuations, { aggressiveness = 0.5 } = {}) { const ceiling = Math.max(0, Math.min(Math.round(rawCeiling), myMax)); let verdict; - if (!val) verdict = 'unvalued'; + if (!val && !target) verdict = 'unvalued'; else if (!iCanFit) verdict = 'no-slot'; else if (nom.highBid >= ceiling) verdict = 'pass'; else if (nom.highBid < walkAway) verdict = 'bid'; @@ -236,10 +262,10 @@ export function bidAdvice(state, valuations, { aggressiveness = 0.5 } = {}) { currentBid: nom.highBid, highBidder: nom.highBidder, parValue, - adjustedValue: Math.round(adjusted * 10) / 10, + adjustedValue: Math.round(marketValue * 10) / 10, walkAway, ceiling, - surplus: Math.round((adjusted - nom.highBid) * 10) / 10, + surplus: Math.round((basis - nom.highBid) * 10) / 10, tierCliff: Math.round(cliff.cliff * 10) / 10, nextBest: cliff.next?.name ?? null, inflation: Math.round(inf.discretionary * 100) / 100, @@ -248,6 +274,13 @@ export function bidAdvice(state, valuations, { aggressiveness = 0.5 } = {}) { topRival: rivals[0] ?? null, /** Rivals who could still take this player away from you at `ceiling`. */ threats: rivals.filter((r) => r.maxBid > nom.highBid).length, + /** Set when the player is on your board. */ + isTarget: Boolean(target), + targetTier: target?.tier ?? null, + targetPrice, + targetNote: target?.note || null, + /** How far your target sits from the market read, in dollars. */ + targetVsMarket: targetPrice != null ? Math.round(targetPrice - marketValue) : null, verdict, }; } @@ -273,7 +306,27 @@ export function budgetPressure(state) { /** One call for the whole sidebar, so the UI never assembles analytics itself. */ export function snapshot(state, valuations, opts = {}) { + const targets = opts.targets ?? []; + const inf = inflation(state, valuations); return { + tiers: tierPressure(state, targets), + plan: planFeasibility(state, targets, { + valuations, + inflation: inf.discretionary, + autoAdjust: opts.autoAdjustTargets ?? false, + }), + // The board with live status plus the price actually in force, so the + // Targets tab shows what auto-adjust is doing rather than the raw input. + targetBoard: targetStatus(state, targets).map((t) => ({ + ...t, + parValue: valuationFor(valuations, t.key)?.value ?? null, + effective: effectivePrice(t, { + parValue: valuationFor(valuations, t.key)?.value ?? null, + inflation: inf.discretionary, + autoAdjust: opts.autoAdjustTargets ?? false, + }), + })), + autoAdjustTargets: opts.autoAdjustTargets ?? false, teams: allTeamSummaries(state).map((t) => ({ teamId: t.teamId, teamName: t.teamName, @@ -284,7 +337,7 @@ export function snapshot(state, valuations, opts = {}) { needs: t.needs, roster: t.roster, })), - inflation: inflation(state, valuations), + inflation: inf, scarcity: scarcity(state, valuations), pressure: budgetPressure(state), advice: bidAdvice(state, valuations, opts), diff --git a/fantasy-auction-tracker/src/core/reducer.js b/fantasy-auction-tracker/src/core/reducer.js index 66771b5..c32b5b5 100644 --- a/fantasy-auction-tracker/src/core/reducer.js +++ b/fantasy-auction-tracker/src/core/reducer.js @@ -146,7 +146,13 @@ export function reduce(log, opts = {}) { switch (event.type) { case EventType.LEAGUE_CONFIGURED: - Object.assign(state.config, event.payload); + // Merge only values that are actually present. A config event derived + // from the draft room describes the league, not you -- letting its + // null myTeamId overwrite yours would silently disable your max-bid + // cap, the no-slot verdict and the whole target plan. + for (const [key, value] of Object.entries(event.payload)) { + if (value !== null && value !== undefined) state.config[key] = value; + } // Resize existing teams' slot arrays to the new roster shape. for (const team of state.teams.values()) { const filled = team.slots.filter((s) => s.filled); diff --git a/fantasy-auction-tracker/src/core/targets.js b/fantasy-auction-tracker/src/core/targets.js new file mode 100644 index 0000000..52a1d4d --- /dev/null +++ b/fantasy-auction-tracker/src/core/targets.js @@ -0,0 +1,244 @@ +/** + * Your target board. + * + * Valuations say what a player is worth to the market. Targets say what he is + * worth *to you* and how badly you want him -- which is a different question, + * and the one you actually bid off. + * + * Two pieces: + * tier - a bucket of interchangeable-to-you players. The point of tiers + * is that you need N players from a tier, not one specific name, + * so losing one is only a problem when the tier runs dry. + * maxPrice - the most you will pay for that player. Explicitly yours, and it + * overrides the market-derived ceiling. + * + * The number that makes this earn its keep is plan feasibility: whether the + * targets you still want actually fit in the money you have left. Finding out + * at $12 remaining that your plan needed $60 is the classic auction death, and + * it is entirely avoidable. + */ + +import { playerKey, normalizePosition, normalizeName } from './players.js'; +import { slotAccepts, rosterSize } from './config.js'; + +/** Normalize one target entry. */ +export function makeTarget(entry) { + const position = normalizePosition(entry.position); + const name = String(entry.name ?? '').trim(); + return { + key: playerKey(name, position), + name, + position, + /** 1 is the top tier. Lower number = want more. */ + tier: Number.isFinite(entry.tier) ? Math.max(1, Math.round(entry.tier)) : 1, + /** Your ceiling. null means "fall back to par value". */ + maxPrice: Number.isFinite(entry.maxPrice) ? entry.maxPrice : null, + note: entry.note ?? '', + }; +} + +/** Normalize and de-duplicate a target list, ordered by tier then price. */ +export function buildTargets(entries = []) { + const seen = new Map(); + for (const raw of entries) { + const t = makeTarget(raw); + if (!t.name) continue; + // A later entry for the same player wins, so editing is idempotent. + seen.set(t.key, t); + } + return [...seen.values()].sort( + (a, b) => a.tier - b.tier || (b.maxPrice ?? 0) - (a.maxPrice ?? 0) || a.name.localeCompare(b.name), + ); +} + +/** + * Seed a starting board from the valuation CSV. + * + * FantasyPros-style exports carry their own tier column; when present it is + * used directly, otherwise players are cut into `tierCount` bands by value so + * there is always something to edit rather than an empty screen. + */ +export function seedFromValuations(valuations, { topN = 60, tierCount = 6, positions = null } = {}) { + const pool = valuations + .filter((p) => p.value > 0) + .filter((p) => !positions || positions.includes(p.position)) + .sort((a, b) => b.value - a.value) + .slice(0, topN); + + const hasTiers = pool.some((p) => Number.isFinite(p.tier)); + const band = Math.max(1, Math.ceil(pool.length / tierCount)); + + return buildTargets(pool.map((p, i) => ({ + name: p.name, + position: p.position, + tier: hasTiers && Number.isFinite(p.tier) ? p.tier : Math.floor(i / band) + 1, + maxPrice: Math.round(p.value), + }))); +} + +/** + * The price you are actually willing to pay right now. + * + * An explicit maxPrice is yours and is used as written. When `autoAdjust` is + * on it is scaled by market inflation, because a $40 target in a market + * running 1.2x is really a $48 target -- holding the nominal number in an + * inflated market means quietly targeting a worse player. + */ +export function effectivePrice(target, { parValue = null, inflation = 1, autoAdjust = false } = {}) { + const base = target.maxPrice ?? parValue; + if (base == null) return null; + return autoAdjust ? Math.round(base * inflation) : Math.round(base); +} + +/** + * Annotate each target with what happened to it. + * + * open - still available + * won - you got him + * lost - somebody else did + */ +export function targetStatus(state, targets, { teamId = null } = {}) { + const me = teamId ?? state.config.myTeamId; + return targets.map((t) => { + const sale = state.drafted.get(t.key); + if (!sale) return { ...t, status: 'open', wonBy: null, soldFor: null }; + return { + ...t, + status: sale.teamId === me ? 'won' : 'lost', + wonBy: sale.teamId, + soldFor: sale.price, + /** Positive means it went for more than you were willing to pay. */ + overshoot: t.maxPrice != null ? sale.price - t.maxPrice : null, + }; + }); +} + +/** Per-tier rollup: what is left, what you got, and what it cost. */ +export function tierSummary(state, targets, opts = {}) { + const annotated = targetStatus(state, targets, opts); + const tiers = new Map(); + + for (const t of annotated) { + if (!tiers.has(t.tier)) { + tiers.set(t.tier, { + tier: t.tier, total: 0, open: 0, won: 0, lost: 0, + openPlayers: [], spent: 0, plannedCost: 0, + }); + } + const row = tiers.get(t.tier); + row.total += 1; + if (t.status === 'open') { + row.open += 1; + row.openPlayers.push(t); + row.plannedCost += t.maxPrice ?? 0; + } else if (t.status === 'won') { + row.won += 1; + row.spent += t.soldFor ?? 0; + } else { + row.lost += 1; + } + } + + return [...tiers.values()].sort((a, b) => a.tier - b.tier); +} + +/** + * Can you still afford the targets you have left? + * + * Walks your open targets in tier order, assigning each to the tightest open + * roster slot that accepts him -- the same greedy fit the reducer uses -- and + * stops when your roster is full. Then compares the cost of that plan against + * the money you actually have, reserving $1 for every slot the plan does not + * cover. + * + * @returns {{ feasible, plan, plannedCost, budget, shortfall, unslotted, reserve }} + */ +export function planFeasibility(state, targets, { teamId = null, inflation = 1, autoAdjust = false, valuations = [] } = {}) { + const config = state.config; + const me = teamId ?? config.myTeamId; + const team = me ? state.teams.get(me) : null; + + const size = rosterSize(config); + const filled = team ? team.roster.length : 0; + const openSlotTypes = team + ? team.slots.filter((s) => !s.filled).map((s) => s.type) + : config.rosterSlots.slice(); + const budget = config.budget - (team?.spent ?? 0); + const openSlots = Math.max(0, size - filled); + + const parFor = new Map(valuations.map((p) => [playerKey(p.name, p.position), p.value])); + const open = targetStatus(state, targets, { teamId: me }).filter((t) => t.status === 'open'); + + const remainingSlots = [...openSlotTypes]; + const plan = []; + const unslotted = []; + + for (const target of open) { + // Tightest fit first: a dedicated slot before flex, flex before bench, + // so a WR does not eat the FLEX that a later RB needs. + const rank = (slot) => (slot === 'BN' ? 2 : ['FLEX', 'SUPERFLEX', 'WRRB', 'OP'].includes(slot) ? 1 : 0); + const candidates = remainingSlots + .map((slot, i) => ({ slot, i })) + .filter(({ slot }) => slotAccepts(slot, target.position)) + .sort((a, b) => rank(a.slot) - rank(b.slot)); + + if (!candidates.length) { + unslotted.push(target); + continue; + } + const price = effectivePrice(target, { + parValue: parFor.get(target.key) ?? null, inflation, autoAdjust, + }); + remainingSlots.splice(candidates[0].i, 1); + plan.push({ ...target, slot: candidates[0].slot, price: price ?? config.minBid }); + } + + const plannedCost = plan.reduce((s, p) => s + p.price, 0); + // Slots the plan does not cover still need a body at the minimum bid. + const reserve = Math.max(0, openSlots - plan.length) * config.minBid; + const required = plannedCost + reserve; + + return { + feasible: required <= budget, + plan, + plannedCost, + reserve, + required, + budget, + openSlots, + shortfall: Math.max(0, required - budget), + headroom: Math.max(0, budget - required), + /** Targets with no roster slot left to hold them. */ + unslotted, + }; +} + +/** + * Tier pressure: how close a tier is to running dry relative to how many of + * its players are still realistically gettable. + * + * A tier with two names left and eight rivals who need the position is not two + * players deep -- it is nearly empty, and that is when you stop being + * disciplined about a dollar. + */ +export function tierPressure(state, targets, opts = {}) { + return tierSummary(state, targets, opts).map((row) => ({ + ...row, + /** 0 = untouched, 1 = every target in this tier is gone. */ + depletion: row.total ? (row.total - row.open) / row.total : 1, + critical: row.open > 0 && row.open <= 2, + exhausted: row.open === 0 && row.total > 0, + })); +} + +/** Find a target by the nominated player, if any. */ +export function targetFor(targets, key) { + return targets.find((t) => t.key === key) ?? null; +} + +/** Look up by loose name so the sidebar can add "the player on the block". */ +export function findByName(targets, name, position) { + const n = normalizeName(name); + const pos = normalizePosition(position); + return targets.find((t) => normalizeName(t.name) === n && (!pos || t.position === pos)) ?? null; +} diff --git a/fantasy-auction-tracker/src/sidebar/sidebar.css b/fantasy-auction-tracker/src/sidebar/sidebar.css index c5bd0da..b75fae1 100644 --- a/fantasy-auction-tracker/src/sidebar/sidebar.css +++ b/fantasy-auction-tracker/src/sidebar/sidebar.css @@ -149,3 +149,59 @@ button.danger { color: var(--bad); } } .alert.review { border-left-color: var(--bad); } .alert button { margin-left: 6px; padding: 1px 6px; font-size: 10px; } + +/* --- targets -------------------------------------------------------------- + The board is edited in place: every price and tier cell is a live input, so + retuning a number mid-draft is one click rather than a dialog. */ + +.tierbadge { + margin-left: 6px; + padding: 1px 6px; + border-radius: 8px; + background: var(--line); + color: var(--muted); + font-size: 10px; + letter-spacing: 0; + text-transform: none; +} +.tierbadge.critical { background: var(--warn); color: #fff; } +.tierbadge.gone { background: var(--bad); color: #fff; } + +table.targets td { padding: 2px 6px; } +table.targets input.price, +table.targets input.tier { + width: 100%; + padding: 2px 4px; + text-align: right; + background: var(--bg); + color: var(--fg); + border: 1px solid var(--line); + border-radius: 3px; + font-variant-numeric: tabular-nums; +} +table.targets input.tier { max-width: 34px; } +table.targets input.price { max-width: 52px; } +table.targets input:focus { border-color: var(--accent); outline: none; } +table.targets input:disabled { background: transparent; border-color: transparent; color: var(--muted); } + +table.targets tr.won { background: color-mix(in srgb, var(--good) 12%, transparent); } +table.targets tr.lost td { color: var(--muted); } +table.targets tr.lost td:first-child { text-decoration: line-through; } + +.tag { font-size: 10px; padding: 1px 5px; border-radius: 3px; white-space: nowrap; } +.tag.won { background: var(--good); color: #fff; } +.tag.lost { background: var(--line); color: var(--muted); } + +.eff { font-size: 10px; color: var(--accent); margin-left: 3px; } + +button.drop { + padding: 0 5px; + line-height: 1.2; + color: var(--muted); + border-color: transparent; + background: none; +} +button.drop:hover { color: var(--bad); border-color: var(--bad); } + +.target-line { color: var(--accent); font-weight: 500; } +label.inline { display: flex; align-items: center; gap: 4px; font-size: 11px; color: var(--muted); } diff --git a/fantasy-auction-tracker/src/sidebar/sidebar.html b/fantasy-auction-tracker/src/sidebar/sidebar.html index d128d60..08e31a6 100644 --- a/fantasy-auction-tracker/src/sidebar/sidebar.html +++ b/fantasy-auction-tracker/src/sidebar/sidebar.html @@ -13,6 +13,7 @@

Auction Tracker