Skip to main content

Ingestion

Ingestors pull player, league, projection and score data into Postgres. VORP and the optimizers read it. Run order matters, because each step depends on columns the previous one fills.

Schedules live in fantasy-infra

The times below come from the CronJob manifests in fantasy-infra (base/core-pipeline/) as of 2026-10-09. All use the America/Chicago time zone. If a time here disagrees with the manifest, the manifest wins.

In what order do they run?​

DependencyWhy
sync_id_crosswalk after sync_playersIt updates existing player rows.
sync_league after sync_id_crosswalkRoster rows are matched on players.espn_id.
metrics_extractor after sync_id_crosswalkIt needs a fresh gsis_id.
sync_kicker_dst after sync_leagueIt reuses the ESPN connection and needs the K and D/ST rows from sync_players.
vorp_matrix after projections and metricsVORP is computed from projected_points and the usage metrics.
lineup_solver after vorp_matrixIts objective is each player's weekly_stats.vorp_score.

What does each ingestor do?​

ModuleSourceWritesNotes
sync_playersSleeper player masterplayersRetags Sleeper's DEF as D/ST. Gated to once a day by a Redis lock; pass --force to override. It also publishes injury alerts.
sync_id_crosswalkDynastyProcess crosswalkplayers.espn_id, players.gsis_idCovers most skill players, and no K or D/ST.
sync_leagueESPN league APIleague_settings, league_teams, league_rostersNeeds ESPN_LEAGUE_ID, ESPN_S2, ESPN_SWID. --season is required.
sync_kicker_dstESPN kona_player_infoplayers.espn_id, weekly_stats.projected_points for K and D/STTakes --season and --week. A matched player with no projection gets no row, never a 0.0 row.
sync_skill_position_crosswalkESPN kona_player_info, no ownership filterplayers.espn_idFills the QB, RB, WR and TE players the crosswalk missed. Matches by name and team.
sync_projectionsSleeper projectionsweekly_stats.projected_pointsQB, RB, WR and TE only. Takes --season and --week.
metrics_extractornflverse play-by-play, snaps, scheduleweekly_stats usage and betting columnsDefaults to the last completed week. See below.
vorp_matrixweekly_stats, league_settingsweekly_stats VORP columnsTakes --season and --week.
sync_defense_strengthnflverse, previous seasondefense_position_strengthOnly needed when MATCHUP_WEIGHTING_ENABLED=true.
sync_rumorsRotoWire RSS, r/fantasyfootballraw_signalsFetch and store only. The LLM interpretation happens in the worker.
sync_actual_scoresESPN scoring weeksfantasy_* tables, weekly_stats.actual_pointsSee Score history.

When do they run?​

CronJobScheduleRuns
fantasy-rapid-status-syncerEvery 10 min, Thursday, Sunday, Mondaysync_players
fantasy-weekly-ingestorTuesday 16:00sync_league, then metrics_extractor
fantasy-sync-actual-scoresTuesday 17:05sync_actual_scores
fantasy-sync-actual-scores-reconcileFriday 10:00sync_actual_scores again, to pick up stat corrections
fantasy-draft-prep-refresherDaily 18:00For the upcoming week and up to three more: sync_projections, sync_kicker_dst, vorp_matrix
fantasy-lineup-optimizerTuesday 19:15lineup_solver
fantasy-weekly-waiver-triggerTuesday 20:30waiver_evaluator
fantasy-rumor-syncerEvery 30 minsync_rumors
fantasy-rumor-action-triggerDaily 21:45rumor_action_service
Not scheduled

No CronJob runs sync_id_crosswalk, sync_skill_position_crosswalk or sync_defense_strength. Run them by hand after a large sync_players change, in the order shown above.

How does metrics_extractor pick a week?​

By default it processes the last completed regular-season week: every game final in the nflverse schedule. nflreadpy's own "current week" is already the upcoming week by Tuesday, so it isn't used.

SituationResult
Preseason, no completed weekExit 0, nothing written.
Resolved week writes 0 rowsExit 1.
Play-by-play is missing a game the schedule marks finalIncompletePlayByPlayError, exit 1, and the missing games are named. Nothing partial is written.
A rerun after nflverse catches upFills the week. The upsert is idempotent.

How is the target week chosen elsewhere?​

src/nfl_schedule.py returns the earliest regular-season week with a game not yet final, retrying the schedule download before it raises WeekResolutionError. It never substitutes another week. When the regular season is over, the optimizer CLIs exit 0 as a no-op.

How is score history stored?​

sync_actual_scores reprocesses every completed week on each run, so a later run recovers a missed week and picks up ESPN stat corrections. A week ESPN hasn't finished fails the Job rather than being skipped or guessed; weeks before it stay committed.

TableGrainKey
fantasy_matchupsTeam and matchup period(league_id, season, matchup_period, team_id)
fantasy_team_week_scoresTeam and week(league_id, season, week, team_id)
fantasy_player_week_scoresPlayer, team and week(league_id, season, week, team_id, espn_id)

It also sets weekly_stats.actual_points, with actual_points_source and actual_points_updated_at recording where each value came from. Use --dry-run to validate a week without opening the database.