RIVAL.FANTASY INTELLIGENCE
● DATA STATUS: NOT CONNECTED
THE FRONT OFFICE IS YOURS

Make the move.
Own the advantage.

Trade intelligence, player scouting, waiver opportunities and roster strategy — built around your league, not generic rankings.

Your league setup

Select your sport and ESPN scoring format. Custom scoring can be adjusted without typing an entire ruleset.

Presets are based on common ESPN formats. Always compare with your league's exact settings.

YOU SEND

Your side of the deal

Add multiple players for package trades.

YOU RECEIVE

Coming to your roster

Compare both immediate and rest-of-season value.

Trade verdict

Risk-adjusted value, team fit, upside and opportunity cost.

Select players on both sides to begin.

Player intelligence report

Current production, trends, opportunity, risks and historical context.

Select a player to inspect available information.
THE WAIVER WIRE

This week's opportunities

Games scheduled, emerging usage and available roster upgrades.

Connect a current player-stat feed and your league's available-player list to rank genuine pickups. No invented recommendations.
A player being available in the NBA does not mean they are available in your ESPN fantasy league. League-specific availability requires roster access.

Build your roster

Add your team, then inspect depth, consistency and trade priorities.

Front office report

Add your players to see the analysis.
const rival = {players: [], loaded: false, days: 7}; const rivalFields = { PTS: 'points', '3PM': 'threePointersMade', FGM: 'fieldGoalsMade', FGA: 'fieldGoalsAttempted', FTM: 'freeThrowsMade', FTA: 'freeThrowsAttempted', REB: 'rebounds', AST: 'assists', STL: 'steals', BLK: 'blocks', TOV: 'turnovers' }; const rivalNum = n => Number.isFinite(Number(n)) ? Number(n) : 0; const rivalScore = row => Object.entries(currentWeights).reduce( (sum, [key, weight]) => sum + rivalNum(row[rivalFields[key]]) * rivalNum(weight), 0 ); const rivalAvg = p => p?.recentGames?.length ? p.recentGames.reduce( (sum, g) => sum + rivalScore(g), 0 ) / p.recentGames.length : NaN; const rivalFmt = n => Number.isFinite(n) ? n.toFixed(1) : 'N/A'; const rivalName = p => escapeHtml(p.name); const rivalStatus = document.getElementById('liveStatus'); const rivalInfo = document.createElement('div'); rivalInfo.className = 'notice'; rivalInfo.innerHTML = ` NBA recent form: Last days of completed games. These are observed statistics, not rest-of-season projections. `; document.getElementById('settings').appendChild(rivalInfo); document.getElementById('rivalDays').onchange = e => { rival.days = Number(e.target.value); rivalLoad(); }; function rivalPlayerCard(p) { const avg = rivalAvg(p); return `

${rivalName(p)} ${escapeHtml(p.team || 'NBA')}

${rivalFmt(avg)}

Fantasy pts/game · ${p.games || 0} completed games

Points ${rivalFmt(p.averages?.points)}
Rebounds ${rivalFmt(p.averages?.rebounds)}
Assists ${rivalFmt(p.averages?.assists)}
Steals / Blocks ${rivalFmt(p.averages?.steals)} / ${rivalFmt(p.averages?.blocks)}
Minutes ${rivalFmt(p.averages?.minutes)}
`; } function rivalRefreshSides() { for (const side of Object.keys(sides)) { sides[side] = sides[side].map(old => rival.players.find(p => p.id === old.id) || rival.players.find(p => p.name === old.name) || old ); } renderChips(); } async function rivalLoad() { if (sport.value !== 'nba') { rival.loaded = false; playerCache = []; rivalStatus.textContent = '● NBA LIVE FEED ONLY'; document.getElementById('waiverResult').textContent = 'This connected feed currently supports NBA only.'; return; } rivalStatus.textContent = '● LOADING NBA BOX SCORES…'; try { const response = await fetch( '/api/players?sport=nba&days=' + rival.days, {cache: 'no-store'} ); const data = await response.json(); if (!response.ok || !Array.isArray(data.players)) { throw Error(data.details || data.error || 'Invalid response'); } rival.players = data.players.filter(p => p.id && p.name); rival.loaded = true; playerCache = rival.players; rivalRefreshSides(); rivalStatus.textContent = `● ESPN NBA · ${data.playerCount ?? rival.players.length}` + ` PLAYERS · ${data.completedGames ?? '?'} GAMES` + (data.partial ? ' · PARTIAL' : ''); rivalWaivers(); } catch (err) { rival.loaded = false; rivalStatus.textContent = '● NBA DATA UNAVAILABLE'; const el = document.getElementById('waiverResult'); el.className = 'notice'; el.textContent = 'Could not load NBA data: ' + err.message; } } function rivalWaivers() { const el = document.getElementById('waiverResult'); const ranked = rival.players .filter(p => p.games >= 2 && Number.isFinite(rivalAvg(p))) .sort((a, b) => rivalAvg(b) - rivalAvg(a)) .slice(0, 15); el.className = 'result'; el.innerHTML = `

Top recent performers with at least 2 games in this window. These are NOT verified free agents in your ESPN league.

${ranked.map(rivalPlayerCard).join('')}
`; } document.getElementById('evaluate').onclick = () => { const el = document.getElementById('tradeResult'); if (!sides.send.length || !sides.receive.length) { el.className = 'empty'; el.textContent = 'Add at least one player on each side.'; return; } const all = [...sides.send, ...sides.receive]; const invalid = all.filter(p => !p.recentGames?.length); if (invalid.length) { el.className = 'notice'; el.textContent = 'No completed-game data for: ' + invalid.map(p => p.name).join(', ') + '. Try a 14-day window. No verdict can be calculated.'; return; } const a = sides.send.reduce((n, p) => n + rivalAvg(p), 0); const b = sides.receive.reduce((n, p) => n + rivalAvg(p), 0); const delta = b - a; el.className = 'result'; el.innerHTML = `
RECENT FANTASY PRODUCTION · ${rival.days}-DAY WINDOW

${ Math.abs(delta) < 0.05 ? 'Equal combined recent averages' : delta > 0 ? 'Receiving side leads by ' + rivalFmt(delta) + ' FP/game' : 'Sending side leads by ' + rivalFmt(-delta) + ' FP/game' }

You send

${rivalFmt(a)}

Combined FP/game

You receive

${rivalFmt(b)}

Combined FP/game

Difference (receive − send)

${delta >= 0 ? '+' : ''}${rivalFmt(delta)}

FP/game

Recent sample only. Multi-player package sums do not account for roster spots, replacement players, injuries, future schedules or rest-of-season projections. This is not a final fairness verdict.
${all.map(rivalPlayerCard).join('')}
`; }; document.getElementById('scoutBtn').onclick = () => { const el = document.getElementById('scoutResult'); const q = document.getElementById('scoutInput') .value.trim().toLowerCase(); const p = rival.players.find(x => x.name.toLowerCase() === q) || rival.players.find(x => x.name.toLowerCase().includes(q)); if (!p) { el.className = 'empty'; el.textContent = 'No matching NBA player with recent box-score data. ' + 'Try a different spelling or a 14-day window.'; return; } el.className = 'result'; el.innerHTML = ` ${rivalPlayerCard(p)}

Recent game log

${[ 'Date','MIN','PTS','REB','AST', 'STL','BLK','TOV','Fantasy' ].map(x => `` ).join('')} ${p.recentGames.map(g => ` ${[ g.date?.slice(0, 10) || '', rivalFmt(g.minutes), g.points, g.rebounds, g.assists, g.steals, g.blocks, g.turnovers, rivalFmt(rivalScore(g)) ].map(x => ` `).join('')} `).join('')}
${x}
${escapeHtml(x)}

Scoring uses your current league weights. Historical results only; no injury or future-game verification.

`; }; document.getElementById('teamBtn').onclick = () => { const el = document.getElementById('teamResult'); if (!sides.roster.length) { el.className = 'empty'; el.textContent = 'Add your roster players first.'; return; } const valid = sides.roster.filter(p => p.recentGames?.length); el.className = 'result'; el.innerHTML = `

${valid.length} of ${sides.roster.length} players have recent games

${rivalFmt(valid.reduce((n, p) => n + rivalAvg(p), 0))}

Sum of available player FP/game averages (not a weekly projection)

${ sides.roster.filter(p => !p.recentGames?.length).length ? '

Missing recent games: ' + sides.roster .filter(p => !p.recentGames?.length) .map(p => escapeHtml(p.name)) .join(', ') + '

' : '' }
${valid .sort((a, b) => rivalAvg(b) - rivalAvg(a)) .map(rivalPlayerCard) .join('')}
`; }; sport.onchange = () => { sides = {send: [], receive: [], roster: []}; renderChips(); format.innerHTML = Object.keys(presets[sport.value]) .map(k => ``) .join(''); drawWeights(); rivalLoad(); }; format.onchange = () => { drawWeights(); rivalWaivers(); }; weights.addEventListener('change', () => { if (rival.loaded) rivalWaivers(); }); rivalLoad();