All notable changes to this Actor.
Fourteen further audit rounds after 0.3.11 — twenty-five in all, plus my own
passes between them. Every defect below was fixed and verified against live
data or a runnable check.
Rows that were paid for and never delivered. A row held back by the result
cap was recorded as if the buyer had it, so no later run would ever offer it
again. The same went for a row the platform dropped from a truncated write, and
for an expiry that did not fit the cap. Delivery is now taken from what the
platform reports it accepted, and only that is recorded.
Rows delivered twice. A worker migration restarts a run from the beginning;
nothing recorded what had already been sent, so those rows went to the dataset a
second time and were charged a second time. A run now resumes: it skips what it
delivered and counts it toward its own cap. A run cut short by the deadline
guard, by an exception, or by an abort persists the same way.
Baselines that split for no reason. Reordering the same promotions, listing
one twice, writing "UFC" instead of "ufc", pasting the same fighter URL with and
without the name on the end, typing a filter the selected mode ignores, or
leaving a typo in the list — each landed on a different saved baseline and
charged for the whole universe again.
Numbers that were wrong. A bout whose status could not be read was published
as unfinished rather than unknown. A run that lost part of a read banked the
thin version as the truth. The connected app and every table export named the
wrong opponent on half the rows.
Copy that promised more than the data holds. Odds coverage is stated season
by season from the measurement, in the input, the README and here.
Six more audit rounds on top of 0.3.3 — ten in all, plus my own passes between
them. Thirty further defects, each fixed and verified against live data or a
runnable check.
Rows that went missing. A search with a selective filter stopped early. The
source applies the filter before returning, so a page can legitimately come back
with nothing on it; the guard that ends a blocked search counted those as
blocked. Measured on a gym filter: 41 rows delivered before, 44 after.
Numbers that were wrong. A row whose own reads lost something was banked as
the truth — delivered as a change with numbers missing, and charged again when
they came back. Rows now carry readIncomplete, attributed per row rather than
per run, so one row's failure cannot mark its neighbours. Career defence voids
its whole measured block on any failure inside that fighter's own log rather
than publishing an average over a denominator it knows is short. The connected
app and every table export named the wrong opponent on half the rows.
Bills that were wrong. Reordering the same promotions, listing one twice,
writing "UFC" instead of "ufc", pasting the same fighter URL with and without
the name on the end, or leaving a typo in the list — each of these landed on a
different saved baseline, so the next run found nothing to compare against and
charged for the whole universe again. The identity now describes the universe a
run actually fetches. Turning odds or career defence on no longer looks like a
different universe either.
Alerts that said nothing. Telegram, Discord, Slack and WhatsApp rendered
every record as "(untitled)" with no link. Fight alerts now name both fighters.
A market move that read wrong. American odds jump from -100 straight to
+100, so a line that moved from -138 to +110 reported as +248 when the market
had moved six points of probability. impliedProbabilityMovement sits beside it
and is continuous across the gap.
Two more audit rounds on top of 0.3.0. Twenty further defects, all fixed and
each verified against live data or a runnable check.
Numbers that were wrong. A missing height, reach or weight was published as
0 — not a short reach, but no reach, and it dragged every roster average down.
Two unrelated completed cards with the same bout count hashed identically, so
the duplicate filter dropped the second one.
Numbers that went stale. The tracked signature is now taken over the whole
delivered row rather than a hand-kept list of columns, so a correction to any
column marks the record UPDATED. Reordering the same promotions or seasons no
longer starts a fresh baseline and re-delivers everything as new.
Runs that returned the wrong thing. A pasted card URL in fighters mode
returned bout rows under fighter column mappings; each mode now states which
kinds of URL it can read. An unknown promotion was reported as skipped and then
requested anyway, and the rejection suppressed expiry for the promotions that
were valid.
Runs that could hang or over-run. The internal read deadline was counted one
attempt short and ignored the body timeout, so the outer deadline could abandon
a live read; the two are now checked against each other. A page whose contents
all failed to load counted as progress and kept the pager queueing more. The
request ceiling is now held by construction rather than by scheduling order.
Store copy. The examples advertised keyword search and contact fields that
this actor has never produced, the input summary described a detail page that
does not exist, and odds and career defence were filed under Filters where
nobody would look for them.
Hardening pass. Three independent audits of the derived numbers and the run
loop, plus my own passes between them, found 34 defects. All are fixed, and
each was verified against live data or a runnable check.
Nothing in the output shape changed except where it was wrong.
Numbers that were wrong. Career defence read only the first page of a
fighter’s bout log, so anyone with more than 25 logged bouts was measured on a
truncated career. The no-vig probabilities are computed from exact values and
one corner is now the complement of the other, so a pair sums to exactly 1
across every line combination tested. A failed read inside career defence voids
the whole measured block instead of publishing a biased average. Decimal odds
are derived when the source omits them, and a level market is reported as a
pick-em rather than marking both corners the underdog.
Numbers that went stale. A corrected statline, a re-priced market or newly
fetched career rates now mark a record UPDATED; they were classified UNCHANGED
and suppressed. A rolling date window no longer opens fresh incremental state
and re-delivers the universe.
Runs that lied. A run where every search failed to read anything now fails
instead of reporting an empty search, and releases its state lock. Coverage is
never reported complete after a truncated scan. An incremental result limit
counts rows delivered, not rows read, so a warm "give me 50 new" run no longer
returns nothing when the first 50 records happen to be unchanged. Expired rows
count against the result limit instead of being pushed past it.
Robustness. In-flight requests are capped for the whole run rather than per
call site — nested fan-out had reached 293 concurrent — and a 403 or 429 halves
that ceiling for the rest of the run, with retries backing off over ~15s.
Contract. The listing no longer advertises email, phone, URL or
social-profile extraction: those fields never existed. Compact mode keeps
recordType and the core of every record type rather than gutting three of the
four. The connected-app export follows the selected mode. Every rate in the
output is a fraction 0-1; counts per minute are not, and the docs now say so.
Sportsbook odds (includeOdds). Money line per fighter — open, close and
current — plus the implied win probability with and without the bookmaker
margin, line movement, the best price across every book on the bout, how far
the books disagree, favourite or pick-em, and the rounds over/under. This is a
HISTORICAL archive, not a live feed, and not a complete one: sampled
2026-09-01, ESPN priced 20-24 of every 21-24 bouts checked in 2020-2025, about
half of 2019, and none at all in 2018 or 2026. Closing lines exist only for
2024-2025.
Three feed defects are handled rather than passed through: in-play books are
excluded, quotes with the two corners swapped are rejected against the bout
median, and the favourite is read off the prices because ESPN's own flags mark
both corners underdog on a level bout.
Breaking — every rate is now a fraction 0-1. ESPN publishes per-bout
accuracy as 0.33 and career accuracy as 61.54 under the same name. Carrying
both scales in one dataset is how a model ends up a hundred times off, so the
career figures are converted.
Breaking — renamed fields:
| was | now | why |
|---|
careerKoPercentage | careerKoTkoRate | ESPN reported KO and TKO identically on 60 of 60 sampled fighters |
careerTkoPercentage | (removed) | byte-identical to the column above |
careerDecisionPercentage | careerDecisionRate | now a fraction, so "Percentage" would have lied |
careerStrikesAbsorbedPerMinute | measuredStrikesAbsorbedPerMinute | measured over the bout log, not ESPN's own career basis |
careerStrikeDefense | measuredStrikeDefense | same |
careerTakedownDefense | measuredTakedownDefense | same |
careerDefenseBouts | measuredBouts | same |
Added alongside those: measuredStrikesLandedPerMinute,
measuredStrikeDifferentialPerMinute and measuredMinutes. Landed and
absorbed are now computed from both statlines of the same bouts, so their
difference rests on one denominator instead of two.
Verification. npm run audit:invariants checks 34 identities against live
data — position totals reconstruct ESPN's own significant-strike totals, no-vig
probabilities sum to 1, absorbed equals the other corner's landed, the career
record string matches its W/L/D columns.
First release.
- Four record types in one dataset, selected with
mode and labelled by
recordType: fights (one row per fighter per bout), fighters, rankings and
events.
- 42 measured stats per fighter per bout — strikes by target and position,
takedowns, advances, reversals, submissions and control time — for every bout
ESPN scored, back to 1994.
- Career rates and record breakdown on fighter rows: strikes landed per
minute, striking and takedown accuracy, KO/TKO and decision shares, and wins,
losses, draws, no contests, title wins and finish counts.
- Career defence (
includeCareerDefense): strikes absorbed per minute,
striking defence and takedown defence, computed from the opponent side of
every bout in the fighter's log. ESPN publishes no defensive career stat.
- Per-bout defence on every fight row — what the opponent landed is what
this fighter absorbed, so it needs no extra request.
- Referee, venue with state, and a flat
location string.
- Filters: date range, weight class, gender, country, stance, gym, minimum
wins and losses, title fights only, method, result, name search, active only.
- Start URLs: paste an ESPN fightcenter or fighter URL.
- Incremental mode with NEW / UPDATED / UNCHANGED / EXPIRED labels. A date
window marks coverage incomplete, so a narrowed run never expires records it
did not look at.