Skip to main content

Lineup optimization

Every Tuesday the lineup solver picks the starters with the highest total VORP and sends the swaps to ESPN as one roster transaction. The UI and rumor actions use the same trigger for manual and event-driven moves.

What triggers it?​

TriggerPath
fantasy-lineup-optimizer CronJob, Tuesday 19:15 Chicagopython -m src.optimization.lineup_solver calls POST /v1/trigger/lineup
Lineup page in the UInginx adds X-API-Key and forwards to the same route. The UI sends a random idempotency key per submission.
Rumor actionsbench_watch signals call the same route. See Rumor actions.

What is involved?​

ItemValue
Queuelineup_updates, broker-declared, dead-letters to lineup_updates.dlq
Tablesweekly_stats, league_rosters (inputs); activity_actions, activity_outbox, activity_events, bot_decisions, execution_log (record)
Idempotency keylineup-<team>-<hash>: SHA-256 of the CronJob's Job name, team, season and week, and the sorted swaps. Released 30 min after the action finishes.
Worker claimactivity.claim(tracking_id) under a Postgres advisory lock
ESPN writePOST …/transactions/ with type: "ROSTER", one LINEUP item per move
VerificationEvery moved player sits in its intended slot on a fresh roster read
DRY_RUN defaultLive. Set DRY_RUN=true to plan without writing.

The Job name in the key means each new Job publishes once, while pod retries inside that Job dedupe.

What can stop it?​

SituationResult
Missing target-week metrics and no fresh prior weeksSolver raises InsufficientLineupInputError. Nothing is published.
No better lineupEmpty swaps. Nothing is published.
Lineup locked on ESPNPermanentError. The week's optimization no longer applies.
A player in two swaps, an unknown slot or a bad idPermanentError, dead-lettered
Verification fails after four readsRequeued. The retry finds the moves in place and completes.
Expired ESPN cookiesRequeued after 30 s

Read more​