Static copy captured for readers and tools without JavaScript. The live site is at https://epldesk.com/.

Summary

Gameweek 6

Priced, not yet called

0 of 10 locked by the desk

The desk can lock a call at any point before its kickoff, and no later than 45 minutes before it. A match the desk has not called by then is locked to the model’s own pick, labelled a fallback and kept out of the desk’s record.

Next up

2d 10hto kickoff

Arsenal1 – 0Leeds

Likeliest score in an Arsenal win · Arsenal 52% · draw 27% · Leeds 21%

Kickoff
Sat 10 Oct, 12:30 ⁠BST
Call locks
11:45 ⁠BST

All 10 matches and what the model thinks →

How has the desk done?

11 of 27results right since calls were locked before kickoff (GW3–5) · 41%

Since calls started being locked before kickoff (GW3), the desk has 11 of 27 results right. Backing the betting favourite in the same 27 would have got 10: one match apart, which is not yet a signal either way.

Desk calls: 11 of 27 right · desk coverage: 27 of 30 settled fixtures locked by the desk · all 30 fixtures, including 3 never called: 11 of 30 right. The desk record counts human calls only; this line closes the gap by counting every fixture.

GW3 ✓✕✕✕✕✓✕✓✕✓ 4/10
GW4 ✕✓✕✕✕✕✓✓✓✓ 5/10
GW5 ✕–✕✓✓✕–✕–✕ 2/7 3 matches not called, left out of the count, shown here

Retrospective: scored after the fact, not locked

GW1 ✓✕✕✕✕✕✓✓✕✓ 4/10
GW2 ✓✕✕✓✓✓✓✕✓✓ 7/10

right result wrong result not called model fallback

The full record on the Scoreboard →

Does it beat the betting market?

Getting the winner right is only half of it: the desk also says how likely each result is. Scored that way (Brier, lower is better), there is no daylight yet between the desk and the closing Betfair price: 0.726 v 0.728 over the 17 matches with locked percentages, too few to say either is better. The desk won GW4 and lost GW5.

On the same 17 matches the model on its own scored 0.700, and the model on market ratings 0.691: the desk’s changes to the model helped in GW4 and cost it in GW5. On the matches each also priced, Polymarket at lock scored 0.704 to the desk’s 0.691 (n 11) and Opta’s supercomputer scored 0.729 to the desk’s 0.726 (n 17).

Pooled Brier over the 17 matches of GW4–5 with locked percentages; a row that priced fewer of them shows its n. Lower is better; the axis shows 0.50–0.90 of a 0–2 scale. ← more accurate

All six scored worse than an uninformative ⅓-⅓-⅓ probability forecast, which scores 0.667 on every match. On a small, upset-heavy sample that does not mean worse than random: these were rounds of upsets.

How the market comparison is scored, and the full numbers

A Brier score is the squared gap between the percentages given and what happened, summed over home win, draw and away win: 0 is perfect, 2 is 100% sure and wrong, and a flat ⅓-⅓-⅓ guess scores 0.667. Every row is scored on the desk’s own called matches where it also has a probability; a cell on fewer matches than the desk carries its n, and the best in a column is marked only among cells on the desk’s own matches. “Model · market ratings” is the same engine starting from team ratings learned from Betfair closing prices (the Scoreboard’s “market prior”), then updated by this season’s xG like the other.

Brier, lower is betterGW4GW5GW4–5 pooled
Desk0.6740.8000.726
Model · pre-season ratings0.6790.7310.700
Model · market ratings0.6780.7100.691
Polymarket · at lock0.789 (n 7)0.557 (n 4)0.704 (n 11)
Opta supercomputer0.7360.7190.729
Betfair close0.7280.7290.728
Matchesn 10n 7n 17

Opta’s supercomputer probabilities on 17 of the 17 matches: 11 from the line logged in the ledger beside the lock; 6 transcribed from The Analyst’s GW5 article (theanalyst.com/articles/premier-league-match-predictions, retrieved 2026-10-07). On 6 of them the source gives both win chances and the draw is the remainder. Polymarket at lock on 11 of the 17, leaving out 6 whose draw leg traded under $500.

Pooled RPS, each row on the matches it and the desk both priced: Desk 0.239 · Model · pre-season ratings 0.227 · Model · market ratings 0.225 · Polymarket · at lock 0.242 (n 11) · Opta supercomputer 0.238 · Betfair close 0.238.

In GW5 the desk scored 0.800 against Betfair’s closing 0.729 on the same 7 fixtures; the market was sharper.

Every round scored on six rows, Brier and RPS, on the Scoreboard →

Gameweek 6 at a glance

10 matches · Sat 10 Oct to Mon 12 Oct · times ⁠BST · nothing below is a desk call yet

Clearest favourite
Arsenal v Leeds: Arsenal 52%, the only side above half, still under the 55% the desk treats as a confident call. Open the match →
Coin-flips
Ipswich v Fulham (37 v 37) and Liverpool v Man City (37 v 36): the top two outcomes within 3 points, too close to call. Open the match →
Model split on itself
Ipswich v Fulham: the model’s two versions can’t agree who edges it, 37.6–36.6 to Ipswich on pre-season ratings, 36.8–37.1 to Fulham on market ratings (home–away). Liverpool v Man City: the model’s two versions can’t agree who edges it, 36–37 to Man City on pre-season ratings, 37–36 to Liverpool on market ratings (home–away). Open the match →

home windrawaway winthe desk’s locked pick

SAT 10 OCT · 6 MATCHES

Now · 01:51 ⁠BST · 2d 10h to the next kickoff

12:30locks 11:45
Arsenal1 – 0Leeds

Likeliest score in an Arsenal win · Arsenal 52% · draw 27% · Leeds 21%

○ OpenMatch →
Model · pre-season ratings, now
522721
15:00locks 14:15
Aston Villa1 – 2Brentford

Likeliest score in a Brentford win · Aston Villa 25% · draw 25% · Brentford 50%

○ OpenMatch →
Model · pre-season ratings, now
252550
15:00locks 14:15
Chelsea2 – 1Bournemouth

Likeliest score in a Chelsea win · Chelsea 45% · draw 26% · Bournemouth 29%

○ OpenMatch →
Model · pre-season ratings, now
452629
15:00locks 14:15
Ipswich2 – 1Fulham

Likeliest score in an Ipswich win · Ipswich 37% · draw 26% · Fulham 37%

○ OpenMatch →
Model · pre-season ratings, now
372637
15:00locks 14:15
Sunderland1 – 2Brighton

Likeliest score in a Brighton win · Sunderland 34% · draw 25% · Brighton 41%

○ OpenMatch →
Model · pre-season ratings, now
342541
17:30locks 16:45
Man United2 – 1Tottenham

Likeliest score in a Man United win · Man United 49% · draw 26% · Tottenham 25%

○ OpenMatch →
Model · pre-season ratings, now
492625

SUN 11 OCT · 3 MATCHES

14:00locks 13:15
Crystal Palace1 – 2Nott’m Forest

Likeliest score in a Nott’m Forest win · Crystal Palace 32% · draw 27% · Nott’m Forest 41%

○ OpenMatch →
Model · pre-season ratings, now
322741
14:00locks 13:15
Hull0 – 1Everton

Likeliest score in an Everton win · Hull 30% · draw 29% · Everton 41%

○ OpenMatch →
Model · pre-season ratings, now
302941
16:30locks 15:45
Liverpool1 – 2Man City

Likeliest score in a Man City win · Liverpool 36% · draw 27% · Man City 37%

○ OpenMatch →
Model · pre-season ratings, now
362737

MON 12 OCT · 1 MATCH

20:00locks 19:15
Coventry2 – 1Newcastle

Likeliest score in a Coventry win · Coventry 38% · draw 28% · Newcastle 34%

○ OpenMatch →
Model · pre-season ratings, now
382834

Every match with both model versions, the market and the desk’s why and risk →

Elsewhere on the desk

  • ScoreboardEach round scored on six rows, Brier and RPS
  • FPL teamGW6 squad, captured Wed 7 Oct, 20:29 ⁠BST
  • Diagnostics83 checks · 71 pass · 1 fail · 8 blocked · 3 manualBlocked means no data yet; manual means not machine-checked.

Rounds

Gameweek 6

Priced, not yet locked

10 matches, Sat 10 Oct 12:30 to Mon 12 Oct 20:00 ⁠BST. The model has priced every match; the desk will lock its calls before each kickoff. Two matches are genuine coin-flips (Ipswich v Fulham and Liverpool v Man City) and no side reaches the 55% the desk treats as a confident call: a round of leans, not bankers. Polymarket’s price is captured about 24 h before each kickoff and again when the call locks; Betfair’s closing price arrives after the match.

Season so far

Record · GW1–2 retrospective, then locked
GW1 4/10 · GW2 7/10 · GW3 4/10 · GW4 5/10 · GW5 2/7 (of 10)
Brier · desk, model v Betfair close (GW5)
desk 0.800 · model 0.731 v 0.729 · 7 of 10 called
First kickoff · desk not yet locked
2d 10h · Sat, 10 Oct, 12:30 BST · FPL deadline Sat 10, 11:00 BST
How to read this round

Three rows per fixture, never blended: the engine on the pre-season prior, the engine on the market-implied (Betfair closing, GW1–5, 50 fixtures), and Polymarket, captured about 24 h before each kickoff (the reference price) and again when the call locks. Evidence = FotMob xG on both; w = 0.500.

First kickoff · Sat, 10 Oct, 12:30 BST · in 2d 10h · 10 fixtures over 55.5h · FPL deadline Sat, 10 Oct, 11:00 BST

Each match shows the model twice and never blends them: Model · pre-season ratings is the engine on its pre-season team ratings, updated by this season’s xG; Model · market ratings is the same engine starting from ratings learned from Betfair closing prices (the Scoreboard calls it the market prior). Neither is a live price. Polymarket · reference is the prediction market captured automatically about 24 h before kickoff, and Polymarket at lock the same market when the call locks; Betfair close is the exchange’s last price before kickoff, the benchmark every call is scored against.

A call is locked into a write-once ledger before kickoff and cannot be edited. A match the desk has not called 45 minutes before kickoff is locked to the model’s pick and labelled a fallback; it never counts as a desk hit or miss. A missed lock is a match with no call at all: it is left out of the desk’s score and still shown. A margin of 3 points or less is a dead heat: the scoreline is published but it is not really a call. Under 55% is a lean; 55% or more is a confident call.

Every match

home windrawaway winthe desk’s pickFigures are percentages, home · draw · away.

SAT 10 OCT · 6 MATCHES

12:30 locks 11:45 ○ Open

Arsenal1 – 0Leeds

Likeliest score in an Arsenal win · Arsenal 52% · draw 27% · Leeds 21%

The model leans Arsenal win (52%), well ahead of a draw (27%). Most likely score overall 1⁠–⁠1; the model’s prediction, 1⁠–⁠0, is the likeliest score within an Arsenal win.

Expected goals 1.65 – 0.99 · clean-sheet chance 37% · 19% · days of rest 21 · 20

Team news, out or doubtful: Arsenal 7 · Leeds 3

Technical detail
Call
52% home · margin +24.9
Flag
Low conviction
Flag
CS Arsenal 37% · Leeds 19%
Flag
λ 1.65 / 0.99
Flag
Rest 20.9 / 19.94 days
Flag
Polymarket not captured yet
Engine
Conditional mode 1–0 inside the home region; unconditional mode 1–1. On the market prior: 1–0 at 46%.
Desk
No desk prose until the desk locks this match (the routine is due to lock it from Fri, 9 Oct, 12:30 BST, 24 h before kickoff). Team news is on the Underlying tab.
Model · now
52 · 27 · 21
Market prior · now
46 · 29 · 25
Desk
not yet locked
Polymarket at lock
stages with the lock

How is this priced? →

Model · pre-season ratings, now
522721
Model · market ratings, now
462925
Desk call

Not locked yet · locks 11:45 ⁠BST, or falls back to the model

Polymarket at lock

Stages with the lock

Betfair close

After kickoff: the price every call is scored against

15:00 locks 14:15 ○ Open

Aston Villa1 – 2Brentford

Likeliest score in a Brentford win · Aston Villa 25% · draw 25% · Brentford 50%

The model leans Brentford win (50%), well ahead of a draw (25%). Most likely score overall 1⁠–⁠1; the model’s prediction, 1⁠–⁠2, is the likeliest score within a Brentford win.

Expected goals 1.21 – 1.78 · clean-sheet chance 17% · 30% · days of rest 8 · 22

Team news, out or doubtful: Aston Villa 7 · Brentford 7

Technical detail
Call
50% away · margin +24.0
Flag
Low conviction
Flag
CS Aston Villa 17% · Brentford 30%
Flag
λ 1.21 / 1.78
Flag
Rest 7.81 / 21.79 days
Flag
Polymarket not captured yet
Engine
Conditional mode 1–2 inside the away region; unconditional mode 1–1. On the market prior: 1–2 at 50%.
Desk
No desk prose until the desk locks this match (the routine is due to lock it from Fri, 9 Oct, 15:00 BST, 24 h before kickoff). Team news is on the Underlying tab.
Model · now
25 · 25 · 50
Market prior · now
25 · 25 · 50
Desk
not yet locked
Polymarket at lock
stages with the lock

How is this priced? →

Model · pre-season ratings, now
252550
Model · market ratings, now
252550
Desk call

Not locked yet · locks 14:15 ⁠BST, or falls back to the model

Polymarket at lock

Stages with the lock

Betfair close

After kickoff: the price every call is scored against

15:00 locks 14:15 ○ Open

Chelsea2 – 1Bournemouth

Likeliest score in a Chelsea win · Chelsea 45% · draw 26% · Bournemouth 29%

The model leans Chelsea win (45%), well ahead of Bournemouth winning (29%). Most likely score overall 1⁠–⁠1; the model’s prediction, 2⁠–⁠1, is the likeliest score within a Chelsea win.

Expected goals 1.72 – 1.37 · clean-sheet chance 25% · 18% · days of rest 22 · 20

Team news, out or doubtful: Chelsea 7 · Bournemouth 6

Technical detail
Call
45% home · margin +15.0
Flag
Low conviction
Flag
CS Chelsea 25% · Bournemouth 18%
Flag
λ 1.72 / 1.37
Flag
Rest 21.79 / 20.04 days
Flag
Polymarket not captured yet
Engine
Conditional mode 2–1 inside the home region; unconditional mode 1–1. On the market prior: 2–1 at 42%.
Desk
No desk prose until the desk locks this match (the routine is due to lock it from Fri, 9 Oct, 15:00 BST, 24 h before kickoff). Team news is on the Underlying tab.
Model · now
45 · 26 · 29
Market prior · now
42 · 26 · 32
Desk
not yet locked
Polymarket at lock
stages with the lock

How is this priced? →

Model · pre-season ratings, now
452629
Model · market ratings, now
422632
Desk call

Not locked yet · locks 14:15 ⁠BST, or falls back to the model

Polymarket at lock

Stages with the lock

Betfair close

After kickoff: the price every call is scored against

15:00 locks 14:15 ○ Open

Ipswich2 – 1Fulham

Likeliest score in an Ipswich win · Ipswich 37% · draw 26% · Fulham 37%

Too close to call: Ipswich 37%, Fulham 37%, draw 26%. The top two outcomes are within 1.0 points. Most likely score overall 1⁠–⁠1; the model’s prediction, 2⁠–⁠1, is the likeliest score within an Ipswich win.

The two versions of the model disagree on the winner.

Expected goals 1.58 – 1.56 · clean-sheet chance 21% · 21% · days of rest 21 · 20

Team news, out or doubtful: Ipswich 2 · Fulham 1

Technical detail
Call
38% home · margin +1.0
Flag
Dead heat · under 3 points
Flag
CS Ipswich 21% · Fulham 21%
Flag
λ 1.58 / 1.56
Flag
Rest 21 / 19.94 days
Flag
Polymarket not captured yet
Flag
priors disagree on direction
Engine
Conditional mode 2–1 inside the home region; unconditional mode 1–1. On the market prior: 1–2 at 37%.
Desk
No desk prose until the desk locks this match (the routine is due to lock it from Fri, 9 Oct, 15:00 BST, 24 h before kickoff). Team news is on the Underlying tab.
Model · now
37 · 26 · 37
Market prior · now
37 · 26 · 37
Desk
not yet locked
Polymarket at lock
stages with the lock

How is this priced? →

Model · pre-season ratings, now
372637
Model · market ratings, now
372637
Desk call

Not locked yet · locks 14:15 ⁠BST, or falls back to the model

Polymarket at lock

Stages with the lock

Betfair close

After kickoff: the price every call is scored against

15:00 locks 14:15 ○ Open

Sunderland1 – 2Brighton

Likeliest score in a Brighton win · Sunderland 34% · draw 25% · Brighton 41%

The model leans Brighton win (41%), ahead of Sunderland winning (34%). Most likely score overall 1⁠–⁠1; the model’s prediction, 1⁠–⁠2, is the likeliest score within a Brighton win.

Expected goals 1.52 – 1.69 · clean-sheet chance 18% · 22% · days of rest 20 · 21

Team news, out or doubtful: Sunderland 4 · Brighton 10

Technical detail
Call
41% away · margin +7.1
Flag
Low conviction
Flag
CS Sunderland 18% · Brighton 22%
Flag
λ 1.52 / 1.69
Flag
Rest 20.04 / 21 days
Flag
Polymarket not captured yet
Engine
Conditional mode 1–2 inside the away region; unconditional mode 1–1. On the market prior: 1–2 at 39%.
Desk
No desk prose until the desk locks this match (the routine is due to lock it from Fri, 9 Oct, 15:00 BST, 24 h before kickoff). Team news is on the Underlying tab.
Model · now
34 · 25 · 41
Market prior · now
35 · 26 · 39
Desk
not yet locked
Polymarket at lock
stages with the lock

How is this priced? →

Model · pre-season ratings, now
342541
Model · market ratings, now
352639
Desk call

Not locked yet · locks 14:15 ⁠BST, or falls back to the model

Polymarket at lock

Stages with the lock

Betfair close

After kickoff: the price every call is scored against

17:30 locks 16:45 ○ Open

Man United2 – 1Tottenham

Likeliest score in a Man United win · Man United 49% · draw 26% · Tottenham 25%

The model leans Man United win (49%), well ahead of a draw (26%). Most likely score overall 1⁠–⁠1; the model’s prediction, 2⁠–⁠1, is the likeliest score within a Man United win.

Expected goals 1.69 – 1.16 · clean-sheet chance 31% · 18% · days of rest 20 · 21

Team news, out or doubtful: Man United 9 · Tottenham 6

Technical detail
Call
49% home · margin +22.3
Flag
Low conviction
Flag
CS Man United 31% · Tottenham 18%
Flag
λ 1.69 / 1.16
Flag
Rest 20.04 / 21.21 days
Flag
Polymarket not captured yet
Engine
Conditional mode 2–1 inside the home region; unconditional mode 1–1. On the market prior: 2–1 at 50%.
Desk
No desk prose until the desk locks this match (the routine is due to lock it from Fri, 9 Oct, 17:30 BST, 24 h before kickoff). Team news is on the Underlying tab.
Model · now
49 · 26 · 25
Market prior · now
50 · 25 · 25
Desk
not yet locked
Polymarket at lock
stages with the lock

How is this priced? →

Model · pre-season ratings, now
492625
Model · market ratings, now
502525
Desk call

Not locked yet · locks 16:45 ⁠BST, or falls back to the model

Polymarket at lock

Stages with the lock

Betfair close

After kickoff: the price every call is scored against

SUN 11 OCT · 3 MATCHES

14:00 locks 13:15 ○ Open

Crystal Palace1 – 2Nott’m Forest

Likeliest score in a Nott’m Forest win · Crystal Palace 32% · draw 27% · Nott’m Forest 41%

The model leans Nott’m Forest win (41%), ahead of Crystal Palace winning (32%). Most likely score overall 1⁠–⁠1; the model’s prediction, 1⁠–⁠2, is the likeliest score within a Nott’m Forest win.

Expected goals 1.31 – 1.50 · clean-sheet chance 22% · 27% · days of rest 8 · 22

Team news, out or doubtful: Crystal Palace 6 · Nott’m Forest 2

Technical detail
Call
41% away · margin +8.7
Flag
Low conviction
Flag
CS Crystal Palace 22% · Nott’m Forest 27%
Flag
λ 1.31 / 1.50
Flag
Rest 8.06 / 21.85 days
Flag
Polymarket not captured yet
Engine
Conditional mode 1–2 inside the away region; unconditional mode 1–1. On the market prior: 1–2 at 41%.
Desk
No desk prose until the desk locks this match (the routine is due to lock it from Sat, 10 Oct, 14:00 BST, 24 h before kickoff). Team news is on the Underlying tab.
Model · now
32 · 27 · 41
Market prior · now
31 · 28 · 41
Desk
not yet locked
Polymarket at lock
stages with the lock

How is this priced? →

Model · pre-season ratings, now
322741
Model · market ratings, now
312841
Desk call

Not locked yet · locks 13:15 ⁠BST, or falls back to the model

Polymarket at lock

Stages with the lock

Betfair close

After kickoff: the price every call is scored against

14:00 locks 13:15 ○ Open

Hull0 – 1Everton

Likeliest score in an Everton win · Hull 30% · draw 29% · Everton 41%

The model leans Everton win (41%), well ahead of Hull winning (30%). Most likely score overall 1⁠–⁠1; the model’s prediction, 0⁠–⁠1, is the likeliest score within an Everton win.

Expected goals 1.20 – 1.42 · clean-sheet chance 24% · 30% · days of rest 22 · 22

Team news, out or doubtful: Hull 8 · Everton 3

Technical detail
Call
41% away · margin +10.4
Flag
Low conviction
Flag
CS Hull 24% · Everton 30%
Flag
λ 1.20 / 1.42
Flag
Rest 21.96 / 21.96 days
Flag
Polymarket not captured yet
Engine
Conditional mode 0–1 inside the away region; unconditional mode 1–1. On the market prior: 0–1 at 41%.
Desk
No desk prose until the desk locks this match (the routine is due to lock it from Sat, 10 Oct, 14:00 BST, 24 h before kickoff). Team news is on the Underlying tab.
Model · now
30 · 29 · 41
Market prior · now
30 · 29 · 41
Desk
not yet locked
Polymarket at lock
stages with the lock

How is this priced? →

Model · pre-season ratings, now
302941
Model · market ratings, now
302941
Desk call

Not locked yet · locks 13:15 ⁠BST, or falls back to the model

Polymarket at lock

Stages with the lock

Betfair close

After kickoff: the price every call is scored against

16:30 locks 15:45 ○ Open

Liverpool1 – 2Man City

Likeliest score in a Man City win · Liverpool 36% · draw 27% · Man City 37%

Too close to call: Man City 37%, Liverpool 36%, draw 27%. The top two outcomes are within 0.2 points. Most likely score overall 1⁠–⁠1; the model’s prediction, 1⁠–⁠2, is the likeliest score within a Man City win.

The two versions of the model disagree on the winner.

Expected goals 1.43 – 1.44 · clean-sheet chance 24% · 24% · days of rest 21 · 21

Team news, out or doubtful: Liverpool 8 · Man City 3

Technical detail
Call
36% away · margin +0.2
Flag
Dead heat · under 3 points
Flag
CS Liverpool 24% · Man City 24%
Flag
λ 1.43 / 1.44
Flag
Rest 21.1 / 21.1 days
Flag
Polymarket not captured yet
Flag
priors disagree on direction
Engine
Conditional mode 1–2 inside the away region; unconditional mode 1–1. On the market prior: 2–1 at 37%.
Desk
No desk prose until the desk locks this match (the routine is due to lock it from Sat, 10 Oct, 16:30 BST, 24 h before kickoff). Team news is on the Underlying tab.
Model · now
36 · 27 · 37
Market prior · now
37 · 27 · 36
Desk
not yet locked
Polymarket at lock
stages with the lock

How is this priced? →

Model · pre-season ratings, now
362737
Model · market ratings, now
372736
Desk call

Not locked yet · locks 15:45 ⁠BST, or falls back to the model

Polymarket at lock

Stages with the lock

Betfair close

After kickoff: the price every call is scored against

MON 12 OCT · 1 MATCH

20:00 locks 19:15 ○ Open

Coventry2 – 1Newcastle

Likeliest score in a Coventry win · Coventry 38% · draw 28% · Newcastle 34%

The model leans Coventry win (38%), ahead of Newcastle winning (34%). Most likely score overall 1⁠–⁠1; the model’s prediction, 2⁠–⁠1, is the likeliest score within a Coventry win.

Expected goals 1.40 – 1.33 · clean-sheet chance 26% · 25% · days of rest 23 · 23

Team news, out or doubtful: Coventry 7 · Newcastle 9

Technical detail
Call
38% home · margin +3.2
Flag
Low conviction
Flag
CS Coventry 26% · Newcastle 25%
Flag
λ 1.40 / 1.33
Flag
Rest 23.1 / 23.21 days
Flag
Polymarket not captured yet
Engine
Conditional mode 2–1 inside the home region; unconditional mode 1–1. On the market prior: 2–1 at 40%.
Desk
No desk prose until the desk locks this match (the routine is due to lock it from Sun, 11 Oct, 20:00 BST, 24 h before kickoff). Team news is on the Underlying tab.
Model · now
38 · 28 · 34
Market prior · now
40 · 28 · 32
Desk
not yet locked
Polymarket at lock
stages with the lock

How is this priced? →

Model · pre-season ratings, now
382834
Model · market ratings, now
402832
Desk call

Not locked yet · locks 19:15 ⁠BST, or falls back to the model

Polymarket at lock

Stages with the lock

Betfair close

After kickoff: the price every call is scored against

FPL team

GW6 · captured Wed 7 Oct, 20:29 BST, 62.5h before the deadline

Shown as captured; it can still change until the deadline (Sat 10 Oct, 11:00 BST).

Deadline in
2d 9h
Sat 10 Oct, 11:00 BST
Bank
£4.9m
Squad value
£95.9m
sells for £95.1m
Free transfers
2
Chip
Wildcard
0 of 4 left
Transfers
0
no cost under the Wildcard

The squad

Formation 3-4-3 · as picked for GW6 · tap a player for details

Benchin substitution order

GKP
1. DEF
2. DEF
3. MID
  • captain, points ×2
  • vice-captain
  • doubt, chance of playing
  • out: injured, suspended or unavailable
  • Fixture difficulty 1 easiest – 5 hardest (FPL)

Snapshot GW6 · schema pl-desk-fpl/1 · captured Wed 7 Oct, 20:29 BST · routine · fpl-me.mjs · sheet

Underlying

Goals are noisy; chances are steadier. Expected goals (xG) score every shot by how often a chance like it goes in. This page shows which clubs have more, or fewer, points than their chances deserve, who is missing players this weekend, and what the model makes of the round as a result.

  • +5.75 points Man City have 15 points from chances worth 9.25: 1st in the table, 2nd on expected points. Every club’s gap ↓
  • 1.80 starters Newcastle miss the most: about 1.80 regular starters’ worth of minutes. Hull list 11 names but add up to 0.37. Team news by club ↓
  • 2 of 10 GW6 fixtures where the model’s top two outcomes are within three points: too close to call. This round, fixture by fixture ↓

Points against expected points

Right: more points than their chances usually earn. Left: fewer. FotMob’s expected-points model, so far this season: after 5 matches a club, the direction is informative and the size is a rate too, still on a small sample. Furthest right: Man City, +5.75. Furthest left: Bournemouth, −3.93.

Man City
+5.75 15 v 9.25
Newcastle
+3.84 8 v 4.16
Hull
+2.71 8 v 5.29
Brighton
+1.90 10 v 8.10
Everton
+1.78 9 v 7.22
Arsenal
+1.72 12 v 10.28
Leeds
+1.34 9 v 7.66
Chelsea
+1.05 7 v 5.95
Liverpool
+0.55 9 v 8.45
Brentford
+0.52 9 v 8.48
Aston Villa
−0.64 4 v 4.64
Crystal Palace
−0.73 4 v 4.73
Ipswich
−0.80 6 v 6.80
Coventry
−1.34 3 v 4.34
Man United
−3.48 5 v 8.48
Nott’m Forest
−3.60 5 v 8.60
Sunderland
−3.68 4 v 7.68
Tottenham
−3.76 2 v 5.76
Fulham
−3.78 2 v 5.78
Bournemouth
−3.93 3 v 6.93

Bar = points − expected points. Each club links to its row in the Table.

The full expected-points table and what it says

FotMob’s model, ingested and labelled as an external one. It is the fastest way to see which league positions are unsupported.

#ClubPtsxPtsGap
1Arsenal1210.28+1.72
2Man City159.25+5.75
3Nott’m Forest58.60−3.60
4Brentford98.48+0.52
5Man United58.48−3.48
6Liverpool98.45+0.55
7Brighton108.10+1.90
8Sunderland47.68−3.68
9Leeds97.66+1.34
10Everton97.22+1.78
11Bournemouth36.93−3.93
12Ipswich66.80−0.80
13Chelsea75.95+1.05
14Fulham25.78−3.78
15Tottenham25.76−3.76
16Hull85.29+2.71
17Crystal Palace44.73−0.73
18Aston Villa44.64−0.64
19Coventry34.34−1.34
20Newcastle84.16+3.84

Man City are first in the actual table and second here, +5.75 on points against expected. Newcastle are ninth in the actual table and twentieth here, +3.84 on points against expected. Hull are eighth in the actual table and sixteenth here, +2.71 on points against expected. The largest overperformance gaps in the division. Newcastle (R3) and Hull (R1) are reads the desk already held, arrived at by a different route.

Who’s missing this weekend

Each bar is a club’s injury list, one block per player, sized by how much that player usually plays. What matters to the model is the length of the bar: starters’ worth of minutes missing, with doubtful players counted by their chance of missing out. 1.00 is one ever-present starter. The scale runs to 2.

Count isn’t impact. Hull list 11 names but they add up to 0.37 starters’ worth: 5 of them have played no minutes this season. Newcastle’s 1.80 is mostly Dedić (0.73) and Elanga (0.52).

  • out or suspended
  • doubtful, weighted by FPL’s chance
  • left the club, still counted by the engine
  • a weight the engine assumes (no FPL figure)
Newcastle

Dedić 0.73, Elanga 0.52, L.Miley 0.22 (doubtful) · +3 lighter · Woltemade left the club (still counted 0.04) · González fit per FPL · Jaouen, Burn, Joelinton no minutes

1.80 11 names
Crystal Palace

Henderson 0.60, Disasi 0.31 (susp.), Khalaili 0.21 (doubtful) · +3 lighter

1.52 6 names
Coventry

Amenda 0.80, Awoniyi 0.52 (susp.), Ewijk 0.20 (doubtful) · Kesler-Hayden, Woolfenden, Eccles, Wright no minutes

1.52 7 names
Aston Villa

Maatsen 0.74, Torres 0.42, Bizot 0.20 · +1 lighter · Onana, Goretzka, Madjo no minutes

1.41 7 names
Chelsea

Palmer 0.24 (doubtful), Pedro 0.20 (doubtful), James 0.16 (doubtful) · +1 lighter · Satpaev 0.50 assumed (no FPL record) · Sánchez left the club (still counted 0.20) · Emegha, Palestra no minutes

1.32 8 names
Bournemouth

Kluivert 0.65, Scott 0.49 (doubtful) · Araujo, Milosavljević, Kroupi, Adli no minutes

1.15 6 names
Arsenal

Havertz 0.24 (doubtful), Rice 0.23 (doubtful), Tzolis 0.20 (doubtful) · +3 lighter · Mosquera fit per FPL · Saliba no minutes

1.00 8 names
Everton

Röhl 0.55, Mykolenko 0.25 (doubtful) · Beto left the club (still counted 0.07) · Nørgaard no minutes

0.87 4 names
Man City

Foden 0.48 (susp.), Semenyo 0.25 (doubtful) · +1 lighter

0.82 3 names
Ipswich

Fatawu 0.78 (susp.) · Matusiwa fit per FPL · Taylor no minutes

0.78 3 names
Brighton

Wieffer 0.35, Dunk 0.20 (doubtful), Hinshelwood 0.14 · +2 lighter · Ferguson, Azeez, Mitoma, Tzimas, Minteh no minutes

0.78 10 names
Tottenham

Hecke 0.25 (doubtful), Porro 0.11 (doubtful) · +1 lighter · Moore, Richarlison left the club (still counted 0.35) · Kulusevski, Simons, Odobert no minutes

0.75 8 names
Brentford

Collins 0.44, Damsgaard 0.16 (doubtful), Jensen 0.15 · Berg, Milambo, Dasilva, Furo no minutes

0.75 7 names
Liverpool

Jacquet 0.23 (doubtful), Isak 0.23 (doubtful), Gakpo 0.20 (doubtful) · Bradley, Leoni, Chiesa, Ekitiké, Jaros no minutes

0.67 8 names
Nott’m Forest

Milenković 0.60 · Savona no minutes

0.60 2 names
Leeds

Wilson 0.32, Rodon 0.27 · Joseph no minutes

0.59 3 names
Man United

Mainoo 0.20 (doubtful), Rashford 0.20 (doubtful) · +3 lighter · Heaton, Ligt, Ugarte, Diallo no minutes

0.58 9 names
Sunderland

Ballard 0.25 (doubtful), Brobbey 0.22 (doubtful) · +1 lighter · Reinildo fit per FPL · Mundle no minutes

0.53 5 names
Hull

Morita 0.10 · +2 lighter · Drameh, Millar left the club (still counted 0.25) · Mendy fit per FPL · Butland, Hughes, Gyabi, Matazo, Ansah no minutes

0.37 11 names
Fulham

Tete fit per FPL · Cairney no minutes

0.00 2 names

Each club links to its fixture this round. Every name is in the line under its bar and in the list below.

Every listed player, suspensions and return dates

Availability — 20 clubs, 128 entries

ClubnOut, with expected return
Arsenal8White doubtful; Mosquera (Doubtful); Saliba (Mid October 2026); Rice doubtful; Dowman doubtful; Havertz doubtful; Konsa doubtful; Tzolis doubtful
Aston Villa7Bizot (Doubtful); Maatsen (Late October 2026); Torres (Mid October 2026); Onana (Mid April 2027); Goretzka (Mid October 2026); Madjo (Mid October 2026); Alysson doubtful
Bournemouth6Araujo (Mid October 2026); Milosavljević (Doubtful); Kroupi (Late October 2026); Adli (Mid October 2026); Scott doubtful; Kluivert (Unspecified injury - Unknown return date)
Brentford7Collins (Mid October 2026); Berg (Mid October 2026); Milambo (Mid October 2026); Jensen (Mid October 2026); Dasilva (Mid October 2026); Damsgaard doubtful; Furo (Unspecified injury - Unknown return date)
Brighton10Wieffer (Mid October 2026); Hinshelwood (Mid October 2026); Ferguson (Mid October 2026); Azeez (Mid October 2026); Mitoma (Mid October 2026); Tzimas (Late September 2026); Minteh (Mid November 2026); Yohanna (Mid October 2026); Dunk doubtful; Struijk doubtful
Chelsea8James doubtful; Emegha (Doubtful); Satpaev (Late September 2026); Caicedo doubtful; Palestra doubtful; Pedro doubtful; Sánchez (Has joined Como on loan for the rest of the season); Palmer doubtful
Coventry7Amenda (Mid October 2026); Kesler-Hayden (Mid October 2026); Woolfenden (Mid October 2026); Eccles (Mid October 2026); Wright (Early November 2026); Awoniyi (susp.); Ewijk doubtful
Crystal Palace6Henderson (Mid October 2026); Disasi (susp.); Riad (Late June 2027); Mateta (Mid October 2026); Tomiyasu doubtful; Khalaili doubtful
Everton4Röhl (Doubtful); Nørgaard (Mid October 2026); Mykolenko doubtful; Beto (Has joined Fiorentina permanently)
Fulham2Tete (Mid October 2026); Cairney (Mid October 2026)
Hull11Targett doubtful; Butland (Late October 2026); Hughes (Late October 2026); Mendy (Mid October 2026); Gyabi (Late October 2026); Matazo (Mid January 2027); Morita (Mid October 2026); Ansah (Mid October 2026); Drameh (Has joined Genoa permanently); Millar (Has joined Birmingham on loan for the rest of the season); Norton-Cuffy doubtful
Ipswich3Matusiwa (Doubtful); Taylor (Late September 2026); Fatawu (susp.)
Leeds3Rodon (Late September 2026); Joseph (Early January 2027); Wilson (Thigh injury - Unknown return date)
Liverpool8Bradley (Early January 2027); Leoni (Mid October 2026); Chiesa (Mid October 2026); Ekitiké (Early January 2027); Jaros (Knee injury - Unknown return date); Jacquet doubtful; Gakpo doubtful; Isak doubtful
Man City3Foden (susp.); O'Reilly doubtful; Semenyo doubtful
Man United9Šeško doubtful; Heaton (Late September 2026); Ligt (Doubtful); Ugarte (Early April 2027); Diallo (Late September 2026); Dorgu doubtful; Mazraoui doubtful; Rashford doubtful; Mainoo doubtful
Newcastle11Jaouen (Mid October 2026); Dedić (Mid October 2026); Burn (Late September 2026); Ramsey (Mid October 2026); Joelinton (Mid October 2026); González (Mid October 2026); Elanga (Mid October 2026); Osula (Mid October 2026); Livramento doubtful; L.Miley doubtful; Woltemade (Has joined Juventus on loan for the rest of the season)
Nott’m Forest2Savona (Mid October 2026); Milenković (Mid October 2027)
Sunderland5Reinildo (susp.); Diarra (Late October 2026); Mundle (Early January 2027); Ballard doubtful; Brobbey doubtful
Tottenham8Kulusevski (Doubtful); Simons (Early February 2027); Mudryk (Early November 2026); Odobert (Early November 2026); Hecke doubtful; Porro doubtful; Moore (Has joined FC Köln on loan for the rest of the season); Richarlison (not included in squad.)

All 20 clubs captured, 128 entries, typed as injury or suspension with a return window where the sources give one. Heaviest lists: Hull 11, Newcastle 11, Brighton 10, Man United 9.

Suspensions — 5 league-wide

  • Taiwo Awoniyi · Coventry · Suspended until 19 Oct
  • Axel Disasi · Crystal Palace · Suspended until 25 Oct
  • Phil Foden · Man City · Suspended until 17 Oct
  • Reinildo · Sunderland · Available Mid September 2026
  • Fatawu · Ipswich · Suspended until 17 Oct

Return dates where the sources disagree

3 player(s) where both sources name a different month. A missing date on one side is an absence, not a disagreement, and two dates inside the same month are the same claim at different precision — neither is listed. FotMob names the earlier return for 3 of the 3, FPL for 0. Neither source is treated as authoritative: a read that turns on a return date quotes both.

PlayerFPLFotMob
Junior KroupiFoot injury - Expected back 7 NovLate October 2026
Joe RodonHamstring injury - Expected back 21 NovLate September 2026
Daniel BurnAnkle injury - Expected back 12 OctLate September 2026

GW6: what the model makes of it

The model’s chances of a home win, draw and away win for each match. Under each bar, the same model as if it had started the season from the betting odds instead of the pre-season ratings: where the two agree, the pick does not depend on that choice. How one fixture gets its price

  1. Sat 10 Oct, 12:30 ⁠BST Arsenal 1 – 0 Leeds

    Model prediction · Arsenal win 52%

    Same model started from the betting odds: home 46 · draw 29 · away 25 · 1⁠–⁠0

    Polymarket · reference: no price captured yet

  2. Sat 10 Oct, 15:00 ⁠BST Aston Villa 1 – 2 Brentford

    Model prediction · Brentford win 50%

    Same model started from the betting odds: home 25 · draw 25 · away 50 · 1⁠–⁠2

    Polymarket · reference: no price captured yet

  3. Sat 10 Oct, 15:00 ⁠BST Chelsea 2 – 1 Bournemouth

    Model prediction · Chelsea win 45%

    Same model started from the betting odds: home 42 · draw 26 · away 32 · 2⁠–⁠1

    Polymarket · reference: no price captured yet

  4. Sat 10 Oct, 15:00 ⁠BST Ipswich 2 – 1 Fulham

    Model prediction · Ipswich win 37%

    Same model started from the betting odds: home 37 · draw 26 · away 37 · 1⁠–⁠2 (a different score)

    ≈ Too close to call: the top two outcomes are 1.0 points apart

    Polymarket · reference: no price captured yet

  5. Sat 10 Oct, 15:00 ⁠BST Sunderland 1 – 2 Brighton

    Model prediction · Brighton win 41%

    Same model started from the betting odds: home 35 · draw 26 · away 39 · 1⁠–⁠2

    Polymarket · reference: no price captured yet

  6. Sat 10 Oct, 17:30 ⁠BST Man United 2 – 1 Tottenham

    Model prediction · Man United win 49%

    Same model started from the betting odds: home 50 · draw 25 · away 25 · 2⁠–⁠1

    Polymarket · reference: no price captured yet

  7. Sun 11 Oct, 14:00 ⁠BST Crystal Palace 1 – 2 Nott’m Forest

    Model prediction · Nott’m Forest win 41%

    Same model started from the betting odds: home 31 · draw 28 · away 41 · 1⁠–⁠2

    Polymarket · reference: no price captured yet

  8. Sun 11 Oct, 14:00 ⁠BST Hull 0 – 1 Everton

    Model prediction · Everton win 41%

    Same model started from the betting odds: home 30 · draw 29 · away 41 · 0⁠–⁠1

    Polymarket · reference: no price captured yet

  9. Sun 11 Oct, 16:30 ⁠BST Liverpool 1 – 2 Man City

    Model prediction · Man City win 37%

    Same model started from the betting odds: home 37 · draw 27 · away 36 · 2⁠–⁠1 (a different score)

    ≈ Too close to call: the top two outcomes are 0.2 points apart

    Polymarket · reference: no price captured yet

  10. Mon 12 Oct, 20:00 ⁠BST Coventry 2 – 1 Newcastle

    Model prediction · Coventry win 38%

    Same model started from the betting odds: home 40 · draw 28 · away 32 · 2⁠–⁠1

    Polymarket · reference: no price captured yet

  • home win
  • draw
  • away win
  • ≈ too close to call: the top two outcomes within three points
All three rows per fixture, with margins

GW6 · 10 fixtures from the payload · Three rows per fixture, never blended. Prior = market-implied (Betfair closing, GW1–5, 50 fixtures) on the second row; evidence = FotMob xG on both; w = 0.500. The third row is the market: Polymarket’s reference price, captured by the 15-minute tick about 24 h before kickoff, then its price at the lock; Betfair’s closing price arrives after the match. Which prior the desk locks on is the desk’s call; the ledger scores both.

FixtureKickoffRowHDAReadsMargin
Arsenal v LeedsSat 10, 12:30Model · pre-season prior52%27%21%1–0 at 52%+24.9
Model · market-implied prior46%29%25%1–0 at 46%+17.5
Polymarket · reference———no price captured
Aston Villa v BrentfordSat 10, 15:00Model · pre-season prior25%25%50%1–2 at 50%+24.0
Model · market-implied prior25%25%50%1–2 at 50%+25.3
Polymarket · reference———no price captured
Chelsea v BournemouthSat 10, 15:00Model · pre-season prior45%26%29%2–1 at 45%+15.0
Model · market-implied prior42%26%32%2–1 at 42%+10.4
Polymarket · reference———no price captured
Ipswich v FulhamSat 10, 15:00Model · pre-season prior37%26%37%2–1 at 38%+1.0
Model · market-implied prior37%26%37%1–2 at 37%+0.3
Polymarket · reference———no price captured
Sunderland v BrightonSat 10, 15:00Model · pre-season prior34%25%41%1–2 at 41%+7.1
Model · market-implied prior35%26%39%1–2 at 39%+3.5
Polymarket · reference———no price captured
Man United v TottenhamSat 10, 17:30Model · pre-season prior49%26%25%2–1 at 49%+22.3
Model · market-implied prior50%25%25%2–1 at 50%+24.3
Polymarket · reference———no price captured
Crystal Palace v Nott’m ForestSun 11, 14:00Model · pre-season prior32%27%41%1–2 at 41%+8.7
Model · market-implied prior31%28%41%1–2 at 41%+9.4
Polymarket · reference———no price captured
Hull v EvertonSun 11, 14:00Model · pre-season prior30%29%41%0–1 at 41%+10.4
Model · market-implied prior30%29%41%0–1 at 41%+11.3
Polymarket · reference———no price captured
Liverpool v Man CitySun 11, 16:30Model · pre-season prior36%27%37%1–2 at 36%+0.2
Model · market-implied prior37%27%36%2–1 at 37%+1.0
Polymarket · reference———no price captured
Coventry v NewcastleMon 12, 20:00Model · pre-season prior38%28%34%2–1 at 38%+3.2
Model · market-implied prior40%28%32%2–1 at 40%+8.1
Polymarket · reference———no price captured

Standing reads, still awaiting a verdict

Claims the desk wrote down, each with a test that would prove it wrong. 8 of 8 still read PENDING in the payload: not yet graded by the desk. Issued for GW3; 3 rounds have been played since (GW3–5). The page prints the desk’s verdict and never grades a read itself.

  1. R1 · Hull's points are a goalkeeping story, not a defensive one

    ○ PENDING · issued GW3

    As written in GW3: Hull have conceded 0 on 3.11-3.12 xGA across two models (GA-xGA -3.12) while facing 32 shots. 17th on xPoints (1.63) against 6 actual - a +4.37 gap, largest in the league.

    The test, as written: If Hull face 12+ shots and concede 0 again, weakens. If they concede 2+, confirmed early.

    Evidence, not a verdict: Hull this season, 5 matches: 61 shots (19 on target) and 69 faced (21 on target). Its xG row

    Since GW3, per match: GW3 11 shots (4 on target), faced 13 · GW4 14 shots (5 on target), faced 12 · GW5 22 shots (4 on target), faced 12

  2. R2 · Everton's defensive record is the same illusion

    ○ PENDING · issued GW3

    As written in GW3: Conceded 1 on 4.11-4.12 xGA (GA-xGA -3.12) from 28 shots faced. 15th on xPoints against 4 actual.

    The test, as written: Man United shot volume at Goodison is the sharpest available test.

    Evidence, not a verdict: Everton this season, 5 matches: 80 shots (23 on target) and 69 faced (18 on target). Its xG row

    Since GW3, per match: GW3 18 shots (6 on target), faced 14 · GW4 12 shots (3 on target), faced 14 · GW5 21 shots (2 on target), faced 13

  3. R3 · Newcastle are third in the goalkeeping-overperformance queue

    ○ PENDING · issued GW3

    As written in GW3: Conceded 2 on 4.02-4.10 xGA (GA-xGA -2.10) while facing 44 shots, joint-most in the league. 18th on xPoints against 4 actual.

    The test, as written: If shots-faced stays above 20 vs Bournemouth, the read holds regardless of result.

    Evidence, not a verdict: Newcastle this season, 5 matches: 51 shots (18 on target) and 98 faced (28 on target). Its xG row

    Since GW3, per match: GW3 8 shots (2 on target), faced 17 · GW4 7 shots (5 on target), faced 15 · GW5 12 shots (5 on target), faced 22

  4. R4 · Arsenal's suppression is real and structurally different

    ○ PENDING · issued GW3

    As written in GW3: 11 shots faced in two matches, 0.52-0.54 xGA, 0.047 xGA per shot - all best in the division. Shot PREVENTION, not shot stopping. The one defensive read that does NOT predict regression.

    The test, as written: Falsified if Arsenal concede 10+ shots in a single match. Caveat: no Understat splits behind it.

    Evidence, not a verdict: Arsenal this season, 5 matches: 65 shots (24 on target) and 53 faced (14 on target). Its xG row

    Since GW3, per match: GW3 16 shots (9 on target), faced 13 · GW4 11 shots (5 on target), faced 12 · GW5 11 shots (2 on target), faced 17

  5. R5 · Chelsea are the highest-variance side at both ends

    ○ PENDING · issued GW3

    As written in GW3: G-xG +1.83 AND GA-xGA +2.26. Two overperformances in opposite directions compound rather than cancel. Model disagreement on their xG is the largest in the dataset (4.63 FD vs 5.17 FotMob).

    The test, as written: A low-scoring Chelsea match in the next three is the first evidence the variance is closing.

    Evidence, not a verdict: Chelsea this season, 5 matches: 72 shots (26 on target) and 72 faced (32 on target). Its xG row

    Since GW3, per match: GW3 13 shots (5 on target), faced 16 · GW4 12 shots (7 on target), faced 14 · GW5 12 shots (2 on target), faced 13

  6. R6 · Aston Villa have not had a shot on target in 180 minutes

    ○ PENDING · issued GW3

    As written in GW3: 0 SoT from 13 shots, 0.30 xG per match, 0.046 xG per shot - worst in the division on all four measures. Both xG models agree (0.60 / 0.62).

    The test, as written: Directly falsifiable: any Villa shot on target. A Villa win falsifies this AND the Hull v Villa call together.

    Evidence, not a verdict: Aston Villa this season, 5 matches: 48 shots (12 on target) and 81 faced (26 on target). Its xG row

    Since GW3, per match: GW3 13 shots (1 on target), faced 11 · GW4 7 shots (5 on target), faced 22 · GW5 15 shots (6 on target), faced 20

  7. R7 · Man United's volume outruns its quality, and goals came anyway

    ○ PENDING · issued GW3

    As written in GW3: 54 shots (most in the league, by six) at a mid-table 0.118 xG per shot. 2nd on xPoints with 3 actual points - a -1.82 gap.

    The test, as written: Volume is the most repeatable attacking input; if shots stay 25+ per match the read holds.

    Evidence, not a verdict: Man United this season, 5 matches: 113 shots (28 on target) and 53 faced (23 on target). Its xG row

    Since GW3, per match: GW3 14 shots (3 on target), faced 18 · GW4 16 shots (3 on target), faced 6 · GW5 29 shots (6 on target), faced 12

  8. R8 · Spurs' problem is chance quality, not chance volume

    ○ PENDING · issued GW3

    As written in GW3: 26 shots (mid-table) for 1.61-1.69 xG - 0.062 xG per shot, second-worst in the league. Reaching shooting positions and shooting from bad ones.

    The test, as written: Falsified if Spurs' xG per shot rises above 0.10 while volume holds.

    Evidence, not a verdict: Tottenham this season, 5 matches: 71 shots (19 on target) and 76 faced (21 on target). Its xG row

    Since GW3, per match: GW3 11 shots (0 on target), faced 12 · GW4 14 shots (2 on target), faced 12 · GW5 20 shots (7 on target), faced 15

Two xG models, side by side

Football-Data (A) and FotMob (B) differ by 0.10 xG or more, on xG or on xGA, for 10 of 20 clubs (marked ≠). Both numbers are shown; neither is averaged, and the engine fits on B alone. Largest gaps in this payload: Fulham xGA 8.57 v 9.12 (0.55) and Chelsea xG 6.77 v 7.29 (0.52).

ClubGoalsxG A / BxGA A / BFinishing (G − xG)Keeping (GA − xGA)
Arsenal8–48.49 / 8.484.02 / 4.04−0.48−0.04
Aston Villa4–94.51 / 4.62 ≠9.23 / 9.21−0.62−0.21
Bournemouth6–86.69 / 6.727.06 / 6.94 ≠−0.72+1.06
Brentford10–49.79 / 9.90 ≠6.31 / 6.31+0.10−2.31
Brighton16–511.48 / 11.488.68 / 8.67+4.52−3.67
Chelsea10–126.77 / 7.29 ≠8.83 / 8.82+2.71+3.18
Coventry1–104.86 / 4.878.63 / 8.63−3.87+1.37
Crystal Palace6–116.26 / 6.279.46 / 9.46−0.27+1.54
Everton6–37.32 / 7.326.93 / 6.94−1.32−3.94
Fulham5–87.74 / 7.748.57 / 9.12 ≠−2.74−1.12
Hull6–45.28 / 5.277.38 / 7.39+0.73−3.39
Ipswich7–117.69 / 7.728.75 / 8.98 ≠−0.72+2.02
Leeds7–37.62 / 7.72 ≠6.33 / 6.46 ≠−0.72−3.46
Liverpool7–48.34 / 8.346.10 / 6.12−1.34−2.12
Man City13–510.61 / 10.50 ≠7.23 / 7.25+2.50−2.25
Man United8–89.68 / 9.89 ≠6.75 / 6.80−1.89+1.20
Newcastle9–95.32 / 5.328.89 / 9.05 ≠+3.68−0.05
Nott’m Forest4–57.26 / 7.264.78 / 4.77−3.26+0.23
Tottenham2–84.93 / 5.007.82 / 7.89−3.00+0.11
Sunderland6–109.76 / 9.778.65 / 8.65−3.77+1.35

A is Football-Data, B is the FotMob table endpoint. ≠ marks a cell where the two differ by 0.10 or more. Finishing is goals minus xG: positive means scoring more than the chances suggest. Keeping is goals against minus xG against: negative GA − xGA means conceding fewer than the model expects.

Both are read by the data layer’s daily refresh, which last finished Wed 7 Oct, 06:10 ⁠BST. A comes from football-data.co.uk’s E0.csv, in this run 50 fixtures from football-data direct (staged 0h ago); B from the FotMob table endpoint, 20/20 clubs.

Underneath both, Understat splits each club’s shots by situation (open play, corners, set pieces, free kicks, penalties) and by game state: 20 of 20 clubs carry the situation split and 20 the game-state split in this payload, and the engine reads both (How a match is priced, on the Model tab).

The sample behind it

Two independent xG models, side by side and never averaged. Five matches per club: every figure here is directional and, from five, a rate.

How the data arrives

Scoreboard

The desk locks a call on each match before kickoff, with a probability since GW4 (GW3 was locked as a direction only); each call is scored against the result and compared with the betting market. Since GW3 it has made 11 of 27 calls right (41%); Betfair’s favourite got 10 of the same 27 right.

On the quality of the probabilities (Brier score, lower is better), on the same 17 fixtures the desk scores 0.726, its model on pre-season ratings 0.700, the model on market ratings 0.691 and Betfair’s closing odds 0.728: the desk is level with the market. In a run of draws and upsets (6 of the 17 finished level) all four scored worse than an uninformative ⅓-⅓-⅓ probability forecast, which scores 0.667 on every match; on a sample this small that does not mean worse than random. On the matches each also priced, Polymarket at lock scored 0.704 to the desk’s 0.691 (n 11) and Opta’s supercomputer scored 0.729 to the desk’s 0.726 (n 17). Seventeen fixtures is too few to call it skill or luck. How the scores work

11 of 27 calls right

Locked before kickoff, GW3–5: 41%. On the same 27 matches: Betfair’s favourite 10, always the home side 7.

Desk calls: 11 of 27 right · desk coverage: 27 of 30 settled fixtures locked by the desk · all 30 fixtures, including 3 never called: 11 of 30 right. The desk record counts human calls only; this line closes the gap by counting every fixture.

  • GW3✓✕✕✕✕✓✕✓✕✓4/10
  • GW4✕✓✕✕✕✕✓✓✓✓5/10
  • GW5✕–✕✓✓✕–✕–✕2/73 not called

Retrospective, scored after the fact

  • GW1✓✕✕✕✕✕✓✓✕✓4/10
  • GW2✓✕✕✓✓✓✓✕✓✓7/10

hit miss not called model fallback

Not called, so outside the denominator: GW5 Tottenham v Aston Villa, GW5 Bournemouth v Liverpool, GW5 Man City v Sunderland.

Is the desk beating the market?

Not yet: level with Betfair over 17 matches (0.726 v 0.728), and the gap (−0.003) is far inside the noise (±0.062).

GW4 n 10

Desk 0.674
Model · pre-season ratings 0.679
Model · market ratings 0.678
Polymarket · at lockn 7 · desk 0.716 on these 0.789
Opta supercomputer 0.736
Betfair close 0.728

GW5 n 7

Desk 0.800
Model · pre-season ratings 0.731
Model · market ratings 0.710
Polymarket · at lockn 4 · desk 0.647 on these 0.557
Opta supercomputer 0.719
Betfair close 0.729

GW4–5 together n 17

Desk 0.726
Model · pre-season ratings 0.700
Model · market ratings 0.691
Polymarket · at lockn 11 · desk 0.691 on these 0.704
Opta supercomputer 0.729
Betfair close 0.728

Dashed line: a ⅓-⅓-⅓ guess (0.667). Model · market ratings is the desk’s engine started from betting odds, captured at lock: a model, not the market. Each row is scored on the matches where it and the desk both have a probability; a row on fewer matches than the desk prints its n and the desk’s score on those same matches. Opta’s supercomputer probabilities on 17 of the 17 matches: 11 from the line logged in the ledger beside the lock; 6 transcribed from The Analyst’s GW5 article (theanalyst.com/articles/premier-league-match-predictions, retrieved 2026-10-07). On 6 of them the source gives both win chances and the draw is the remainder. Polymarket at lock on 11 of the 17, leaving out 6 whose draw leg traded under $500. From GW6, three shadow models (a rating prior, a market prior and deep completions) are priced beside every lock and join this comparison as rows of their own once those matches are settled; none is a call and none moves a price. GW4: desk 0.674, Betfair 0.728 on 10 matches (desk better) · GW5: desk 0.800, Betfair 0.729 on 7 matches (market better). GW3 has no desk price and no model price at lock, so it is not drawn; Betfair’s close scored 0.647 on its 10 fixtures.

The noise band is ±1.96 × the standard deviation of the 17 per-match differences (desk − Betfair) ÷ √17. Betfair over the same 17 matches is the n-weighted mean of its round rows, 0.728 (the pooled table’s Betfair row is over all 27 called matches).

Where GW5 was lost to the market, match by match

Each bar is the desk’s Brier minus Betfair’s on one match: right of the line the desk scored worse, left of it better. GW5: desk 0.800 v Betfair 0.729 over 7 matches.

Newcastle 2 – 1 Hullcalled home win at 38% · ✓ Hit 0.577 v 0.237 +0.341
Brentford 3 – 0 Chelseacalled draw at 36% · ✕ Miss 0.735 v 0.596 +0.139
Nott’m Forest 0 – 1 Coventrycalled home win at 63% · ✕ Miss 1.211 v 1.105 +0.106
Everton 1 – 0 Ipswichcalled home win at 50% · ✓ Hit 0.376 v 0.310 +0.066
Fulham 1 – 1 Man Unitedcalled away win at 51% · ✕ Miss 0.880 v 0.847 +0.034
Leeds 0 – 0 Crystal Palacecalled home win at 57% · ✕ Miss 0.920 v 0.937 −0.018
Brighton 3 – 0 Arsenalcalled away win at 51% · ✕ Miss 0.900 v 1.068 −0.168

About two-thirds of the desk’s gap to Betfair in GW5 came from Newcastle v Hull, a hit the desk gave 38% against the market’s 60% (0.577 v 0.237). The desk’s worst single score was Nott’m Forest 0 – 1 Coventry (1.211), but both priced it almost the same: desk 63% on the home win, Betfair 59%, which scored 1.105. It won some back at Brighton v Arsenal (0.900 v 1.068).

Open GW5

The same chart for GW4 (desk 0.674 v Betfair 0.728)
Crystal Palace 2 – 3 Ipswichcalled home win at 48% · ✕ Miss 0.846 v 0.774 +0.071
Liverpool 0 – 0 Fulhamcalled home win at 64% · ✕ Miss 1.056 v 1.054 +0.002
Man United 0 – 1 Man Citycalled away win at 44% · ✓ Hit 0.472 v 0.483 −0.011
Coventry 0 – 5 Brightoncalled away win at 51% · ✓ Hit 0.360 v 0.378 −0.018
Leeds 4 – 1 Newcastlecalled home win at 42% · ✓ Hit 0.505 v 0.527 −0.022
Aston Villa 1 – 2 Nott’m Forestcalled draw at 36% · ✕ Miss 0.715 v 0.760 −0.045
Sunderland 0 – 2 Arsenalcalled away win at 64% · ✓ Hit 0.198 v 0.256 −0.059
Tottenham 0 – 0 Evertoncalled home win at 44% · ✕ Miss 0.751 v 0.842 −0.091
Bournemouth 2 – 2 Brentfordcalled draw at 35% · ✓ Hit 0.634 v 0.806 −0.172
Chelsea 2 – 2 Hullcalled home win at 72% · ✕ Miss 1.201 v 1.402 −0.201

The desk scored better than Betfair across GW4. The biggest gain was Chelsea v Hull, where it gave the draw 18% against the market’s 13% (1.201 v 1.402). Its worst match against the market was Crystal Palace v Ipswich (0.846 v 0.774).

Open GW4

Reading the scores

Brier score
Adds up, across home, draw and away, the squared gap between the probability given and what happened. 0 is perfect and 2 the worst possible. An uninformative ⅓-⅓-⅓ probability forecast scores 0.667 on every match whatever happens, so it is a fair line at any sample size: above it, a forecast scored worse than saying nothing, which on a small, upset-heavy sample does not mean worse than random.
RPS (ranked probability score)
The same idea with the outcomes in order, home, draw, away, so a home call that ends in a draw costs less than one that ends in an away win. Lower is better. A ⅓-⅓-⅓ guess scores 0.219 on the same 17 matches (it depends on how many were draws) and 0.244 on GW1⁠–⁠2.
n
How many matches a figure is scored on. Rows with different n were scored on different matches and should not be compared.
Locked
Written to a write-once sheet before kickoff; it cannot be edited afterwards. GW1⁠–⁠2 were scored from the desk’s own cards after the fact and are kept apart.
Model · pre-season ratings
The desk’s engine run from its pre-season club ratings, captured at lock. The ledger tables call it “Model at lock · pre-season prior”.
Model · market ratings
The same engine started from ratings implied by betting odds, captured at lock: a model seeded by the market, not the market itself. The ledger tables call it “Market prior at lock”.
Betfair close
The exchange’s three-way price at kickoff, with the margin (overround) removed.
Polymarket at lock and reference
The prediction market at the lock and at an unattended capture the day before; a match whose draw leg traded under $500 is left out and says so.
Reference family
The same instruments scored over every match with a capture, called or not.
Direction, conviction, MAE
Direction: home, draw or away was right. Conviction: a call at 55% or more. MAE: the average error, in goals.

Retrospective: were GW1⁠–⁠2’s confident calls right as often as claimed?

20 calls, self-attested and scored after the fact, so indicative only. Bands this small cannot carry a verdict.

Called at 65% or more

72% claimed · 2 of 3 happened (67%)

Called at 55–64%

59% claimed · 5 of 6 happened (83%)

Called below 55%

46% claimed · 4 of 11 happened (36%)

claimed (average confidence) happened. Each band’s note is in the retrospective record below.

The record, round by round

Every row of the forward ledger as the desk scores it, each with its own n. Lower is better on Brier and RPS; ▼ best marks the lowest score among rows on the same n. “Model at lock · pre-season prior” is the model on pre-season ratings and “Market prior at lock” the model on market ratings.

GW5 forward ledger · scored on probabilities Open GW5

RownDirectionBrierRPS
Desk call (sheet ledger)desk_split at lock 7 2 / 7 0.800 0.289
Model at lock · pre-season priormodel_split at lock from the ledger 7 3 / 7 0.731 0.258
Market prior at lockmkt_split at lock from the ledger 7 3 / 7 0.710 0.249
Betfair closingthree-way closing price, overround removed 7 3 / 7 0.729 0.263
Polymarket at lockpm_lock at lock from the ledger, normalised · 3 of 7 excluded: draw leg under $500 4 2 / 4 0.557 0.250
Polymarket · referencepm_ref from the unattended capture, normalised · 1 of 7 excluded: draw leg under $500 6 2 / 6 0.674 0.225
Notes and the GW5 reference family

GW5, 10 of 10 settled, 7 of 10 called. 3 fixtures are excluded from every row because no call was made: Tottenham v Aston Villa, Bournemouth v Liverpool, Man City v Sunderland. n per row: Desk call (sheet ledger) 7 · Model at lock · pre-season prior 7 · Market prior at lock 7 · Betfair closing 7 · Polymarket at lock 4 (3 excluded: draw leg under $500) · Polymarket · reference 6 (1 excluded: draw leg under $500). Rows with a different n are scored on different fixtures; compare only rows with the same n. Lower is better on both.

GW5 reference family · every fixture with a reference capture

RownDirectionBrierRPS
Model · referencemodel_ref from the unattended capture · n 10 of 10 10 4 / 10 0.698 0.264
Market prior · referencemkt_ref from the unattended capture · n 10 of 10 10 4 / 10 0.687 0.259
Polymarket · referencepm_ref from the unattended capture, normalised, every capture · n 10 of 10 10 4 / 10 0.663 0.253
Polymarket · reference, thin draw excludedpm_ref, normalised, draw leg under $500 excluded · n 9 of 10 · 1 excluded: draw leg under $500 9 4 / 9 0.608 0.220
Betfair closingthree-way closing price on the same fixtures, overround removed · n 10 of 10 10 5 / 10 0.659 0.250

GW5 reference family, scored over the 10 of 10 settled fixtures with a reference capture, called or not, 3 of them with no desk call. Each row carries its own n; the thin-draw row drops 1 of 10 with a draw leg under $500. Lower is better on both.

GW4 forward ledger · scored on probabilities Open GW4

RownDirectionBrierRPS
Desk call (sheet ledger)desk_split at lock 10 5 / 10 0.674 0.204
Model at lock · pre-season priormodel_split at lock from the ledger 10 5 / 10 0.679 0.206
Market prior at lockmkt_split at lock from the ledger 10 4 / 10 0.678 0.208
Betfair closingthree-way closing price, overround removed 10 4 / 10 0.728 0.220
Polymarket at lockpm_lock at lock from the ledger, normalised · 3 of 10 excluded: draw leg under $500 7 2 / 7 0.789 0.237
Polymarket · referencepm_ref from the unattended capture, normalised 10 4 / 10 0.742 0.226
Notes and the GW4 reference family

GW4, 10 of 10 settled, 10 of 10 called. n per row: Desk call (sheet ledger) 10 · Model at lock · pre-season prior 10 · Market prior at lock 10 · Betfair closing 10 · Polymarket at lock 7 (3 excluded: draw leg under $500) · Polymarket · reference 10. Rows with a different n are scored on different fixtures; compare only rows with the same n. Lower is better on both.

GW4 reference family · every fixture with a reference capture

RownDirectionBrierRPS
Model · referencemodel_ref from the unattended capture · n 10 of 10 10 5 / 10 0.680 0.206
Market prior · referencemkt_ref from the unattended capture · n 10 of 10 10 4 / 10 0.676 0.207
Polymarket · referencepm_ref from the unattended capture, normalised, every capture · n 10 of 10 10 4 / 10 0.742 0.226
Polymarket · reference, thin draw excludedpm_ref, normalised, draw leg under $500 excluded · n 10 of 10 10 4 / 10 0.742 0.226
Betfair closingthree-way closing price on the same fixtures, overround removed · n 10 of 10 10 4 / 10 0.728 0.220

GW4 reference family, scored over the 10 of 10 settled fixtures with a reference capture, called or not. Each row carries its own n. Lower is better on both.

GW3 forward ledger · scored on probabilities Open GW3

RownDirectionBrierRPS
Desk call (sheet ledger)no desk split in the ledger; direction only — 4 / 10 direction — —
Model at lock · pre-season priorno snapshot at lock — the ledger carries no model_split for this round — — — —
Market prior at lockno snapshot at lock — the ledger carries no mkt_split for this round — — — —
Betfair closingthree-way closing price, overround removed 10 3 / 10 0.647 0.150
Polymarket at lockno data — the ledger carries no pm_lock for this round — — — —
Polymarket · referenceno data — no reference capture in the ledger for this round — — — —
Notes and the GW3 reference family

GW3, 10 of 10 settled, 10 of 10 called. n per row: Betfair closing 10. Unscored rather than expanded into a vector, because the ledger furnishes none for this round: Desk call (sheet ledger) (direction 4 of 10 printed), Model at lock · pre-season prior, Market prior at lock, Polymarket at lock, Polymarket · reference. Lower is better on both.

GW3 reference family · every fixture with a reference capture

RownDirectionBrierRPS
Reference familyno data — no reference capture in the ledger for this round — — — —

GW3 has no reference capture in the ledger, so the reference family is null for this round.

Forward ledger pooled · GW3–GW5
RowBrierRPS
Desk call (sheet ledger)n 17 over GW4–GW5 0.726 0.239
Model at lock · pre-season priorn 17 over GW4–GW5 0.700 0.227
Market prior at lockn 17 over GW4–GW5 0.691 0.225
Betfair closing · calledn 27 over GW3–GW5 0.698 0.205
Polymarket at lockn 11 over GW4–GW5 · 6 of 17 excluded: draw leg under $500 0.704 0.242
Polymarket · reference · calledn 16 over GW4–GW5 · 1 of 17 excluded: draw leg under $500 0.717 0.226
Model · referencen 20 over GW4–GW5 0.689 0.235
Market prior · referencen 20 over GW4–GW5 0.681 0.233
Polymarket · reference · every capturen 20 over GW4–GW5 0.702 0.240
Polymarket · reference · every capture, thin draw excludedn 19 over GW4–GW5 · 1 of 20 excluded: draw leg under $500 0.679 0.223
Betfair closing · every capturen 20 over GW4–GW5 0.693 0.235

Pooled over every forward round, each row on its own n: the commit rows over the called fixtures of each round, the reference rows over every fixture with a reference capture. A round that carries no vector for a row adds nothing to it. Lower is better on both.

The model and market-prior rows are read from the ledger at lock for GW4, GW5. GW3 carries no model or market snapshot at lock, so those rows are null and enter no tally. Direction on 27 settled forward calls is noise; Brier against the closing market is the number that will say whether the desk has skill.

Two ledgers, one denominator each

Every metric is computed from the graded matches, against baselines that need no skill at all. Five rounds graded: 20 retrospective matches (GW1–GW2) and 27 forward calls of 30 fixtures (GW3–GW5). The tiles and the baseline table are the retrospective set alone, n 20; the forward-tested ledger is rendered from the sheet and scored per round below, each row with its own n. The retrospective rounds are self-attested and reported apart from it. Scoring runs across all three outcomes rather than on the stated call as a coin.

Retrospective baseline · Unverified · 11 / 20

GW1 and GW2, scored from the desk’s own cards. The upstream data layer holds no pre-kickoff record of either round and will not backfill one, so nothing here is externally auditable. The metric tiles below are computed over these 20 matches and inherit that caveat.

Forward-tested ledger · Graded · 11 / 27

GW3–GW5, rendered from the sheet’s locked_calls: GW3 4/10 · GW4 5/10 · GW5 2/7 (of 10). 27 calls locked across 30 fixtures, 27 settled. 3 fixtures were never called and are outside the denominator: GW5 Tottenham v Aston Villa, GW5 Bournemouth v Liverpool, GW5 Man City v Sunderland. This is the number the desk asks to be judged on. Brier and RPS are scored per round below, each row with its own n.

Two ledgers, and the difference between them

Until v1.6 these two records were reported as one. They are not one: the upstream data layer holds GW1 and GW2 as permanently unscored, because no auditable record of the calls locked before those kickoffs was recoverable there, and its own denominator starts at GW3 (27 calls across 30 fixtures so far). The desk’s GW1 and GW2 cards are real, printed on the round tabs and unedited — but they are self-attested, and combining them with a timestamped forward test flattered the whole page. They are now separated, and the twenty-match figures below are labelled as the retrospective ones.

The GW1⁠–⁠2 retrospective record: metrics, baselines, Opta, calibration
Result accuracy
11 / 20 55% across the two retrospective rounds (n 20), against 50% for backing every home side and 55% for the closing-market favourite.
Exact scorelines
2 / 20 GW2 (2 of 10): Coventry City v Hull City 0–1, Leeds United v Brentford 1–1.
Brier, multinomial
0.566 Scored across all three outcomes from the published expansion rule. The binary figure the desk used to print was 0.232, and it was not comparable to anything outside this page.
Ranked probability score
0.193 K = 3, ordered home–draw–away, so calling a home win and getting a draw costs less than getting an away win. Baselines in the table below.
Total-goals MAE
1.20 GW2 ran hot: 32 goals arrived against 24 called.
Goal-difference MAE
1.40 Chelsea–Brighton and Man Utd–Ipswich were each three goals of margin out.
Conviction hit rate
7 / 9 Calls at 55% or above. The gated weak band went 4 from 11, after 0 from 5 in GW1.

Against the baselines · GW1⁠–⁠2 retrospective

StrategyResultsHit rateBrierRPS
Desk model, v1.0 → v1.411 / 2055%0.5660.193
Betfair closing (from payload)11 / 2055%0.5720.206
Always the home side10 / 2050%1.0000.400
Always the away side6 / 2030%1.4000.600
Always 1–14 / 2020%1.6000.400
Flat third across H/D/A——0.6670.244

Both scores run across all three outcomes, lower is better. Brier sums the squared error over home, draw and away — 0 perfect, 2 worst. RPS is the ordered version, so calling a home win and getting a draw costs less than getting an away win. Until v1.6 the desk scored the stated call as a coin and discarded the mass on the other two outcomes: that number looked better and could not be compared with anything.

Residual split sensitivity: desk 0.566 at K = 0.55; 0.573 at K = 0.50. The rule is published so every figure here can be recomputed under either.

Desk against Opta

FixtureDeskOptaResult
Arsenal v Coventry (GW1)78% H80.4% H✓ Both hit
Hull City v Man Utd (GW1)66% AUnited fav.✕ Both missed
Fulham v Chelsea (GW1)61% A51.6% A✓ Both hit
Coventry v Hull (GW2)47% A51.0% H✓ Desk hit
Tottenham v Newcastle (GW2)44% A41.6% H✓ Desk hit
Sunderland v Fulham (GW2)38% D39.0% H✕ Opta hit

Opta published pre-match lines for three GW1 fixtures we hold. For GW2 the comparison ran across all ten, with the disagreements labelled on the fixture cards; three of them are listed here. From GW3 the ledger logs Opta’s line beside each lock: GW3 9 of 10 (8 agree, 1 disagree) · GW4 10 of 10 (8 agree, 2 disagree) · GW5 1 of 10 (1 disagree). Where a round shows fewer, Opta published no line for the rest.

Calibration by confidence band

BandClaimedActualNote
Called at 65% or more72%67%Used three times in twenty. Arsenal and Manchester City landed; Hull–United did not.
Called at 55–64%59%83%Five from six. Only the 2–0 at Anfield missed, and it finished level. The band the model should live in.
Called below 55%46%36%Four from eleven. All five GW1 hedges missed; four of the six gated GW2 calls landed, including both reversals made on the splits.
Per-gameweek log
RoundStateResultsExactGoals MAEBrierHeadline
GW1Retrospective4/100/100.800.621Beaten by “always the home side”, which went 7 from 10. Every hit came from the confident band; every draw call missed.
GW2Retrospective7/102/101.600.511Best round to date. Both reversals made on the situation splits landed, one of them exactly. The misses were the 2–0 at Anfield and the draw gate’s second admission.
GW3Forward4/100/101.20—10 of 10 called, locked 4 Sept — 6 home, 2 away, 2 draw. No desk split in the ledger, so direction only.
GW4Forward5/101/101.700.674 (10)10 of 10 called, locked 11 Sept — 5 home, 3 away, 2 draw. Desk Brier 0.674 on 10 fixtures with a desk split at lock.
GW5Forward2/7 of 100/71.290.800 (7)7 of 10 called, locked 18 Sept — 4 home, 2 away, 1 draw. No call on Tottenham v Aston Villa, Bournemouth v Liverpool, Man City v Sunderland. Desk Brier 0.800 on 7 fixtures with a desk split at lock.

GW1⁠–⁠2 retrospective: 11 results from 20 · 2 exact · Brier 0.566 · closing-market favourite 11/20 · home-side baseline 10/20. Forward ledger, GW3–5: 11 from 27 settled calls.

Where the error came from, and what was done about it (17 items)

The finding that drove v2.0

The v2.0 finding is about this section. Every QA row this page published was a typed string literal — seventy-six of them, all reading Pass, attached to a model that had shipped four defects in two days. They are now an executed harness that publishes what it measured, and it does not read all-pass. Two engine defects came out of the same pass. The number of matches played was hard-coded in three places, so the next payload would have halved every rate in the league; and every card was re-derived from the live engine on every render, so each model change re-priced matches that had already been played while the page claimed its calls were immutable. Both are fixed, with gates. Fourteen of twenty clubs had a first-season manager, and v1.8 charged every one of them an absolute goal penalty. A fixture between two of them paid it twice, which deflated league-wide goal expectancy and left one factor consuming most of the card’s adjustment against a nominal 12%. v1.9 zero-centres the relative factors within each fixture: the differential between two sides is preserved exactly, the goal total is untouched, and gate G16 clears. At v2.0 the game-state filter that would fix the Newcastle read was built, tested and switched off, because the data layer then carried no score-state splits and the desk does not reverse a published call on assumed numbers. Understat’s game-state splits now reach the payload for 20 of 20 clubs, and the filter runs on them. Legacy note: the engine had a venue bug and a credibility gap. The home term was applied twice — multiplied onto the home side and divided out of the away side — which squared a 14% advantage into a 30% goal ratio, inflated every home price on the card, and manufactured the contrarian call at Hull that the desk had been defending for two versions. It was withdrawn, and at v1.8 the desk agreed with Opta on that fixture. The second finding is worse in kind: eleven of thirteen published weights were never read by the simulation. They are bound in now, and the first thing that produced was managerial reset realising 65% of this card’s adjustment against a nominal 12% — printed, not smoothed. Four versions across two days is not a good look, and the log is the point: v1.6 normalised weights that summed to 108%, v1.7 replaced two prediction paths with one joint distribution, v1.8 found the venue bug inside it. The retrospective record is unchanged at 11 from 20, with a Brier of 0.566 against 0.667 for a flat third. The forward ledger stands at 11 from 27 settled calls, with 3 fixtures never called: GW3 4/10 · GW4 5/10 · GW5 2/7 (of 10).

  1. A test suite that could not fail

    Seventy-six QA rows, every one reading Pass, every result a string literal typed beside its description. Some of the claims were true, some were true of an earlier version, and none of them were checked. The page was at its most confident exactly where it had least evidence — and it made the four defects below harder to find, because the suite said they were not there.

    What was done. v2.0 executes the list. Each check runs against the live objects and publishes the figure it measured, so a pass is auditable and a failure is specific. States are Pass, Fail, Blocked — built but starved of data — and Manual, which is what the page now says instead of claiming a pass it cannot support. The action register is derived from the failures.

  2. A fixed point that was never reached, and could not be

    The opponent adjustment was described everywhere as solved to a fixed point over three passes. It was three undamped sweeps, and the harness measured the last one still moving multipliers by 0.40 — nowhere near a solution. Solving it properly was worse: with two matches per club the schedule graph is a union of cycles, the system is genuinely unidentified, and the true fixed point put Bournemouth top of the league for attack because they had faced Manchester City, whose defence the same solve had pushed to 0.16 because City had faced Bournemouth. Three undamped passes had been hiding an unidentified model behind an unfinished calculation.

    What was done. v2.0 averages opponents geometrically, which makes the system linear in logs and therefore solvable; damps the sweep; and adds a ridge keyed to the sample — 3 / matches played — so the schedule correction enters at exponent 0.40 on two matches and grows towards 0.86 across a season. The solved level is normalised so the adjustment cannot inflate the league goal mean, which it was doing by 6.7%. Residual, pass count and every club’s multiplier are published.

  3. Two matches, hard-coded three times

    Season xG totals were divided by four: two models, and a literal two matches. The shrinkage weight was n / (n + 5) with n typed as 2. The ingest contract had named a played field since v1.8 and nothing read it. The first payload to append a round would have halved every rate in the division and under-weighted the evidence in the same move — twenty clubs wrong in the same direction, which is the failure mode no eyeball catches.

    What was done. v2.0 reads played from the payload and uses it everywhere a total becomes a rate. Gate G19 asserts the declared count against the fixture list and against every club’s appearance count, so a fixture list that has moved on without the counter says so.

  4. Immutable calls, re-derived every render

    The desk’s first rule is that a call is locked before kickoff and never edited. But nine of the ten GW3 cards held no price of their own: they printed whatever the current engine said. Every version from v1.6 to v1.9 therefore silently re-priced matches that had already been played, and the page went on asserting immutability while doing it. The frozen Ipswich card was the only one where the rule was actually enforced, and only because someone typed the number in.

    What was done. v2.0 stores the price each fixture carried at its kickoff and renders that once the clock passes. Gate G20 asserts it. The live engine is still published beside the snapshot, and where the two now differ the difference is printed on the card rather than resolved in the engine’s favour.

  5. Draw hedging — now structural

    GW1 priced three unreadable matches as 1–1 and got none of them. GW2 published two draws under the v1.4 gate and got one, exactly. The gate was a rule bolted on because the scoreline and the probability were produced separately, so the desk needed to be told when it was allowed to say 1–1.

    What was done. v1.7 retires the draw prior outright. A draw is published when the joint distribution makes it the modal outcome and not otherwise. Fulham v Palace splits 38 / 24 / 38 — the closest fixture of the round — and the 1–1 was withdrawn, because 24% was never the most likely result, only the most comfortable thing to say.

  6. Two paths to one prediction

    The scoreline came from phase-level reads and the confidence number from a weighted factor sum. Nothing tied them together, so a 2–0 could sit next to 49% and a clean-sheet call next to a 31% clean-sheet line. It is also why scoring needed a residual-expansion rule at all — and that rule broke plurality on low-confidence draws.

    What was done. v1.7 runs one Dixon-Coles adjusted bivariate Poisson per fixture. λ for each side gives the joint matrix; the matrix gives the 1X2 split, the clean sheets and the modal scoreline, which every match card prints, and gate G13 fails the build if a printed scoreline falls outside the region its own probabilities favour.

  7. A venue effect counted twice

    v1.7 multiplied the home intensity by 1.14 and divided the away one by it, which squares a 14% advantage into a 30% goal ratio and suppresses away attacks by 12% below baseline. It inflated every home price on the GW3 card and manufactured the contrarian call at Hull outright.

    What was done. v1.8 applies the term once. Nine calls re-priced, the Hull call withdrawn, and gate G15 now asserts that a fixture and its reverse differ by the multiplier rather than its square. The withdrawn call is left on the page with its reason.

  8. Weights that did not reach the model

    v1.7 published thirteen normalised weights while the simulation read only the xG rates and the pre-season prior. Managerial reset, fixture load, squad change, injuries, rule changes and market priors — 43% of the stated allocation — were display text.

    What was done. v1.8 binds all six into log-intensity and prints each factor’s measured share of the card’s adjustment beside its nominal weight. That immediately showed managerial reset realising 65% against a nominal 12%, which is now a published divergence rather than an unexamined claim.

  9. Factors that moved the whole league

    The six factors bound in v1.8 were absolute offsets on each side’s log-intensity. Fourteen of twenty clubs carried a first-season manager, so a fixture between two of them took the penalty twice and the model quietly deflated total goals across the division. It also consumed most of the card’s realised adjustment, which is what put gate G16 into partial.

    What was done. v1.9 separates relative factors from tempo factors. Managers, squads, injuries and congestion shift the expected margin and are zero-centred within the fixture, so the differential between the two sides is preserved exactly while the goal total is untouched. Rule changes stay uncentred, because added time genuinely adds goals. G16 passes.

  10. Names as a join key

    The rates table, the fixture list and five context maps are joined on club name, including one with a typographic apostrophe. A mismatch dropped a club from the opponent adjustment or let a played fixture be simulated again, inflating the projection — and it failed silently, which is worse than failing.

    What was done. v1.9 reconciles every key against the rates table at solve time and reports orphans on the build tab under gate G18. None this run, and a future payload with a stray spelling will say so instead of quietly mispricing a club.

  11. Raw rates at face value

    Brighton’s 4.55 open-play xG was earned largely against a Villa side in disarray; Arsenal’s 2.93 came against organised blocks. Both entered the ratings unadjusted, with the difference handled as a sentence in a match note.

    What was done. v1.7 adds an opponent adjustment at 4%, funded by the retired draw prior. Each club’s rates are divided through by the strength of the opponents actually faced, solved to a fixed point. Brighton stopped reading as the league’s third-best attack, and Newcastle’s 2.30 xG was marked down — which is what reversed their call.

  12. Margins, not directions

    GW2 went 7 from 10 on direction while 32 goals arrived against 24 called. Chelsea–Brighton finished 4–3 and Man Utd–Ipswich 5–2.

    What was done. v1.5 does not chase margin. The primary metric stays 1X2, exact score is secondary, and the goal-MAE column is published so the gap stays visible.

  13. Records that are goalkeeping, not defending

    After GW2, Hull, Everton and Newcastle carried defensive records their underlying did not support — −3.12, −3.12 and −2.10 on GA − xGA — corroborated by a second model and by an expected-points table built without reference to the reads.

    What was done. v1.5 added an overperformance-regression factor at 4%. v1.6 keeps the factor and scales it by sample: n / (n + 6), so it applies 1.8% at two matches rather than 4%. Applied at full strength it was reading two rounds of schedule noise as a systematic trait, and that is what re-priced Hull, Everton, Newcastle, Brentford and Forest.

  14. Weights that did not normalise

    The fifteen v1.5 factors summed to 108%. Weights are adjustment shares and must sit on the unit simplex; at 108% every linear combination expanded variance and over-scaled the ratings against the pre-season base. The defect shipped and priced two rounds of calls.

    What was done. v1.6 normalises to 100% across thirteen factors. World Cup residue and unofficial transfer reporting are retired outright — one is two seasons stale, the other closed with the window on 1 September. A build gate now asserts the sum before any card renders.

  15. A binary score on a three-outcome market

    Brier was computed on the stated call as a coin: hit or miss against the confidence number, with the probability mass on the other two outcomes discarded. Calling an away win at 54% and getting a draw scored identically to getting a home win.

    What was done. v1.6 publishes the multinomial Brier and the ranked probability score over home, draw and away, with the expansion rule that turns one call into three probabilities printed beside them. Both are now comparable to anything scored the same way.

  16. A named scorer who was already out

    The Brighton card named Hinshelwood while the availability manifest in the same data layer had him out to mid-September. The card generator read per-90 metrics; the manifest was never consulted.

    What was done. v1.6 runs every named scorer through the manifest before the card ships. The Hinshelwood selection is rejected and replaced, the rejection is printed rather than swallowed, and gate G10 fails the build if any selection is unavailable.

  17. Volume mistaken for threat

    After GW2, United were second on expected points and tenth in the table; Liverpool took open-play shots worth 0.07 xG each. Both were called correctly in GW2 for reasons the underlying only half supports.

    What was done. v1.5 raises chance quality by phase from 6% to 8%. Liverpool are read as a set-piece side until open play improves.

Season calls

Eleven season-long calls, priced on 21 August and last revised on 4 September, before GW3; they are kept as made, to be graded at the end of the season. On 4 Sep the desk had Arsenal for the title (66%), Coventry, Fulham and Hull to go down and Erling Haaland for the Golden Boot. Today’s simulation is less sure: Arsenal win the title in 46.2% of simulated seasons, and Hull are only 7th most likely to go down. On today’s table, 2 of the 6 calls it can judge are on track.

Eleven markets. Each shows its 21 Aug price beside its 4 Sep one, and neither is rewritten. The status and the “Now” line are read from today’s payload, which carries no goal or award tallies; a status the table cannot judge is the one the desk set on 4 Sep. The simulation is the desk’s own 5,000-run season on the Table, run on today’s payload: a separate instrument, shown beside the call it can speak to and never blended with the desk’s price.

Where the call and the simulation disagree

  • Champion They disagree: the desk’s 4 Sep price is 20 points higher than today’s simulation.
  • Top four They disagree: the desk’s 34% (4 Sep) is above today’s simulation ceiling of 29.1% (Liverpool alone).
  • Bottom three They disagree on the third name. Today’s simulation sends Coventry down clearly (58.1%), then has Aston Villa and Fulham too close to rank (41–42%), with Hull only 7th most likely (23.1%). The desk’s 24% (4 Sep) for all three is also above today’s simulation ceiling (23.1%, Hull alone).

How the prices moved between 21 August and 4 September

Hollow: the price on 21 August. Filled: the last revision, on 4 September, before GW3; the prices have not moved since. Each row is a different market (one club, four clubs together, a player award), so compare a row only with itself.

ChampionArsenal 58% → 66% ▲ 8 pts
Top fourArsenal, Man City, Chelsea, Liverpool 31% → 34% ▲ 3 pts
Bottom threeCoventry, Fulham, Hull 27% → 24% ▼ 3 pts, new pick
Golden BootErling Haaland 41% → 44% ▲ 3 pts
PlaymakerBruno Fernandes 34% → 39% ▲ 5 pts
Player of the SeasonBukayo Saka 24% → 25% ▲ 1 pt
Young PlayerMax Dowman 29% → 26% ▼ 3 pts
Golden GloveDavid Raya 44% → 53% ▲ 9 pts
Manager of the SeasonXabi Alonso 19% → 24% ▲ 5 pts
First sackedÁlvaro Arbeloa (Fulham) 22% → 33% ▲ 11 pts
Best promoted sideHull City 47% → 42% ▼ 5 pts, new pick

Champion

○ Watch

Arsenal

66% desk’s price on 4 Sep (from 58% on 21 Aug, ▲ 8 pts) · 46.2% in today’s simulation

Arsenal finish first in 46.2% of 5,000 seasons simulated on today’s payload.

Now. Arsenal 2nd on 12 points, 3 behind Man City; 1st on expected points.

They disagree: the desk’s 4 Sep price is 20 points higher than today’s simulation.

Hedge. Manchester City (18%)

  • The case says “no goal conceded in 180 minutes”; Arsenal have conceded 4 in 5 matches so far.
The case as made by 4 Sep, and what breaks it

Case — Two wins, two clean sheets and no goal conceded in 180 minutes, on an xGA of 0.52⁠–⁠0.54 that both models agree on — eleven shots faced at 0.047 each. The one defensive read in the file that does not predict regression.

Breaks if — Saliba and Timber are both out; a third centre-half injury would bite.

Priced 58% on 21 Aug → 66% on 4 Sep, and frozen there to be graded at the end of the season.

Top four

○ Watch

Arsenal, Man City, Chelsea, Liverpool

34% desk’s price on 4 Sep (from 31% on 21 Aug, ▲ 3 pts) · 29.1% today’s simulation ceiling

Arsenal 91.2% · Man City 84.9% · Chelsea 31.5% · Liverpool 29.1%. Today’s simulated top four: Arsenal 91.2%, Man City 84.9%, Brighton 57.0%, Brentford 39.2%.

Now. Arsenal 2nd, Man City 1st, Chelsea 10th, Liverpool 6th: 2 of the 4 in the top four.

They disagree: the desk’s 34% (4 Sep) is above today’s simulation ceiling of 29.1% (Liverpool alone).

Hedge. Brighton for Liverpool

The case as made by 4 Sep, and what breaks it

Case — Chelsea won 3⁠–⁠2 at Fulham and have one game a week. United’s defeat at Hull does not change their talent, but the structural read does change the confidence in them.

Breaks if — Alonso’s first Chelsea autumn goes the way most Chelsea autumns go.

Priced 31% on 21 Aug → 34% on 4 Sep, and frozen there to be graded at the end of the season.

Bottom three

○ Watch

Coventry, Fulham, Hull

24% desk’s price on 4 Sep (from 27% on 21 Aug on a different pick, ▼ 3 pts, new pick) · 23.1% today’s simulation ceiling

Coventry 58.1% (most likely) · Fulham 41.4% (3rd most likely) · Hull 23.1% (7th most likely).

Three views, kept apart. The call (4 Sep): Coventry, Fulham and Hull. The desk’s hand-priced table (after GW1): Fulham, Coventry City and Ipswich Town. Today’s simulation: Coventry, Aston Villa and Fulham, then Ipswich and Crystal Palace.

Now. Coventry 18th, Fulham 19th, Hull 8th: 2 of the 3 in the bottom three.

They disagree on the third name. Today’s simulation sends Coventry down clearly (58.1%), then has Aston Villa and Fulham too close to rank (41–42%), with Hull only 7th most likely (23.1%). The desk’s 24% (4 Sep) for all three is also above today’s simulation ceiling (23.1%, Hull alone).

Hedge. Ipswich back in for Hull

  • The case calls Coventry and Fulham “both clear of the field”; on today’s run Aston Villa (42.4%) sit above one of them.
  • “Hull’s six points” and “the first two rounds” date the case; Hull now have 8 from 5.
The case as made by 4 Sep, and what breaks it

Case — Reversed on the desk’s own simulation rather than on a read. 5,000 seeded runs put Coventry down (figure not recorded) of the time and Fulham (figure not recorded), both clear of the field. The third name is the change: Hull at (figure not recorded) against Ipswich at (figure not recorded), the opposite ordering to the one this call has carried since v1.2. Hull’s six points are banked and real; the engine does not believe the rest of the season looks like the first two rounds, and their −3.12 GA − xGA is why. Ipswich come out of the call on the worst defensive sample in the league, which is an uncomfortable thing to publish and is what the numbers say. Every figure here is read off the run printed on the Table tab, not typed.

Breaks if — Hull’s defensive record turns out to be a method rather than variance — the same bet the best-promoted call is making.

(figure not recorded): a figure the case read live from the desk’s simulation or engine. It was not stored when the case was written, and today’s run is not put in its place.

Priced 27% on 21 Aug → 24% on 4 Sep, and frozen there to be graded at the end of the season.

Golden Boot

○ Live · as set on 4 Sep

Erling Haaland

44% desk’s price on 4 Sep (from 41% on 21 Aug, ▲ 3 pts)

Now. Haaland: 450 minutes, 4.95 expected goal involvements; Man City 1st on 15 points.

Hedge. João Pedro, Igor Thiago

The case as made by 4 Sep, and what breaks it

Case — Two at Selhurst after the opening blank, now 10 in 6 against Palace, and Coventry at home next. City are first on expected points and he is the whole of their attack.

Breaks if — City sold Savinho and Marmoush and Doku is out to mid-September. The supply line behind him is thin.

Priced 41% on 21 Aug → 44% on 4 Sep, and frozen there to be graded at the end of the season.

Playmaker

○ Watch · as set on 4 Sep

Bruno Fernandes

39% desk’s price on 4 Sep (from 34% on 21 Aug, ▲ 5 pts)

Now. Fernandes: 450 minutes, 3.88 expected goal involvements; Man United 12th on 5 points.

Hedge. Ødegaard, Palmer

The case as made by 4 Sep, and what breaks it

Case — A hat-trick against Ipswich, and United sit second on expected points with three actual — the most under-rewarded process in the league. The volume is his to convert.

Breaks if — Champions League rotation costs him 700 minutes.

Priced 34% on 21 Aug → 39% on 4 Sep, and frozen there to be graded at the end of the season.

Player of the Season

● On track · as set on 4 Sep

Bukayo Saka

25% desk’s price on 4 Sep (from 24% on 21 Aug, ▲ 1 pt)

Now. Saka: 416 minutes, 4.20 expected goal involvements; Arsenal 2nd on 12 points.

Hedge. Palmer, Haaland, Bruno Fernandes

The case as made by 4 Sep, and what breaks it

Case — The best player in the best team, and the one Arsenal attacker whose output does not depend on the striker they have not signed.

Breaks if — Arsenal spread the load so evenly that the vote splits with Ødegaard.

Priced 24% on 21 Aug → 25% on 4 Sep, and frozen there to be graded at the end of the season.

Young Player

○ Live · as set on 4 Sep

Max Dowman

26% desk’s price on 4 Sep (from 29% on 21 Aug, ▼ 3 pts)

Now. Dowman is listed doubtful (Unspecified injury - 75% chance of playing).

Hedge. Bouaddi, Ngumoha

The case as made by 4 Sep, and what breaks it

Case — Sixteen, and Arsenal have said his workload goes up. In a title-winning side with cup rotation, a highlight reel wins this award.

Breaks if — Ngumoha started at St James’ Park and Bouaddi is now available for City. Dowman has not started.

Priced 29% on 21 Aug → 26% on 4 Sep, and frozen there to be graded at the end of the season.

Golden Glove

● On track

David Raya

53% desk’s price on 4 Sep (from 44% on 21 Aug, ▲ 9 pts)

Now. Arsenal have 3 clean sheets in 5, joint most in the league with Brighton, Liverpool, Everton and Hull.

Hedge. Verbruggen, Tzolakis

The case as made by 4 Sep, and what breaks it

Case — One clean sheet up already, behind a defence that lost nobody and added Konsa. Arsenal’s opponent-adjusted defence is (figure not recorded) of the league mean, the best in the division, and the price on their clean sheet at the lock was 35% — the highest line on the card even without Saliba. The frozen call is 2⁠–⁠0.

Breaks if — Arsenal rotate for the Champions League and Raya loses six league starts.

(figure not recorded): a figure the case read live from the desk’s simulation or engine. It was not stored when the case was written, and today’s run is not put in its place.

Priced 44% on 21 Aug → 53% on 4 Sep, and frozen there to be graded at the end of the season.

Manager of the Season

○ Watch

Xabi Alonso

24% desk’s price on 4 Sep (from 19% on 21 Aug, ▲ 5 pts)

Now. Chelsea 10th on 7 points.

Hedge. Jakirović, Moyes, Farke

The case as made by 4 Sep, and what breaks it

Case — Won 3⁠–⁠2 away in his first league game with the largest available upside: Chelsea 10th to top four. Brighton at home on Sunday is the first real test.

Breaks if — Chelsea finish sixth and it goes to whoever keeps Hull up.

Priced 19% on 21 Aug → 24% on 4 Sep, and frozen there to be graded at the end of the season.

First sacked

○ Live · as set on 4 Sep

Álvaro Arbeloa (Fulham)

33% desk’s price on 4 Sep (from 22% on 21 Aug, ▲ 11 pts)

Now. Fulham 19th on 2 points; 14th on expected points. The payload records no manager changes.

Hedge. Pierre Sage, Frank Lampard

The case as made by 4 Sep, and what breaks it

Case — Two defeats, no points, 19th on expected points and a 1⁠–⁠0 loss at Sunderland. Palace at home is winnable on the underlying and unbenchmarked on the models — which is exactly the sort of fixture that decides these things.

Breaks if — Fulham scored twice against Chelsea and the ownership is patient by Premier League standards.

Priced 22% on 21 Aug → 33% on 4 Sep, and frozen there to be graded at the end of the season.

Best promoted side

● On track

Hull City

42% desk’s price on 4 Sep (from 47% on 21 Aug on Coventry, ▼ 5 pts, new pick) · Hull City is today’s simulation’s answer (the promoted side least likely to go down)

Promoted sides relegated in: Hull City 23.1% · Ipswich Town 39.3% · Coventry City 58.1%. Mean points are not compared: the gap is inside the simulation’s noise.

Now. Hull City 8th on 8 points, Ipswich Town 11th on 6 points, Coventry City 18th on 3 points.

On today’s run the simulation agrees: Hull City are the promoted side least likely to go down.

Hedge. Coventry City

  • The case says the engine puts Hull on “the wrong side of a split”; on today’s run they are on the right side of it.
  • The case cites a +4.37 points-over-expected gap “that is the largest in the league”; today Hull City’s is +2.71, and Man City’s +5.75 is the largest.
  • “Six points from two” describes the first two rounds; Hull City now have 8 from 5.
The case as made by 4 Sep, and what breaks it

Case — Six points from two, a second clean sheet at Coventry, and the call held. The tension is internal: Hull are third in the table and seventeenth on expected points, a +4.37 gap that is the largest in the league. v1.8 takes back what v1.7 gave and then some: with the venue term applied once, Villa edge the Saturday fixture at 38% and Hull are no longer favourites in it. The simulation is blunter still: it finishes Hull (figure not recorded) and relegates them in (figure not recorded) of runs, against (figure not recorded) for Ipswich — so on the desk’s own engine Hull are the wrong side of a split this market is supposed to reward. Mean points separate the two by less than the simulation’s own noise, so that comparison is not published. The call is held on six banked points against Ipswich’s three, and cut nine to 42% to say what it is: a lead in the table the model does not think is repeatable, on a fixture list that has not tested it. Hull also now appear in the bottom-three call, which is not a contradiction — all three promoted sides are in trouble and this market only asks which is least so.

Breaks if — Coventry win the head-to-head and the squad-quality case reasserts itself.

(figure not recorded): a figure the case read live from the desk’s simulation or engine. It was not stored when the case was written, and today’s run is not put in its place.

Priced 47% Coventry on 21 Aug → 42% Hull on 4 Sep, and frozen there to be graded at the end of the season.

Table

After five rounds Man City lead on 15 points, three clear of Arsenal. On the chances each side has created and conceded, Arsenal have been the better team (10.3 expected points to Man City’s 9.2). Man City are the furthest above their expected points (+5.8), followed by Newcastle (+3.8) and Hull (+2.7). Simulated 5,000 times to May, Arsenal win the title in 46% and Man City in 32% of seasons; Coventry go down in 58%.

Live table, with expected points

Live — after five complete rounds. Ordered on points, then goal difference, then goals scored. xPts: the points a side would average from the chances it created and conceded (FotMob); ± is points minus xPts, in bold at 2 or more either way. How expected points are built

#ClubPGF–GAGDPtsxPts±
Champions League · 1–5
1 Man CityP5 · 13–5 5 13–5 +8 15 9.22nd +5.8
2 ArsenalP5 · 8–4 5 8–4 +4 12 10.31st +1.7
3 BrightonP5 · 16–5 5 16–5 +11 10 8.17th +1.9
4 BrentfordP5 · 10–4 5 10–4 +6 9 8.54th +0.5
5 LeedsP5 · 7–3 5 7–3 +4 9 7.79th +1.3
Europa League · 6–7
6 LiverpoolP5 · 7–4 5 7–4 +3 9 8.56th +0.5
7 EvertonP5 · 6–3 5 6–3 +3 9 7.210th +1.8
Conference League · 8
8 HullP5 · 6–4 5 6–4 +2 8 5.316th +2.7
Mid-table · 9–17
9 NewcastleP5 · 9–9 5 9–9 0 8 4.220th +3.8
10 ChelseaP5 · 10–12 5 10–12 −2 7 5.913th +1.1
11 IpswichP5 · 7–11 5 7–11 −4 6 6.812th −0.8
12 Man UnitedP5 · 8–8 5 8–8 0 5 8.55th −3.5
13 Nott’m ForestP5 · 4–5 5 4–5 −1 5 8.63rd −3.6
14 SunderlandP5 · 6–10 5 6–10 −4 4 7.78th −3.7
15 Crystal PalaceP5 · 6–11 5 6–11 −5 4 4.717th −0.7
16 Aston VillaP5 · 4–9 5 4–9 −5 4 4.618th −0.6
17 BournemouthP5 · 6–8 5 6–8 −2 3 6.911th −3.9
Relegation · 18–20
18 CoventryP5 · 1–10 5 1–10 −9 3 4.319th −1.3
19 FulhamP5 · 5–8 5 5–8 −3 2 5.814th −3.8
20 TottenhamP5 · 2–8 5 2–8 −6 2 5.815th −3.8
About this table

The real table after 5 complete rounds, read from the payload, and the projected finish underneath it. Columns: played · GF–GA, GD, points. Source: payload clubs[]. On a phone, played (P) and GF–GA sit under the club’s name. xPts and its rank (xRank, shown small beside it) are FotMob’s expected points, read from the same payload; ± is Pts − xPts to one decimal, the subtraction the Underlying tab prints to two.

Where the season ends

Simulated season — 5,000 runs. The chance of each finish, then the range of final points the club ended inside in 90% of runs, with the median marked. The width of the range is the honest part: clubs whose ranges overlap cannot be told apart.

1Arsenal Title 46.2% Top four 91.2% Down — 61–84 pts · median 73
2Man City Title 31.8% Top four 84.9% Down — 59–83 pts · median 71
3Brighton Title 9.2% Top four 57.0% Down — 52–76 pts · median 64
4Brentford Title 3.9% Top four 39.2% Down — 49–72 pts · median 61
5Chelsea Title 2.6% Top four 31.5% Down 0.2% 47–72 pts · median 60
6Liverpool Title 2.6% Top four 29.1% Down 0.2% 48–72 pts · median 59
7Man United Title 1.1% Top four 19.6% Down 0.8% 45–69 pts · median 57
8Leeds Title 1.4% Top four 19.1% Down 0.7% 45–69 pts · median 57
9Everton Title 0.6% Top four 11.5% Down 1.6% 42–67 pts · median 54
10Bournemouth Title 0.2% Top four 6.6% Down 3.5% 40–64 pts · median 52
11Nott’m Forest Title 0.1% Top four 4.2% Down 5.2% 39–63 pts · median 50
12Sunderland Title 0.1% Top four 3.7% Down 6.2% 38–63 pts · median 50
13Tottenham Title — Top four 0.8% Down 19.0% 33–57 pts · median 45
14Hull Title — Top four 0.3% Down 23.1% 32–55 pts · median 44
15Newcastle Title — Top four 0.4% Down 26.8% 32–55 pts · median 43
16Crystal Palace Title — Top four 0.2% Down 31.5% 30–53 pts · median 41
17Ipswich Title — Top four 0.2% Down 39.3% 29–52 pts · median 40
18Fulham Title — Top four 0.2% Down 41.4% 28–52 pts · median 40
19Aston Villa Title — Top four 0.2% Down 42.4% 28–51 pts · median 39
20Coventry Title — Top four — Down 58.1% 25–48 pts · median 37

“—” means under 0.1%. Down is in bold at 25% or more. The desk’s relegation call against this run · How the engine prices a fixture

The full simulated table and how it is run
#ClubMean ptsMedian · 5th–95thTitleTop fourRelegated
1Arsenal72.573 · 61–8446.2%91.2%—
2Man City71.071 · 59–8331.8%84.9%—
3Brighton64.364 · 52–769.2%57.0%—
4Brentford60.861 · 49–723.9%39.2%—
5Chelsea59.760 · 47–722.6%31.5%0.2%
6Liverpool59.559 · 48–722.6%29.1%0.2%
7Man United57.157 · 45–691.1%19.6%0.8%
8Leeds57.057 · 45–691.4%19.1%0.7%
9Everton54.154 · 42–670.6%11.5%1.6%
10Bournemouth51.752 · 40–640.2%6.6%3.5%
11Nott’m Forest50.550 · 39–630.1%4.2%5.2%
12Sunderland50.350 · 38–630.1%3.7%6.2%
13Tottenham45.245 · 33–57—0.8%19.0%
14Hull43.644 · 32–55—0.3%23.1%
15Newcastle43.143 · 32–55—0.4%26.8%
16Crystal Palace41.641 · 30–53—0.2%31.5%
17Ipswich40.540 · 29–52—0.2%39.3%
18Fulham39.940 · 28–52—0.2%41.4%
19Aston Villa39.339 · 28–51—0.2%42.4%
20Coventry36.737 · 25–48——58.1%

5,000 runs on a fixed seed (20262027) over the 330 fixtures not yet played, sampled from the same goal intensities the match cards use — so the projection and the round’s calls cannot disagree about a club. Points banked from the five completed rounds are carried, not simulated. The band is the 5th to 95th percentile of final points; the median sits inside it. Nothing here is a point forecast, and the width of the band is the honest part.

The desk’s own projected table · hand-priced after GW1, 1,054 points across the league

The desk’s judgement, priced by hand after GW1 (between 24 and 28 August) and kept as made, not re-priced since: a third view beside the live table and today’s simulation, not the simulation’s mean. Its reads date from then; Hull’s “out of the relegation call” comes before the 4 September revision of that call, which took Hull in for Ipswich. Where the desk’s own table and today’s simulation part company: Aston Villa 10th here, 19th in the simulation; Leeds United 14th here, 8th in the simulation; Tottenham 8th here, 13th in the simulation; Nottingham Forest 15th here, 11th in the simulation.

#ClubManagerPts25⁠–⁠26ΔRead
Champions League · 1–5
1ArsenalMikel Arteta851st · 850Deepest squad, no reset, best defence retained and Konsa added.
2Manchester CityEnzo Maresca762nd · 78−2Won without control, then sold both wide options. Still a tier up in talent.
3ChelseaXabi Alonso7210th · 52+20Three goals away on debut, one game a week, and the best untapped core in the league.
4LiverpoolAndoni Iraola675th · 60+7A point at St James’ Park, and nine fast-break goals conceded across two seasons.
5Manchester UnitedMichael Carrick613rd · 71−10Elite volume, no answer to a low block, set pieces conceded. Cut ten on structure.
Europa League · 6–7
6BrightonFabian Hürzeler618th · 53+8League-leading press numbers on MD1 and a 4–0 to open. Raised three.
7BrentfordKeith Andrews569th · 53+3Thirteen set-play shots and three goals. Raised three.
Conference League · 8
8TottenhamRoberto De Zerbi5517th · 41+14Cut five. Savinho and Marmoush in, five attackers out, and the worst xG conceded since 2022.
Mid-table · 9–17
9BournemouthMarco Rose536th · 57−4Led at the Etihad for an hour. Four dated absentees into October.
10Aston VillaUnai Emery514th · 65−14Cut fourteen. Rogers, Tielemans, Digne and Konsa gone, and no goal on MD1.
11EvertonDavid Moyes4913th · 490Three points banked and Ndiaye still there — for now.
12SunderlandRégis Le Bris487th · 54−6Continuity is the asset, and Opta now makes them home favourites against Fulham.
13NewcastleMatthias Jaissle4812th · 49−1A creditable point against Liverpool inside a genuine rebuild.
14Leeds UnitedDaniel Farke4714th · 470Stach’s free kick won it away from home. A repeatable trick in a set-piece league.
15Nottingham ForestOliver Glasner4415th · 440Glasner is the upgrade; the squad still needs a season.
16Crystal PalacePierre Sage4116th · 45−4Hit the post twice and lost. Sarr and Riad both out.
17Hull CitySergej Jakirović38Promoted (PO)—Raised twelve and out of the relegation call. Beat United from set pieces.
Relegation · 18–20
18FulhamÁlvaro Arbeloa3511th · 52−17Scored twice at home and lost. Untested coach, thin squad, hard September.
19Coventry CityFrank Lampard34Promoted (C)—Beaten at the Emirates, Haji Wright out up to 12 weeks. Into the bottom three.
20Ipswich TownGary O’Neil33Promoted (2)—Three points and still last on squad depth.

Zones: 1⁠–⁠5 Champions League · 6⁠–⁠7 Europa League · 8 Conference League · 18⁠–⁠20 relegation. Brentford’s 2025⁠–⁠26 line is reconstructed; the published listings we hold omit it.

Model

Every club gets two numbers: how much it creates (attack) and how much it allows (defence), each measured against the league average. Before the season those came from a projection; now the engine blends that projection with this season’s xG. Two clubs’ numbers, a home bonus and a few small adjustments give each side’s expected goals, and expected goals give every scoreline a probability.

How one fixture gets its price: Arsenal v Leeds

Sat 10 Oct, 12:30 ⁠BST. The card will read “1⁠–⁠0 at 52%”. Here is where both numbers come from, recomputed in your browser from the engine.

  1. 1Club strengths

    Arsenal
    attack 1.24 · defence 0.69
    Leeds
    attack 0.98 · defence 0.99

    Arsenal create 24% more than an average side and allow 31% less; Leeds create 2% less and allow 1% less.

    After 5 matches the engine takes 50% of each number from this season’s xG and 50% from the pre-season projection.

  2. 2Base goals

    A league-average side scores 1.33 from open play and 0.34 from set pieces. Each is scaled by the attacker’s strength in that phase and the defender’s in it.

    Arsenal
    1.86 1.49 open play + 0.37 set pieces
    Leeds
    1.25 0.88 open play + 0.37 set pieces

    These are on the engine’s raw scale, where an average match has 3.79 goals. Step 3 scales every fixture by 0.771 so the league averages 2.92, which is why the expected goals come out lower than the base goals.

  3. 3Expected goals

    Home advantage
    × 1.14 (Arsenal only)
    Small nudges
    × 1.008 / × 1.023
    League tempo
    × 0.771

    Arsenal λ 1.65Leeds μ 0.99

    The nudges are team news, squad changes and this season’s rule changes. The rule-changes part (+0.015 log-goals on both sides) is the same on every fixture, so the tempo step cancels it exactly and it moves no price; the net tilt is 0.7% towards Leeds.

  4. 4Every scoreline

    From those two averages every scoreline gets a probability (Poisson, with a small Dixon–Coles tweak, ρ −0.10, that makes 0⁠–⁠0 and 1⁠–⁠1 a little likelier).

    Leeds goals →

    Arsenal goals ↓

    01234
    08.35.93.51.10.3
    110.612.85.81.90.5
    29.79.64.81.60.4
    35.45.32.60.90.2
    42.22.21.10.40.1

    Percent of the time. Solid outline: the likeliest score overall, 1⁠–⁠1. Dashed outline: the card’s score, 1⁠–⁠0. Scores outside this corner hold 3.0% of the probability; the engine’s grid runs to 12 goals a side.

Reading the card. “1⁠–⁠0 at 52%” means an Arsenal win is the likeliest outcome at 52%, and 1⁠–⁠0 is the likeliest score within an Arsenal win (10.6%). The single likeliest score overall is 1⁠–⁠1 (12.8%), the most common score in this fixture’s grid. That is why the card names the outcome first: the model’s prediction is the likeliest score inside the likeliest outcome.

The engine’s structure: λ = (1.33 × open-play attack × opponent’s open-play defence + 0.34 × set-piece attack × opponent’s set-piece defence) × 1.14 home × e^(nudges) × 0.771 tempo. μ is the same for the away side, without the home term.

The Diagnostics tab’s strength note prints a shortcut, λ = 1.52 × home attack × away defence × 1.14 × tempo, which is the form the engine uses only when the payload has no situations block. For this fixture the shortcut gives base goals of 1.86 for Arsenal and 1.03 for Leeds, against the 1.86 and 1.25 the engine actually uses.

The exact arithmetic
League open-play / set-piece base (muOp, muSp)
1.3337 / 0.3446
Arsenal open-play attack × Leeds open-play defence
1.2018 × 0.9301
Arsenal set-piece attack × Leeds set-piece defence
1.0558 × 1.0285
Leeds open-play attack × Arsenal open-play defence
0.9881 × 0.6682
Leeds set-piece attack × Arsenal set-piece defence
1.0560 × 1.0243
Base goals Arsenal / Leeds
1.8649 / 1.2534
Home term
1.14
Σ nudges Arsenal / Leeds (log-goals)
+0.0076 / +0.0224
Tempo scale (target ÷ raw mean)
2.9249 ÷ 3.7938 = 0.7710
λ / μ
1.6517 / 0.9882
Dixon–Coles ρ · grid
−0.1 · 0–12 goals a side
Home / draw / away
0.5175 / 0.2683 / 0.2143
Margin to the runner-up
24.92 points
Nudge termKindHomeAway
Managerial resetrelative+0.0000+0.0000
Fixture load and European commitmentsrelative+0.0000+0.0000
Squad change, net of qualityrelative+0.0030−0.0030
Injuries and availabilityrelative−0.0104+0.0104
Rule changestempo+0.0150+0.0150
Market and model priorstempo+0.0000+0.0000

Recomputed here and checked against the engine’s own output: identical.

Every club’s attack and defence

Right means creates more; up means allows less. Best attack: Man City 1.32. Tightest defence: Arsenal 0.69. Weakest attack: Coventry 0.75. Leakiest defence: Ipswich 1.26. Each badge opens that club’s fixture this round.

Dashed lines mark the league average. 11 badges are nudged sideways where they would overlap; the table has every exact pair.

  1. MCIMan Cityattack 1.32 · defence 0.88
  2. BHABrightonattack 1.27 · defence 1.03
  3. ARSArsenalattack 1.24 · defence 0.69
  4. BREBrentfordattack 1.16 · defence 0.93
  5. CHEChelseaattack 1.16 · defence 0.93
  6. MUNMan Unitedattack 1.14 · defence 0.92
  7. LIVLiverpoolattack 1.11 · defence 0.87
  8. SUNSunderlandattack 1.04 · defence 1.05
  9. BOUBournemouthattack 1.00 · defence 0.95
  10. EVEEvertonattack 0.98 · defence 1.03
  11. LEELeedsattack 0.98 · defence 0.99
  12. NFONott’m Forestattack 0.94 · defence 0.99
  13. NEWNewcastleattack 0.91 · defence 1.14
  14. TOTTottenhamattack 0.89 · defence 1.01
  15. FULFulhamattack 0.88 · defence 1.23
  16. AVLAston Villaattack 0.88 · defence 1.10
  17. IPSIpswichattack 0.86 · defence 1.26
  18. CRYCrystal Palaceattack 0.83 · defence 1.16
  19. HULHullattack 0.78 · defence 1.17
  20. COVCoventryattack 0.75 · defence 1.22
All twenty, with the solved, game-state and market-prior figures
ClubAttackDefenceSolved, before shrinkageGame stateMarket prior
Man City1.320.881.33 / 1.001.101.30 / 0.84
Brighton1.271.031.42 / 1.161.061.01 / 0.99
Arsenal1.240.691.04 / 0.681.001.24 / 0.73
Brentford1.160.931.28 / 0.911.081.06 / 0.96
Chelsea1.160.931.06 / 1.061.071.16 / 0.93
Man United1.140.921.16 / 0.940.991.20 / 0.93
Liverpool1.110.871.02 / 0.900.931.22 / 0.89
Sunderland1.041.051.16 / 1.020.900.86 / 1.04
Bournemouth1.000.950.99 / 0.911.060.99 / 1.03
Everton0.981.031.01 / 1.001.040.95 / 1.02
Leeds0.980.991.04 / 0.881.040.96 / 0.96
Nott’m Forest0.940.991.00 / 0.841.010.94 / 1.00
Newcastle0.911.140.88 / 1.211.081.02 / 1.03
Tottenham0.891.010.74 / 1.060.911.04 / 0.99
Aston Villa0.881.100.78 / 1.170.990.95 / 1.05
Fulham0.881.231.02 / 1.110.970.89 / 1.04
Ipswich0.861.261.01 / 1.110.950.87 / 1.16
Crystal Palace0.831.160.84 / 1.110.900.89 / 1.07
Hull0.781.170.77 / 1.050.990.77 / 1.27
Coventry0.751.220.77 / 1.040.920.86 / 1.22

What moves a price

Strengths do almost all the work. 7 factors are built into the strengths themselves; 6 more are applied as small nudges on top. This round the largest nudge is Brentford at Aston Villa: about +0.08 expected goals.

Built into the strengths

  • Base rating (xG, xGA, points) 13% −5 points since pre-season
  • Home advantage and travel 10% +3 points since pre-season
  • Defensive structure and set pieces 10% +10 points since pre-season
  • Chance quality by phase (xG/Sh) 9% +9 points since pre-season
  • Promoted-side baseline 7% +2 points since pre-season
  • Opponent adjustment (strength of schedule) 4% +4 points since pre-season
  • Overperformance regression (G − xG, GA − xGA) 4% +4 points since pre-season

Applied as nudges, with their share of this card’s adjustment

Bar = declared weight (the effective one where a factor is suspended; its outline is the nominal weight).

Rule changes adds +0.015 log-goals to both sides of every fixture. Like every published term it is multiplied into λ (check E6: Pass on this payload), but it is the same on every fixture, so the goals calibration (the tempo step, check E9) then cancels it exactly and it moves no price. Check E6

Each factor’s full note, the total, shrinkage and retired factors

The tab’s original introduction: A base rating per club from three seasons of underlying numbers, adjusted by the factors below, then run as paired match simulations. Weights move after every round under a fixed update rule, not on instinct.

Weights, v2.1

Share of the total adjustment each factor can move, summing to 100%.

Base rating (xG, xGA, points) · 13% · −5 points since pre-season
Three-season weighted underlying performance, last season heaviest. In v1.7 it is the shrinkage prior the engine falls back on: at 5 matches it carries 50% of every club’s attack and defence estimate.
Home advantage and travel · 10% · +3 points since pre-season
Raised from 0.30 to 0.38 goals for rounds 1–6 in v1.1 (23 Aug). Home sides have won 18 of the 50 matches played, and always calling the home side is the baseline the model has to beat.
Defensive structure and set pieces · 10% · +10 points since pre-season
Raised one in v1.7 out of the retired draw prior. Non-penalty set plays were 27.4% of goals last season. It is the term that sets the defence multiplier in the engine, and set-piece rates are the slowest thing in the model to stabilise — declared at τ = 14 matches against open play’s 5.
Chance quality by phase (xG/Sh) · 9% · +9 points since pre-season
Raised one in v1.7. It sets the attack multiplier that becomes λ, so it is now doing arithmetic rather than informing a note. Open-play generation is declared at τ = 5 matches.
Promoted-side baseline · 7% · +2 points since pre-season
Home baseline raised 12% in v1.1, away unchanged. At GW3 Hull had six points from two matches and the expected-points table said almost none of it was repeatable. After 5 matches they have 8 points against 5.3 expected.
Opponent adjustment (strength of schedule) · 4% · +4 points since pre-season
Regularised in v2.0: the correction is raised to the power 1 / (1 + 3 / n), which is 0.63 at 5 matches, because solved in full on this sample it is demonstrably wrong — it made Bournemouth the league’s best attack off two fixtures. New in v1.7, funded by the retired draw prior. Every club’s xG and xGA are divided through by the strength of the opponents they actually faced, solved to a fixed point (check S4 prints the passes and the residual). It is what turns Brighton’s 11.48 xG from 5 matches into a 1.27 attack and Newcastle’s record into a 0.91 one.
Overperformance regression (G − xG, GA − xGA) · 4% · +4 points since pre-season
Nominal 4%, from two independent models plus an expected-points table, firing only where both models agree on the sign and the size. From v1.6 it is scaled by sample — n / (n + 6) — so at 5 matches it applies 1.8%. The nominal figure is what it converges to, not what it is doing now.
Managerial reset · 0% effective · 12% nominal · 0% of the card · no change since pre-season
Suspended: its engine penalties are 0 / 0 until they are fitted on results, so it carries 0% effective weight and moves no price. 12% is the pre-registered nominal, and it returns when the penalties are fitted. Cut two in v1.6 towards normalisation — it was the largest single share in the model on the thinnest re-fitting evidence.
Fixture load and European commitments · 10% · 0% of the card · no change since pre-season
Midweek European travel costs more in the following weekend. Cut one at GW3 (4 Sep), when the European group stage had not started and the term had priced nothing in three rounds but a small rotation discount at City. Since v2.1 it reads each club’s rest days before the fixture, −0.03 × max(0, 3.5 − rest days); 9 clubs carry a European campaign this season.
Squad change, net of quality · 8% · 38% of the card · −7 points since pre-season
Cut again at GW3 (4 Sep), once the window had shut: Spurs had been the division’s biggest net upgrade and had not scored, and Hull had barely spent and had six points. After 5 matches, Spurs have scored 2 and Hull have 8 points.
Injuries and availability · 9% · 26% of the card · no change since pre-season
Keyed to dated return windows. Two-sourced across all 20 clubs from v1.5: FotMob with Premier Injuries up to GW3, FotMob with FPL’s own status flags since. Where the sources disagree on a date both are carried and neither is reconciled (at GW3: Saliba, Onana, Wright). From v1.6 it also vetoes named goalscorers, which is a hard gate rather than a weight.
Rule changes · 3% · 36% of the card · no change since pre-season
New VAR protocol, stricter time-wasting sanctions, faster substitutions. A small nudge to added time and goals after minute 80.
Market and model priors · 1% · 0% of the card · −2 points since pre-season
Cut one at GW3 (4 Sep), when the external favourite printed next to nine of ten calls and set none of them. The closing market is scored as a published baseline instead of being folded in as a prior.

Total allocated weight: 100% nominal · 88% effective, 12 suspended

v1.5 summed to 108% across fifteen factors. Weights are adjustment shares and have to sit on the unit simplex; above it, every linear combination expands variance and over-scales the ratings against the pre-season base. v1.6 removed eight points and normalised. v1.7 keeps the total at 100% and rearranges within it: the draw prior is retired and its six points fund a new opponent-adjustment term at 4%, with one point each to defensive structure and chance quality — the two factors the engine now reads directly as multipliers. At v1.7 nothing had moved more than two points in a round, inside the update rule’s four-point cap; the cap is now read from the version log (check W2, manual), and gate G11 asserts the total before any card renders.

Sample shrinkage, by phase: One τ per phase, not one per model

Anything keyed to a season-to-date residual is scaled by n / (n + τ), so its weight grows with the sample instead of arriving whole. v1.6 used a single τ = 6 everywhere. v1.7 declares one per phase, because they do not stabilise at the same rate: open-play generation at τ = 5, the strength estimates the engine runs on at τ = 5 — 5 matches therefore carry 50% of a club’s attack and defence and the pre-season prior carries 50% — overperformance regression at τ = 6, giving 1.8% effective against a nominal 4%, and set-piece conversion at τ = 14, the slowest thing in the model. In v1.5, at full strength, the regression term was treating two rounds of schedule noise as a systematic trait: Hull’s opponents created low-probability chances that fell to cold finishers, and the model read that as a defence due to collapse.

Retired factors

Draw prior
Retired in v1.7, from 9% pre-season to 6% and now 0%. It existed because the desk published a scoreline and a confidence number by separate routes and needed a rule for when to say 1–1. The joint distribution produces draws endogenously — at GW3 Fulham v Palace split 38 / 24 / 38 and the draw was simply not the modal outcome — so the gate has nothing left to do. Its 4% went to the opponent adjustment and the remaining two points to defensive structure and chance quality.
World Cup residue
Retired in v1.6, from 8% pre-season to 2% and now 0%. Two seasons past the tournament, with no fatigue signal in the first three rounds, the residual variance is not distinguishable from zero.
Unofficial transfer reporting
Retired in v1.6, from 6% pre-season to 1% and now 0%. The window shut on 1 September. Four reported deals were never confirmed in a captured source and stay recorded as open rather than assumed.

Four numbers set by hand, and what the data says

Each is also fitted to the matches played, but a fitted value is only adopted once the sample is big enough and has held its sign for two rounds.

SettingSet toFittedWhat it means
Home advantage× 1.14× 1.14The home side’s expected goals are multiplied by this; the away side’s are not.
Dixon–Coles ρ−0.1−0.075Makes 0⁠–⁠0 and 1⁠–⁠1 a little likelier, and 1⁠–⁠0 and 0⁠–⁠1 a little less, than independent goals imply.
Trust in this season50% (τ 5)τ 3n / (n + τ) with n = 5: the share of each strength taken from this season’s xG. The fit only tries τ 3, 5, 8 and 14, and its log-likelihood (−146.77 v −146.83 typed) differs negligibly at this sample.
League tempo2.92 goals a matchderived(5 × 2.82 goals a match from 50 results + 5 × 3.03 xG a match) ÷ 10.
The parameter fit
ParameterTypedFitted
Home term1.141.14
ρ-0.1-0.075
τ (shrinkage)53
Log-likelihood-146.83-146.77

Maximum likelihood on 50 played fixtures, grid-searched (home 1.00–1.40, ρ −0.20–0.05, τ 3/5/8/14), tempo re-normalised for each home term. Diagnostic only: adopted at n ≥ 8 once the fitted values have held sign for two rounds. Currently n = 5. Diagnostics

The update rule: fixed, applies every week

Stops one bad Saturday rewriting the model.

  1. No single weight moves more than 4 points in one gameweek, however bad the round was.
  2. A factor needs the same signed error in two consecutive gameweeks before it moves more than 2 points.
  3. Structural corrections — a term applied symmetrically that should not have been — are exempt, because they are bug fixes rather than tuning.
  4. A call against the round’s external favourite requires a named mechanism in the card. A hunch is not a mechanism, and gets reversed.
  5. Each call is locked before its own fixture kicks off (locks close 15 minutes before kickoff; an uncalled fixture gets the model’s call as a fallback 45 minutes before) and is not re-priced on new evidence. Evidence that arrives later and agrees with it changes nothing: corroboration is not grounds to move a call.
  6. A defect in the model itself is different, and is the one thing that reopens a price: weights that do not normalise, a factor applied at the wrong scale, a named player who is unavailable. Fixtures that have not kicked off are re-priced, the original price stays printed on the card beside the new one, and anything already under way is frozen where it stood.
  7. Every version is logged with the matches that caused it. Nothing is retuned quietly, and old calls are never edited.
Shadow models (GW6–10)

Three candidate changes to the engine, approved by the owner on 7 Oct 2026. From GW6 each is priced at every lock, desk call or model fallback, on the same engine as the published price, and written to the ledger beside the call (shadow_rp_h/d/a, shadow_mk_h/d/a, shadow_dc_h/d/a and shadow_ver, the module version, s1). None of them feeds a price: the published price, the desk’s call and the model of record are unchanged, and a shadow is never a call. Each is graded on the Scoreboard as a row of its own, and after GW10 the owner decides, on that log and the backtest below, whether any becomes engine v2.2.

ShadowWhat it changesSettingBacktest ΔLL [95% CI]n
Rating priorPulls each club’s attack and defence towards a blend of its Opta power rating and its ClubElo rating, by τ ÷ (matches played + τ), so most strongly early in the season.τ 5 · Opta weight 0.5+0.0021 [−0.0028, +0.0065]760
Market priorPulls each club towards the strengths implied by Betfair’s closing prices for the matches already played, by τm ÷ (matches played + τm); priced once those matches link every club.τm 20+0.0027 [−0.0060, +0.0116]665
Deep completionsScales each club’s attack by its deep completions per match, and its defence by those it allows, each against the league average, to the power β.β 0.2+0.0042 [−0.0030, +0.0114]740
All three at onceEach applied where its inputs exist: a backtest check only, since the log prices the three separately.as above+0.0113 [−0.0015, +0.0239]760

Backtest run 7 Oct 2026 (tools/shadow-backtest/out_shadow.txt, summarised in docs/SHADOW.md): a walk-forward on a stand-in for the engine, each setting chosen on 2023/24 alone and tested on 2024/25 and 2025/26. ΔLL is the change in mean log-loss against that stand-in (1.0041 over the 760 test matches); below zero is better. Every interval spans zero, and every estimate is above it: none of the three improved on the baseline out of sample, and all three at once scored worse on the ranked probability score (+0.0046 [+0.0006, +0.0086]). The market prior’s gain in 2023/24 (ΔLL −0.0152, t −2.90) did not hold up. The rating prior’s test ran on ClubElo alone (Opta weight 0), since no Opta rating history exists before 2026/27; at the shipped Opta weight, 0.5, it has only the 2026/27 GW1–5 replication: ΔLL +0.0018 [−0.0235, +0.0289], n 50. A 50-match log can only detect a difference of about 0.04 to 0.10 in mean log-loss, at least eight times the effects measured here, so it shows that the shadows run and price sensibly; it cannot overturn the backtest.

In this payload no ledger row carries a shadow price yet: the log starts with the first lock of GW6. Scoreboard

25 versions since 21 Aug 2026

12 of 25 entries changed prices (v1.0 to v2.1). The engine has been v2.1 since 9 Sep 2026; the 13 entries since then changed the page, the ledger or the checks, not the pricing chain.

v1.0 · 21 Aug 2026v2.1 · 9 Sep 2026v3.11 · 7 Oct 2026

  • prices changed
  • page, ledger or checks only
v3.11 · 7 Oct 2026 · page, ledger or checks

The shadow log, approved by the owner on 7 Oct: three candidate changes to the engine (a rating prior, a market prior and deep completions), each backtested on three past seasons, are priced beside every lock from GW6 by Payload v5.10.0 and written to the ledger beside the call. No price moves: the seventeen hashed engine functions are untouched, and no shadow feeds a price or counts as a call.

  • Scoreboard · The market comparison gains Shadow · rating prior, Shadow · market prior and Shadow · deep completions, read from the ledger’s shadow_* columns and scored like every other row, each on the matches it shares with the desk with its own n. They appear once a settled desk call carries one; a round before that reads “logged from GW6”, never a dash. Only rows priced by the module version this page ships are pooled. The Summary’s “Does it beat the betting market?” panel keeps its six rows and shows no shadow.
  • Model · A “Shadow models (GW6⁠–⁠10)” disclosure under the update rule: what each candidate changes, its setting as the module carries it, and its backtest as run on 7 Oct 2026 (none improved on the baseline out of sample), with how many ledger rows carry a shadow price.
  • Data and How it’s built · Opta’s power rankings, ClubElo and Understat’s league data are marked as shadow model inputs, logged beside every lock and never in the prices; the ledger step says the same in one line, and the footer and the Model tab’s source hierarchy call them shadow-model inputs rather than trials.
  • One module, two runtimes · site/js/shadow.js is the shadow’s only definition; the Apps Script copy is generated from it, and a test fails if the two differ.
v3.10 · 7 Oct 2026 · page, ledger or checks

Three outside reviews of a saved Summary tab (7 Oct), triaged by the lead and approved by the owner. Wording, rounding, navigation and the no-JavaScript copy; no price moves: the seventeen hashed engine functions are untouched.

  • Percentages add up · Three shares of one distribution printed side by side are rounded by largest remainder (pct3), so a split never reads 99 or 101; rankings and single percentages stay on the raw values.
  • Labels · A model pick is a “Model prediction”, never a call. A card’s score is “Most likely score” only when it is the mode of the whole scoreline grid; otherwise “Likeliest score in an Arsenal win”, with the model’s three percentages beside it so the 1X2 probability and the score read as two different things.
  • Record · Beside the desk’s record one computed line gives the complete record: calls right of graded, how many settled fixtures the desk locked, and every fixture including model fallbacks and missed locks.
  • Baseline · The ⅓-⅓-⅓ line is an uninformative probability forecast, not random picks; scoring above it on a small, upset-heavy sample does not mean worse than random.
  • Navigation · Two tiers: six primary tabs and a Research control that opens Underlying, Model, Diagnostics, Data and How it’s built, pinned at the right on phones. The page title names the view; the clock and countdowns re-render once a minute while the tab is visible; link previews (Open Graph) for shared links.
  • First paint and no JavaScript · The template loads as one bundle (views/_bundle.html); a static copy of every tab, regenerated twice a day by desk-gate, lives at /snapshot/, with robots.txt and a sitemap for readers and tools that do not run scripts.
v3.9 · 7 Oct 2026 · page, ledger or checks

A sitewide truth audit (owner, 6 Oct): sentences written for GW1–GW3 were still on the page as if current. No price moves: the seventeen hashed engine functions are untouched.

  • Now and history are kept apart · A sentence about the present is computed from the payload; a sentence about the past is dated and frozen, and is never filled with today’s engine numbers. The GW3 cards grade the ledger’s 16:33 calls and keep the 4⁠–⁠5 Sep re-prices as dated history.
  • Sources · The footer, the Model tab and How it’s built describe the pipeline as it runs: FPL, football-data.co.uk, FotMob and Understat read directly by the sheet’s scripts, Polymarket on the tick, Opta and Forebet lines logged into the ledger when found; Coupler, Solio and Premier Injuries retired.
  • Season calls · Kept as made on 21 Aug and 4 Sep, frozen to be graded at the end of the season, with a computed line on where each call stands today.
  • Market comparison · Summary and Scoreboard compare the desk with both model versions, Polymarket at lock, Opta’s supercomputer and Betfair close, each scored on the matches it shares with the desk.
  • Layout · Running text runs to the edge of its column on every tab.
v3.8 · 5 Oct 2026 · page, ledger or checks

The Step 3 data contract, shared with Payload v5.6.0: the producer now locks the model’s call automatically when the desk has made none by kickoff − 45 min, marks the row call_source model_fallback, and publishes its fallback decisions, missed locks and lock refusals under gates. No price moves: the seventeen hashed engine functions are untouched.

  • Fallback locks · A row with call_source model_fallback is not a desk call. It is kept out of the record strip, the forward ledger tile, the round log, the desk Brier and RPS rows and every check that counts desk calls, and is shown on a Model fallback record line of its own, with its n. A blank call_source, every row before v5.6.0, is a desk row.
  • Scoring · The desk row scores the desk’s calls only. The model, market prior, Betfair close and Polymarket commit rows also score the fallback locks, which are real commits at that instant. The strip compares desk, model and Betfair on the desk’s fixtures alone, so its n stays equal.
  • Cards · A fallback-locked fixture shows the model call with the status ‘Fallback lock (model)’ on a dashed pill, and its sub-line says the desk made no call by kickoff − 45 min. It is never a missed lock, and the round line reads ‘called k of n · fallback f · missed m’ once one exists.
  • Gates · When the payload carries gates.missed_locks, it is a second opinion: a fixture is a missed lock when the producer lists it or the page derives it, so neither side can hide one. The Data tab prints the fallback mode with its recent decisions and the last seven days of lock refusals when the payload carries them, and nothing when it does not.
  • L15 also fails when a fallback lock renders with any other status, or a desk call with the fallback’s. New L16 checks every fallback lock against the raw rows: out of the desk record, the called count and the desk Brier, and labelled as a fallback. It reports Blocked when the ledger has no fallback row.
v3.7 · 5 Oct 2026 · page, ledger or checks

The owner’s instruction after v3.6: fix every remaining failure that comes from a bug, a stale assumption or a typed literal, keep visible the ones that tell the truth about the model, and correct the in-file record where the payload contradicts it. No price moves: engine output is identical on all 380 pairings and the port hash is unchanged.

  • K7 · Exempts and names the ledger rows committed before the earliest reference capture in the ledger, a cutoff read from the data; every later row is still held to the rule.
  • W5 · Managerial reset is published as suspended at 0% effective (12% nominal) while both penalties are zero; W5 checks the realised share against the effective weight, and W1 reconciles nominal against effective plus suspended.
  • X3 · Traces only cards whose prose is filled from the live engine; frozen cards are historical records and are counted as exempt.
  • Missed locks · The card shows the model, market and Polymarket reference captures with their capture instant, or a red null row, never the live engine.
  • Prose · The E6, E10 and P7 notes and every version-log sentence that named a check state are generated from the measurement and the live tally.
  • In-file record · Three GW2 kickoffs typed an hour behind UK time are corrected to the payload’s, the GW1 and GW2 lists print the payload’s kickoff, and a test holds every in-file GW1–GW3 fixture to the payload’s results.
  • Commentary · Three claims the data contradicted are corrected: GW1 above 55% went four from five, GW1 priced three matches as 1⁠–⁠1, and one of the three GW2 revisions landed. The GW1 set-piece figure is marked unverified.
v3.6 · 5 Oct 2026 · page, ledger or checks

The 4 October audit of the v3.5 page against the GW6 payload, fixed on the website that replaced Claude Design. No price moves: the seventeen hashed engine functions are untouched and the port hash still equals the payload’s. What changed is which rounds are scored, how ledger rows are joined, how a missed lock looks, and what two checks assert.

  • SC1 · The forward record is read from the ledger for every round it covers, not from the in-file GW3 cards: the strip, the forward ledger tile, the round log and the scoring prose now count GW4 and GW5, with ‘of N’ wherever a round was not fully called.
  • SC2 · H1, L3 and L5 compare against figures folded straight from locked_calls and results, so a renderer that drops a round fails them instead of agreeing with itself.
  • SC4 · Each round gains a reference-family table over every fixture with a capture, each row with its own n, and a pooled forward table that L3 reconciles.
  • SC6 · A round with no model or market split at lock shows a null row in red, never today’s engine re-run in-sample.
  • LED-1 · Ledger rows join on FPL’s fixture code; fixture_id is a fallback only within 200 days of kickoff, so next season’s rows cannot overwrite this season’s.
  • SC3 · A fixture that kicked off with no call shows ‘Missed lock’ in red, each round states ‘called k of n · missed m’, and L15 fails if a missed fixture renders as anything else.
  • SC5 · K7 checks every ledger round, not only the target round, and reports the GW4 rows locked before the first reference capture.
  • H3 · A Blocked row is data that has not arrived yet, not a printed invariant, so H3 no longer fails when data lands.
  • LED-9 · Every UK time goes through one helper that names the UK zone from that instant’s own offset from UTC.
  • LED-4/5/7 · Vocabulary chips colour only known values, and the Polymarket bar at lock carries its own capture time.
v3.5 · 14 Sep 2026 · page, ledger or checks

The integrity brief, applied in five runs — the first four against the v5.3 payload, the last against v5.4. No price moves, but the claim is narrower than it was: the text of solve, factorTerms, phaseStrengths and playedBy did change, for D4’s null handling, and the output is identical on all 380 pairings under both the pre-season and the market prior. ENGINE_VER stays v2.1 with one caveat — a club reporting zero played now prices at its prior instead of at the league count, which is a different number for that club and the correct one. What else changed is what the page reads, how it folds a ledger, what it is able to assert, and what it now admits it cannot.

  • D1 · moved_after_capture is read as a non-blank string, never a boolean, and the chip prints the producer’s own sentence beside the flag.
  • D2 · No render path dereferences an absent result: a missing score prints an em dash, and renderVals is wrapped so a throw in any presenter leaves the paste, clear and fetch controls standing.
  • D3 · The harness self-tests. chk() refuses a typed Pass, so a Pass or Fail can only come from a measurement; every asserted row is re-run against its negated input and must flip; a row that does not flip is re-labelled Not asserted and counted against a declared PRINTED_INVARIANTS list. Seventeen rows rebuilt, eight added — C6, C6b, C7, L12, L13, S10, P7, P8 — E2b folded into E2, C3 split into a state row and C3b.
  • D4 · Absence is null, and null is not zero. playedCount() returns null with no played block and the solve refuses rather than dividing by a literal two; the adapter returns a league count only when every club carries a finite figure, because Math.min over an absent value is NaN and a NaN divided into a season total propagates through the whole solve in silence; playedBy() carries null for a club with no entry and distinguishes it from a club that has played nothing, which now sits on its prior and is labelled prior only; a rest-day figure must be a finite number. strengthsSafe() degrades the page to the pure prior rather than taking it down, and every line that used to quote a substituted count says not reported instead.
  • D5 · One scoring rule per row. The desk row is a distribution or it is unscored: no one-hot fallback, Brier and RPS print an em dash, and the direction hit prints beside them. Both non-called legs are capped, and the retrospective prints its K = 0.55 sensitivity.
  • D6 · Every denominator is read. The Betfair baseline counts rs.length against the rounds[] fixture count and reports n of N priced; gradeSet returns null on an ungraded round instead of 0.000; the header Brier pair carries its n and H1 fails when the two differ; the forward ledger scores every row over one intersection per round and prints its size once; the pm_ref row completes the six-row promise.
  • D7 · Nothing at lock is priced now. With no split in the ledger the desk row is titled direction only and carries no percentage; a result with no call reads Ungraded on a flat pill; a payload with no priced round renders the No priced round view with the producer’s clock authority and the forward list.
  • D8 · The fold is order-independent and keeps history. Rows sort by their own stamps before the last-non-blank-wins pass and ties break on content; gw leaves the pair key; a bare-pair stub cannot displace an id-keyed snapshot; _rows keeps the rows themselves so L12 can audit them; L14 folds the reversed ledger and compares.
  • D9 · Parity is a hash. The sha256 of the ported region is printed on the Diagnostics tab beside ENGINE_VER, with the region named method by method rather than counted — impliedGoals, playedPairs and resultsIndex were inside the port and outside the hash until the fifth run caught it, which a bare count could never have shown. L11 compares the hash to gates.engine_hash the moment the producer publishes one. New H4 asserts the version log leads with VER and descends strictly.
  • D10 · The renderer has no clock. kickoffOf, RMONTH and RYEAR are deleted; every kickoff comes from results[], fixtures[] or forward[] joined on fixture_id, code or club pair; every label goes through one Europe/London formatter; C8 prints which array each card’s kickoff came from.
  • D11 · Validate before you save, and the endpoint beats the paste. A pasted payload is stored only after it validates; load() tries the endpoint first and falls back to storage, printing which won and why; ingestClear walks one EXT_KEYS list.
  • D12 · The unanchored flag is per leg. The sub-line daggers the thin leg, the fixture-level chip fires only on the favourite’s leg or a total under $5,000, and the Polymarket scored row excludes an unanchored draw and says how many.
  • D13 · Small drops. review_gw is read from the payload and says when it had to be derived; R2 reports Blocked instead of vanishing; the harness declares its own size and asserts its ids are unique; G0 counts from rounds[] and forward[].
  • Run-4 rulings · One row reports Fail on this payload — E10 — and its note on the Diagnostics tab says why. Two rows were re-scoped rather than tuned: C6 measured freshness from the page’s wall clock, which duplicated C1’s payload-age assertion and went red for every payload over two hours old whatever the producer had done — it now measures from the build stamp and reports producer health at build time — and E8 asserts on the payload instead of on card prose: no scorers[] entry marked available may appear in availability[] as out or suspended, and the in-file sentences it used to police are deleted. E6 was rebuilt to test whether the published factor sum is in the price; the row it replaces asserted only that the term was non-zero somewhere in a table. W5 was made two-sided on the same principle — a published weight that binds nothing is a defect, not a blocked assertion. K8 scans every round the ledger covers rather than the target round alone, and H3 and this tab’s header read one tally.
v3.4 · 13 Sep 2026 · page, ledger or checks

Triggered by the page at 400px. Eleven data tables squashed their columns to min-content rather than scrolling: .table is width 100%, so a table without a min-width has no width to overflow and the section padding does the rest.

  • Eleven tables now scroll rather than squash. Each carries a min-width computed from its own columns — fixed widths plus 150px per auto column — inside an overflow-x wrapper: round locked ledger 410, underlying fixture table 780, A/B reconcile 530, availability split 370, GW3 forward ledger 390, strategy 460, fixture grade 410, per-gameweek log 660, club/manager/read 710, club table 600, diagnostics solved strengths 490. The wrapper alone does nothing; the min-width is what converts a squash into a scroll.
  • Four tables are deliberately left bare because they fit inside the 336px content width of a 400px viewport and wrap better than they scroll: the round-graded table, the availability out-list, the FotMob model table and the diagnostics parameter table.
v3.3 · 13 Sep 2026 · page, ledger or checks

Triggered by one label carrying two facts. The header read a single version number for both the page and the pricing chain, so a layout change and an engine change were indistinguishable from the outside — and the sheet’s model_ver had nothing on this page it could be compared against.

  • VER and ENGINE_VER are split. VER is this page; ENGINE_VER is the pricing chain of which the Apps Script port is a byte copy. The header reads both, and every prior-read label and scoreboard row tags the engine version it came from.
  • New check L11. Every target-round ledger row that carries model_ver is asserted equal to ENGINE_VER, so a drifted port is caught by the page instead of by hand-diffing two files before a lock.
v3.2 · 13 Sep 2026 · page, ledger or checks

Triggered by two things the page was measuring against itself. K6 timed the capture window off the render clock rather than off the payload, and the prose overlay still carried a whitelist entry for a field the ledger had started writing.

  • K6 runs on stamp_epoch. The capture-window check now measures from the payload’s own stamp rather than from the moment the page happened to render, so the result stops depending on when it is read.
  • The duplicate whitelist entry is deleted. The overlay may only supply fields the payload has no column for; once the ledger writes one, the whitelist entry is a second source for the same fact.
v3.1 · 13 Sep 2026 · page, ledger or checks

Triggered by the reasoning behind a call being held in a column nobody rendered. The sheet writes why and risk with each locked row, and the card printed the call, the prices and the drift but not the sentence that explains them.

  • Why and risk render in the card’s Notes region, ledger first and prose overlay second. Where the ledger carries the sentence, the ledger owns it.
  • New check L10. Every why and risk the ledger carries is asserted to appear verbatim in its own card, so a note that exists and is not shown fails rather than passes quietly.
v3.0 · 12 Sep 2026 · page, ledger or checks

Triggered by the upstream layer dropping every calendar assumption the page was built on. The routine no longer decides the round by weekday, the ledger is 57 columns wide and captures each fixture twice, and a correction arrives as an appended row rather than an edit.

  • v3.0 · the round stops being a unit of work. The page read a calendar it had assumed: ten fixtures, 38 rounds numbered 1 to 38, one kickoff per fixture, history in three in-file arrays. None of those are true of a Premier League season. Every count now comes from rounds[], the selector is built from the event ids that exist rather than from 1 to 38, any settled round draws itself from the payload, and a fixture with no date says so. The sheet now captures each fixture twice — unattended inside 24 hours of its own kickoff, and again at the moment the desk commits — and the card shows both, with the drift between them.
  • The three in-file round arrays are demoted to a prose overlay on a built card. Structure comes from the payload on every round; the overlay may only supply fields the payload has no column for, on a whitelist. Where a round holds any ledger row at all, the ledger owns the call.
  • ref_horizon_h is printed as measured. The 12 September dry run captured seven fixtures between 1.84 and 6.84 hours out because the trigger was installed on match day; the design intent of 24 hours is not what happened, so it is not what the card says.
  • The payload and the page disagree on two club names by design — the payload carries FPL’s "Man Utd" and "Spurs" — and the page wins. PLIngest.NAMES stays the single spelling table and no name is ever read from the payload.
  • Two checks were counting and not rendering. The Rounds and Calendar groups were missing from the diagnostics group list, so R1 and R2 contributed to the tally while appearing nowhere on the page — a number that exists and is not shown, which is the 11 September failure in miniature. Both groups are now in the list, and the new Calendar checks render with them.
v2.1.1 · 11 Sep 2026 · page, ledger or checks

Triggered by the first automated lock. At 16:30 the day before kickoff the sheet staged the model, market-prior and Polymarket columns for GW4 and the routine submitted the desk calls; the page parsed the ledger, L7 passed on engine parity, and the round view still priced the desk and market slots from the live engine. The ledger was right and the cards were not showing it. Six corrections, no new modelling.

  • 1 · lockedCalls() keeps a row that carries no desk call. From sheet v4.7 a row exists from 16:30, before the desk locks it; the parser required pred and discarded every staged row.
  • 2 · A graded card freezes only on a row that carries a desk call, so a staged-only row can never freeze one.
  • 3 · The target round is rebuilt ledger-first through one builder: model and market-prior bars from model_split_* / mkt_split_* when staged, else the live engine labelled · now; the desk bar from desk_split_* or a one-hot at the lock split; Polymarket from pm_lock_* with volumes and the unanchored flag. The headline is the desk call once locked_at is set, with the engine printed beside it, never in place of it. Status reads Open → Staged → Locked. A fixture with no ledger row renders exactly as it did before.
  • 4 · Two chips read the sheet’s v4.8 columns when present: the 09:00 → lock Polymarket move, and the raw overround.
  • 5 · New check L8. Every target-round card with a ledger row is compared to that row bar by bar and headline by headline. L7 proved the ledger agreed with the engine; nothing proved the card showed the ledger. This is the check that would have caught 11 Sep.
  • 6 · The header strip and round box report desk locked N of 10 with the lock time once the desk has locked; H1 still finds the deadline text inside the strip.
v2.1 · 9 Sep 2026 · prices changed

Triggered by the GW4 desk review, run in Node against the live engine on thirty results. Fourteen changes ship as one version under the structural-correction clause. All fourteen changes are in.

  • 01 · Tempo calibrated to the league after the solve: every fixture scaled so the mean of λ + μ over all pairings equals a shrunk blend of goals and xG per match. The engine was pricing 3.49 goals a match against 2.83 actual and 3.10 xG, which is why it priced draws at 21% in a season running at 33%. New check E9 publishes the pre- and post-scale means; E10 holds the target round to 24⁠–⁠27% draws and 2.9⁠–⁠3.1 goals.
  • 02 · Dixon-Coles ρ moved from a typed −0.045 to −0.10, stated as a prior; fitted at n ≥ 8 (change 13).
  • 03 · Manager penalties set to zero, term still wired. It was 51% of the realised adjustment and moved Brier by 0.001. A2 is blocked until n ≥ 8.
  • 04 · Congestion from the payload’s rest days, continuous: −0.03 × max(0, 3.5 − rest days). The european list is deleted — it was wrong and unread.
  • 05 · Absences weighted by minutes and by the xGI / defensive-contribution share of the players missing, from the payload’s availability block, instead of a headcount.
  • 06 · The solve fits on FotMob only; Football-Data is never averaged in. The spread between the two sets each fixture’s interval; the hand-typed per-card delta is gone. S7 is now an assertion.
  • 07 · One ledger. Locked calls are rendered from the sheet’s locked_calls tab in the payload; lockedPrices() is deleted. The engine’s own price sits beside the desk call, labelled model at lock. GW3 grades on the desk call.
  • 08 · A market-implied prior: every played fixture’s Betfair closing 1X2 inverted to implied goals and solved with the same opponent-adjusted machinery. The engine runs on both priors with the same evidence and the same w; the GW4 table shows three rows per fixture and the ledger scores both. Checks R1, R2.
  • 09 · λ from open-play and set-piece components with τ 5 and 14, built on the payload’s situations block. Blocked until the routine emits it (E11); at n = 3 it would barely move a call.
  • 10 · The game-state filter runs whenever the payload carries per-club score-state xG, with the multipliers published on the strengths table. The sample payload of the time carried none, so C4 was blocked on data rather than on a decision.
  • 11 · Per-club match counts from the payload; ids are the join key in the adapter and every name is derived from one table. Result rows with unknown clubs are reported through S1 instead of dropped. Check S9.
  • 12 · The season simulation samples each fixture’s outcome from its Dixon-Coles matrix rather than two independent Poissons. Check P6 asserts the sampled draw rate against the matrix.
  • 13 · Home term, ρ and τ fitted by maximum likelihood on the played fixtures and published beside the typed values on the Diagnostics tab. Diagnostic only; adopted at n ≥ 8 (K1, K2).
  • Sweep 3 · G2, G17 and G21 are derived from the payload’s producer gates and the live checks instead of describing a previous round; the return-date panel lists only cross-month conflicts.
  • Sweep 2 · Captions under the payload-bound tables are derived from those tables (expected-points gaps, heaviest absence lists), the return-date conflict table requires two parsable dates, and the G0–G21 gate list is stamped with the gameweek and payload it attests plus the producer’s own gate summary and run log.
  • Sweep · Every block the payload carries is read from the payload and its in-file copy is deleted: rates, played pairs, context, banked points, the live table, expected points, availability, suspensions, return-date splits, structural reads, GW1⁠–⁠2 results and the Betfair-favourite baseline. Without a payload the page renders only the Data tab. Check T1 asserts the Table and Expected-points panels against clubs[].
  • Aesthetics · Five-size type scale (11 / 13 / 15 / 20 / 32), 11px floor, no caption under 70% ink. Semantic tokens for home / draw / away (green / blue / rust) and good / critical for hit and miss, all distinct from the accent, which now marks interactive and live only. One round view with a GW1⁠–⁠38 selector and a persistent strip (record, Brier v market, next lock from deadline_epoch). Cards lead with the call and stacked home / draw / away bars for model, desk and market on one scale; prose sits behind a disclosure. The nine-column joint table is retired — the bars carry it. Diagnostics unchanged.
  • 14 · GW3 onward is scored on probabilities: desk call, model on each prior, Betfair closing and Polymarket at lock on multinomial Brier and RPS over the same fixtures. The Betfair-favourite baseline for GW1⁠–⁠2 is computed from the payload instead of typed. The literal nPlayed = 2 in the scoring path is gone. Check L6.
v2.0 · 7 Sep 2026 · prices changed

Triggered by a full diagnostics pass rather than by a round. Three findings, and the first is about the diagnostics themselves: the seventy-six QA rows this page published were typed string literals. A suite that cannot fail was attesting to a model that had shipped four defects in two days. The other two are engine defects — the number of matches played was hard-coded in three places, and every card was re-derived from the live engine on every render.

  • The QA list is now an executed harness. 83 checks run at load against the live objects, each publishing the figure it measured; 1 fail, 8 are blocked on data that does not exist yet, and 3 are declared manual instead of claiming a pass. The action register on the Diagnostics tab is derived from the failures, so it cannot drift from them.
  • Matches played is read from the payload. v1.9 divided season totals by four — two models and, hard-coded, two matches — and set the shrinkage weight from a literal 2. A GW4 payload would have halved every rate in the league and under-weighted the evidence at the same time, silently and in the same direction for all twenty clubs. New gate G19 asserts the declared count against the fixture list and against every club’s appearance count.
  • Locked prices are stored. A fixture past its kickoff renders the price it carried at that moment; only an open fixture reads the engine. Every version to v1.9 re-derived all ten cards on every render, so each model change re-priced played matches while the page claimed its calls were immutable. New gate G20, and the live engine is still printed beside the snapshot so the gap is visible.
  • The round clock is real. Kickoffs are computed from each card’s own day and time, so a round whose matches have been played and whose results have not arrived reports as closed and ungraded rather than as live. New gate G21 — which is what GW3 now trips.
  • The season projection can carry banked points derived from a results block instead of the transcribed two-round table. It falls back to the table until the payload supplies results.
  • The opponent adjustment is solved rather than approximated, and regularised rather than trusted. v1.7 ran three undamped Jacobi passes and called the result a fixed point; the harness measured the final pass still moving multipliers by 0.40. Solving it properly then exposed the real problem: on two matches per club the fixture graph is a union of cycles, the system is not identified, and the true fixed point is garbage — it made Bournemouth the best attack in the division because they had faced Manchester City, whose defence the same solve had driven to 0.16 because City had faced Bournemouth. The adjustment now carries a ridge keyed to the sample, 3 / matches played, so the schedule correction enters with exponent 0.40 at two matches and grows towards 0.86 by the end of the season. Opponents are averaged geometrically, which is what makes the system solvable at all, and the solved level is normalised so the goal mean is not quietly inflated — it was running 6.7% hot.
  • Every club’s solved attack and defence is published on the Diagnostics tab, and any prose quoting a strength, a price or a clean-sheet line now fills from the object that produced it. Five of the eight standing reads were quoting multipliers the corrected solve no longer produces. A second prose check traces every percentage a card prints back to that card — the token scan can only see unresolved braces, and a stale hard-coded number looks exactly like a correct one.
  • Scoreline grids size themselves to the fixture. A fixed 9×9 grid was discarding 1.1% of the probability mass on the highest-scoring fixture on the card and renormalising it onto the low scores.
  • Convergence residual, pass count, grid mass, the Dixon-Coles mass shift, schedule-graph connectivity, strength centring and realised factor shares are measured and published rather than asserted in prose.
v1.9 · 5 Sep 2026, 20:40 BST · prices changed

Triggered by a third audit pass. Two findings accepted, one declined. The accepted ones: the six factors bound in v1.8 were absolute rather than relative, so with fourteen of twenty clubs carrying a first-season manager the model was deflating league-wide goal expectancy and burning most of its adjustment budget on one term; and club names were an unchecked join key across seven separate tables.

  • Factors split into relative and tempo terms. Managers, squads, injuries and congestion are zero-centred within the fixture — the home-away differential is preserved exactly, the goal total is not touched. Rule changes stay uncentred. Gate G16 moves from partial to pass.
  • Club keys reconciled across the rates table, the fixture list and all five context maps at solve time, with orphans reported on the build tab. New gate G18.
  • Game-state-neutral xG filter built and wired to a gameStateSplits payload field: 0.86 while two or more behind, 1.28 while ahead, normalised to a league mean of 1.00 so it redistributes attacking credit without moving the goal mean.
  • Gate G17 stays unmet, and this is a decision rather than an omission. The filter would raise Newcastle’s adjusted attack and reverse the Bournemouth call — the audit puts the fixture back at Newcastle 2⁠–⁠1 and back in line with Opta. There are no score-state splits in the data layer. Reversing a published call on assumed inputs is the failure mode the gate exists to catch, so the call stands and the fix waits for real data.
  • The payload can now supply the played-fixture list and the full context block as well as the rates table. Each falls back to the in-file copy independently, so a partial payload degrades one input rather than the page.
v1.8 · 4 Sep 2026, 23:55 BST · prices changed

Triggered by an audit of the engine v1.7 had just shipped. Two findings, both structural: the home-advantage term was multiplying the home intensity and dividing the away one, squaring a 14% venue effect into a 30% goal ratio; and eleven of the thirteen published factor weights were never read by the simulation at all — they were display text beside a model running on shrunk xG rates.

  • Home advantage applied once, to the home intensity only. The away side is no longer penalised for playing away twice. Every home price on the GW3 card falls, some by six points.
  • The Hull call is withdrawn. It existed because of the double-counted venue term; corrected, Villa edge the fixture at 38% and the desk now agrees with Opta. Third revision of one fixture, caused by a defect the desk shipped and is recording rather than tidying.
  • Six bypassed factors bound into log-intensity: managerial reset, fixture load, squad change, injuries, rule changes, market priors. A manager change or an injury list now moves λ.
  • Every factor now publishes its measured share of this card’s total adjustment beside its nominal weight. Managerial reset is 12% by design and 65% of what actually moved — fourteen of twenty clubs have a first-season manager and nobody has played a European midweek yet. The divergence is printed, not smoothed.
  • Dead-heat rule: outcomes are ranked on raw probabilities, never rounded percentages, and the margin to the runner-up is published. Inside three points the fixture is declared a dead heat. At the time of this version Fulham v Palace sat at 0.6 and Hull v Villa at 2.9; both figures move with the model, and the live ones are on the GW3 tab.
  • The unconditional mode of each joint matrix is published beside the conditional one. It is 1⁠–⁠1 on six of the ten fixtures, and calling the conditional mode “modal” was overstating what the distribution says.
  • Season projection replaced by Monte Carlo: 2,000 runs over every unplayed pairing, sampled from the same intensities the match cards use, reporting mean points, the 5th–95th band, and title, top-four and relegation probabilities.
  • The engine will read pl-datalayer.json from the project if the weekly routine has written one, falling back to the transcribed table. The build tab reports which spine is live.
v1.7 · 4 Sep 2026, 23:10 BST · prices changed

Triggered by a second audit pass, and by the defect v1.6 exposed rather than fixed: the scoreline and the win probability were produced by two separate paths, so nothing stopped a 2⁠–⁠0 sitting next to 49% confidence. The same decoupling is what forced a residual-expansion rule for scoring, and that rule was itself unsound — a draw published below 35.5% expanded into a vector whose largest element was the home win.

  • One engine replaces the two tracks: a Dixon-Coles adjusted bivariate Poisson. Attack and defence multipliers give λ for each side, the joint matrix gives the 1X2 probabilities, the clean-sheet lines and the scoreline together, and all three are published per fixture.
  • The scoreline on each card is the modal score inside the region the probabilities already favour, and the confidence is that region’s own probability. Neither is typed.
  • Opponent adjustment added at 4%: every club’s xG and xGA are divided through by the strength of the opponents actually faced, solved to a fixed point. Brighton’s 5.17 xG becomes a 1.22 attack; Newcastle’s record loses its call.
  • Draw prior retired. Draws now arrive when the distribution produces them, so the gate has nothing left to gate. Fulham v Palace splits 38 / 24 / 38 and the 1⁠–⁠1 is withdrawn.
  • Phase-specific shrinkage: τ = 5 for open-play generation and the strength estimates, 6 for overperformance regression, 14 for set-piece conversion. A single τ was treating three different stabilisation rates as one.
  • Residual expansion for retrospective scoring constrained so the stated call always keeps plurality. GW1 and GW2 Brier and RPS figures move slightly as a result.
  • Two GW3 directions changed — Bournemouth at Newcastle and Hull over Villa — taking the disagreements with Opta from one to two. Seven calls re-priced without changing direction. Ipswich v Liverpool remains frozen.
  • Runtime: the three round spines, the club rate table and the strength solve are memoised per instance, so a tab change re-runs the view mapping rather than the whole model. Tab state is bound to the URL fragment, so a view can be linked and survives a reload.
v1.6 · 4 Sep 2026, 21:40 BST · prices changed

Triggered by an external audit of the desk rather than by a round: the v1.5 factor weights summed to 108%, the published Brier was a binary score on a three-outcome market, the new regression term was firing at full strength on two matches, and the Brighton card named a goalscorer the same data layer had listed as unavailable. Three of the four are defects in the model, which is the one thing that reopens a locked price.

  • Weights normalised to 100% across thirteen factors. World Cup residue and unofficial transfer reporting retired; managerial reset, fixture load, squad change and market priors trimmed. No factor moved more than two points.
  • Overperformance regression scaled by sample, n / (n + 6), so it applies 1.8% at two matches instead of 4%. It converges on the nominal weight rather than starting there.
  • Scoring replaced: multinomial Brier and ranked probability score across home, draw and away, with the expansion rule from one call to three probabilities printed on the scoreboard. The old binary figure is kept beside the new one so the difference is visible.
  • Named goalscorers now vetoed against the availability manifest before a card ships. Hinshelwood rejected on the Brighton card and replaced; gate G10 fails the build on any unavailable selection.
  • Nine of the ten GW3 calls re-priced under the defect clause — four scorelines revised, no direction changed. Ipswich v Liverpool had kicked off and is frozen. Every original price stays printed on its card.
  • Confidence intervals now scale with the two xG models’ disagreement per fixture: ±3 as a floor, ±7 on Arsenal v Chelsea and Fulham v Palace where the spread is 0.54 xG.
  • The retrospective GW1–GW2 record and the forward-tested ledger from GW3 are reported as two records with two denominators, instead of one combined figure.
v1.5 · 4 Sep 2026, 19:03 BST · prices changed

Triggered by the GW3 data layer v2: a second independent xG model beside the first, an expected-points table that corroborates three standing reads without having seen them, and an availability layer that meets the cross-source gate for all 20 clubs for the first time.

  • New factor: overperformance regression at 4%, funded by trims to base rating, squad change and World Cup residue. It fires only where both models agree on the sign.
  • Chance quality by phase raised 6% → 8% on the GW2 grade — the two calls reversed on the splits both landed, one exactly.
  • GW2 graded: 7 of 10 on direction, 2 exact. The first round in which the desk beat the home-side baseline.
  • Availability two-sourced for all 20 clubs, injuries and suspensions separated. Where FotMob and Premier Injuries disagree on a return date, both are carried.
  • GW3 calls locked at 16:33 BST and not re-priced by the 19:03 refresh. The lock rule is now written into the update rule.
  • Opta power rankings retired after a fourth failed retrieval. FotMob expected points ingested as a labelled external model, not as a replacement ranking.
v1.4 · 29 Aug 2026 · prices changed

Triggered by the GW3 data layer: 20-club situation splits (xG/Sh and xGA/Sh by open play, corner and set piece) — the first input that separates chance quality from finishing and locates both in a phase.

  • New factor: chance quality by phase, at 6%, funded by trims to base rating and squad change.
  • Three GW2 calls revised pre-kick-off on the splits — Liverpool to 2⁠–⁠0, Bournemouth–Everton to 1⁠–⁠2, Coventry–Hull reversed to the away side. One of the three landed, the Hull reversal, and exactly; Liverpool–Forest finished 2⁠–⁠2 and Bournemouth–Everton 1⁠–⁠1.
  • Tottenham–Newcastle reversed with the reasoning corrected: the corner mismatch cancels, and the call rested on the open-play edge. It landed 0⁠–⁠2.
  • Draw gate widened: also publishable when the benchmark simulation makes the fixture its most-drawn of the round, or on exact clean-sheet parity where no lines exist. It went 1 from 2.
  • Solio Analytics adopted as the GW3 benchmark for clean sheets; Opta’s MD3 win probabilities returned for 9 of 10 fixtures.
v1.3 · 28 Aug 2026 · prices changed

Triggered by the complete GW1 grade: 4 from 5 at 55% or above, 0 from 5 below it. The problem was never the confident band.

  • Hard draw gate: publishable only when the two win probabilities are within three points.
  • Contrarian gate: a call against the external favourite needs a named mechanism in the card.
  • Coventry v Hull reversed under that gate, then reversed again in v1.4 on the splits — where it landed.
  • Set-piece and defensive-structure weight raised to 9% on the 27.4% league-wide share.
  • Every card now carries a conviction flag so the weak band is visible before the result, not after.
v1.2 · 23 Aug 2026, evening · prices changed

Triggered by the nine settled matches and the Opta match reports.

  • New factor: defensive structure and set pieces. Rates how a side concedes.
  • Injuries keyed to dated return windows instead of a flat penalty.
  • Manchester United cut ten projected points on structure, not on one result.
  • Bottom-three and best-promoted calls switched: Hull out, Coventry in.
v1.1 · 23 Aug 2026, afternoon · prices changed

Triggered by the first six settled matches: one result correct, every draw call wrong.

  • Draw prior 0.30 → 0.24. Home term 0.30 → 0.38 goals for rounds 1⁠–⁠6.
  • New-manager penalty split by venue: 0.10 home, 0.28 away.
  • Promoted-side home baseline +12%; away baseline unchanged.
v1.0 · 21 Aug 2026 · prices changed

Pre-season build. Three seasons of underlying numbers, no in-season evidence.

  • Twelve factors, symmetric manager penalty, flat promoted-side conversion.
  • Called four draws in ten and got one, in a match called 1⁠–⁠2. Retired after one round.

Sources, inputs and known gaps

Source hierarchy
Results · FPL fixtures + Football-Data
Two independent pipelines: FPL’s fixture feed, read by the Payload script’s 15-minute tick (an idle tick, while no kickoff is within 26 hours, fetches nothing; a full check still runs at least hourly), and football-data.co.uk’s results file, read by the sheet’s morning refresh. The round is the one holding the earliest unstarted scheduled kickoff, never a next-round flag.
In this payload: 50 results · GW1–5 · last morning refresh Wed 7 Oct 2026 06:10 BST.
Model A — xG · Football-Data.co.uk
Per-match xG and xGA from the same results file. The first of two independent xG models — quoted, never averaged with the second.
In this payload: 20 of 20 clubs carry both models · last morning refresh Wed 7 Oct 2026 06:10 BST.
Model B — xG, xPoints · FotMob API
Team xG, xGA and expected points from FotMob’s table and team endpoints, read directly by the sheet’s morning refresh, which replaced Coupler on 9 September.
In this payload: FotMob OK 20/20 clubs · last morning refresh Wed 7 Oct 2026 06:10 BST.
Availability · FotMob API + FPL
Injuries and suspensions with FotMob’s expected-return windows, joined to FPL’s status flag, chance of playing and news for the same player.
In this payload: 128 entries across 20 clubs, 5 suspensions.
Situation splits and game state · Understat
Shots, goals and xG by phase (open play, corners, set pieces) and by game state, for and against, read daily at 06:20; the tab is replaced only when at least 17 of 20 clubs come back.
In this payload: 20 of 20 clubs.
Prediction market · Polymarket
Match-winner prices about 24 hours before kickoff (the reference) and again at the lock, with volume and overround; a thin or one-sided book is flagged on the card.
In this payload: reference price on 20 and lock price on 17 of 30 ledger rows.
Ledger benchmark · Betfair Exchange, via Football-Data
Closing exchange prices for every played fixture, with the overround removed. The favourite’s record is the number to beat.
In this payload: 50 played fixtures; the favourite won 23 of 50.
Logged by the routine · Opta, Forebet, Solio (ledger text)
The routine writes any Opta or Forebet match line it finds into the ledger’s benchmarks column, as text beside the call; Solio clean-sheet lines were logged up to GW3. None of them prices anything.
In this payload: Opta lines GW3 9 of 10 · GW4 10 of 10 · GW5 1 of 10 · Forebet GW4 10 of 10 · GW5 4 of 10 · Solio clean sheets GW3 10 of 10.
Benchmarks and shadow inputs (Payload v5.9.0–v5.10.0) · Opta, ClubElo, Solio, Dratings, football-data, Understat, FotMob
Opta’s match probabilities (captured by the tick before each kickoff), the pre-match bookmaker odds and the Solio, Dratings and ClubElo match lines are stored as benchmarks: scored beside the desk, never fed into it. Opta’s power rankings (retired on 1 September after four failed retrievals, now read again), ClubElo’s ratings and Understat’s deep completions feed two of the three shadow models (the rating prior and deep completions), priced beside every lock from GW6 and graded on the Scoreboard (docs/SHADOW.md); none of them feeds a price. Each source’s status and last update are on the Data tab.
Retired · Premier Injuries, Coupler.io, WhoScored
Premier Injuries was dropped after GW3, once FPL’s own status data joined FotMob’s; Coupler.io was replaced on 9 September by the sheet’s own scripts; WhoScored was never captured.
Inputs and freshness
InputIn this payload
Results50 results · GW1–5
Ledger calls30 ledger rows · GW3–5
xG models2 · 20 of 20 clubs carry both
Expected points20 clubs · n = 5
Availability20 clubs · 128 entries
Suspensions5 league-wide
Situation splits20 clubs
Polymarket reference prices20 of 30 ledger rows
Betfair closing prices50 of 50 results
Opta, Forebet and Solio linesOpta GW3 9 of 10 · GW4 10 of 10 · GW5 1 of 10 · Forebet GW4 10 of 10 · GW5 4 of 10 · Solio GW3 10 of 10 (ledger text; check M2 is manual)
Sample size5 matches per club
Known gaps

From this payload: the schedule graph is 1 connected component after 5 rounds (check S8), and the current largest gaps between the two xG models are on the Underlying tab.

  • The schedule adjustment is deliberately held back to exponent 0.63 of its unregularised size, because at a small sample the unregularised version is demonstrably wrong. It grows on its own as the schedule interleaves.
  • Five matches per club. Every per-match figure is directional and a rate — the models locate mechanisms, they do not yet size them.
  • The pre-season projection still carries 50% of every club’s attack and defence numbers; it fades as matches accumulate.
  • There is no player-rating layer: WhoScored was never captured, so player quality enters only through FPL’s expected goal involvement and the availability join.
  • The prices captured before a match (Polymarket, Opta’s probabilities, the pre-match bookmaker odds and the other stored lines) are benchmarks only: none feeds the model, and Polymarket is thin on some fixtures. Betfair’s closing price arrives with the results file, after the match.
  • GW1 and GW2 were never locked in the ledger; the desk grades them from its own published cards and labels the difference on the scoreboard.

Diagnostics

Every time this page opens it runs 83 checks on the numbers in front of you and prints what each one measured. Right now 71 pass, 1 fails, 8 can’t run yet and 3 have no automatic check.

On the payload built Thu 8 Oct 2026 00:40 BST · buildPayload

Jump to what it means · what did not pass · checks by area · engine proof · the full list

This run

  • Pass 71
  • Fail 1
  • Blocked 8
  • Manual 3

83 checks · 71 pass · 1 fail · 8 blocked · 3 manual

The checker’s own summary

72 asserted · 8 blocked on data · 0 not asserted · 3 manual

Asserted
72
Pass
71
Fail
1
Blocked on data
8
Not asserted
0
Declared manual
3
Model
v3.11 · engine v2.1

Run at page load against the live objects, on the payload stamped Thu 8 Oct 2026 00:40 BST · buildPayload. A check states the figure it measured, so a pass is auditable and a fail is specific. Blocked means the assertion is built and the data it needs does not exist yet — it is not a pass and it is not a failure of the model. Manual means no assertion covers it and the page says so instead of claiming a pass.

What it means for the predictions

  • The engine in your browser is the one that will lock GW6.

    Its code hashes to the same value the producer publishes (97cc49a9abb3…); L11 checks it. Rounds already locked are kept exactly as locked. The ledger records the engine that priced each row: GW3, 10 rows with no version recorded; GW4, 10 rows at v2.1 (this engine); GW5, 7 rows at v2.1 (this engine) and 3 rows with no version recorded.

  • The record adds up.

    Every split sums to 100% (L1), the header record equals the ledger rows (H1), and GW5’s 3 uncalled fixtures are listed as missed locks and kept out of the scores (L15). No assertion covers whether graded cards were edited after their lock (M3, manual).

  • One check fails, on the model pricing GW6 now.

    E10 fails: GW6 · mean draw 26.7% (band 24–27) · mean goals 2.90 (band 2.9–3.1) · 0 draw calls of 10. Calls already locked for GW1–5 are frozen and unaffected; the GW6 calls still to lock are priced by this engine.

  • Eight checks can’t run yet; three have no automatic check.

    They are waiting for data, not failing: two when the GW6 calls lock (L7, L8), one as each fixture’s reference market capture lands (K6), one when the sample is big enough (K2) and four only if a correction, moved kickoff, fallback lock or live-prose card ever exists (L16, K8, L12, X3). No assertion covers W2, M2 and M3; the page says so instead of claiming a pass.

ACTION REGISTER

Every check that did not pass, the failures first. Each entry ends with the row exactly as the checker printed it: its id and name, its state, the figure it measured and why it exists.

Failing · 1 of 83

GW6 prices goals and draws outside their ranges

The model expects 2.90 goals a match against a range of 2.9–3.1; draw chances average 26.7%, against a range of 24–27%. For context, 32% of the season’s 50 matches so far were draws, and none of the 10 model calls for GW6 is a draw.

This is the engine pricing the GW6 cards now. Calls already locked for GW1–5 are frozen and unaffected; the GW6 calls still to lock are priced by this engine.

E10 · Target-round card: draw mass and goals per fixture in the league’s range
FAIL
GW6 · mean draw 26.7% (band 24–27) · mean goals 2.90 (band 2.9–3.1) · 0 draw calls of 10
v2.1 acceptance band for changes 01 and 02, pre-registered and not widened. The season is running at 32% draws over 50 results; the engine priced 21% in v2.0. Goals per fixture 2.90 are below their band.

Can’t run yet · 8 of 83

Blocked means the assertion is built and the data it needs does not exist yet. It is not a pass, and it is not a failure of the model.

When the GW6 calls lock · 2

They compare the ledger’s locked rows with the cards; there is no row to compare yet.

Will compare the model splits written at lock with this engine, leg by leg. GW6 cards →

L7 · Engine parity: the ledger’s splits at lock equal this engine on the loaded payload (±1 point per leg)
BLOCKED
no locked_calls row of GW6 carries model_split or mkt_split yet
Why this check exists

Follow-up 3. ±1 is the sheet’s largest-remainder rounding to 100, not a tolerance for drift. Unblocks when the first GW6 fixture is locked and its ledger row carries model_split.

Will check that the GW6 cards show exactly the ledger rows they were built from. GW6 cards →

L8 · Target-round cards and the Underlying market row render the ledger they were built from: both capture families, the desk headline, the horizon label
BLOCKED
no locked_calls row for GW6 yet
Why this check exists

Added 11 Sep after the first automated lock: the ledger was parsed and L7 passed while the round view still priced the desk and market slots from the live engine and the fixture object. Render parity is now asserted, not assumed.

As market captures land · 1

The data layer records a reference market price for each fixture ahead of kickoff.

Will check the reference capture on every fixture more than 15 minutes from kickoff. None is in the ledger yet. Underlying →

K6 · Reference family: present on every target fixture more than 15 minutes from kickoff, ref_horizon_h numeric on every row that has one, and no row claiming exactly 24.0 unless the timestamps agree
BLOCKED
no reference capture in the ledger yet · clock stamp_epoch · 1.2h old
Why this check exists

D12. The design intent is T−24; the 12 September dry run captured at 1.84h to 6.84h because the trigger was installed on match day. Those are honest captures and the card prints them as they are.

When the sample is big enough · 1

Adopting fitted values on a few matches would replace one guess with a noisier one.

Re-fitted on 50 matches: home term unchanged at 1.14; draw correction ρ −0.075 (typed −0.1); shrinkage τ 3 (typed 5). Fitted values are adopted only at n ≥ 8 matches per club, once their signs have held for two rounds; n is 5 now. Parameter fit →

K2 · Fitted parameters adopted
BLOCKED
n = 5 · adopt at n ≥ 8 once the fitted values have held sign for two rounds
Why this check exists

Sample-gated by design. At thirty matches (v2.1, 9 Sep) the standard error on the home term was about ±0.15, wider than the choice it would replace; the fit above now runs on 50 matches.

Only if the event ever happens · 4

Each tests something that has not occurred: a correction, a moved kickoff, a fallback lock or a live-prose card.

Will check that a model fallback lock stays out of the desk’s record. No fallback lock has fired. Lock gates →

L16 · Every fallback lock is excluded from the desk’s record and labelled as a fallback
BLOCKED
no locked_calls row carries call_source model_fallback · 30 row(s): 0 desk, 30 blank (before v5.6.0, read as desk)
Why this check exists

Step 3. call_source on locked_calls[]: desk, model_fallback, or blank on every row before Payload v5.6.0, which is a desk row. A fallback row locks the engine’s call at kickoff − 45 min when the desk has made none. It is scored on the model, market prior, Betfair and Polymarket rows like any commit, and on no desk row; the card shows ‘Fallback lock (model)’ and the record shows it on a line of its own.

Will check that a moved kickoff or a correction shows on its card. No ledger row is flagged.

K8 · Every moved-kickoff row renders its chip, and every corrected row renders the corrected chip, in every round the ledger covers
BLOCKED
no ledger row in any round carries moved_after_capture or a correction: 30 card(s) scanned across 3 round(s)
Why this check exists

D12. A correction or a moved kickoff the card does not mention is indistinguishable from a clean capture, which is the failure the ledger exists to prevent. D3: Blocked on an empty subject set — with nothing flagged anywhere there is nothing to assert, and saying Pass claimed a check that never ran.

Will check that no correction changed a graded field after kickoff. No fixture has a second ledger row.

L12 · No correction changed a graded field of a fixture that had already kicked off
BLOCKED
no fixture in the ledger carries more than one row
Why this check exists

D3/D8. Readable only because the fold keeps _rows as the rows themselves: a count cannot be audited. The commit family is what the desk is on record as having called; a capture cell may still be corrected afterwards.

Traces every percentage on a card whose text is filled from the live engine. Only the in-file GW3 card carries such text, and it is frozen at its lock. GW3 cards →

X3 · Every percentage a card prints traces back to that card, and carries one unit
BLOCKED
0 of 10 GW3 cards carry prose against the live engine · 10 frozen at lock, exempt as historical records · no live card carries prose, so there is nothing to trace
Why this check exists

The gap X1 could not see: an unresolved token is visible, a stale hard-coded number is not. It caught three figures the corrected solve had moved, and a doubled unit left behind when one of them was tokenised. Scoped to cards whose prose is filled from the live engine: a frozen card’s prose was written at its lock against the engine of that date and is not re-traced against today’s.

No automatic check · 3 of 83

Manual means no assertion covers it, and the page says so instead of claiming a pass.

The cap on how far a weight may move per round is read from the version log; the factor table keeps no per-version weights to assert against. Model →

W2 · Per-round weight moves inside the update rule’s 4-point cap
MANUAL
largest cumulative move 10 points since v1.0
Why this check exists

Not machine-checkable as built: the factor table stores one delta against the pre-season build, not a vector per version, so the cap can only be verified by reading the version log. Storing the weight vector with each version would make this executable.

The external benchmarks are logged by the desk routine into the ledger as text; the page counts them but cannot verify them.

M2 · External benchmarks captured and verified by fixture name
MANUAL
ledger benchmarks text · Opta GW3 9 of 10 · GW4 10 of 10 · GW5 1 of 10 · Forebet GW4 10 of 10 · GW5 4 of 10 · Solio GW3 10 of 10
Why this check exists

Captured upstream by the desk routine, which writes each line it finds into the ledger’s benchmarks column as text. The page counts the lines; it cannot verify them.

No assertion covers whether graded cards were edited after their lock; ledger rounds render from write-once rows, and GW1 and GW2 rest on the version log. Scoreboard →

M3 · Graded cards unedited since their lock timestamp
MANUAL
GW1–GW2: version log only · GW3, GW4, GW5: rendered from write-once ledger rows
Why this check exists

No assertion covers the card text itself. Ledger rounds render their calls from rows that are never edited (a correction is a new row; L12, L14), and the in-file GW3 card also carries its price snapshot (X2); GW1 and GW2 were never locked and rest on the version log.

BY WHAT THEY PROTECT

The 83 checks, grouped by the part of the desk they guard. Each name opens its group in the full list.

  • Engine maths

    Guards the probability maths that turns two clubs’ ratings into a price for the match.

    10 / 11 pass

    1 fail · E10

  • Fixtures and calendar

    Guards the round, its fixtures and their kickoffs: all from the fixture list, never a typed date.

    5 / 5 pass

    all pass

  • Team strength ratings

    Guards how each club’s attack and defence are solved, shrunk towards the priors and weighted.

    17 / 19 pass

    1 waiting · K2

    1 no automatic check · W2

  • Locked calls and scoring

    Guards the write-once ledger of calls and how each round is scored against the market.

    14 / 20 pass

    6 waiting · L7 L8 K6 L16 K8 L12

  • Season projection and tables

    Guards the season simulation and the league tables it starts from.

    9 / 9 pass

    all pass

  • Card text

    Guards the card text: every sentence quotes the numbers beside it, with nothing left unfilled.

    2 / 3 pass

    1 waiting · X3

  • Data freshness

    Guards the payload: is it complete, recent, and built by a healthy producer?

    11 / 11 pass

    all pass

  • The checker itself

    Guards the checker: every check can fail, none has gone missing, and what no assertion covers is declared.

    3 / 5 pass

    2 no automatic check · M2 M3

Proof the engine matches

The pricing code this page runs, the part the Payload Apps Script copies byte for byte, hashes (SHA-256) to:

97cc49a9abb300438863b7cba83e2ff69fdbe89740303a06ebfe7f5882ec86e8

The producer’s engine_hash in this payload is the same value, so the Payload script prices and locks with this code. L11 →

Port region · 17 methods

PLIngest.validate, PLIngest.fromPayload, ratesData, resultsIndex, playedPairs, playedCount, playedBy, contextData, gsnFactors, factorTerms, impliedGoals, phaseStrengths, solveMultipliers, solve, strengths, marketPrior, engineFor

ALL 83 CHECKS

Engine maths

11 checks · 10 pass · 1 fail · Sections: Engine

E1 PASS

Joint matrix normalises to 1.000 per fixture

max deviation 8.9e-16

E2 PASS

Scoreline grid captures the fixture’s probability mass, and the Dixon-Coles correction is renormalised

worst grid 1.00000 of 1.000 · largest Dixon-Coles mass shift 0.00%

Why this check exists

D3. E2b folded in: the grid truncation and the low-score correction are two ways the same mass goes missing, and they were two rows measuring one invariant. A grid holding less than 0.999 (v1.9 used a fixed 9×9) would be cutting off scorelines a fixture can actually produce; the correction moves total mass and the engine divides it back out.

E3 PASS

Published scoreline lies inside its own favoured outcome region

10 of 10

E4 PASS

Home term appears once: with the factor terms zeroed, a fixture and its reverse differ by ENGINE.HOME, not its square

max deviation 0 against ENGINE.HOME = 1.14 (squared would be 1.2996)

E5 PASS

Relative factors sum to zero across the two sides of every fixture

max residual 3.5e-18

Why this check exists

The v1.9 fix. An uncentred relative term deflates the goal total instead of moving the margin.

E6 PASS

Every published factor term is in the price: the adjustment a priced card carries equals the factor sum the table publishes

league goals scale ×0.771 divided out · largest gap between the applied adjustment and the full factor sum 1.3e-16 · against the centred terms alone 0.015000 · largest non-centred term 0.030 log-goals

Why this check exists

D3. On this payload the adjustment every priced card carries, once the league-wide goals scale is divided out, equals the published factor sum: every term is multiplied into λ. The non-centred term, rule changes (+0.015 log-goals a side), is the same on every fixture, so the goals calibration (E9) then cancels it exactly and it moves no price; this row cannot see that, by construction. Until 5 Oct 2026 this row left the goals scale in and reported it as a missing factor; the failure was the check, not the engine. The old row asserted only that the term was non-zero somewhere in a table, which is true of an inert term.

E9 PASS

Post-solve tempo: mean λ + μ over all pairings equals the goals/xG blend recomputed from results and clubs

pre 3.79 → post 2.92 · target recomputed 2.92 = (5 × 2.82 goals from 50 results + 5 × 3.03 xG) / 10 · solve states 2.92 · scale 0.771 · league draw mass 25.9%

Why this check exists

v2.1 change 01. Removes the Jensen inflation, the uncompensated home term and the rule-change tempo in one normalisation.

E11 PASS

λ built from open-play and set-piece components (τ 5 / 14)

active on 20 clubs · set-piece share of λ 21% · set-piece evidence weight 26%

Why this check exists

Change 09. Built in v2.1, when at n = 3 the set-piece component was 82% prior and barely moved a call; at n = 5 it is 74% prior. It has to exist by GW8 to have a track record.

E10 FAIL

Target-round card: draw mass and goals per fixture in the league’s range

GW6 · mean draw 26.7% (band 24–27) · mean goals 2.90 (band 2.9–3.1) · 0 draw calls of 10

Why this check exists

v2.1 acceptance band for changes 01 and 02, pre-registered and not widened. The season is running at 32% draws over 50 results; the engine priced 21% in v2.0. Goals per fixture 2.90 are below their band.

In the register →
E7 PASS

Dead-heat badge agrees with the printed margin at 3.0 inclusive

0 inside the band

E8 PASS

No scorers[] entry marked available appears in availability[] as out or suspended (gate G10)

60 scorer entr(ies) across 20 club(s) · 43 marked available · none contradicted by availability[] · manifest keyed on 20 of 20 clubs

Why this check exists

D3. The check must find the conflict in the data rather than be told the answer, and it must be able to find it before a card ships — which means reading the producer’s own scorer block, not a sentence the desk typed. A player carrying a percentage chance is a doubt the desk priced knowingly and is not a conflict.

Fixtures and calendar

5 checks · 5 pass · Sections: Calendar · Rounds

K3 PASS

rounds[] is present, strictly increasing by gw, and scheduled + unscheduled equals fixtures on every round

38 rounds · ids increase · shapes agree · 380 fixtures in total

Why this check exists

D12. The total is printed, never asserted equal to 380: a season is 380 only once every fixture is scheduled, and asserting it would fail honestly-incomplete calendars.

K4 PASS

target_round.fixtures equals the fixture card, and every card fixture appears in forward[] under the same fixture_id

10 on the card v 10 in the round · all 10 found in forward[]

Why this check exists

D12. The card and the forward list are two views of one spine; if they disagree the page is pricing a round the routine is not.

K5 PASS

Every round the selector offers exists in rounds[], and every played round is either renderable or reported as not

6 offered · all in rounds[] · 5 played · every played round renders

Why this check exists

D12. A round that cannot be drawn is a fact to report, not a chip to hide; an offered round that is not in the spine is a defect.

C8 PASS

Every card’s kickoff came from the payload — results[], fixtures[] or forward[]

10 × fixtures[] · 10 × results[]

Why this check exists

D10. The renderer owns no clock: no month, no year, no date arithmetic. A fixture the payload has not dated renders “date to be confirmed” rather than an inferred kickoff, and a card with no kickoff can never be frozen.

R3 PASS

No denominator printed in the round view differs from rounds[gw].fixtures

GW6 · denominator 10 · every printed count agrees

Why this check exists

D12. Asserted on the rendered strings, because the defect this catches is a literal surviving in a sentence, not in a variable.

Team strength ratings

19 checks · 17 pass · 1 waiting · 1 no automatic check · Sections: Strengths · Prior · Fit · Weights

S9 PASS

Join key is the FPL team id; names are derived, never read from the payload

20 clubs keyed 1–20 via PLIngest.NAMES · per-club played 5–5 · results orphans 0

Why this check exists

Change 11. The adapter maps every id to one spelling; a rate is divided by that club’s own match count.

S1 PASS

Club keys reconcile across rates, fixtures and all five context maps

no orphans across 7 tables

S2 PASS

Rates table carries exactly 20 unique clubs

20 rows, 20 unique

S3 PASS

Fixture list matches the declared match count for every club

50 pairs for played = 5 · all 20 clubs on 5

Why this check exists

The check that would have caught a payload appending a round without updating played, which halves every rate.

S4 PASS

Opponent adjustment reaches a fixed point

residual 7.9e-7 after 25 passes (cap 3000)

Why this check exists

v1.7 ran three undamped passes and called the output a fixed point. The harness measured the last pass still moving multipliers by 0.40. v2.0 damps the step in log space, pins the scale of each schedule component and solves to 1e-6.

S8 PASS

Schedule graph connectivity

schedule graph in 1 component(s) after 5 rounds

Why this check exists

After two rounds (GW2) the fixture graph was a union of cycles. Across two components the data does not identify relative strength at all — the shrinkage prior carries that comparison, and it is one more reason every figure here is directional. Resolves itself as the schedule interleaves.

S5 PASS

Adjusted strengths centre on the league mean

geometric 0.992 / 1.017 · arithmetic 1.005 / 1.027

Why this check exists

Stated on the geometric mean: these are ratios, and the arithmetic mean of a defence multiplier sits above 1.00 by Jensen’s inequality because the prior enters as s^−0.75. The first version of this check tested the arithmetic mean and failed on a property of the maths rather than a defect.

S6 PASS

Shrinkage weight is n / (n + τ) at the sample the payload declares

solve n = 5 · payload min(clubs[].played) = 5 · τ = 5 → 50% evidence

Why this check exists

D3/D4. Fail, not Blocked, when played is absent: an absent sample is not a missing figure, it is a weight computed from something the payload never said.

S7 PASS

Two xG models compared per club, never averaged in a cell: every solve input equals that club’s FotMob column

solve fits on FotMob · per-club solve input not published by the solve, label checked only · 10 of 20 clubs diverge ≥ 0.10 xG · mean spread 0.03 xG per match

Why this check exists

v2.1 change 06. Until v2.0 the solve averaged the two models in a cell while this row claimed a typed Pass.

S10 PASS

∂logλ / ∂log(xG) equals the published shrinkage weight on both the whole-xG and set-piece paths

whole-xG path slope error 1.4e-14 against w = 0.500 · set-piece path 2.7e-14 against w = 0.263 · tolerance 0.02

Why this check exists

D3. The elasticity of a power-law blend is its exponent, so this fails if the solve ever applies a weight other than the one it prints — which is the failure mode the published w exists to rule out.

R1 PASS

Market-implied prior solved from closing prices and labelled

market-implied (Betfair closing, GW1–5, 50 fixtures) · 25 passes · residual 7.8e-7

Why this check exists

Change 08. Runs beside the pre-season prior with the same evidence and the same w; both are scored. Never averaged into an unlabelled figure.

R2 PASS

Market-prior engine is centred like the pre-season one

geometric mean attack 1.003 · defence 1.002 · tempo 2.92

K1 PASS

Home term, ρ and τ fitted by maximum likelihood and published beside the typed values

fitted home 1.14 · ρ -0.075 · τ 3 · LL -146.77 against typed -146.83 on 50 fixtures

Why this check exists

Change 13. A diagnostic, not a parameter change.

K2 BLOCKED

Fitted parameters adopted

n = 5 · adopt at n ≥ 8 once the fitted values have held sign for two rounds

Why this check exists

Sample-gated by design. At thirty matches (v2.1, 9 Sep) the standard error on the home term was about ±0.15, wider than the choice it would replace; the fit above now runs on 50 matches.

In the register →
W1 PASS

Factor weights sum to exactly 100%

100% nominal across 13 factors · 88% effective + 12% suspended (Managerial reset)

W2 MANUAL

Per-round weight moves inside the update rule’s 4-point cap

largest cumulative move 10 points since v1.0

Why this check exists

Not machine-checkable as built: the factor table stores one delta against the pre-season build, not a vector per version, so the cap can only be verified by reading the version log. Storing the weight vector with each version would make this executable.

In the register →
W3 PASS

Realised factor shares account for 100% of the card’s adjustment, against an independently recomputed total

100.0% attributed · Σ|dh|+|da| 0.8328 against 0.8328 recomputed

W4 PASS

Every bound factor is a published weight, and vice versa

6 bound, 13 published · 1 of them suspended at 0% effective, still bound and published

W5 PASS

Realised share matches the effective weight the page publishes: zero while the factor is suspended, else within a factor of two of it in both directions

managerial reset 0.0% realised against 0% effective (band 0–0.5%) · suspended: penalties 0 / 0, 12% nominal returns when fitted

Why this check exists

14 of 20 clubs carry a first-season manager, so this term touches most fixtures once it binds. Both penalties are zero until they are fitted on results, so the page publishes the factor at 0% effective and this row asserts it moves nothing; it holds the realised share to the 12% nominal, within a factor of two, the moment the penalties are non-zero.

Locked calls and scoring

20 checks · 14 pass · 6 waiting · Sections: Ledger

L1 PASS

Expansion rule residuals sum to 1.000 on every graded call, and so does every desk split in the ledger

max deviation 1.1e-16 over 20 expanded + 17 ledger split(s)

L2 PASS

Expanded vector keeps the stated call at plurality, and so does every desk split in the ledger

20 of 20 expanded · 17 of 17 ledger split(s)

L3 PASS

Round Briers reconcile with the combined figure: retrospective, and pooled forward per family (Σ n·B / Σ n on the same Σ n)

GW1 0.621 + GW2 0.511 → 0.566 · forward pooled from locked_calls[] v rounds: Desk call (sheet ledger) 0.726 (17) · Model at lock · pre-season prior 0.700 (17) · Market prior at lock 0.691 (17) · Betfair closing · called 0.698 (27) · Polymarket at lock 0.704 (11) · Polymarket · reference · called 0.717 (16) · Model · reference 0.689 (20) · Market prior · reference 0.681 (20) · Polymarket · reference · every capture 0.702 (20) · Polymarket · reference · every capture, thin draw excluded 0.679 (19) · Betfair closing · every capture 0.693 (20) · every family reconciles

L4 PASS

Baseline outcomes partition the graded set

10 home / 4 draw / 6 away = 20

L6 PASS

Forward ledger scores every row it can furnish over one fixture set per round: desk, model, market prior, Betfair close, Polymarket at lock and reference

GW5 6 of 6 rows over 7 of 10 fixtures · GW4 6 of 6 rows over 10 of 10 fixtures · GW3 1 of 6 rows over 10 of 10 fixtures · no vector in the ledger: GW3 Desk call (sheet ledger), GW3 Model at lock · pre-season prior, GW3 Market prior at lock, GW3 Polymarket at lock, GW3 Polymarket · reference · excluded for an unanchored draw leg: GW5 Polymarket at lock (3), GW5 Polymarket · reference (1), GW4 Polymarket at lock (3)

Why this check exists

D6. Every row is scored over the fixtures every priceable row can furnish, and each round prints its intersection size once. D5 retires the 55/45 expansion for GW3 onward: an absent desk split is a dash, not a point mass.

L7 BLOCKED

Engine parity: the ledger’s splits at lock equal this engine on the loaded payload (±1 point per leg)

no locked_calls row of GW6 carries model_split or mkt_split yet

Why this check exists

Follow-up 3. ±1 is the sheet’s largest-remainder rounding to 100, not a tolerance for drift. Unblocks when the first GW6 fixture is locked and its ledger row carries model_split.

In the register →
L5 PASS

Retrospective and forward ledgers carry separate denominators: forward calls equal Σ called over the ledger rounds, and no fixture is in both

retrospective 20 of 20 (GW1–GW2) · forward 27 called v Σ GW3 10 + GW4 10 + GW5 7 = 27 · no fixture id in both (20 retrospective, 30 forward)

L11 PASS

Every target-round ledger row was priced by this page’s engine: model_ver equals ENGINE_VER, and the port hash equals the producer’s engine_hash once it publishes one

no locked_calls row of GW6 carries model_ver yet · port hash 97cc49a9abb3… v gates.engine_hash 97cc49a9abb3…

Why this check exists

D16/D9. A mismatch means the Apps Script port and this engine have diverged and the port must be regenerated from the export before the next lock. D9 replaces the name list with a hash: sha256 over the normalised source of PLIngest.validate through engineFor, printed on this tab. Until the producer publishes engine_hash (v5.4) the desk compares it by eye once per port, and this row falls back to model_ver.

L10 PASS

Where the ledger carries a why or a risk, the card’s Notes region renders that exact sentence

14 note(s) across 7 why and 7 risk · every note found in its card

Why this check exists

D14. Both are optional and a round before GW5 has neither, so absence reads Blocked rather than Pass: nothing was asserted.

L9 PASS

The archives supply prose only: nothing outside the whitelist reaches a card with a ledger row, a written why or risk is never displaced by an archive sentence, and no round the ledger covers shows a call the ledger does not hold

every merged card checked against its built card across 3 overlaid round(s)

Why this check exists

A10. The whitelist is only worth what the rendered cards actually obey, so this reads the merged card rather than the merge function.

H1 PASS

Header strip equals the tabs it summarises: record and ledger tile against the raw ledger rows, Brier trio on equal n, deadline

strip “GW1 4/10 · GW2 7/10 · GW3 4/10 · GW4 5/10 · GW5 2/7 (of 10)” v ledger rows “GW1 4/10 · GW2 7/10 · GW3 4/10 · GW4 5/10 · GW5 2/7 (of 10)” · ledger tile 11 / 27 v 11 / 27 · Brier “desk 0.800 · model 0.731 v 0.729 · 7 of 10 called” v ledger rows “desk 0.800 · model 0.731 v 0.729 · 7 of 10 called” · deadline 2d 10h · Sat, 10 Oct, 12:30 BST · FPL deadline Sat 10, 11:00 BST

Why this check exists

Added in the v2.1 aesthetics follow-up after the scoreboard tile read 0 / 10 while the strip read 4 / 10. SC2: the expected record is computed from locked_calls[] and results[] directly, so it no longer shares the in-file GW3 path with the strip.

L8 BLOCKED

Target-round cards and the Underlying market row render the ledger they were built from: both capture families, the desk headline, the horizon label

no locked_calls row for GW6 yet

Why this check exists

Added 11 Sep after the first automated lock: the ledger was parsed and L7 passed while the round view still priced the desk and market slots from the live engine and the fixture object. Render parity is now asserted, not assumed.

In the register →
K6 BLOCKED

Reference family: present on every target fixture more than 15 minutes from kickoff, ref_horizon_h numeric on every row that has one, and no row claiming exactly 24.0 unless the timestamps agree

no reference capture in the ledger yet · clock stamp_epoch · 1.2h old

Why this check exists

D12. The design intent is T−24; the 12 September dry run captured at 1.84h to 6.84h because the trigger was installed on match day. Those are honest captures and the card prints them as they are.

In the register →
K7 PASS

Where both families exist, locked_at is at or after pm_ref_at — the commit never precedes the reference

17 row(s) carry both families across GW5, GW4 · 7 bound (window open at lock) · 10 captured legitimately after the commit, window opened later · first reference capture in the ledger Sat, 12 Sept, 13:09 BST · 6 locked before the first reference capture existed, exempt: GW4 6: Aston Villa v Nott’m Forest, Bournemouth v Brentford, Chelsea v Hull, Crystal Palace v Ipswich, Liverpool v Fulham, Tottenham v Everton — locked Fri, 11 Sept, 17:39 BST, pm_ref_at Sat, 12 Sept, 13:09 BST · 1 held to the rule: every capture precedes its commit

Why this check exists

D12, scoped; SC5, widened to every ledger round. The brief asserts locked_at >= pm_ref_at unconditionally; from routine v5.0 (the routine is now v6.1) the lock phase has no lower bound, so a commit days before the reference window opens is by design. Only rows whose window was already open at the lock are asserted. A row committed before the earliest reference capture anywhere in the ledger (the minimum pm_ref_at or model_ref_at over every row, read from the data) is exempt and counted: no capture mechanism existed, so the commit could not have been bound by it. Every row committed after that instant is held to the rule. The other waiver is data: capture_status late_install or legacy on the row, stamped by the producer.

L15 PASS

Every kicked-off fixture in a ledger round is either called or shown as a missed lock

GW5 called 7 of 10 · 3 missed: Tottenham v Aston Villa, Bournemouth v Liverpool, Man City v Sunderland · GW4 called 10 of 10 · 0 missed · GW3 called 10 of 10 · 0 missed · every missed lock renders as Missed lock

Why this check exists

SC3/LED-3. A fixture whose lock closed with no desk call is a missed lock: kickoff (ko_final, then ko_at_ref) less 15 minutes against the payload’s stamp_epoch, or the producer’s desk_state / gates.missed_locks when it carries them. Missed fixtures are listed and stay out of Brier and RPS. n per round is rounds[].fixtures.

L16 BLOCKED

Every fallback lock is excluded from the desk’s record and labelled as a fallback

no locked_calls row carries call_source model_fallback · 30 row(s): 0 desk, 30 blank (before v5.6.0, read as desk)

Why this check exists

Step 3. call_source on locked_calls[]: desk, model_fallback, or blank on every row before Payload v5.6.0, which is a desk row. A fallback row locks the engine’s call at kickoff − 45 min when the desk has made none. It is scored on the model, market prior, Betfair and Polymarket rows like any commit, and on no desk row; the card shows ‘Fallback lock (model)’ and the record shows it on a line of its own.

In the register →
K8 BLOCKED

Every moved-kickoff row renders its chip, and every corrected row renders the corrected chip, in every round the ledger covers

no ledger row in any round carries moved_after_capture or a correction: 30 card(s) scanned across 3 round(s)

Why this check exists

D12. A correction or a moved kickoff the card does not mention is indistinguishable from a clean capture, which is the failure the ledger exists to prevent. D3: Blocked on an empty subject set — with nothing flagged anywhere there is nothing to assert, and saying Pass claimed a check that never ran.

In the register →
L12 BLOCKED

No correction changed a graded field of a fixture that had already kicked off

no fixture in the ledger carries more than one row

Why this check exists

D3/D8. Readable only because the fold keeps _rows as the rows themselves: a count cannot be audited. The commit family is what the desk is on record as having called; a capture cell may still be corrected afterwards.

In the register →
L13 PASS

Every scored row in a round covers the identical fixture set

GW5 6 row(s) on 7 fixtures · GW4 6 row(s) on 10 fixtures · GW3 1 row(s) on 10 fixtures · every row on its round’s full set

Why this check exists

D3/D6. Two Briers over different fixture sets are not comparable, and the only legitimate difference is an exclusion the row states — which is why the excluded count is added back before the comparison.

L14 PASS

The fold is order-independent: reversing locked_calls yields the identical snapshot

identical snapshot over 30 rows, forward and reversed

Why this check exists

D8. Each fixture’s rows are ordered by their own stamps — locked_at, then pm_ref_at, then model_ref_at — before the last-non-blank-wins pass, and ties break on row content, so no arrival order reaches the snapshot.

Season projection and tables

9 checks · 9 pass · Sections: Projection · Tables

P6 PASS

Season sampler draws outcomes from the Dixon-Coles matrix, so its draw rate matches the engine’s own mean draw mass

sampled draws 25.9% · engineFor mean pD 25.9% over 380 pairings · sampler’s own expectation 26.0% over 330 fixtures

Why this check exists

Change 12. Two independent Poissons ignored ρ and put the projection’s draw rate under the cards’.

P1 PASS

Every simulated run relegates exactly three clubs

relegation mass 300.0%

P2 PASS

Every simulated run crowns exactly one champion

title mass 100.0%

P3 PASS

Simulation covers every unplayed pairing and no played one

330 simulated of 380 − 50

P4 PASS

Banked points carried from results rather than a literal table

derived from 50 results

Why this check exists

Wired in v2.0. Banked points come from results[] alone; the in-file two-round table it once fell back on was deleted in v2.1.

P5 PASS

Simulation reports its seed and run count

5,000 runs, seed 20262027

B1 PASS

Live table: 20 clubs and a goal difference summing to zero

20 clubs · GD 0 · 134 points

B2 PASS

Every club has an availability row or is declared clean, with suspensions separated

20 of 20 clubs in the manifest · 128 entries · 5 suspensions · every club carries at least one entry

Why this check exists

D3. The old row counted table rows, which are built from clubs[] and are therefore twenty whatever the availability block contains.

T1 PASS

Live table and the expected-points table equal the payload’s clubs[], and clubs[] equals the table recomputed from results[]

20 rows checked on four fields · 20 xPts rows · no render differences · clubs[] agrees with results[] on 20 club(s)

Why this check exists

Added in the v2.1 follow-up after the table and expected-points panels were found rendering the in-file GW2 copy beside a GW4 payload. D3 adds the independent recomputation from results[].

Card text

3 checks · 2 pass · 1 waiting · Sections: Prose

X1 PASS

No unresolved engine token remains anywhere on the page

111 strings scanned, 0 open tokens

X2 PASS

Frozen cards quote the price they were locked at — call and split — not the current engine

in-file GW3 card · 10 of 10 frozen · 0 checked against a ledger model_split · every frozen card matches its ledger row · 3 now differ from the live engine

Why this check exists

The v2.0 fix. Before it, every card was re-derived on every render, so a model change re-priced matches that had already been played. Scoped to the in-file GW3 card, the only one that carries its own price; every later round renders its calls from the ledger rows (L9, L13).

X3 BLOCKED

Every percentage a card prints traces back to that card, and carries one unit

0 of 10 GW3 cards carry prose against the live engine · 10 frozen at lock, exempt as historical records · no live card carries prose, so there is nothing to trace

Why this check exists

The gap X1 could not see: an unresolved token is visible, a stale hard-coded number is not. It caught three figures the corrected solve had moved, and a doubled unit left behind when one of them was tokenised. Scoped to cards whose prose is filled from the live engine: a frozen card’s prose was written at its lock against the engine of that date and is not re-traced against today’s.

In the register →
Data freshness

11 checks · 11 pass · Sections: Ingest

C1 PASS

Live numeric spine: the payload carries a build stamp and it is under six hours old

pl-datalayer/2 via site proxy /api/payload · refreshed, 178 s old (nothing stored in this browser) · Thu 8 Oct 2026 00:40 BST · buildPayload · built 1.2h ago (bound 6h)

Why this check exists

D3. Freshness is the one property of a spine the page can check for itself, and the row that claimed to be checking it was a string.

C2 PASS

Every block the contract requires is present: rates, fixtures, played, gameweek, results, rounds, target_round, forward

all eight required blocks present · also carried: context, game-state splits, locked calls (30), closing prices (50), situations, heartbeat, refresh, spine

Why this check exists

D3. The old row printed what it found and stayed green on what it did not. Anything absent still falls back in-file, independently — which is exactly why the absence has to be a failure and not a footnote.

C3b PASS

The payload was built after the last result it carries, and less than six hours ago

last result kickoff 20 Sept, 16:30 · built 8 Oct, 00:40 · 1.2h ago

Why this check exists

D3. A payload stamped before a kickoff it reports the result of has taken a result from somewhere other than the round it claims to describe.

C3 PASS

Round clock: last kickoff has passed and results are not ingested

GW5 · closed Sun, 20 Sept · 10 of 10 results ingested

Why this check exists

Reports a round whose kickoffs have passed but whose results are not yet in results[]: such a round cannot be graded yet, and this row says so instead of letting it pass as complete.

C4 PASS

Game-state splits present, so the score-state filter can run

active on 20 clubs · multipliers published on the strengths table

Why this check exists

Gate G17, change 10. The filter switches on the moment the payload carries per-club score-state xG; it is never run on assumed splits.

C5 PASS

Sample is large enough for a rate rather than a direction

5 matches per club

Why this check exists

Everything on the Underlying tab is directional until the sample reaches five. Not a defect — a stated limit that expires on its own.

C6 PASS

Producer health at build: the tick had succeeded within two hours of the build stamp, the refresh within twenty-six, with no failures outstanding

tick succeeded 0.25h before the build (bound 2h) · refresh finished 18.5h before the build (bound 26h) · 0 consecutive tick failure(s) · payload itself is 1.2h old, which is C1’s row

Why this check exists

D3, re-scoped in the run-4 second pass. Both bounds follow the producer’s cadence: the tick fires every 15 minutes (an idle tick, with no kickoff within 26 hours, still stamps the heartbeat, and a full check runs at least hourly), and the sheet refresh runs every morning. Measuring either from the page’s wall clock made this row a duplicate of C1 that could only ever go red as the payload aged, so it is measured from the build stamp and reports what the producer was doing when it wrote. Fail when a block is absent — a payload that does not carry its own freshness cannot be assumed to have been built by a healthy producer.

C7 PASS

The clock is the fixture spine and the producer says the clock source is live

clock_authority “fixture spine (FPL live, 0s old): earliest unstarted scheduled kickoff” · clock_source live

Why this check exists

D3. The producer’s fallback text is printed verbatim when it is not on the live spine, because that sentence is the only record of which clock the round was actually chosen by.

C6b PASS

No load-bearing read is standing on an in-file fallback

all 10 load-bearing blocks came from the payload

Why this check exists

D3/D4. The page keeps its in-file tables so it renders without a payload; every one of them silently replaces a producer block, and this row is where that shows.

P7 PASS

The producer reports no FotMob played-count skew

empty

Why this check exists

D3. A club whose FotMob match count disagrees with the fixture list has every per-match rate divided by the wrong denominator. The producer reports no skewed club on this payload.

P8 PASS

The odds join produced no collisions

empty

Why this check exists

D3. Two fixtures joining to one odds row is how a closing price ends up on the wrong match, and every market row on the page reads that join.

The checker itself

5 checks · 3 pass · 2 no automatic check · Sections: Harness · Manual

H3 PASS

Every assertion flips when its measured input is negated, and every row that only prints an invariant is declared as one

72 asserted (50 self-tested) · 8 blocked on data · 0 not asserted against 0 declared · 3 manual · every probed row flipped

Why this check exists

D3. A row that cannot fail is not a check. An assertion that quietly stops flipping, or a new row that only prints, fails this row instead of inflating the pass count; DECLARED_INVARIANTS names the rows that print one by design, and a declared row that starts asserting is reported here, not failed. Blocked is counted apart: it is an assertion waiting on data, not an invariant.

H4 PASS

The version log leads with this build and descends strictly

25 entries · leads with v3.11 against VER v3.11 · strictly descending

Why this check exists

D9. The export is what the Apps Script port is regenerated from, so the name at the top of the log is the name of the thing being ported.

H2 PASS

The harness is the size it declares, and every check has its own id

83 rows against 83 declared · all ids unique

Why this check exists

D13. EXPECTED_CHECKS is updated by hand with each brief, which is the point: a check that disappears into a branch, or an id typed twice, changes the count and fails this row instead of shrinking the tally unnoticed.

M2 MANUAL

External benchmarks captured and verified by fixture name

ledger benchmarks text · Opta GW3 9 of 10 · GW4 10 of 10 · GW5 1 of 10 · Forebet GW4 10 of 10 · GW5 4 of 10 · Solio GW3 10 of 10

Why this check exists

Captured upstream by the desk routine, which writes each line it finds into the ledger’s benchmarks column as text. The page counts the lines; it cannot verify them.

In the register →
M3 MANUAL

Graded cards unedited since their lock timestamp

GW1–GW2: version log only · GW3, GW4, GW5: rendered from write-once ledger rows

Why this check exists

No assertion covers the card text itself. Ledger rounds render their calls from rows that are never edited (a correction is a new row; L12, L14), and the in-file GW3 card also carries its price snapshot (X2); GW1 and GW2 were never locked and rest on the version log.

In the register →
Parameter fit · diagnostic
ParameterTypedFitted
Home term1.141.14
ρ-0.1-0.075
τ (shrinkage)53
Log-likelihood-146.83-146.77

Maximum likelihood on 50 played fixtures, grid-searched (home 1.00–1.40, ρ −0.20–0.05, τ 3/5/8/14), tempo re-normalised for each home term. Diagnostic only: adopted at n ≥ 8 once the fitted values have held sign for two rounds. Currently n = 5.

Solved strengths · 20 clubs

Attack and defence multipliers on the league mean, after the opponent solve and the shrinkage. λ for a fixture is (open-play mean × home open-play attack × away open-play defence + set-piece mean × home set-piece attack × away set-piece defence) × 1.14 × e^(factor nudges) × tempo scale 0.771; without a situations block it reduces to 1.52 × attack × defence. Solved shows the pre-shrinkage pair from the opponent adjustment alone, which after 5 rounds is carrying 50% of the estimate against the pre-season prior’s 50%. The same ratings drive the Model tab. Bold marks the strongest attacks and the meanest defences.

ClubAttackDefenceSolvedGame stateMarket prior
Man City1.320.881.33 / 1.001.101.30 / 0.84
Brighton1.271.031.42 / 1.161.061.01 / 0.99
Arsenal1.240.691.04 / 0.681.001.24 / 0.73
Brentford1.160.931.28 / 0.911.081.06 / 0.96
Chelsea1.160.931.06 / 1.061.071.16 / 0.93
Man United1.140.921.16 / 0.940.991.20 / 0.93
Liverpool1.110.871.02 / 0.900.931.22 / 0.89
Sunderland1.041.051.16 / 1.020.900.86 / 1.04
Bournemouth1.000.950.99 / 0.911.060.99 / 1.03
Everton0.981.031.01 / 1.001.040.95 / 1.02
Leeds0.980.991.04 / 0.881.040.96 / 0.96
Nott’m Forest0.940.991.00 / 0.841.010.94 / 1.00
Newcastle0.911.140.88 / 1.211.081.02 / 1.03
Tottenham0.891.010.74 / 1.060.911.04 / 0.99
Aston Villa0.881.100.78 / 1.170.990.95 / 1.05
Fulham0.881.231.02 / 1.110.970.89 / 1.04
Ipswich0.861.261.01 / 1.110.950.87 / 1.16
Crystal Palace0.831.160.84 / 1.110.900.89 / 1.07
Hull0.781.170.77 / 1.050.990.77 / 1.27
Coventry0.751.220.77 / 1.040.920.86 / 1.22
Carried forward · 6 items
A1 CLOSED

Ingest results and grade the forward ledger

results[] carries 50 results (GW1–5)

The single highest-value input when it was opened at GW3: it settled the first auditable ten calls and switched the banked table from transcription to derivation. Every ledger round since is graded from results[].

A2 BLOCKED

Re-fit the managerial-reset penalty against outcomes

zeroed in v2.1 (was 0.065 home / 0.190 away) · n = 5, fit at n ≥ 8

Was 51% of the realised adjustment on GW4 and moved in-sample Brier by 0.001 — an unfitted factor dominating the margin. The term stays wired at zero; it is re-fitted on results, not tuned.

A3 CLOSED

Give the score-state filter a data source

Understat, read daily by the Payload script · active on 20 clubs (C4)

Per-club xG by score state. Understat has been read by the Payload script every morning since 9 Sep, for all twenty clubs, and the filter runs on those splits.

A4 CLOSED

Assert the scorer veto in code

check E8, v2.1

Gate G10 now executes: scorer names are parsed off each card and looked up in the availability manifest for both clubs. Built to find the conflict rather than to recognise it — a version that tests for a known name would pass on a card nobody had checked.

A6 OPEN

Store the weight vector with each version

W2 is manual

The factor table holds a single cumulative delta against the pre-season build, so the update rule’s four-point-per-round cap cannot be asserted — only read out of the log. One array of thirteen numbers per version makes it executable.

A5 OPEN

Second rating model for the player layer

G3 partial since 4 Sep

WhoScored was never captured. The player layer stays single-model and directional until it is.

Data

Every number on this site comes from one file, the payload, which the data layer rebuilds through the day; the desk’s calls reach it through the lock service. This page loaded it through the site’s own /api/payload route, which serves one shared copy that it re-reads from the live endpoint every five minutes (this copy was read 178 s before the page asked). It was built 1.2 hours before this page loaded it (the limit is 6 hours).

Where this page’s numbers came from

Live payload · through the site

Built
Thu 8 Oct 2026 00:40 BST · buildPayload
Loaded from
site proxy /api/payload · refreshed, 178 s old (nothing stored in this browser)
Schema
pl-datalayer/2

From the last result to the next kickoff

  1. Last result in the file

    20 Sept, 16:30

    C3b →
  2. Morning sheet refresh finished

    Wed 7 Oct, 06:10 BST

    18.5h before the build (bound 26h)

    C6 →
  3. Last tick (every 15 min) succeeded

    Thu 8 Oct, 00:24 BST

    0.25h before the build (bound 2h) · 0 consecutive tick failure(s)

    C6 →
  4. Payload built

    Thu 8 Oct 2026 00:40 BST · buildPayload

    clock source live

    C1 →
  5. This page loaded it · now

    Thu 8 Oct, 01:51 BST

    1.2h after the build (limit 6h)

  6. FPL deadline

    Sat 10 Oct, 11:00 BST

    FPL team →
  7. GW6 kicks off

    Sat 10 Oct, 12:30 BST

    10 fixtures, the last Mon 12 Oct, 20:00 BST

    GW6 cards →

Where the data comes from

Every source the desk reads, with its status and last update as this payload reports them. Model inputs feed the prices. Benchmarks are scored beside the desk and never feed it. Shadow model inputs feed three candidate models, backtested on past seasons, that are priced beside every lock and graded on the Scoreboard; none of them feeds the prices.

14 of 15 live · 1 skipped on the last run

StatusSourceHow it’s usedWhere it’s usedLast updated
Live FPL APIRounds, fixtures, kickoff times, results and league standings; players, prices and availability status Model inputThe clock for every round and lock, the scores that grade every call, and the availability join SummaryRoundsScoreboardUnderlyingTableFPL teamDiagnostics Thu 8 Oct, 00:39 BSTfixtures read live at the last build; players and teams each morning
Live football-data.co.uk results fileResults, per-match xG and Betfair Exchange closing prices Model inputResults grade every call; Betfair closes seed the market-ratings model; xG (model A) sets each fixture’s interval beside model B, which the solve fits on ScoreboardSummaryRoundsUnderlyingModel Wed 7 Oct, 06:10 BST50 results, 50 with a Betfair close
Live FotMobTeam xG, xG conceded and expected points (model B); injuries and suspensions with return dates Model inputxG (model B) in the strength solve; the availability adjustments UnderlyingRoundsTableModel Wed 7 Oct, 06:10 BST20/20 clubs · 85 availability rows
Live Understat (team pages)Shots and xG by phase (open play, corners, set pieces) and by game state Model inputThe phase split and the game-state filter in the engine ModelUnderlyingDiagnostics Each morning (time not published)20 of 20 clubs in this payload · a refused refresh keeps the previous day’s rows and is not reported here
Live PolymarketMatch-winner prices about 24 h before kickoff and at the lock, with volume BenchmarkPolymarket at the lock is scored beside the desk; the 24 h price is kept in the ledger SummaryScoreboardRounds Sat 19 Sep, 16:40 BSTcaptured by the 15-minute tick near each kickoff · a lock price on 17 of 30 ledger rows
Live Opta supercomputer (match probabilities)Home, draw and away probabilities for each fixture BenchmarkScored beside the desk in the market comparison; never an input SummaryScoreboardRounds Not yetlines: 0 new, 0 changed, 0 unchanged · captured by the tick in the 60 h before each kickoff · 20 earlier ledger rows carry a line the routine logged
Skipped Pre-match bookmaker odds (football-data)Average, Bet365, best and exchange prices before each round BenchmarkThe market price available before every lock Stored, not shown on another tab yet Not yetfixtures.csv has no Premier League (E0) rows yet
Live ClubEloAn Elo rating for every club; Elo match probabilities for the next round Shadow model inputRatings feed the shadow rating prior, a model logged beside every lock from GW6, not the prices; match probabilities kept as a benchmark ScoreboardModel Wed 7 Oct, 06:10 BST20 rows · 20 clubs (ratings of 2026-10-05) · match lines: 10 rows · 10 fixtures written (ARS-LEE, AVL-BRE, CHE-BOU, IPS-FUL, SUN-BHA, MUN-TOT, CRY-NFO, HUL-EVE, LIV-MCI, COV-NEW); 10 team pages read
Live Solio AnalyticsClean-sheet probabilities and projected goals (built from market odds) BenchmarkA clean-sheet benchmark; never an input Stored, not shown on another tab yet Wed 7 Oct, 06:10 BST10 rows · 10 fixtures written (ARS-LEE, HUL-EVE, CHE-BOU, MUN-TOT, COV-NEW, SUN-BHA, CRY-NFO, IPS-FUL, AVL-BRE, LIV-MCI); Solio GW6 lists 10 fixtures
Live DratingsAn independent model’s match probabilities BenchmarkA second outside model to compare against; never an input Stored, not shown on another tab yet Wed 7 Oct, 06:10 BST10 rows · 10 fixtures written (ARS-LEE, AVL-BRE, SUN-BHA, CHE-BOU, IPS-FUL, MUN-TOT, CRY-NFO, HUL-EVE, LIV-MCI, COV-NEW); 3 pages read
Live ForebetMatch predictions, logged as text by the desk routine BenchmarkKept in the ledger’s benchmarks column ModelHow it’s built With each lock14 ledger rows carry a line
Live Understat (league data)Season npxG, npxGA, PPDA and deep completions; player npxG and xA Shadow model inputDeep completions feed the shadow deep-completions model, logged beside every lock from GW6, not the prices ScoreboardModel Wed 7 Oct, 06:10 BSTteams: 20 rows · 20 clubs · players: 421 rows · 421 players
Live Opta power rankingsA strength rating for every club, updated after each matchday Shadow model inputFeeds the shadow rating prior, a model logged beside every lock from GW6, not the prices; the history is kept for next season’s starting ratings ScoreboardModel Wed 7 Oct, 06:10 BST20 rows · 20 clubs appended (Opta updated 06 Oct 04:16); 20 rows in the history
Live FotMob season statsTeam xG, xG conceded, rating, big chances, clean sheets, possession ContextA cross-check on the xG the engine already reads; not a separate input Stored, not shown on another tab yet Wed 7 Oct, 06:10 BST116 rows · 6 stats, 116 rows
Live FPL squad snapshotThe desk’s own FPL squad, captured before each deadline ContextThe FPL team tab FPL team Wed 7 Oct, 20:29 BSTGW6 squad
Retired Premier InjuriesAn injury table RetiredDropped after GW3: FPL’s status data and FotMob cover the same players — —
Retired Coupler.ioThe old connector into the sheet RetiredReplaced on 9 Sep by the sheet’s own scripts — —
Retired WhoScoredPlayer and team ratings RetiredNever captured — —

What the producer checked before publishing

The producer’s own verdict on this build: pass · integrity held

  • 20 clubs, ids unique
  • 10 fixtures in the target round (GW6: 10 scheduled, 0 undated, 20 clubs playing)
  • 50 results
  • 50 with a Betfair close
  • 50 with Football-Data xG
  • 0 missing from Football-Data
  • 0 score mismatches between the two results pipelines
  • 128 availability rows
  • Understat on, 20 clubs
  • 0 clubs with an unequal played count
  • 0 duplicate fixture ids
  • 0 FotMob played-count skews (P7)
  • 0 odds-join collisions (P8)
  • clock source live (C7)
  • engine_hash 97cc49a9abb3… matches this page
  • 17 functions in the port region

Also reported: missing columns: fm_id (tried fpl_team_id) · el_news_added (tried news_added)

The producer’s gates, as published
{"clubs":20,"ids_unique":true,"fixtures_target_gw":10,"target_round_shape":{"gw":6,"fixtures":10,"scheduled":10,"unscheduled":0,"finished_count":0,"complete":false,"first_ko":1791631800,"last_ko":1791831600,"span_h":55.5,"clubs_playing":20,"clubs_doubled":0,"placeholder_fixtures":0,"placeholder":false},"results":50,"results_missing_fd":0,"results_with_bfe":50,"results_with_fd_xg":50,"score_mismatches":0,"availability_rows":128,"understat":true,"understat_clubs":20,"unequal_played":[],"duplicate_fixture_ids":[],"fotmob_played_skew":[],"odds_join_collisions":[],"clock_source":"live","engine_hash":"97cc49a9abb300438863b7cba83e2ff69fdbe89740303a06ebfe7f5882ec86e8","engine_port_region":["PLIngest.validate","PLIngest.fromPayload","ratesData","resultsIndex","playedPairs","playedCount","playedBy","contextData","gsnFactors","factorTerms","impliedGoals","phaseStrengths","solveMultipliers","solve","strengths","marketPrior","engineFor"]}

Locks that did not happen the normal way

A missed lock is a fixture whose lock window closed with no desk call: it is listed and kept out of every score. A fallback lock is the model’s call, locked automatically 45 minutes before kickoff when the desk has made none; it is scored on the model and market rows, never as the desk’s. The fallback is switched on (mode write) and has not fired yet.

Fallback · mode write · locks at kickoff − 45 min · 0 recent decisions

Lock refusals · last 7 days · 0 of 0 published

The calendar

Clock authority: fixture spine (FPL live, 0s old): earliest unstarted scheduled kickoff

Round
GW6
Fixtures
10
Scheduled
10 / 10
Undated
0
First kickoff
Sat, 10 Oct, 12:30 BST
Last kickoff
Mon, 12 Oct, 20:00 BST
Span
55.5h
Clubs twice
0
Provisional
no

Filled: this round · played rounds open their cards · faded: to come · dashed outline: provisional slots · 38 rounds · 380 fixtures · no doubles · every fixture dated · ·· twice, ? undated

The next ten fixtures

How it’s built

Nothing on this site is typed by hand. A private Google Sheet collects the data every morning, an Apps Script turns it into one file, the payload, and this page re-runs the model from that file in your browser. Calls go into a write-once ledger before kickoff and are scored afterwards, and the fixtures the desk failed to call are shown as missed, not hidden.

Data
1.2 hours old C1: built 1.2h ago (bound 6h) Data tab
Self-checks
71 of 83 pass 1 fail · 8 waiting for data · 3 manual Diagnostics
Same engine?
page = producer port hash 97cc49a9abb3… v producer 97cc49a9abb3… The gates
Missed locks
3 all in GW5; shown as missed, never scored GW5

From data to a locked call

Two paths meet in the payload: the data path every morning, and the call path before each kickoff.

Data path · each morning

  1. 1SourcesFPL (fixtures, players, status flags), FotMob (xG B, expected points, team news), football-data.co.uk’s results file (xG A, results, Betfair closing odds), Understat (chance and game-state splits, 20 of 20 clubs) and Polymarket (match prices).
  2. 2Private Google SheetAn Apps Script reads FPL, FotMob and football-data.co.uk directly each morning (it replaced Coupler on 9 September) and refreshes the source tabs: 17 of 18 datasets OK, finished 06:10 ⁠BST.Run log
  3. 3Payload scriptA tick every 15 minutes captures the reference prices about a day before each kickoff, takes Polymarket’s, runs the fallback lock and keeps the payload warm; since v5.8.0 (6 Oct) a tick with no kickoff within 26 hours is idle and only stamps the heartbeat; a full check still runs at least hourly. Each morning it reads Understat and rebuilds the payload. It runs a byte-for-byte copy of the engine and its own checks (all passing at this build).Gates
  4. 4The payloadOne JSON file: 380 fixtures, 50 results, 128 absences, 30 ledger rows. Built Thu 8 Oct, 00:40 ⁠BST; the last tick succeeded at 00:24 ⁠BST.Data tab
  5. 5This pageStatic. Reads the payload through the site’s /api/payload route, one shared copy (a Cloudflare Durable Object) re-read from Apps Script every five minutes; if that fails it asks Apps Script directly, then falls back to the bundled copy and says so. It re-runs the engine and 83 self-checks in your browser.Data tab

Call path · before each kickoff

  1. AThe deskThe owner on his phone, or the cloud routine that desk-gate starts when a lock is due (its locks are dry runs while its shadow mode is on): score, home/draw/away split, why and risk.This round’s calls
  2. BLock serviceA private Cloudflare Worker behind Cloudflare Access holds the lock key, so neither the phone nor the routine carries it. It also carries the routine’s weekly FPL squad snapshot to the payload.FPL team
  3. CPayload scriptRefuses late or malformed locks; locks close 15 min before kickoff. Refusals are published: none recorded. Polymarket is captured on the regular tick within a time budget, and a fixture it has to skip is retried on the next tick.
  4. DWrite-once ledgerOne row per call, never edited; a correction is a new row. 30 rows across GW3–GW5. From GW6 each lock also logs three shadow models’ prices beside the call (none yet), graded on the Scoreboard where the lock was a desk call, and never a call or a price.Scoreboard
  5. EScoringDesk, model and market are scored on the same fixtures once results land (Brier and RPS). A missed lock has no call and is not scored.

Safety nets

Model fallback
If the desk has not called a fixture 45 min before kickoff, the producer locks the model’s call, labelled as a fallback and kept out of the desk’s record. Mode: write; 0 recent decisions.
Desk gate
Twice a day (07:07 and 19:07 UTC) a GitHub Action checks whether a lock, news, results or the FPL squad is due, starts the routine only if something is, and runs the health check, emailing the owner when it finds a problem.
Watchdogs
Each morning the Payload script’s watchdog records a kickoff that moved after its capture on its ledger row, and the sheet’s freshness check emails the owner if a run log is more than 26 hours old.
Daily live check
A GitHub Action fails if the live payload is invalid, stale, or priced by a different engine from this page.
Keyless health
The endpoint’s ?health=1 reports the build and tick heartbeat, fallback decisions, missed locks and refusals without rebuilding anything.

A fixture’s last 24 hours

Times for Arsenal v Leeds, which kicks off Sat 10 Oct, 12:30 ⁠BST.

  1. about a day outReference capture: model, market and Polymarket, unattended
  2. any time beforeThe desk locks its call
  3. 11:45 ⁠BSTFallback: the model is locked if nobody has called
  4. 12:15 ⁠BSTLocks close
  5. 12:30 ⁠BSTKickoff

Not to scale. The reference capture is meant to land about a day out; what actually happened is printed on each card as its measured horizon, and the ledger’s 20 measured horizons run from 1.84 h to 23.84 h.

Self-checks, run on every load

83 checks · 71 pass · 1 fail · 8 blocked · 3 manual

  • 71 pass
  • 1 fail
  • 8 waiting for data
  • 3 manual
  • FAIL

    E10 · Target-round card: draw mass and goals per fixture in the league’s range

    This round’s cards sit just outside the league range this check holds them to.

    GW6 · mean draw 26.7% (band 24–27) · mean goals 2.90 (band 2.9–3.1) · 0 draw calls of 10

    E10 on Diagnostics

Full test output, with what each check measured
  • E1 · Joint matrix normalises to 1.000 per fixture — max deviation 8.9e-16PASS
  • E2 · Scoreline grid captures the fixture’s probability mass, and the Dixon-Coles correction is renormalised — worst grid 1.00000 of 1.000 · largest Dixon-Coles mass shift 0.00%PASS
  • E3 · Published scoreline lies inside its own favoured outcome region — 10 of 10PASS
  • E4 · Home term appears once: with the factor terms zeroed, a fixture and its reverse differ by ENGINE.HOME, not its square — max deviation 0 against ENGINE.HOME = 1.14 (squared would be 1.2996)PASS
  • E5 · Relative factors sum to zero across the two sides of every fixture — max residual 3.5e-18PASS
  • E6 · Every published factor term is in the price: the adjustment a priced card carries equals the factor sum the table publishes — league goals scale ×0.771 divided out · largest gap between the applied adjustment and the full factor sum 1.3e-16 · against the centred terms alone 0.015000 · largest non-centred term 0.030 log-goalsPASS
  • E9 · Post-solve tempo: mean λ + μ over all pairings equals the goals/xG blend recomputed from results and clubs — pre 3.79 → post 2.92 · target recomputed 2.92 = (5 × 2.82 goals from 50 results + 5 × 3.03 xG) / 10 · solve states 2.92 · scale 0.771 · league draw mass 25.9%PASS
  • R1 · Market-implied prior solved from closing prices and labelled — market-implied (Betfair closing, GW1–5, 50 fixtures) · 25 passes · residual 7.8e-7PASS
  • R2 · Market-prior engine is centred like the pre-season one — geometric mean attack 1.003 · defence 1.002 · tempo 2.92PASS
  • E11 · λ built from open-play and set-piece components (τ 5 / 14) — active on 20 clubs · set-piece share of λ 21% · set-piece evidence weight 26%PASS
  • K1 · Home term, ρ and τ fitted by maximum likelihood and published beside the typed values — fitted home 1.14 · ρ -0.075 · τ 3 · LL -146.77 against typed -146.83 on 50 fixturesPASS
  • K2 · Fitted parameters adopted — n = 5 · adopt at n ≥ 8 once the fitted values have held sign for two roundsBLOCKED
  • P6 · Season sampler draws outcomes from the Dixon-Coles matrix, so its draw rate matches the engine’s own mean draw mass — sampled draws 25.9% · engineFor mean pD 25.9% over 380 pairings · sampler’s own expectation 26.0% over 330 fixturesPASS
  • S9 · Join key is the FPL team id; names are derived, never read from the payload — 20 clubs keyed 1–20 via PLIngest.NAMES · per-club played 5–5 · results orphans 0PASS
  • E10 · Target-round card: draw mass and goals per fixture in the league’s range — GW6 · mean draw 26.7% (band 24–27) · mean goals 2.90 (band 2.9–3.1) · 0 draw calls of 10FAIL
  • E7 · Dead-heat badge agrees with the printed margin at 3.0 inclusive — 0 inside the bandPASS
  • S1 · Club keys reconcile across rates, fixtures and all five context maps — no orphans across 7 tablesPASS
  • S2 · Rates table carries exactly 20 unique clubs — 20 rows, 20 uniquePASS
  • S3 · Fixture list matches the declared match count for every club — 50 pairs for played = 5 · all 20 clubs on 5PASS
  • S4 · Opponent adjustment reaches a fixed point — residual 7.9e-7 after 25 passes (cap 3000)PASS
  • S8 · Schedule graph connectivity — schedule graph in 1 component(s) after 5 roundsPASS
  • S5 · Adjusted strengths centre on the league mean — geometric 0.992 / 1.017 · arithmetic 1.005 / 1.027PASS
  • S6 · Shrinkage weight is n / (n + τ) at the sample the payload declares — solve n = 5 · payload min(clubs[].played) = 5 · τ = 5 → 50% evidencePASS
  • S7 · Two xG models compared per club, never averaged in a cell: every solve input equals that club’s FotMob column — solve fits on FotMob · per-club solve input not published by the solve, label checked only · 10 of 20 clubs diverge ≥ 0.10 xG · mean spread 0.03 xG per matchPASS
  • S10 · ∂logλ / ∂log(xG) equals the published shrinkage weight on both the whole-xG and set-piece paths — whole-xG path slope error 1.4e-14 against w = 0.500 · set-piece path 2.7e-14 against w = 0.263 · tolerance 0.02PASS
  • W1 · Factor weights sum to exactly 100% — 100% nominal across 13 factors · 88% effective + 12% suspended (Managerial reset)PASS
  • W2 · Per-round weight moves inside the update rule’s 4-point cap — largest cumulative move 10 points since v1.0MANUAL
  • W3 · Realised factor shares account for 100% of the card’s adjustment, against an independently recomputed total — 100.0% attributed · Σ|dh|+|da| 0.8328 against 0.8328 recomputedPASS
  • W4 · Every bound factor is a published weight, and vice versa — 6 bound, 13 published · 1 of them suspended at 0% effective, still bound and publishedPASS
  • W5 · Realised share matches the effective weight the page publishes: zero while the factor is suspended, else within a factor of two of it in both directions — managerial reset 0.0% realised against 0% effective (band 0–0.5%) · suspended: penalties 0 / 0, 12% nominal returns when fittedPASS
  • L1 · Expansion rule residuals sum to 1.000 on every graded call, and so does every desk split in the ledger — max deviation 1.1e-16 over 20 expanded + 17 ledger split(s)PASS
  • L2 · Expanded vector keeps the stated call at plurality, and so does every desk split in the ledger — 20 of 20 expanded · 17 of 17 ledger split(s)PASS
  • L3 · Round Briers reconcile with the combined figure: retrospective, and pooled forward per family (Σ n·B / Σ n on the same Σ n) — GW1 0.621 + GW2 0.511 → 0.566 · forward pooled from locked_calls[] v rounds: Desk call (sheet ledger) 0.726 (17) · Model at lock · pre-season prior 0.700 (17) · Market prior at lock 0.691 (17) · Betfair closing · called 0.698 (27) · Polymarket at lock 0.704 (11) · Polymarket · reference · called 0.717 (16) · Model · reference 0.689 (20) · Market prior · reference 0.681 (20) · Polymarket · reference · every capture 0.702 (20) · Polymarket · reference · every capture, thin draw excluded 0.679 (19) · Betfair closing · every capture 0.693 (20) · every family reconcilesPASS
  • L4 · Baseline outcomes partition the graded set — 10 home / 4 draw / 6 away = 20PASS
  • L6 · Forward ledger scores every row it can furnish over one fixture set per round: desk, model, market prior, Betfair close, Polymarket at lock and reference — GW5 6 of 6 rows over 7 of 10 fixtures · GW4 6 of 6 rows over 10 of 10 fixtures · GW3 1 of 6 rows over 10 of 10 fixtures · no vector in the ledger: GW3 Desk call (sheet ledger), GW3 Model at lock · pre-season prior, GW3 Market prior at lock, GW3 Polymarket at lock, GW3 Polymarket · reference · excluded for an unanchored draw leg: GW5 Polymarket at lock (3), GW5 Polymarket · reference (1), GW4 Polymarket at lock (3)PASS
  • L7 · Engine parity: the ledger’s splits at lock equal this engine on the loaded payload (±1 point per leg) — no locked_calls row of GW6 carries model_split or mkt_split yetBLOCKED
  • L5 · Retrospective and forward ledgers carry separate denominators: forward calls equal Σ called over the ledger rounds, and no fixture is in both — retrospective 20 of 20 (GW1–GW2) · forward 27 called v Σ GW3 10 + GW4 10 + GW5 7 = 27 · no fixture id in both (20 retrospective, 30 forward)PASS
  • P1 · Every simulated run relegates exactly three clubs — relegation mass 300.0%PASS
  • P2 · Every simulated run crowns exactly one champion — title mass 100.0%PASS
  • P3 · Simulation covers every unplayed pairing and no played one — 330 simulated of 380 − 50PASS
  • P4 · Banked points carried from results rather than a literal table — derived from 50 resultsPASS
  • P5 · Simulation reports its seed and run count — 5,000 runs, seed 20262027PASS
  • B1 · Live table: 20 clubs and a goal difference summing to zero — 20 clubs · GD 0 · 134 pointsPASS
  • B2 · Every club has an availability row or is declared clean, with suspensions separated — 20 of 20 clubs in the manifest · 128 entries · 5 suspensions · every club carries at least one entryPASS
  • X1 · No unresolved engine token remains anywhere on the page — 111 strings scanned, 0 open tokensPASS
  • X2 · Frozen cards quote the price they were locked at — call and split — not the current engine — in-file GW3 card · 10 of 10 frozen · 0 checked against a ledger model_split · every frozen card matches its ledger row · 3 now differ from the live enginePASS
  • X3 · Every percentage a card prints traces back to that card, and carries one unit — 0 of 10 GW3 cards carry prose against the live engine · 10 frozen at lock, exempt as historical records · no live card carries prose, so there is nothing to traceBLOCKED
  • E8 · No scorers[] entry marked available appears in availability[] as out or suspended (gate G10) — 60 scorer entr(ies) across 20 club(s) · 43 marked available · none contradicted by availability[] · manifest keyed on 20 of 20 clubsPASS
  • C1 · Live numeric spine: the payload carries a build stamp and it is under six hours old — pl-datalayer/2 via site proxy /api/payload · refreshed, 178 s old (nothing stored in this browser) · Thu 8 Oct 2026 00:40 BST · buildPayload · built 1.2h ago (bound 6h)PASS
  • C2 · Every block the contract requires is present: rates, fixtures, played, gameweek, results, rounds, target_round, forward — all eight required blocks present · also carried: context, game-state splits, locked calls (30), closing prices (50), situations, heartbeat, refresh, spinePASS
  • C3b · The payload was built after the last result it carries, and less than six hours ago — last result kickoff 20 Sept, 16:30 · built 8 Oct, 00:40 · 1.2h agoPASS
  • C3 · Round clock: last kickoff has passed and results are not ingested — GW5 · closed Sun, 20 Sept · 10 of 10 results ingestedPASS
  • C4 · Game-state splits present, so the score-state filter can run — active on 20 clubs · multipliers published on the strengths tablePASS
  • C5 · Sample is large enough for a rate rather than a direction — 5 matches per clubPASS
  • C6 · Producer health at build: the tick had succeeded within two hours of the build stamp, the refresh within twenty-six, with no failures outstanding — tick succeeded 0.25h before the build (bound 2h) · refresh finished 18.5h before the build (bound 26h) · 0 consecutive tick failure(s) · payload itself is 1.2h old, which is C1’s rowPASS
  • C7 · The clock is the fixture spine and the producer says the clock source is live — clock_authority “fixture spine (FPL live, 0s old): earliest unstarted scheduled kickoff” · clock_source livePASS
  • C6b · No load-bearing read is standing on an in-file fallback — all 10 load-bearing blocks came from the payloadPASS
  • P7 · The producer reports no FotMob played-count skew — emptyPASS
  • P8 · The odds join produced no collisions — emptyPASS
  • M2 · External benchmarks captured and verified by fixture name — ledger benchmarks text · Opta GW3 9 of 10 · GW4 10 of 10 · GW5 1 of 10 · Forebet GW4 10 of 10 · GW5 4 of 10 · Solio GW3 10 of 10MANUAL
  • M3 · Graded cards unedited since their lock timestamp — GW1–GW2: version log only · GW3, GW4, GW5: rendered from write-once ledger rowsMANUAL
  • L11 · Every target-round ledger row was priced by this page’s engine: model_ver equals ENGINE_VER, and the port hash equals the producer’s engine_hash once it publishes one — no locked_calls row of GW6 carries model_ver yet · port hash 97cc49a9abb3… v gates.engine_hash 97cc49a9abb3…PASS
  • L10 · Where the ledger carries a why or a risk, the card’s Notes region renders that exact sentence — 14 note(s) across 7 why and 7 risk · every note found in its cardPASS
  • L9 · The archives supply prose only: nothing outside the whitelist reaches a card with a ledger row, a written why or risk is never displaced by an archive sentence, and no round the ledger covers shows a call the ledger does not hold — every merged card checked against its built card across 3 overlaid round(s)PASS
  • T1 · Live table and the expected-points table equal the payload’s clubs[], and clubs[] equals the table recomputed from results[] — 20 rows checked on four fields · 20 xPts rows · no render differences · clubs[] agrees with results[] on 20 club(s)PASS
  • H1 · Header strip equals the tabs it summarises: record and ledger tile against the raw ledger rows, Brier trio on equal n, deadline — strip “GW1 4/10 · GW2 7/10 · GW3 4/10 · GW4 5/10 · GW5 2/7 (of 10)” v ledger rows “GW1 4/10 · GW2 7/10 · GW3 4/10 · GW4 5/10 · GW5 2/7 (of 10)” · ledger tile 11 / 27 v 11 / 27 · Brier “desk 0.800 · model 0.731 v 0.729 · 7 of 10 called” v ledger rows “desk 0.800 · model 0.731 v 0.729 · 7 of 10 called” · deadline 2d 10h · Sat, 10 Oct, 12:30 BST · FPL deadline Sat 10, 11:00 BSTPASS
  • L8 · Target-round cards and the Underlying market row render the ledger they were built from: both capture families, the desk headline, the horizon label — no locked_calls row for GW6 yetBLOCKED
  • K3 · rounds[] is present, strictly increasing by gw, and scheduled + unscheduled equals fixtures on every round — 38 rounds · ids increase · shapes agree · 380 fixtures in totalPASS
  • K4 · target_round.fixtures equals the fixture card, and every card fixture appears in forward[] under the same fixture_id — 10 on the card v 10 in the round · all 10 found in forward[]PASS
  • K5 · Every round the selector offers exists in rounds[], and every played round is either renderable or reported as not — 6 offered · all in rounds[] · 5 played · every played round rendersPASS
  • K6 · Reference family: present on every target fixture more than 15 minutes from kickoff, ref_horizon_h numeric on every row that has one, and no row claiming exactly 24.0 unless the timestamps agree — no reference capture in the ledger yet · clock stamp_epoch · 1.2h oldBLOCKED
  • K7 · Where both families exist, locked_at is at or after pm_ref_at — the commit never precedes the reference — 17 row(s) carry both families across GW5, GW4 · 7 bound (window open at lock) · 10 captured legitimately after the commit, window opened later · first reference capture in the ledger Sat, 12 Sept, 13:09 BST · 6 locked before the first reference capture existed, exempt: GW4 6: Aston Villa v Nott’m Forest, Bournemouth v Brentford, Chelsea v Hull, Crystal Palace v Ipswich, Liverpool v Fulham, Tottenham v Everton — locked Fri, 11 Sept, 17:39 BST, pm_ref_at Sat, 12 Sept, 13:09 BST · 1 held to the rule: every capture precedes its commitPASS
  • L15 · Every kicked-off fixture in a ledger round is either called or shown as a missed lock — GW5 called 7 of 10 · 3 missed: Tottenham v Aston Villa, Bournemouth v Liverpool, Man City v Sunderland · GW4 called 10 of 10 · 0 missed · GW3 called 10 of 10 · 0 missed · every missed lock renders as Missed lockPASS
  • L16 · Every fallback lock is excluded from the desk’s record and labelled as a fallback — no locked_calls row carries call_source model_fallback · 30 row(s): 0 desk, 30 blank (before v5.6.0, read as desk)BLOCKED
  • K8 · Every moved-kickoff row renders its chip, and every corrected row renders the corrected chip, in every round the ledger covers — no ledger row in any round carries moved_after_capture or a correction: 30 card(s) scanned across 3 round(s)BLOCKED
  • C8 · Every card’s kickoff came from the payload — results[], fixtures[] or forward[] — 10 × fixtures[] · 10 × results[]PASS
  • L12 · No correction changed a graded field of a fixture that had already kicked off — no fixture in the ledger carries more than one rowBLOCKED
  • L13 · Every scored row in a round covers the identical fixture set — GW5 6 row(s) on 7 fixtures · GW4 6 row(s) on 10 fixtures · GW3 1 row(s) on 10 fixtures · every row on its round’s full setPASS
  • L14 · The fold is order-independent: reversing locked_calls yields the identical snapshot — identical snapshot over 30 rows, forward and reversedPASS
  • R3 · No denominator printed in the round view differs from rounds[gw].fixtures — GW6 · denominator 10 · every printed count agreesPASS
  • H3 · Every assertion flips when its measured input is negated, and every row that only prints an invariant is declared as one — 72 asserted (50 self-tested) · 8 blocked on data · 0 not asserted against 0 declared · 3 manual · every probed row flippedPASS
  • H4 · The version log leads with this build and descends strictly — 25 entries · leads with v3.11 against VER v3.11 · strictly descendingPASS
  • H2 · The harness is the size it declares, and every check has its own id — 83 rows against 83 declared · all ids uniquePASS

Data gates

Each build is checked against these before anything is priced. Those not marked ✓ say why below.

  • ✓ G0 Clock pass
  • ✓ G1 Currency pass
  • ✓ G2 Integrity pass
  • ▲ partial G3 Provenance
  • ✓ G4 Cross-source pass
  • ✓ G5 Temporal pass
  • ✓ G6 Transport pass
  • ✓ G7 Reconciliation pass
  • ▲ partial G8 Benchmarks
  • ✓ G9 Self-review pass
  • ✓ G10 Selection pass
  • ✓ G11 Normalisation pass
  • ✓ G12 Sample scaling pass
  • ✓ G13 Coherence pass
  • ✓ G14 Opponent adjustment pass
  • ✓ G15 Venue parity pass
  • ✓ G16 Weight realisation pass
  • ✓ G16b Prose coherence pass
  • ✓ G17 Game state pass
  • ✓ G18 Key parity pass
  • ✓ G19 Sample parity pass
  • ✓ G20 Lock immutability pass
  • ✓ G21 Round clock pass
G3 Provenance · Partial
Passed for the team layer — two models, both labelled, disagreements listed. Still unmet for the player layer: no second rating model.
G8 Benchmarks · Partial
External lines the routine logged into the ledger: Opta GW3 9 of 10 · GW4 10 of 10 · GW5 1 of 10; Forebet GW4 10 of 10 · GW5 4 of 10; Solio clean sheets GW3 10 of 10. Polymarket’s reference price is on 20 of 30 rows. A call with no external line is marked unbenchmarked.

Engine port hash on this page: 97cc49a9abb300438863b7cba83e2ff69fdbe89740303a06ebfe7f5882ec86e8. Producer’s engine_hash: 97cc49a9abb300438863b7cba83e2ff69fdbe89740303a06ebfe7f5882ec86e8. They match.

Each gate’s note, and the producer’s raw summary

Gates for GW6, checked on the payload Thu 8 Oct 2026 00:40 BST · buildPayload. Producer’s own gate summary: pass — {"clubs":20,"ids_unique":true,"fixtures_target_gw":10,"target_round_shape":{"gw":6,"fixtures":10,"scheduled":10,"unscheduled":0,"finished_count":0,"complete":false,"first_ko":1791631800,"last_ko":1791831600,"span_h":55.5,"clubs_playing":20,"clubs_doubled":0,"placeholder_fixtures":0,"placeholder":false},"results":50,"results_missing_fd":0,"results_with_bfe":50,"results_with_fd_xg":50,"score_mismatches":0,"availability_rows":128,"understat":true,"understat_clubs":20,"unequal_played":[],"duplicate_fixture_ids":[],"fotmob_played_skew":[],"odds_join_collisions":[],"clock_source":"live","engine_hash":"97cc49a9abb300438863b7cba83e2ff69fdbe89740303a06ebfe7f5882ec86e8","engine_port_region":["PLIngest.validate","PLIngest.fromPayload","ratesData","resultsIndex","playedPairs","playedCount","playedBy","contextData","gsnFactors","factorTerms","impliedGoals","phaseStrengths","solveMultipliers","solve","strengths","marketPrior","engineFor"]}

G0 Clock · Pass
380 fixtures across 38 rounds, 0 unscheduled, 200 placeholder, 10 this round, 330 unfinished ahead. Target round 6, review 5.
G1 Currency · Pass
Team-appearance parity: 100 club-appearances in the fixture list against 100 declared across clubs[]. Exact.
G2 Integrity · Pass
Read from the payload’s producer gates, not typed: Understat delivered for 20 of 20 clubs; no poison markers reported. Score mismatches across the two result pipelines: 0. Producer orphans: none.
G3 Provenance · Partial
Passed for the team layer — two models, both labelled, disagreements listed. Still unmet for the player layer: no second rating model.
G4 Cross-source · Pass
Scorelines from two pipelines, FPL and football-data.co.uk, with 0 mismatches. Availability from two sources, FotMob and FPL’s own status flags, on 20 of 20 clubs (128 entries); where they disagree both are listed rather than reconciled. Premier Injuries was the third source up to GW3.
G5 Temporal · Pass
Every capture keeps its own instant: the reference family is stamped when it is captured (model_ref_at, pm_ref_at) and the commit at locked_at, and nothing is re-fetched or re-dated afterwards. 20 of 30 ledger rows carry a reference capture; K6 and K7 hold the two families to their windows.
G6 Transport · Pass
Every numeric source is fetched directly as JSON or CSV, never through a page extractor: the sheet’s Apps Script reads FPL, FotMob and football-data.co.uk’s E0.csv each morning (it replaced Coupler on 9 September), and the Payload script reads Understat and Polymarket. This morning’s run: 17 of 18 datasets OK.
G7 Reconciliation · Pass
A call’s lock timestamp is the endpoint’s locked_at and its denominator is the round’s fixture count in rounds[]. A rebuild never creates a lock or moves a call, and a correction is an appended row (L12, L14). The ledger holds 30 rows across GW3–GW5.
G8 Benchmarks · Partial
External lines the routine logged into the ledger: Opta GW3 9 of 10 · GW4 10 of 10 · GW5 1 of 10; Forebet GW4 10 of 10 · GW5 4 of 10; Solio clean sheets GW3 10 of 10. Polymarket’s reference price is on 20 of 30 rows. A call with no external line is marked unbenchmarked.
G9 Self-review · Pass
Each routine run reviews its own ingest before it publishes and records any defect in its run note on claude/desk-runs. At GW3 (4 Sep) it caught three: positional column misalignment in the injury union, a stale snapshot served after a query fix, and an unscoped path that flattened a team payload into 21,623 columns.
G10 Selection · Pass
New in v1.6. Every named goalscorer is checked against the availability manifest before its card ships. At GW3 it rejected Hinshelwood (Brighton, out to mid-September), and the rejection was printed on the card rather than quietly swapped. Measured this run: 60 scorer entr(ies) across 20 club(s) · 43 marked available · none contradicted by availability[] · manifest keyed on 20 of 20 clubs.
G11 Normalisation · Pass
New in v1.6. Factor weights asserted to sum to exactly 100% and no single weight to move more than four points in a round. v1.5 shipped at 108% for two gameweeks, which is the whole reason this gate exists. Measured this run: 100% nominal across 13 factors · 88% effective + 12% suspended (Managerial reset).
G12 Sample scaling · Pass
Any factor keyed to a season-to-date residual must declare a shrinkage prior, and from v1.7 one per phase: τ = 5 open play and strengths, 6 regression, 14 set pieces.
G13 Coherence · Pass
New in v1.7. The published scoreline, the confidence number and the clean-sheet line must come from one joint distribution, and the scoreline must lie inside the outcome region the probabilities favour. Asserted on every live card. Measured this run: 10 of 10.
G14 Opponent adjustment · Pass
New in v1.7. Every rate entering the engine is divided through by the strength of the opponents actually faced, solved to a fixed point (check S4 prints the passes and the residual). Raw season-to-date rates can no longer reach a match card. Measured this run: geometric 0.992 / 1.017 · arithmetic 1.005 / 1.027.
G15 Venue parity · Pass
New in v1.8. The home term may appear once, on the home intensity. Asserted by checking that a fixture and its reverse produce goal expectations differing by exactly the home multiplier, not its square. Measured this run: max deviation 0 against ENGINE.HOME = 1.14 (squared would be 1.2996).
G16 Weight realisation · Pass
Every published weight must be readable by the simulation or declared as living inside the strength solve, with its measured share of adjustment printed. v1.8 flagged partial because managerial reset realised most of the card against a nominal 12%: fourteen of twenty clubs carry a first-season manager and the penalty was absolute, so it was deflating every fixture at once. v1.9 splits the factors into relative and tempo terms and zero-centres the relative ones within each fixture, which preserves the margin effect exactly and removes the league-wide deflation. Realised shares are printed beside the nominal weights on the Model tab. Measured this run: 100.0% attributed · Σ|dh|+|da| 0.8328 against 0.8328 recomputed.
G16b Prose coherence · Pass
New in v1.8. No card may state an engine figure in its own text. Splits, margins, confidences, scorelines and the Opta stance are tokens filled from that fixture’s engine object at render, so an engine change cannot leave the prose arguing a withdrawn call — which it did on three successive versions.
G17 Game state · Pass
The decay filter discounts chance volume accumulated while two or more behind to 0.86 and credits volume accumulated while ahead at 1.28, normalised to a league mean of 1.00 so it redistributes attacking credit without moving the goal mean. Met from v2.1 change 10: the payload carries per-club score-state xG, so the filter runs on real splits and the per-club multipliers are published on the strengths table beside the solved pair. The 0.86 / 1.28 weights were set by judgment and this is their first outing, so read the multiplier column as a claim under test. Measured this run: active on 20 clubs · multipliers published on the strengths table.
G18 Key parity · Pass
New in v1.9. Club names are the join key across the rates table, the fixture list and the five context maps, and a single mismatched spelling silently drops a club from the opponent adjustment or lets a played fixture be simulated twice. All keys reconciled against the rates table; orphans are reported here rather than absorbed. None this run. Measured this run: no orphans across 7 tables.
G19 Sample parity · Pass
New in v2.0. The declared match count, the fixture list and the per-club appearance counts must agree, because everything that turns a season total into a rate divides by that number. v1.9 hard-coded two matches in three separate places, so a payload appending a round would have halved every rate in the league and under-weighted the evidence at the same time. Measured this run: 50 pairs for played = 5 · all 20 clubs on 5.
G20 Lock immutability · Pass
New in v2.0. A fixture past its kickoff renders the price it carried at that moment, from a stored snapshot. Every version up to v1.9 re-derived all ten cards from the live engine on every render, so each model change silently re-priced matches already played while the page asserted that calls were immutable. The live engine is still published beside the snapshot, and where they now differ the gap is printed. Measured this run: in-file GW3 card · 10 of 10 frozen · 0 checked against a ledger model_split · every frozen card matches its ledger row · 3 now differ from the live engine.
G21 Round clock · Pass
New in v2.0. The page compares each fixture’s kickoff with the current time and reports a round whose matches have been played but whose results have not been ingested, instead of continuing to present the card as live. Asserted on the review round. Measured this run: GW5 · closed Sun, 20 Sept · 10 of 10 results ingested.

This morning’s run

DatasetStatusRows
fotmob_teamsPASS20/20 clubs
clean_elementsPASS667 rows
clean_eventsPASS38 rows
clean_league_standingsPASS10 rows
clean_fixturesPASS380 rows
clean_fotmob_xgPASS20 rows
clean_injuriesPASS85 rows
clean_fixture_congestionPASS976 rows
clean_oddsPASS50 fixtures from football-data direct (staged 0h ago)
ext_understat_teamsPASS20 clubs
ext_understat_playersPASS421 players
ext_opta_rankingsPASS20 clubs appended (Opta updated 06 Oct 04:16); 20 rows in the history
ext_fotmob_statsPASS6 stats, 116 rows
ext_clubeloPASS20 clubs (ratings of 2026-10-05)
ext_fixture_lines:solioPASS10 fixtures written (ARS-LEE, HUL-EVE, CHE-BOU, MUN-TOT, COV-NEW, SUN-BHA, CRY-NFO, IPS-FUL, AVL-BRE, LIV-MCI); Solio GW6 lists 10 fixtures
ext_fixture_lines:fd_prematchFAILfixtures.csv has no Premier League (E0) rows yet
ext_fixture_lines:dratingsPASS10 fixtures written (ARS-LEE, AVL-BRE, SUN-BHA, CHE-BOU, IPS-FUL, MUN-TOT, CRY-NFO, HUL-EVE, LIV-MCI, COV-NEW); 3 pages read
ext_fixture_lines:clubeloPASS10 fixtures written (ARS-LEE, AVL-BRE, CHE-BOU, IPS-FUL, SUN-BHA, MUN-TOT, CRY-NFO, HUL-EVE, LIV-MCI, COV-NEW); 10 team pages read
run_started2026-10-07 06:09:50Europe/London
run_finished2026-10-07 06:10:42Europe/London
duration_secs52
run_finished_epoch1791349842unix seconds, for checkFreshness

The log prints “2026-10-07 06:10:42” for the finish, but the epoch it carries (1791349842) is Wed 7 Oct, 06:10 ⁠BST, so the run started about 06:09 ⁠BST. The printed time agrees with it.

The run log as the payload carries it
  • fotmob_teams — status OK · finished 20/20 clubs
  • clean_elements — status OK · finished 667 rows
  • clean_events — status OK · finished 38 rows
  • clean_league_standings — status OK · finished 10 rows
  • clean_fixtures — status OK · finished 380 rows
  • clean_fotmob_xg — status OK · finished 20 rows
  • clean_injuries — status OK · finished 85 rows
  • clean_fixture_congestion — status OK · finished 976 rows
  • clean_odds — status OK · finished 50 fixtures from football-data direct (staged 0h ago)
  • ext_understat_teams — status OK · finished 20 clubs
  • ext_understat_players — status OK · finished 421 players
  • ext_opta_rankings — status OK · finished 20 clubs appended (Opta updated 06 Oct 04:16); 20 rows in the history
  • ext_fotmob_stats — status OK · finished 6 stats, 116 rows
  • ext_clubelo — status OK · finished 20 clubs (ratings of 2026-10-05)
  • ext_fixture_lines:solio — status OK · finished 10 fixtures written (ARS-LEE, HUL-EVE, CHE-BOU, MUN-TOT, COV-NEW, SUN-BHA, CRY-NFO, IPS-FUL, AVL-BRE, LIV-MCI); Solio GW6 lists 10 fixtures
  • ext_fixture_lines:fd_prematch — status SKIP · finished fixtures.csv has no Premier League (E0) rows yet
  • ext_fixture_lines:dratings — status OK · finished 10 fixtures written (ARS-LEE, AVL-BRE, SUN-BHA, CHE-BOU, IPS-FUL, MUN-TOT, CRY-NFO, HUL-EVE, LIV-MCI, COV-NEW); 3 pages read
  • ext_fixture_lines:clubelo — status OK · finished 10 fixtures written (ARS-LEE, AVL-BRE, CHE-BOU, IPS-FUL, SUN-BHA, MUN-TOT, CRY-NFO, HUL-EVE, LIV-MCI, COV-NEW); 10 team pages read
  • run_started — status 2026-10-07 06:09:50 · finished Europe/London
  • run_finished — status 2026-10-07 06:10:42 · finished Europe/London
  • duration_secs — status 52 · finished
  • run_finished_epoch — status 1791349842 · finished unix seconds, for checkFreshness

Behind the build

The product, UX, engineering and QA notes the site is built to, kept current with the diagram above.

Product specification: a record, not a hot take

Product, UX, engineering and QA passes, kept in the artefact so every rebuild has a contract to hold to.

  • Goal: a reader who opens this before a deadline gets the round’s calls, and a reader who opens it after gets an honest account of how the last ones did.
  • Every prediction ships with a confidence number, a conviction flag, a named mechanism and a stated failure condition. No naked picks.
  • A call is immutable once it is locked. Grading is additive — the original number stays on the page next to the result.
  • The round’s external favourite sits next to every call that has one. Where the desk disagrees, the disagreement is labelled and justified, not smoothed; where no line exists, the call is marked unbenchmarked rather than left to look checked.
  • The model is measured against a named dumb baseline every round. A metric with no baseline is decoration.
  • Weight changes are published with the matches that caused them, under a fixed update rule that caps how fast the model can move.
  • Season markets were priced before GW3 and are kept as made, to be graded at the end of the season.
  • Success at 38 rounds: beat “always the home side” on hit rate and beat 0.250 on Brier. The Scoreboard tracks both.
Four journeys, no dead ends
Deadline filler
Lands on GW6 → 10 calls with the external favourite, conviction flags and team news → desk picks → copies them and leaves.
Scorekeeper
Scoreboard → hit rate against the baselines → calibration bands → per-gameweek log. Answers “is this getting better?” without reading a match note.
Sceptic
Scoreboard → error sources with their fixes → Underlying → two models side by side and the reads they corroborate → Model → the version log, the update rule and the known gaps.
Season-long predictor
Season calls → priced-then-now on each market → Table for the live and projected standings.
The operating loop
  • Twice a day (07:07 and 19:07 UTC) desk-gate asks tools/desk-due.mjs what is due for the round with the next kickoff, and starts the routine only when something is.
  • Results and season: once results land, score every called fixture and the season markets, and write the round’s retro.
  • Build: more than 72 hours before the round’s first kickoff, price the round and write its handover.
  • News: between 72 and 24 hours out, refresh team news and pressers.
  • Lock: from 24 hours before each fixture, lock the desk’s call through the lock service. A fixture still uncalled 45 minutes before kickoff gets the model’s call as a fallback lock, and locks close 15 minutes before kickoff.
  • FPL: inside 12 hours of the FPL deadline, capture the owner’s squad for the FPL team tab.
  • Weights move only under the update rule, and each model version is logged with the matches that caused it. Nothing is deleted: each round adds rows to the ledger and the log.
Engineering: one data spine, everything derived
  • One payload, everything derived. The page reads one JSON file (fixtures, results, availability, prices and the ledger of locked calls) and nothing on it is typed by hand.
  • The grading engine derives hits, exact scorelines, Brier, RPS, goal MAE, goal-difference MAE and every calibration band from the ledger rows and results[].
  • Baseline Brier scores are computed by scoring each naive strategy over the same fixtures, so they cannot drift from the model’s own denominator.
  • Agreement badges are derived from the captured favourite against the desk’s own scoreline, and a fixture with no line renders as unbenchmarked rather than as agreement.
  • Conviction flags derive from the same confidence number that drives the bar and the calibration band. One source, three views.
  • The simulated season re-runs from the same engine on every load, so a rating change moves the projection and the league totals together.
  • Each round adds rows and edits none: a lock appends to the ledger, a correction is a new row, results arrive in results[], and metrics, baselines, calibration and the log follow.
  • Where two models disagree, both numbers are held in the same row and flagged. Nothing on the page averages them, and from v1.6 the size of the disagreement sets the confidence interval on the affected card.
  • GW1 and GW2, which carry only a call and a confidence number, expand into a home/draw/away distribution by a fixed published rule; from GW4 the ledger carries the desk’s own split, and a GW3 call, which has none, is scored on direction only. Every Brier and RPS figure can be recomputed from what is printed.
  • Named scorers are filtered through the availability manifest at render time. A rejection is displayed on the card, not swallowed by the generator.
  • Static files plus one small Cloudflare Worker for /api/payload and /api/fpl-live; no build step. Design tokens and classes throughout (DESIGN.md), no hard-coded colours.