All notable, consumer-facing changes to the Biotech Catalyst Sentinel API are recorded here.
The format follows Keep a Changelog; the project uses
Semantic Versioning. The live API version is reported at
GET /v1/meta → api.version.
Deprecation policy: breaking changes are announced here and, for a retiring route, via
Deprecation + Sunset response headers at least 30 days before removal.
…"), and the advertised input schema carries each parameter's server default and an
explicit required: []. The parameters, bounds and prices are unchanged; the 402
challenge and the Bazaar listing simply describe the routes more completely.
https://biotechcatalystsentinel.com/terms.html — the licence touse the data, payment and refund terms, what is and is not promised about accuracy and
coverage, what the API logs, liability and governing law. Linked from GET /v1/meta
(api.terms) and openapi.json (info.termsOfService); the site footer links it too.
in the advertised row shape were declared "type": "<scalar>", "nullable": true —
OpenAPI-3.0 vocabulary that a JSON Schema 2020-12 validator ignores, so the schema
rejected the nulls in our own sample row and Coinbase's facilitator marked
GET /v1/events an *invalid discovery configuration* (/v1/catalysts/upcoming passed
only because its sample row carries no nulls). Nullability is now a type union
(["integer", "null"]), and a nullable enum lists null as a member. Prices, payTo
addresses, networks, paths and response payloads are unchanged; a test now validates
each route's example against its own schema exactly as the facilitator does.
listing and /.well-known/x402): the facilitator caps a description at 500 characters
and refuses verify/settle beyond it; both sat at 488. Same meaning, ≥50 characters of
headroom, pinned by a test. The facilitator's per-payment extension verdict
(EXTENSION-RESPONSES) is now recorded and watched, so a rejected refresh is seen on
the first payment rather than discovered from outside.
A verifiable track record, on Base Sepolia first. Every forward PDUFA date the feed
publishes is now EIP-712-signed as an EAS offchain attestation within the hour it is
published — BEFORE the FDA decides — and every scored decision is signed as a resolution
linked back to that claim. The signed ledger is anchored on-chain daily under one Merkle
root. No paid payload changes; the paid rows will carry their attestation UID in a later
release, after the mainnet flip.
GET /attest/verify?uid=0x… (free, key-less): the signed attestation JSON for one UID plusits anchor proof (Merkle root, anchor tx, inclusion proof — foldable from the response
body alone), and for claims the superseded_by / resolution_uid links, so a correction
or a resolution is always visible beside the claim it changed. pending_anchor: true
until the day's anchor runs. Unknown UID → 404.
GET /attest/accuracy/biotech-catalyst (free): ledger-derived counts — claims, resolved,unresolved, overdue (never hidden), corrections and correction rate, median lead time
from claim to outcome. Unknown feed → 404 (also the answer until the first claim exists).
security: [] for x402 crawlers; both are rate-limitedat the edge.
owner-gated step. Every verify response names the chain its anchor happened on.
eventType is always pdufa_date(a set date and an extension are revisions 0 and n of one chain); resolution outcomes
are approved · tentative · crl · withdrawn · retracted (the feed took the
record down — a retraction is disclosed, never deleted).
first sees it is counted but never signed. The first day's claims cover the forward
feed as of go-live; their lead-time statistics are honestly short.
The mechanism for the class 0.7.2's takedown belonged to. No public payload changes.
forward-feed catalyst whose decision the report has already scored against a real FDA
action (the FDA acted early; the sponsor never filed an outcome 8-K) is routed to the
operator review queue as a decided_without_filing flag. Raise-only by design — the
underlying join is fuzzy, so the takedown decision stays human, exactly as performed
for catalyst 131. Until now this class was invisible to every alarm and was caught
only by a blind audit, ~5 weeks late.
name for the filer's CIK and warn on divergence (warn-only) — the class behind the
catalyst-203 sponsor correction in 0.7.2, where a reviewer-typed string was validated
against nothing.
The fourth blind audit — seven lenses, including the first ever to actually pay for and
diff the paid payloads (customer score 9/10; the events cursor and lifecycle contracts
verified on the wire with real settled x402 calls). Every finding below was adjudicated
against code, the database, and primary FDA records before shipping; the fixes are the
audit's residue.
GET /v1/accuracy → unlisted_withheld_forward: when the FDA acts EARLY against apredicted date that has not yet passed, the decision counts in the statistics but its
row is withheld from both track_record and unlisted_scored_decisions until that
date passes (the forward-date protection). Two audit lenses independently recomputed
the within-window ratios from the enumerated rows and could not reconcile to the digit
— one scored decision was enumerable in neither list. The withheld count is now
published and unlisted_note states the full row arithmetic, so "check rather than
trust" closes to the digit again.
windows.action start now extends to the earliest decision actually on the record,mirroring the existing end-side rule: a prediction just inside the window start can
score an FDA action up to 180 days before it, and one receipt (predicted 2023-03-23,
decided 2022-12-19) sat before the stated start.
price on both paid routes (read accepts[].amount, never an example), and the
description now discloses that the discovery extension's method enum is SDK
boilerplate — the paid routes are GET-only.
like its neighbour tiles, now says where its exact figure lives.
records NDA220185 (JIDEYTRO) approved 2026-07-22 — 58 days ahead of the disclosed
2026-09-18 PDUFA date — and the sponsor filed no 8-K about the approval, so no filing
could ever resolve the record. The takedown publishes as a retracted event on
/v1/events, and the decision itself is the one behind unlisted_withheld_forward
above.
stored value had "and CymaBay Therapeutics, Inc." appended — a reviewer-typed apply
edit; every automated source (the filing text, the extraction, SEC identity data) was
verified clean. CymaBay has nothing to do with this application.
The scoring engine's supplement-admission rule was replaced the same day the live regime
shipped, and the daily regeneration means the recoveries served within hours. The old
rule required a supplement decision to sit on the one application the folder had already
anchored — a static proxy that blocked five real decisions *and*, on one folder, awarded
a prediction to the wrong sibling application. The new rule admits any
identity-and-sponsor-corroborated efficacy supplement and lets the scoring walk's own
competition arbitrate; the change was adjudicated per row against openFDA before deploy
(zero wrong matches introduced, zero rejections lost, the whole diff enumerated).
GET /v1/accuracy headline: n 192 → 204 (172 approvals + 32 rejections), median1.0 days held, within-30 90.2%. The twelve decisions that entered: three the old
anchor blocked (Ameluz Δ0, EYLEA 2023 Δ3, Sarclisa Δ7); five previously published
under ambiguous_applications that per-decision pairing now resolves (ZORYVE foam
Δ0, LIVMARLI Δ0, cabozantinib Δ8, ZORYVE 0.05% Δ9, Opdivo+Yervoy Δ10 — each
matching its own long-listed unresolved row); two Jemperli supplements the FDA acted
early on (Δ22, Δ54); VYVGART Hytrulo 2024 at Δ0 (previously *mis-paired* to a
sibling application at Δ−48 — the mis-pairing was caught in pre-deploy measurement and
never served); and Ryoncil at Δ+20 (an operator-verified repair: the stored row had
carried a late 8-K's filing date instead of the FDA's 2024-12-18 action).
an honest scoreboard must be able to move down when previously-invisible decisions
become scoreable.
Every published figure now derives from the live database. Since the project began,
coverage.*, recall_misses and unresolved_predictions were served from a frozen
2023–2025 study committed to the repository; the headline blended that artifact with live
decisions. As of this release the accuracy report is regenerated daily against openFDA
by the same engine that scores live decisions, and the payload says so:
coverage.baseline_source reads "live" with a generated_at stamp (the committed
artifact remains only as a disclosed fallback if a generation fails — the payload would
say "committed_fallback").
GET /v1/accuracy headline: n 198 → 192, within-7 74.75% → 72.92%, within-3091.41% → 90.10%; median (1.0 days) held.** The six decisions that leave the headline
are enumerated, not vanished: five sit behind one scoring-engine limitation (a
supplement decision on an application the folder has not yet anchored — EYLEA 2023,
Sarclisa 2024, VYVGART Hytrulo 2024, WINREVAIR 2025, Ameluz 2024; the fix is designed
and scheduled) and one (JOURNAVX, a Δ0 approval) is a disclosed extraction limitation —
a filing announcing two PDUFA dates currently yields one. The numbers fell because the
engine now serves only what it can derive and audit end-to-end; that is the honest
direction.
additional_rejections (now 5) with its SEC quote and filing link. Every other
rejection receipt carried over, including Pixclara's Δ1 CRL — recovered this release
from a disclosure the extraction had missed (the goal date was spelled in Australian
day-first form).
recall_addressable 0.539 → 0.575 and recall_floor 0.161 → 0.174 — both UP andboth now recomputed daily rather than frozen. unresolved_predictions falls 99 → 67
(32 rows the live engine resolves that the frozen study could not; the unrecorded
era-marker reason class empties and is retired from the served counts).
track_record rows change drug_name spelling only — the live catalyst's spellingsupersedes the study's (same decisions, same numbers).
GET /v1/catalysts/upcoming and GET /v1/events are byte-unaffected — the flipchanges how the free accuracy report is assembled, not what is sold.
The historical corpus now lives in the same database the engine runs on. Until today the
published figures were assembled from two places — a frozen 2023–2025 study and the live
system — and only the live half improved as the engine did. The frozen half has gone from
carrying 47% of the scoreboard to 20%, and the remainder is scheduled to follow.
GET /v1/coverage now reports the real depth of the corpus. first_seen andwindow.corpus_start move back to 2023-01-03 (from 2024-06-25) — roughly 14 months of
filing history becomes 2.5 years. Nothing about the query changed; we simply hold, and
can now honestly report, more.
GET /v1/accuracy figures moved on a structural change rather than a routineresolution.** decisions_matched 196 → 198, within-7 74.49% → 74.24%, within-30 91.33% →
90.40%; median (1.0 days) and rejection count (33) unchanged. Two decisions entered —
valbenazine (Δ2) and beremagene geperpavec (Δ0) — and four SEC-disclosed rejections we
previously could not verify now publish under additional_rejections.
⚑ The two percentage points down are honest and expected. Some newly visible catalysts
hold the FIRST date a company disclosed, while the filing that later extended that date is
still awaiting review — so those rows are scored against a superseded prediction. The
direction is conservative (we read as slightly *less* accurate than we are, never more),
every affected row is identified, and the figures should recover as those filings are
adjudicated.
track_record rows 160 → 186 whileunlisted_scored_decisions falls 37 → 13: twenty-four decisions that previously counted
toward the headline with no row of their own now carry a sponsor-attributed, checkable
receipt. On a page whose argument is *don't trust us, verify*, this is the change that
matters most.
GET /v1/events distinguishes history from change. Events created by the historicalimport are marked as backfill and excluded from the since-cursor stream. A change stream
exists to say *something moved just now*; a 2023 filing arriving in 2026 is history. Without
this a subscriber polling ?since= would have received 208 events from 2023–2025 as
though they had just happened. The events remain on each catalyst's timeline and are
unaffected for anyone reading history directly.
GET /v1/catalysts/upcoming is byte-identical — verified before, during and after.Historical catalysts are past-dated and cannot enter the forward window.
The last integrator ask from the S63 blind audit: the free accuracy document is now typed
in openapi.json instead of being an untyped object an integrator had to reverse-engineer
from a live call.
GET /v1/accuracy publishes a full response schema — 136 fields across 21 models,every one described, covering all three shapes the route can serve as an anyOf: the
full document, the ?summary=1 view, and the degraded {available: false} shape.
Branch on available first, then summary (the degraded shape carries no summary
key even when the parameter was passed).
**recall_misses and unresolved_predictions are OMITTED entirely — never served
empty — when the committed baseline predates those ledgers** (an empty table would read
as "we missed nothing"); a null unresolved_predictions.items[].reason is counted
under the key unrecorded in reason_counts, so joining the two needs that
substitution; and reason_counts/reason_notes are keyed by observed reason codes
only, so their key sets shrink with the data.
?summary=1's elision is documented as the recursive rule it actually is: anylist-valued items becomes items_rows at any depth — four places in today's payload,
two of them nested inside recall_misses.
through responses={200: ...} while the handler keeps returning its dict unfiltered, so
a schema that missed a key could never delete it from the wire. A test asserts the
declared shape and the served shape agree in both modes, and another asserts an
undeclared key still reaches the client.
recall_misses.items[].sponsor_match, closed byconstruction). source, match_tier and outcome are deliberately left as described
strings — enumerating an open vocabulary is what broke rejection events in 0.4.3.
The integrator-contract asks from the S63 blind audit: the payment surface is now
documented in the spec rather than reverse-engineered from a live call.
openapi.json on both paid routes, with a typed schema(PaymentRequiredOut / PaymentOption) for the x402 challenge and its
PAYMENT-REQUIRED header. It names the two facts most easily misread: amounts are
ATOMIC USDC units (6 decimals — 20000 = $0.02), and **two other bodies also use
402** and carry no accepts (challenge-not-built, settlement-failed) — branch on
accepts being present, not on the status alone. Neither charges you.
{"type": "object"} beside a hand-written field list: field names, types,
nullability and every vocabulary, derived from the response models so a new field
appears the moment it is served. (Deliberately trimmed of prose — the listing rides
in a response header with a hard size budget; descriptions live in openapi.json.)
/v1/meta carries a contact block: the address to report a wrong date, what arepair looks like when it lands (corrected on /v1/events, distinct from
date_extension — the FDA moving a date versus us having been wrong), and what to do
about a paid response lost in transit. The address was previously only in
openapi.json's info.contact.
*after* the handler produces the data but *before* the response reaches you, so a
dropped connection means the payment settled and you hold nothing. There is no
replay: the authorization you signed carries a single-use nonce, so re-sending the
same payment header is rejected as already-used rather than re-serving. A retry is a
new authorization at the normal price. Build against /v1/events (a resumable
cursor) where that matters.
instead of redirecting exactly as it does on success. (The JSON admin routes have
always returned 404 here.)
The per-ticker point lookup (operator-requested): both paid routes accept an optional
?ticker= server-side filter at the unchanged flat per-request price.
?ticker= on GET /v1/catalysts/upcoming and GET /v1/events — only recordsfiled by the ticker's issuer or exposing it as a co-filer in tickers[]
(case-insensitive): the exact membership the free GET /v1/coverage?ticker= counts
as upcoming_on_feed, rendered from one shared SQL predicate so the free count and
a filtered paid call cannot drift. Check coverage before paying for a lookup that
may be empty; an unknown ticker returns an empty 200, never an error.
/v1/events the filter composes with the since cursor: limit counts matchedevents, so a filtered backlog drains via next_since/has_more without gaps. A
filtered stream still carries the retracted markers of records exposed to the
ticker — including a record merged into one that is, so a subscriber can always
learn a row it holds is dead (you may see a retraction for an id you never held;
drop it).
ticker values (empty, whitespace, or over 12 characters) are rejected422 before the paywall, like the integer params — you never pay to learn the
request was malformed. Constraints are declared in OpenAPI and both routes'
discovery input_schema.
Wording fixes from the S63 blind re-run of the three original audit lenses (purchasing
8.5, due-diligence 8/8 facts exact, integrator 8 — no factual finding survived).
("2023–25 backtest, frozen") — the numbers are a frozen backtest measurement, not a
live property of the feed, and the tiles now say so where the fine print always did.
close" the biologics gap (was "aren't a blind spot").
unlisted_note states per-DECISION list membership — the old sentence ("no receiptrow under their own application number") was literally false for multi-cycle
applications, which appear in both lists with one row per decision.
effectively complete for CDER-reviewed drugs, and the CBER gap is documented where
the payload already concedes it.
The three low findings from the 0.4.4 acceptance audit (a blind paying customer,
renewal 9/10), fixed the same day.
source_quote no longer claims byte-verbatim. The schema now states thattypographic punctuation may be normalized to ASCII (curly quotes become straight
quotes) — compare fold-insensitively. The sentence is still word-for-word from the
filing and the grounding gate still verifies its presence.
prior_date/delta_days nulls are now documented semantics: a restatement of anunchanged date carries no prior (a delta of 0 would fabricate a move — filter with
date_changed), and a recovered stale filing's prior is never derived. Null means
"nothing moved" or "the old date was not stated", never a data gap.
/v1/meta points at) is nowactively probed by the on-box watchdog — a customer's audit caught a one-minute
origin timeout that only cert expiry was watching for.
The first item of the paying-customer audit's "known, recorded" list: a served quote and
a served URL must describe the same filing.
bare), and a test gate keeps it that way. Two structural additions with them:
catalyst_id on the snapshot tier (the correlation key with /v1/events — the
accession join-tip was prose, and an accession is not unique across a merged record's
history) and merged_into on retracted events (the machine-readable re-point
target; null on takedown retractions — withdrawn, no successor).
retractedlifecycle event has existed since 0.1.x, but every takedown (13) and merge (18) predated
it — the live stream had never emitted one, so a subscriber keyed on any of those
catalyst_ids held a dead row nothing ever contradicted. A standing sweep now appends
the missing markers (31 events on this release's first poll; merge markers name the
surviving catalyst_id to re-point to) and keeps visibility and lifecycle in agreement
from here on. Also documented: event_id gaps are consumed AUTOINCREMENT ids, not
missed events.
/v1/events: filed_at, applied, date_changed on every event (additive; thesince cursor contract is unchanged). filed_at is the SEC publication date — the
freshness signal the stream lacked. applied says whether an event advances a mirror
of the served date: the daily catch-up can append a recovered OLDER filing with a
newer event id (its date was never served), so "apply the latest event" — the rule
this changelog's own tier documentation implied — regresses a subscriber's mirror.
date_changed additionally excludes restatements (~44% of new_date_assigned
events re-assert an unchanged date). The replay rule behind the flags is documented
on the route and was verified against every stream-visible catalyst before shipping
(180/180 reproduce the served date). The /v1/events/sample payload gained a
stale-recovery demonstration row (applied: false).
date_extension events gain their prior_date from the replay mirror — whensaying so is true.** 23 of 40 extension events carried no prior (the filing needn't
restate the old date), leaving delta_days incomputable. A standing repair fills
exactly the honest cases (6 rows live): the event must have applied and the mirror's
prior must differ — an unapplied stale recovery keeps null (its date never served),
and a same-date restatement keeps null (delta 0 would be a fabrication).
("Gilead" beside "Gilead Sciences, Inc."); the fuller legal name now wins wherever
two spellings are provably one identity (equal or subset sponsor tokens — two
genuinely different companies sharing a CIK are token-disjoint and untouched).
recall_floor_note now justifies the name_fuzzy exclusionand names its direction (it shrinks the addressable denominator, which flatters the
conditional figure; the unconditional floor carries no such conditioning).
/v1/coverage now counts co-filer exposure when queried by ticker. A co-filer'sticker (e.g. ZYME beside JAZZ on a merged record) answered `catalysts_tracked: 0,
upcoming_on_feed: 0 while the paid feed carried it in tickers[]` — ~30 tickers were
invisible this way (co-filers, OTC foreign listings, warrant/preferred classes). Ticker
queries now count the paid feed's tickers[] membership (the note says so; a cik=
query still counts that one filer), and a delisted/CVR ticker that lives only as a
co-filer alias resolves instead of 404ing. Filing counts remain the filer's own.
generic.** One decision was sold as two records (Viatris's `MR-141 (phentolamine
ophthalmic solution 0.75%) and the originator's bare Phentolamine Ophthalmic
Solution 0.75%`, same sNDA, same date): the alias guards correctly refuse a
dose/route-bearing parenthetical, so the detector's exact identity keys could never
intersect. A same-date token-subset tier closes the gap (measured: exactly one such
pair existed); detection still only routes to human review — nothing merges
automatically.
/v1/catalysts/upcoming source_quote now comes from the filing source_url names.After a cross-filer merge the partner company's re-parented event was the NEWEST one
asserting the date, so recency served the partner's sentence under the survivor's EDGAR
link (3 of 48 rows: Jazz/Zymeworks, Vertex/Alpine, Gilead/Arcellx). The date was right;
the receipt was crossed. The feed-integrity audit grades the same quote, by the same rule.
/v1/accuracy receipts' filing_url now prefers the scored sponsor's own filing overa partner's newer one (4 of 102 receipts linked a co-filer's filing beside the sponsor's
name). A partner's filing remains the fallback when the sponsor filed nothing at that date.
source_accession is "the filing that set the served date", which after amerge need not be the latest event on /v1/events; source_quote is now described.
Patch from the first blind audit to include a PAYING customer (they bought both tiers and
judged the bytes): three contract defects on the advertised-vs-served seam.
/v1/events outcome is now always the published vocabulary. Rejection events wereserved as complete_response_letter (the extractor's word) while the schema enumerates
crl — a schema-validating consumer rejected every rejection event it paid for. Mapped at
serialisation; the mapping is pinned over the whole vocabulary the pipeline can write.
extensions.bazaar examples are now generated from the freeFICTIONAL samples** by the real serialisers — every field, a forward date, the real
envelope — instead of hand-written rows that had drifted (a past date on a forward product,
half the fields).
now sits beside the conditional "~half" tile — it was published in the API and on the
track-record page but absent from the page most buyers read first; "Three endpoints" →
four; the events ordering copy matches the API (ascending from a since cursor).
source_quote comes from the partner'sfiling while source_url is the survivor's — the date is right, the receipt is crossed.
catch-up recovers it (the served date is correctly held); the replay rule will gain
filed_at + an applied flag.
retracted event.GET /v1/coverage?ticker=XYZ (or ?cik=) — free. Answers the ambiguous emptyresult: an empty ticker-filtered feed could mean "no decision pending", "a company we
cannot see" or "we missed it". The route returns only facts we hold — how many of the
filer's filings matched our EDGAR discovery query, how many disclosed a PDUFA date
(observable), first/last SEEN filing dates, live catalyst folders, and how many rows
the paid route would serve right now (a COUNT; the dates stay paid) — over a stated
window, with a note bounding what an absence means. Never a forward date. Ticker
resolution: the SEC company list, then a company we already track; unknown ticker 404,
unknown CIK 200 with zeros. HEAD supported; listed in /v1/meta and /.well-known/x402.
source_url on /v1/catalysts/upcoming and /v1/events is now the EDGARfiling-INDEX URL** — https://www.sec.gov/Archives/edgar/data/{cik}/{accession_nodash}/{accession}-index.htm,
the same shape /v1/accuracy filing_url has used since 0.4.0. Previously the feed
served the folder URL stored at ingest, whose CIK segment was zero-padded or not
depending on the ingest path, so one filing could appear with three different URLs
across the product's surfaces. Both shapes resolve on sec.gov; only the string
changed. The source_url field now carries a schema description (it had none).
mid-batch used to 500); Restore refuses a merge tombstone with the survivor named.
_Nothing yet._
Integration-friction batch from the latest external audit: try both paid response shapes
for free, and two contracts that lived in prose are now specified where machines read
them. All changes are additive; no existing field changes shape or meaning.
GET /v1/catalysts/upcoming/sample andGET /v1/events/sample return a static response of exactly the paid route's shape
at no charge, so you can build and test a decoder before paying. Rendered by the same
serializers as the paid routes (the sample cannot drift from the real payload) over
fictional rows: drugs, companies, application numbers (a 9999xx range no real FDA
application occupies), accessions and dates are invented, and URLs are shape-only.
Deterministic by design — derived fields are computed against the fixed reference date
2026-08-01, so the payloads are byte-stable and safe to snapshot-test against. The
events sample covers the whole event_type vocabulary (corrected and retracted
included, plus a null-heavy row); its meta block is rendered as the final page of a
cursored drain so next_since/has_more are exercised too. Sample event_id values
sit far above the real stream's id space — never feed them to since. Linked from the
paid routes' OpenAPI descriptions, paid_endpoints on GET /v1/accuracy, and
GET /v1/meta → endpoints.
sample key on paid_endpoints rows (GET /v1/accuracy) — the free preview'spath beside each price.
GET /v1/accuracy?summary=1 — a scalars-and-counts view of the accuracy report(measured ~11 KB against the ~113 KB full document at release). The full document had grown larger than the paid response it
vouches for, making the free verification step the expensive one. The summary keeps
every statistic, both coverage directions' operands, the windows and every note; the
row-level ledgers (receipts, unlisted decisions, miss lists, unresolved predictions)
are elided, each replaced by a _rows / items_rows count so an elided list can
never read as an empty one. The default (no parameter) payload is byte-identical to
before. JSON only; browsers always get the HTML page.
meta envelope keys are now specified in the OpenAPI schema — total,limit, next_since and has_more carry descriptions in openapi.json (the cursor
contract was previously documented only by example in prose).
source_accession (snapshot) vs accession (events) is now explained in theschema.** The two fields share the SEC EDGAR accession vocabulary but name different
things: the snapshot's source_accession is a *moving pointer* to the filing the
current served state came from (each newer filing that advances the date replaces
it), while an event's accession is *fixed per event* and spans the whole history —
a snapshot row's source_accession equals the accession of that catalyst's latest
date-bearing event. Neither field is renamed (no breaking change); both schema
descriptions now state the relationship and the join tip.
error_days' sign convention is now documented on the accuracy surface —track_record_note states: error_days = predicted_date − actual_date, in days;
positive = the FDA acted that many days before the date we relayed (early), negative =
after (late). median_delta_days is the median of the signed errors; the
within_7_days/within_30_days ratios use |error_days|. The HTML receipts page
defines the Error column the same way.
A new public event type distinguishing our own data repairs from FDA actions, the
unconditional coverage floor served beside the conditional figure, and per-receipt EDGAR
links for the filing that disclosed each predicted date. All changes are additive; no
existing field changes shape or meaning.
corrected — a new event_type on GET /v1/events. A vendor data repair of ourserved date was previously indistinguishable from a real FDA action on the change
stream: an integrator's delta_days analytics would count our repairs as FDA
extensions (the strongest finding of the latest external audit). A corrected event
sets the current expected date to new_date exactly like date_extension, but
records our repair, not an FDA move — never count it as an FDA action.
prior_date holds what we served before, so delta_days quantifies the repair.
Two emitters: the automated date-repair sweep (synthetic correction: accession,
source_url null — no filing pretends to stand behind a repair) and reviewer-applied
corrections (real filing provenance kept). Forward compatibility: never count an
unrecognized event type as an FDA action, but do apply its new_date (when set) if
you mirror served state from the stream.
*Not emitted for* (this release): indication/ticker/company display-field repairs
(no date moves, so no delta_days corruption), scoreboard corrections (those live on
GET /v1/accuracy, which documents them per row via operator_approved), and
merges/takedowns (already covered by retracted/restored).
coverage.decisions_total / coverage.decisions_caught / coverage.recall_flooron GET /v1/accuracy** — the unconditional "absolute denominator": every original
NDA/BLA decision in the window, including sponsors that never file with the SEC, so
unconditional coverage is computable in one line beside the conditional
recall_addressable. coverage.sponsor_method_counts is served with it, making the
addressable filter itself checkable arithmetic (addressable = cik + name_exact).
These figures were always computed in the committed baseline; they were never served.
The track-record page states the floor in its scope paragraph.
filing_url on every track_record and unlisted_scored_decisions row — theEDGAR filing-index link for the filing that disclosed the predicted date (the
prediction's own receipt; the FDA decision was always auditable via
application_number). The key is always present; it is null on frozen baseline
receipts until the next baseline regeneration carries the new generator fields
through. Synthetic accessions never render as links.
The frozen 2023–25 baseline regenerated after its new weekly standing check found a wrong
stored fact on its first pass, plus three documentation fixes from a fresh blind audit.
No breaking schema change.
NDA214755). The sponsor'sdisclosed 2024-09-07 goal was for the pediatric-narcolepsy sNDA, whose real approval is
the efficacy supplement of 2024-10-16 — the served row credited a labeling supplement
of 2024-09-26 instead. The row (in unlisted_scored_decisions) now reads Δ−39, and
within-30 ticks down 92.75% → 92.23% — the same "worse and truer" direction as 0.3.5,
found by the same detector class, this time running over the frozen baseline.
disclosed under descriptive co-formulation names the FDA never uses) now resolve to
their real applications; the QVANTIG call lands at Δ+2. ambiguous_applications
in unresolved_predictions fell 15 → 12. Headline decisions_matched, the median and
the rejection count are unchanged.
unresolved_predictions.reason_notes.unrecorded — the one reason code that appearedin the counts without an explanatory note now has one (rows scored before reason
recording existed).
CatalystOut.last_updated now documents its format in the served schema (ISO-8601UTC with explicit offset) and its meaning (last *change*, not last confirmation).
matching the accuracy tiles — a no-JS reader previously saw precision without coverage.
One wrong fact corrected on the free GET /v1/accuracy track record, one served date moved
to the FDA letter's own day, and the matcher that wrote both mistakes now prefers real
decisions. No schema change.
approved BLA761352's original application on 2024-12-04 — two months ahead of the
sponsor's extended 2025-02-04 goal date. A routine labeling supplement then landed four
days before the goal, and the nearest-date matcher credited it, hiding the real early
approval. The receipt now reads Δ+62 against the true approval date. The within-7 and
within-30 figures each tick down accordingly — a 62-day miss reported as 4 days was a
wrong fact, and correcting it makes the scoreboard worse and truer.
supplement) within the plausibility window over near-goal paperwork**, at every tier.
Where no real decision is in the window, nearest-date decides exactly as before — a
presentation or formulation catalyst correctly matched to its manufacturing supplement is
untouched (measured across all 130 matured catalysts: exactly one row moved, the one
above). This closes the single-application variant of the class fixed for competing
applications in 0.3.2.
sNDA).** The served date was the sponsor's announcement date (2026-02-01, error −1); the
sponsor's own resubmission announcement states the letter was received 2026-01-30, so
the receipt now reads +1 — the FDA acted a day early, not a day late. Operator-verified.
Two wrong facts removed from the free GET /v1/accuracy track record, and one FDA decision
that was being published twice is now published once. No schema change.
pembrolizumab with berahyaluronidase alfa" was matched to BLA125514 (IV KEYTRUDA) at
Δ−59; the real decision is BLA761467 (KEYTRUDA QLEX) at Δ+4. Both applications
corroborate on the same bare ingredient, so a curated FDA identity is now judged by
*containment* — the FDA name must contain it — which stops a brand-family parent answering
for its co-formulated child. ⚑ Because the corrected row now shares an application number
and action date with a baseline row for the same decision, the two merged: **one decision
stopped being counted twice**, which is why decisions_matched fell 194 → 193 and receipts
158 → 157. Nothing was lost.
Combination With Padcev" (goal date 2026-04-07) was matched to a routine labelling action
dated 2026-04-06. The disclosure is a priority review of a new indication — an efficacy
decision — and the real approval came on 2025-11-21, roughly 4.5 months *ahead* of the
goal date under the FDA's real-time oncology review. The row now reports the honest
Δ+137 and is marked operator_approved, i.e. human-verified.
date_accuracy: decisions_matched 194 → 193, matched_approvals 161 → 160,within_7_days 0.7577 → 0.7565, within_30_days 0.9330 → 0.9326. median_delta_days
1.0 and matched_crls 33 both unchanged.
⚑ The within-7 and within-30 figures went down, and that is the correction working: a
137-day error we were reporting as 1 day is a real miss, and this proof page exists to show
misses rather than hide them behind a coincidence.
track_record is now 157 rows.operator_approved)instead of re-flagging it, so a manual correction cannot generate a permanent unactionable
review item.
A disclosure change to the free GET /v1/accuracy proof (no schema change, no number
moved). Internally, the first standing self-check on the live scoreboard.
caveat now states that the matched set is not a frozen benchmark. It GROWS as thedrug/sponsor join improves — the committed baseline moved 165 → 173 decisions in a single
session — because identifying a decision we previously could not adds it both to the
matched count and to the addressable denominator. Every figure served here is therefore
conditional on the current matching rules rather than measured against a fixed
population, and a later revision can move it in either direction. Nothing else changed:
a whole-payload diff across the deploy showed caveat as the only differing key.
stored scoreboard row to the internal review queue when a strictly better candidate was
available in the same lookup — typically the wrong FDA *application* for the right drug.
It changes no published figure; it exists so that a wrong row is noticed by a mechanism
rather than by someone happening to look. First live run: 103 rows inspected, 1 flagged.
Scoring corrections to the free GET /v1/accuracy proof (no schema change; the served
JSON shape is untouched). Baseline regenerated: 165 → 173 matched decisions.
OPDIVO's October supplement (BLA125554, 87 days out) when the real decision was
OPDIVO QVANTIG (BLA761381, 2024-12-27, 2 days out). The correct row was already
published from the committed baseline, so one FDA decision appeared twice in the
track record — once wrongly. It now resolves to the correct application.
drug-name match exists, reconciliation falls back to pulling every FDA approval near the
predicted date. That pull matched whole applications but counted individual submission
rows against its cap, so it saw roughly a quarter of the applications it should have —
and *which* quarter moved with openFDA's weekly refresh. Whether a prediction matched
therefore depended on the provider's sort order. The pull is now complete and
order-independent. (Same defect class as the 0.3.1 denominator fix, on the scoring path.)
post-approval housekeeping, never a decision an issuer announces a PDUFA date for, but
one landing near a predicted date could make a genuinely unambiguous match look
ambiguous and be refused. Seven real decisions were being dropped this way, four of them
exact to the day (COBENFY, KALYDECO, subcutaneous efgartigimod, AXS-05).
date_accuracy.decisions_matched 165 → 173; coverage.unmatched 116 → 102.Median error holds at 1.0 day and matched rejections hold at 31.
within_7_days 0.7758 → 0.7630 and within_30_days 0.9394 → 0.9364. These fallbecause the recovered decisions are real ones that were previously invisible: 8 of the
11 are within 7 days, below the prior rate, so adding them dilutes the percentage. All
11 are within 30 days. Part of the movement is also openFDA's own data changing under a
fixed action window, measured separately and reported in the session evidence.
coverage.recall_addressable 0.5455 → 0.5391. Catching a decision also adds it tothe addressable denominator by construction, so the ratio can fall as coverage improves.
Counting-rule and data corrections to the free GET /v1/accuracy proof (no schema
change; the served JSON shape is untouched):
strengths or presentations acted on together, e.g. YEZTUGO NDA220018/NDA220020) is
now counted as ONE decision in the recall denominator and the miss lists. Four
published "misses" were phantoms — decisions we caught under the twin application
number. Merging requires an identical action date plus sponsor and drug corroboration;
two real same-day decisions by one sponsor (including same-generic biosimilar pairs)
stay separate.
a dense month in the provider's server-side page order, dropping real FDA approvals
from the denominator (13 recovered, including 4 decisions we had predicted — one exact
to the day). The pull is now capped on in-window rows only, so membership no longer
depends on upstream ordering.
coverage.recall_addressable moves 0.5203 → 0.5455 and the approval miss list 59 → 55rows from the two corrections above; decisions_matched (165), the 1.0-day median,
and the rejection figures are unchanged. Per-row attribution:
docs/research/s54-twin-appnum.md.
unlisted_scored_decisions rows now carry sponsor and drug_name from our ownprediction records (previously null; ticker remains nullable).
The S52 audit's honesty batch: the free GET /v1/accuracy proof now attributes every
rejection receipt and enumerates every scored decision.
unlisted_scored_decisions (+ unlisted_note) on GET /v1/accuracy: the scoreddecisions counted in date_accuracy.decisions_matched that have no receipt row in
track_record (supplements, Purple Book recoveries, cycle-pass recoveries). Every
decision in the headline n now appears in exactly one of the two lists, keyed by FDA
application number. The receipts HTML page states the count. Additive — no existing key
changed shape.
track_record served sponsor: null (every one a baselineCRL). The sponsor is now named from our own catalyst record — the same reproducible
provenance the drug_name field has used since S42 — so all 152 receipt rows carry a
sponsor. track_record_note reworded to point at the new list (counts unchanged).
An external blind audit (three independent evaluators judging only the public surfaces)
prompted most of this release; every dated release from here on carries its date —
auditors rightly flagged an undated changelog as weak next to a 30-day deprecation promise.
date_confidence vocabulary was wrong on the catalyst snapshot: theschema advertised high/medium/estimated, values the serve path never emits. The
real (and now documented) vocabulary on BOTH paid tiers is confirmed / reported /
estimated — the 402 example that said "confirmed" was right all along. A strict
decoder built from the old schema would have rejected real payloads.
GET /v1/events record vocabularies (confidence, date_confidence, authority) arenow fully typed with enums in the OpenAPI schema (previously untyped strings).
coverage.matched_note on GET /v1/accuracy, explaining whybaseline_decisions_matched + unmatched can exceed predictions_total: matched counts
FDA *decisions* (one prediction folder can score several sequential decisions on the
same application since the per-cycle scoring pass), while the other two count
*prediction rows*.
track_record_note now defines had_prior_crl: it marks an APPROVAL that superseded anearlier Complete Response Letter on the same application; it is never set on a rejection
row itself (a second CRL appears as its own row).
purchase ("verify accuracy AND coverage free at GET /v1/accuracy").
as the accuracy numbers (live-filled, never hardcoded).
error field. No failure path ever populated it — failures neverused the envelope at all — so it advertised a contract that did not exist. The real error
shapes, now documented in the OpenAPI description: 400/500 are RFC 7807 problem details,
422 is FastAPI's {"detail": [...]}, 402 is the x402 payment challenge. Successful
responses are {data, meta}. (Same class of fix as the earlier meta.offset removal.)
HEAD support on the free routes (/, /health, /v1/meta, /v1/accuracy) for cheapliveness probes. The paid routes remain GET-only by design.
GET /v1/accuracy now populates meta.total with the track_record receipt-row count(previously null).
request; a since-cursor drain of N pages costs N requests; a request answered ≥ 400 is
never charged) and the failure/availability semantics (no 429 today; upstream
SEC/openFDA outages surface as data freshness, never as request-time errors).
GET /v1/meta now reports limits (advisory request ceiling — no hard limit is enforcedtoday; recommended_max_rps), freshness (how to read record age vs. active monitoring),
and api (version, changelog link, deprecation policy) so an autonomous consumer can
self-govern and detect change.
GET /v1/catalysts/upcoming) records now carry age_days — days since eachrecord last changed. A high value means the date is unchanged and still confirmed by the
latest filing, not stale.
the package version and the version the API advertises).
Initial machine-first PDUFA-catalyst feed. Notable surfaces added during 0.1.0:
GET /v1/catalysts/upcoming — the paid forward calendar of FDA PDUFA decision dates(x402 paywall, Base + Solana), with grounded provenance per record (source_quote,
source_url, authority, date_confidence).
GET /v1/accuracy — free, self-adjusting accuracy track record (per-decision evidence),now also surfacing captured CRL/rejection outcomes and Purple Book (CBER biologics) coverage.
GET /v1/events — forward-date-redacted change stream (set → extended → resolved).GET /v1/meta — service metadata + live feed counts + last_polled freshness signal.GET /health — liveness + DB-readiness probe.