Methods

How every number on Footybot is made. Each one carries the method version it came from.

Footybot score: method

Method version: score-v0.1, with team ratings team-ratings-v0.1.

Code:

  • pipeline/src/footybot_pipeline/analytics/scoring.py
  • pipeline/src/footybot_pipeline/analytics/team_ratings.py

Run it with footybot score. Outputs go to data/analytics/.

What the number means

What it says. The Footybot score measures how well a player played compared with an average player in the same position (G, D, M or F) in the top five European leagues.

The adjustment. The player's output is first adjusted to what it would have been against an average Premier League opponent at a neutral ground.

The scale:

  • 50 = that average player.
  • Every 10 points = one standard deviation of a full match.
  • A match at 70 is two standard deviations above average, roughly the best 2–3% of full matches by top-five-league players in that position.
  • Scores are clipped to 0–100.

Combining matches. Every match, week and season uses the same scale. A longer period is the minutes-weighted average of its matches, so season scores sit closer to 50 than single matches do. On 2026/27 data, the best players' season scores are in the 65–78 range.

Two numbers per row:

  • score: everything, including finishing (goals scored beyond the xG of the chances).
  • underlying: the same without finishing. It is the steadier measure, and the one predictions and the breakout watchlist should build on.

Steps

1. Team ratings (opposition strength)

The model. A team's xG in a match is Poisson with mean exp(mu + eta·home + att_team − def_opponent).

  • Goals replace xG for matches without xG, such as UEFA qualifiers.
  • Each calendar month gets its own fit over the previous 365 days, with a 180-day half-life.
  • Teams are shrunk toward their group's average with 3 pseudo-matches. A group is the tracked league the team plays most in, otherwise "other" for its region.

Levels relative to the Premier League:

  • A group is linked to the Premier League when it played at least 6 time-weighted matches against other groups, and a chain of group pairs with at least 2 matches each joins it to the Premier League. Linked groups are expressed relative to the Premier League average (network_ref = "Premier League").
  • A group that is not linked in a given month takes its level from the nearest month in which it was (borrowed:<month>). A handful of qualifiers can't set a league's level.
  • Brazil and South America never meet European clubs in the data. They are anchored by assumption: the average Brasileirão team equals the average Liga Portugal team (anchor:…). This is a placeholder until M4 estimates it from players who moved between the leagues.
  • National teams are relative to the average national team.

League levels, October 2026 (attack + defence, Premier League = 0): Bundesliga −0.02, LaLiga −0.19, Ligue 1 −0.30, Serie A −0.39, Belgium −0.46, Turkey −0.58, Netherlands −0.65, Portugal −0.68, Norway −0.69, Championship −0.85, Scotland −0.87.

2. Adjusting for the opposition

Each stat is multiplied by a factor that turns it into output against an average Premier League opponent at a neutral ground. Factors are clipped to 0.4–2.5.

Stat type Factor
Attacking (xG, xA, key passes, progression) exp(def_opponent − venue·eta/2): less credit against a weak defence or at home
Defensive actions exp(att_opponent − venue·eta/2) × (typical team touches ÷ opponent's touches, clipped 0.5–2): more credit against a strong attack, less when the opponent had more of the ball to lose
Possession losses exp(−def_opponent + venue·eta/2): losses against a strong press count less

venue is +1 at home, −1 away and 0 on neutral ground.

3. Components

Each stat becomes (value − position mean per 90 × minutes/90) ÷ position SD per 90. That is standard deviations above the position average, accumulated over the minutes played: 90 minutes at +1 SD contributes 1.0, and 45 minutes contributes 0.5. A component is the average of the stats the match recorded.

Component Stats
shooting xG
creating xA, key passes
progressing accurate passes in the opponent's half, progressive carries, successful dribbles, accurate crosses
retention possessions kept: the position's loss rate per touch × touches − adjusted possession losses
defending tackles won, interceptions, recoveries, blocks, clearances
aerial aerial duels won
finishing goals − xG (in score only)
shot stopping (G) xGOT faced − goals conceded, own goals removed, shared by minutes on the pitch
save rate (G) saves − reference save rate × shots on target faced; used only where a match has no xGOT
claiming (G) good high claims, punches
sweeping (G) accurate sweeper actions
distribution (G) accurate passes, accurate long balls

4. Weights and scale

The v0 weights are judgment calls. M4 replaces them with weights fitted to VAEP per position.

Position Weights
F shooting .30, finishing .20, creating .15, progressing .15, retention .10, defending .05, aerial .05
M progressing .25, creating .20, retention .15, defending .15, shooting .10, finishing .10, aerial .05
D defending .35, progressing .20, retention .15, aerial .15, shooting .05, creating .05, finishing .05
G shot stopping .70, distribution .15, claiming .10, sweeping .05

Missing components. When a component is missing for a match (for example no xG), the remaining weights are rescaled to sum to 1.

Scaling. The weighted sum is centred and scaled per position on the reference pool, so the pool averages exactly 50 with an SD of 10.

Mistakes subtract fixed amounts, in SDs of a full match:

  • error leading to a goal: 1.0 (10 points over 90 minutes);
  • error leading to a shot: 0.3;
  • red card: 1.0.

5. Weeks, seasons, aggregation

  • Contribution: each match's contribution is minutes/90 × rate.
  • Week or season score: 50 + 10 × Σ contribution ÷ Σ minutes/90.
  • Week: a Footybot week runs Tuesday 00:00 to Monday 23:59 UTC, so Monday-night games belong to the weekend round.
  • Season scores are per competition season. A combined all-competitions season comes with M3.

Data handling

  • Fields BSD added in 2026/27 (passes by half, carries, progressive carries, good high claims, sweeper actions, xGOT) come back as 0, not null, for older matches. A match has them only if some player in it has carries above 0. Otherwise they are treated as missing.
  • xG, xA and xGOT are missing where BSD's source did not model the match. Shooting, finishing and shot stopping are then left out for that match.
  • Positions are BSD's current G/D/M/F for each player, applied to all of their history. Player-matches without a position aren't scored yet (25,110 on 2026-10-22, mostly players who have left the tracked squads). They fill in as profiles are fetched.
  • Reference pool: top-five-league matches of 60+ minutes from 2025-07-01 on. It is open-ended because the fields BSD added in 2026/27 have no earlier reference. Baselines are refitted every run and written to score_baselines with their row counts, so past scores can move slightly as the pool grows. They will be frozen per method version once the history backfill is complete.

Outputs

Table Grain Key columns
team_ratings month × team att, def, group, network_ref, home_adv
score_baselines position × stat mean and SD per 90 of each stat; composite centre and scale
player_match_scores player × match score, underlying, contribution, comp_* (component rates per 90), opponent ratings and factors, network_ref
player_week_scores player × week matches, minutes, score, underlying, comp_*
player_season_scores player × competition season same

Validation (2026-10-22, 125,546 scored player-matches)

Calibration. The reference pool averages 50.0 with an SD of 10.0–10.1 for every position.

League averages (60+ minute matches): Bundesliga 52.1, Premier League 51.0, Champions League 50.7, LaLiga 50.1, Ligue 1 48.7, Europa League 48.5, Serie A 48.1, Belgium 47.2, Brasileirão 46.3, Netherlands 46.1, Turkey 45.6, Portugal 45.4, Norway 44.5, Conference League 44.5, Scotland 42.8, Championship 41.0.

2026/27 leaders (600+ minutes):

  • forwards: Kane, Hauge, Mbappé, Saka, Thauvin, Malen, Haaland;
  • midfielders: Güler, Olise, Yamal, Raphinha, Pavlović, Kimmich;
  • defenders: Upamecano, Kabak, Mittelstädt, Chabot, Vušković, Cubarsí.

The breakouts named in PRODUCT.md, the season before their big moves (2025/26, rank among the same position with 450+ minutes):

Player League Score Rank
Christos Tzolis Pro League 63.4 #1 of 108 midfielders
Yan Diomande Bundesliga 60.4 #3 of 67 forwards
Nico Paz Serie A 54.2 #10 of 142 midfielders

This check is in-sample, with today's positions and borrowed 2025/26 league levels. It is encouraging but not a backtest.

Against BSD's own match rating (correlation over full matches): goalkeepers 0.79, forwards 0.68, midfielders 0.52, defenders 0.34. The defender gap is a difference in what is measured. BSD's defender rating follows the team result: it correlates −0.57 with goals the team conceded. Footybot's defender score counts the player's actions and is unrelated to goal difference (−0.04).

Known limitations and next versions

  • Team defence: good defending also shows in what the opponent did not create, which action counts miss. Candidate for v0.2: opponent xG against expectation, shared by the defenders and goalkeeper on the pitch.
  • Weights are judgment calls until calibrated to VAEP (M4).
  • Positions are four coarse groups taken from the current profile. Full-backs and centre-backs, or holding and attacking midfielders, share one baseline. Lineups (one request per match) would allow finer roles.
  • Penalties are inside xG and goals, because BSD does not separate them.
  • The opposition adjustment shifts levels; it does not shrink the spread of performances in weaker leagues. A league-translation estimate from players who move between leagues (M4) will test and correct this.
  • 2025/26 league levels are borrowed from October 2026 until the 2025/26 European matches are archived. That is expected in the next daily runs.

Expected form

Player pages show each week's score beside what was expected of the player (method form-v0.1). The expectation before a match is a decaying average of the player's own earlier match scores, with a half-life of eight matches and each match weighted by its minutes (up to 90), pulled toward 50 as if six full matches at 50 came first. A player's first match is expected at 50. Match scores already account for the opponent, so the expectation needs no opponent term. A week's expectation is the minutes-weighted average over its matches. "Above" or "below expectation" on a player page is the minutes-weighted gap over their last ten weeks with 60+ minutes; within 2 points counts as level.

Breakout watchlist: method

Method version: breakout-v0.1.

  • Code: pipeline/src/footybot_pipeline/analytics/breakout.py.
  • Run: computed by footybot score, as of the latest match date in the data.
  • Output: data/analytics/breakout_watchlist.parquet and the Postgres table breakout_watchlist.

Status: a first version under review with the user. What counts as a breakout is still an open decision in PRODUCT.md, and every threshold and weight below is meant to be argued with.

The idea

A breakout is a player whose performance is already high and rising, while the world hasn't noticed yet. Not noticed means two things: few people read about them, and the market still prices them low. The watchlist ranks players on that gap, not on performance alone, so already-famous stars sink and unknowns who play like them rise.

There is no age limit: late bloomers count (Tzolis broke out at 24). Age still enters the expected attention and value below, because the market prices age.

Who is eligible

All three of:

  • 900+ minutes in the last 365 days, across all tracked competitions;

  • an underlying score of 50 or more over those minutes;

  • not already noticed: below the top tenth of regulars (every player with 900+ minutes) on all four fame measures. On 2026-10-21 the lines were:

    Fame measure Top-tenth line
    English Wikipedia views, last 30 days 19,500
    Best calendar month of views in the last four years 180,000
    Wikipedia language editions with an article on the player (Wikidata sitelinks) 44
    Market value €32M

The score floor of 50 means at least the level of an average top-five-league player in the same position, after the opposition adjustment (docs/scoring.md). So a teenager in Norway qualifies only if their output, scaled for Norwegian opposition, matches an average top-five-league player.

Why the third rule exists. The gaps below can't keep famous players out by themselves. A star's fame is what their performance predicts, so their gaps come out near zero, and their performance z-score (Yamal's is about +5) outweighs everything else.

Why it uses four measures. Each one catches a different kind of fame:

  • Recent views catch a current star.
  • Peak month catches someone who was a story once (Mitoma, Ward-Prowse, a World Cup run) and is quieter now.
  • Language editions catch lasting international fame, such as Tah, Çalhanoğlu or Grimaldo. Their views and age-discounted value fall under the lines.
  • Value catches what the market already knows.

Players above any line have broken out already; a late bloomer has never been there, whatever their age.

Signals

Signal Definition
performance underlying score over the last 365 days, minutes-weighted
momentum underlying score over the last 10 weeks minus the 365-day level; weighted by how much of those weeks the player played (full weight at 450+ minutes)
minutes rise share of the team's available minutes played in the last 10 weeks, minus the share before that. Counts only matches of teams the player appeared for.
attention English Wikipedia views in the last 30 days (users, all access), summed over the article's title and every title that redirects to it, so views from before a page move count. 0 when the player is linked to Wikidata but has no English article; unknown when the player isn't linked yet
value BSD market value (the latest snapshot)

Each row's club and league come from the player's latest league match. Failing that, they come from the latest cup or continental match, and from a national-team match only as a last resort. So a list run just after an international break still shows clubs.

Attention gap and value gap compare a player's attention and value with what similar players usually have:

  • What it's fitted on: a linear fit over every player with 900+ minutes (for attention, only those linked to Wikidata).
  • Inputs: performance, age, age², recent minutes share and position.
  • Outputs: attention_gap = log₁₀(views + 1) − expected, and value_gap = log₁₀(value) − expected.

A gap of −0.5 means about 3× less known, or 3× cheaper, than similar performers. League and club are left out of the fit on purpose. A star of a smaller league should show up as underpriced, because moving up is the breakout.

Index and ranking

Each signal becomes a z-score within the eligible group. The index is their weighted sum:

Signal Weight
performance +1.0
momentum +0.5
minutes rise +0.3
attention gap −0.5 (less known ranks higher)
value gap −0.5 (cheaper ranks higher)

In the index, each gap counts up to 10× either way: beyond that it's more often a data quirk than a signal (a stale €145k value for a Ligue 1 starter, a goalkeeper with no English article). The stored gaps aren't clipped.

A missing signal counts as average (z = 0), so a player whose attention is unknown ranks below an equal player confirmed to be little read. Each row keeps every signal plus a one-line reason, for example:

underlying 63 over 1,840 min in the last year; up 6 in the last 10 weeks; 1,200 English Wikipedia views in 30 days (3.1x less than what similar performers get); valued €8.0M against €22.0M expected for this performance

Data behind it

Signal Source Notes
Scores BSD match stats via docs/scoring.md
Links to Wikipedia and Transfermarkt Wikidata (CC0) matched on birth date plus name (pipeline/src/footybot_pipeline/identity.py); fixes go in pipeline/overrides/player_links.csv
Views Wikimedia pageviews API (CC0) one backfill per title from July 2017, then refreshed weekly; each article's redirects (MediaWiki API) are reread every 4 weeks
Language editions Wikidata sitelinks as of the latest Wikidata fetch
Value BSD squads a snapshot per weekly refresh, so value history builds up from 2026-10-22

Known limitations

  • The ranking inherits score-v0.1's league lean. Bundesliga regulars score higher than the other top-five leagues, most of all defenders:

    League Mean defender score Regulars scoring 60+
    Bundesliga 53.1 8.9%
    Premier League 50.1 3.3%
    LaLiga 49.7 4.0%
    Ligue 1 48.9 2.2%
    Serie A 47.8 0.9%

    So 16 of the top 30 are Bundesliga players and 19 are defenders. Whether that is real or an artefact of the data or the scoring is a question for score-v0.2 (docs/scoring.md), not something to patch here.

  • English Wikipedia only for views. A player famous at home but with no English article reads as little read. The language-editions count softens this for players with articles in many languages.

  • A title that held another page. Views are summed over an article's title and its redirects, so old titles keep their views from before a page move. But when an article moves onto a title that held a different page, usually a disambiguation page, that page's earlier views count too. For Vitinha (plain title since June 2026) the disambiguation page added 150 to 3,000 views a month to the 4,000 to 19,000 his article had in 2021/22.

  • BSD values can be stale. Nagida (Rennes, Cameroon international) is valued at €145k. The 10× gap cap limits the damage, and the reason line shows the number so it can be checked. A second value source (transfermarkt-datasets, M4) will catch these.

  • Only today's language editions and value are stored, so backtests leave both out (below).

  • History starts in July 2025 (top leagues; others from July 2026), so backtests can only start in late 2025.

  • Value is one source's current estimate. There's no value trend yet: that needs the snapshot history to build up, or transfermarkt-datasets (M4).

  • Weights and thresholds are judgment calls. M4 replaces them with a model trained on past breakouts, labelled from value jumps and moves to bigger clubs, and backtested.

  • Positions are BSD's coarse G/D/M/F, as in scoring.

Validation

Run as of 2026-10-21

  • Coverage: 3,089 regulars, 98% of them linked to Wikidata. 520 players are eligible, with a median age of 27.6. The top 30 has 6 players under 24, 6 aged 24–27 and 18 aged 28 or over.

  • Top ten:

    1. Jeff Chabot (Stuttgart)
    2. Ozan Kabak (Hoffenheim)
    3. Maximilian Mittelstädt (Stuttgart)
    4. Loïc Badé (Leverkusen)
    5. Robin Koch (Frankfurt)
    6. Willi Orbán (Leipzig)
    7. Massire Sylla (Union SG)
    8. Aleix García (Leverkusen)
    9. Jens Petter Hauge (Bodø/Glimt)
    10. Waldemar Anton (Dortmund)

    Eight of the ten are Bundesliga players; see the league lean above.

How the rules changed the list

Draft Top of the list
Age 23 or under, no "already noticed" rule stars: Güler, Yamal, Pavlović, Cherki
Age 23 or under, with the rule (recent views and value only) Sylla, Querfeld, Wallace (Grêmio), Weidmann, Freeman
No age limit, recent views and value only Chabot, Kabak, Mittelstädt, Badé, then famous veterans in the top 25: Tah, Çalhanoğlu, Thauvin, Grimaldo, Schick
No age limit, all four fame measures the run above

Reference players

PRODUCT.md names three players as the kind the list should catch. All three have broken out by now: Diomande is valued at €132M, Paz at €86M, and Tzolis is 24. So the test is a backtest within 2025/26:

  • Data used: scores and views as they were on each date.
  • Market value left out: only today's value exists, and it would leak the future.
As of Tzolis Nico Paz Yan Diomande
2025-11-30 #107 of 209 already noticed (51k views) under 900 tracked minutes
2025-12-31 #35 of 323 already noticed (47k views) under 900 tracked minutes
2026-01-31 #25 of 408 already noticed (59k views) already noticed (76k views)
2026-03-31 #9 of 480 already noticed (62k views) already noticed
  • Tzolis climbed the list through the season while still little read. Under the earlier age-23 rule he dropped off in January 2026.
  • Paz was well known throughout, as PRODUCT.md says of him.
  • Diomande was missed. His Leganés half-season (spring 2025) is before the data starts, and by the time he had 900 tracked minutes at Leipzig his article had 70k+ views a month. Catching him needs either earlier history or a lower minutes floor early in a player's spell in the tracked leagues.

Open questions for the user:

  • the league lean: fix it in score-v0.2 first, or review the list as is;
  • the minutes floor;
  • the "already noticed" line: the top tenth, or a fixed number.

League translation

Footybot's scores are adjusted for the opponent, using team ratings that link leagues through European and cup matches. If that adjustment is right, a player who moves to another league should keep roughly the same score. This checks whether they do (footybot translation, analytics/translation.py).

Method. Each player's main league season (900+ minutes) is paired with their main league season the next year. Players who stayed in the same league show how scores move anyway: a fitted line on their starting score (stronger players drift down, weaker ones up). A mover's excess is their change minus that expected change. A league whose arrivals keep scoring below expectation is rated too strong for where they came from; above, too weak.

First reading (23 October 2026, seasons ending in 2025 against those ending in 2026: 243 movers, 1,203 stayers). Leagues with fewer than 8 movers are left out.

Into Movers Excess (points)
Premier League 48 +2.5 ± 0.7
Championship 47 −8.2 ± 0.9
Süper Lig 28 −3.1 ± 0.7
LaLiga 26 +0.6 ± 0.7
Serie A 25 −1.4 ± 0.9
Bundesliga 19 +0.4 ± 0.9
Ligue 1 19 −0.4 ± 0.8
Brasileirão Série A 10 −0.8 ± 0.9
Liga Portugal 8 −1.0 ± 0.9
Eredivisie 8 +2.0 ± 1.2
Out of Movers Excess (points)
Premier League 67 −3.2 ± 0.7
Ligue 1 47 +0.1 ± 0.8
Serie A 37 +0.2 ± 0.8
LaLiga 28 +0.7 ± 0.7
Bundesliga 25 −2.0 ± 1.3
Liga Portugal 24 −4.2 ± 1.0
Süper Lig 14 −3.5 ± 1.6

(± one standard error.)

What it suggests.

  • The Championship looks rated too strong: players who move into it, mostly relegated with their Premier League sides, score about 8 points below expectation. Its level is set through few cup matches against Premier League sides. Relegation also changes a player's team and role, so some of this is not the league's level.
  • Players arriving in the Premier League score a little above expectation, which suggests the adjustment slightly underrates it.
  • Players leaving Liga Portugal and the Süper Lig score 3 to 4 points below expectation, which suggests those leagues are rated slightly too weak, so their players' scores flatter them a little.
  • Moves among the other big-five leagues sit within about a point of zero.

Not yet used in scoring. One season pair is a small sample, and a move changes more than the league. Once a second season pair is in, a consistent gap will feed back into the team ratings, starting with the Championship and the Brazil anchor (ARCHITECTURE.md section 5).

Market values

Footybot shows three market values for a player, and keeps them apart because they are different estimates.

Current estimate

The value BSD reports for the player, recorded each day a squad or profile is fetched (squads refresh weekly). It is an estimate in the Transfermarkt style but not Transfermarkt's own: on 23 October 2026, BSD's latest value matched Transfermarkt's latest valuation exactly for 2% of linked players, with a median ratio of 1.0 and the middle half between 0.92 and 1.09. History builds up from late October 2026, when Footybot started recording it.

Transfermarkt

Transfermarkt's own valuations, from transfermarkt-datasets (CC0), joined to Footybot players through the Transfermarkt ID that Wikidata records. The dump stopped updating in July 2026, so this is history: valuations from July 2015 to early July 2026.

On a player page each source is its own line, and any up or down change is measured within one line, never from one source to the other.

Footybot's prediction (experimental)

The value a year from now, from method value-v1: a ridge regression on the change in log Transfermarkt value over the next 12 months.

Inputs, as of the prediction date, all from the 180 days before it:

  • the current value (log scale; lower values for the level tend to rise);
  • age, with bends at 22, 26 and 30, since values rise into the mid-twenties and fall after;
  • position;
  • minutes played;
  • Footybot score and underlying score (minutes-weighted), the change from the 180 days before, and form among under-24s;
  • the strength of the player's main team (attack plus defence rating).

Training. One row per player per month end that has 180 days of Footybot scores before it and a Transfermarkt valuation a year later (within 60 days). Scores start in July 2024 and the valuations end in July 2026, so the first fit has 11,289 rows from seven month ends, January to July 2025. The training set grows by itself as the history backfill reaches older seasons.

Not yet tested. Seven overlapping month ends can't be split into a training period and a later test period without the same moves appearing in both, so there is no backtest yet. It will be published here once the backfill allows one. Until then, treat the predictions as a reasoned starting point, not a forecast with a known error.

Reasons. A linear model adds up its parts, so each prediction shows the two feature groups that pushed it up most and the two that pushed it down most: age, form, minutes, team and league, the current value for the level, and position.

The mispriced lists.

  • Underpriced: predicted to rise 20% or more, with 900+ minutes and a score of 50 or better in the last 180 days, so the list is about players playing well, not only cheap young ones.
  • Overpriced: predicted to fall 15% or more, with 900+ minutes and a current value of at least €5M.

Goalkeepers are left off both lists for now: v1 adjusts for position with one number per position, which doesn't fit how keepers' values move. Their player pages still show the prediction.

"Now" is the current estimate where BSD has one, otherwise the latest Transfermarkt valuation.

Matchday

Predictions before each match

Every match gets a prediction from the team ratings fitted the month before it, so a match never predicts itself (method predictions-v0.1).

Each side's expected xG is base × exp(home advantage × venue + attack − opponent's defence). Ratings are relative to an average Premier League side; base is that side's xG per match, recovered each month from the Premier League matches the month's ratings were fitted on. Venue is +1 at home, −1 away and 0 on neutral ground.

Goals are then treated as independent Poisson counts (up to 15 each), which gives the chance of a home win, a draw and an away win. A match page shows those chances with the result that happened outlined, the expected and actual xG, and a plain reading of whether the chances backed the result (a margin of half an xG counts as clear).

The week's picks

Each Footybot week (Tuesday to Monday) gets up to ten finished matches worth a closer look, at most three from one competition, by a fixed rule so the reasons can be shown:

Reason When
Two strong sides both sides rated at least as strong as an average Premier League side the month before
Against the odds the prediction gave the result that happened less than 25%
Chances everywhere 3.5 xG or more between the two sides
Standout a player scored 85 or more on Footybot's 0-100 scale
Breakout watch a top-50 player on the breakout watchlist played 45+ minutes
Scotland's top three one of the three highest-rated Scottish Premiership sides played (PRODUCT.md: the emphasis in Scotland is on them)

Matches are ranked by an interest score: how strong the weaker side was (against the rest of the week), plus 1.5 × how unexpected the result was (1 minus its predicted chance), plus 0.5 × total xG / 3, plus 0.8 for a standout, 0.8 for a watchlist player and 0.5 for Scotland's top three.

Transfers

The transfers hub lists every move of a player Footybot tracks, merged from three sources.

Source What it gives Dates
Transfermarkt history (transfermarkt-datasets, CC0) the move's date and fee; a fee of 0 is a free transfer or a loan (the data doesn't say which), a missing fee is undisclosed from July 2021 to early July 2026
BSD squads a change of club between two fetches of the player's current club ("Seen in squad"), usually within a week of the move since Footybot began fetching squads
Matches a change of club between a player's two consecutive club matches, dated by the first match for the new club ("First match"); national teams aside every match Footybot has

One row per move. The same move seen by several sources (same player, same new club, within 150 days) keeps the most informative row: Transfermarkt (it has the fee), then squads (the earliest sighting), then matches. A squad sighting that flips to another club and back within 30 days is treated as a stale squad list, not a move.

Clubs. Transfermarkt's club names are matched to Footybot's teams by name, without accents, punctuation and words like FC or Club; a club that doesn't clearly match one team keeps its name without a link.

Windows. Summer covers 1 June to 15 September and winter 15 December to 15 February, which holds most European leagues' windows. Brazil and Norway run their own calendars, so their moves show under whichever window they fall in.

Last season. Each move shows the player's score in their last season before it with 450+ minutes, in the busiest competition, on the 0-100 scale.

Alerts. New moves are published as an RSS feed at /transfers.rss, which any feed reader can follow.

The assistant

The Ask page answers questions from Footybot's data and nothing else (PRODUCT.md principle 6).

How it answers. A small open model, GLM-4.7 Flash on Cloudflare Workers AI, is given a fixed set of read-only tools and nothing more:

  • find players and teams by name;
  • a player's profile, seasons, recent form, last matches, market values and watchlist place;
  • a team's rating, latest results with xG and best performers;
  • the best players this week or this season, by position, league and age;
  • the breakout watchlist;
  • moves in a transfer window;
  • the week's matchday picks.

It can call up to four rounds of tools before answering, and it is told to answer only from what they return and to say when Footybot doesn't have something. There is no free-form database access. Every player, team or page a tool used comes back as a link under the answer, so it can be checked.

Limits. Each visitor can ask 15 questions an hour, and everyone together 150 a day, which keeps the assistant inside Workers AI's free daily allocation. Visitors are counted by a salted hash of their IP address, kept for seven days; the address itself is never stored.

It can be wrong. A small model can misread a question or a table. The linked pages hold the numbers it used.