Changelog

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/metaapi.version.

Deprecation policy: breaking changes are announced here and, for a retiring route, via

Deprecation + Sunset response headers at least 30 days before removal.

[Unreleased]

Changed

…"), 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.

Added

use 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.

Fixed

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.

[0.8.0] - 2026-09-03

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.

Added

its 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.

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).

at the edge.

Notes

owner-gated step. Every verify response names the chain its anchor happened on.

(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.

[0.7.3] - 2026-08-29

The mechanism for the class 0.7.2's takedown belonged to. No public payload changes.

Added

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.

[0.7.2] - 2026-08-29

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.

Added

predicted 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.

Changed

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.

Data

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.

[0.7.1] - 2026-08-28

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).

Changed

1.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.

Unchanged

[0.7.0] - 2026-08-28

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").

Changed

91.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).

both 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).

supersedes the study's (same decisions, same numbers).

Unchanged

changes how the free accuracy report is assembled, not what is sold.

[0.6.0] - 2026-08-27

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.

Changed

window.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.

resolution.** 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.

unlisted_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.

Added

import 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.

Unchanged

Historical catalysts are past-dated and cannot enter the forward window.

[0.5.2] - 2026-08-25

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.

Added

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.

list-valued items becomes items_rows at any depth — four places in today's payload,

two of them nested inside recall_misses.

Notes

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.

construction). source, match_tier and outcome are deliberately left as described

strings — enumerating an open vocabulary is what broke rejection events in 0.4.3.

[0.5.1] - 2026-08-24

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.

Added

(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.)

repair 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.

Documented

*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.

Fixed

instead of redirecting exactly as it does on success. (The JSON admin routes have

always returned 404 here.)

[0.5.0] - 2026-08-24

The per-ticker point lookup (operator-requested): both paid routes accept an optional

?ticker= server-side filter at the unchanged flat per-request price.

Added

filed 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.

events, 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).

422 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.

[0.4.6] - 2026-08-24

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).

Fixed

("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").

row 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.

[0.4.5] - 2026-08-24

The three low findings from the 0.4.4 acceptance audit (a blind paying customer,

renewal 9/10), fixed the same day.

Fixed

typographic 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.

unchanged 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.

actively probed by the on-box watchdog — a customer's audit caught a one-minute

origin timeout that only cert expiry was watching for.

[0.4.4] - 2026-08-24

The first item of the paying-customer audit's "known, recorded" list: a served quote and

a served URL must describe the same filing.

Added

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).

lifecycle 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.

since 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).

Fixed

saying 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).

and names its direction (it shrinks the addressable denominator, which flatters the

conditional figure; the unconditional floor carries no such conditioning).

ticker (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.

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.

a 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.

merge need not be the latest event on /v1/events; source_quote is now described.

[0.4.3] - 2026-08-23

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.

Fixed

served 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.

FICTIONAL 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).

Known, recorded for the next release (verified, not yet fixed)

filing 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.

[0.4.2] - 2026-08-23

Added

result: 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.

Changed

filing-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._

[0.4.1] - 2026-08-22

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.

Added

GET /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/metaendpoints.

path beside each price.

(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.

Changed

limit, next_since and has_more carry descriptions in openapi.json (the cursor

contract was previously documented only by example in prose).

schema.** 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.

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.

[0.4.0] - 2026-08-21

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.

Added

served 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).

on 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.

EDGAR 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.

[0.3.6] - 2026-08-21

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.

Fixed

disclosed 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.

Added

in the counts without an explanatory note now has one (rows scored before reason

recording existed).

UTC with explicit offset) and its meaning (last *change*, not last confirmation).

matching the accuracy tiles — a no-JS reader previously saw precision without coverage.

[0.3.5] - 2026-08-21

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.

Fixed

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.

[0.3.4] - 2026-08-20

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.

Fixed

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.

Changed

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.

Added (internal, not consumer-visible)

instead of re-flagging it, so a manual correction cannot generate a permanent unactionable

review item.

[0.3.3] - 2026-08-19

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.

Changed

drug/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.

Added (internal, not consumer-visible)

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.

[0.3.2] - 2026-08-19

Scoring corrections to the free GET /v1/accuracy proof (no schema change; the served

JSON shape is untouched). Baseline regenerated: 165 → 173 matched decisions.

Fixed

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).

Changed

Median error holds at 1.0 day and matched rejections hold at 31.

because 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.

the addressable denominator by construction, so the ratio can fall as coverage improves.

[0.3.1] - 2026-08-18

Counting-rule and data corrections to the free GET /v1/accuracy proof (no schema

change; the served JSON shape is untouched):

Fixed

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.

rows 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.

prediction records (previously null; ticker remains nullable).

[0.3.0] - 2026-08-17

The S52 audit's honesty batch: the free GET /v1/accuracy proof now attributes every

rejection receipt and enumerates every scored decision.

Added

decisions 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.

Fixed

CRL). 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).

[0.2.0] - 2026-08-17

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.

Fixed

schema 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.

now fully typed with enums in the OpenAPI schema (previously untyped strings).

Added

baseline_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*.

earlier 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).

Removed

used 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.)

Added

liveness probes. The paid routes remain GET-only by design.

(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).

today; 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.

record 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).

[0.1.0]

Initial machine-first PDUFA-catalyst feed. Notable surfaces added during 0.1.0:

Added

(x402 paywall, Base + Solana), with grounded provenance per record (source_quote,

source_url, authority, date_confidence).

now also surfacing captured CRL/rejection outcomes and Purple Book (CBER biologics) coverage.