Skip to content

release: dev to main, 8 new pages and the keyed cohort's real success rate - #3021

Merged
Flotapponnier merged 196 commits into
mainfrom
dev
Oct 18, 2026
Merged

Flotapponnier merged 196 commits into
mainfrom
dev

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

Not ready to merge until the Validation Cloud question below is answered. Everything else is prepared and verified.

main is now 0 commits ahead of dev and the merge reports 0 conflicts: this is a fast-forward. The 32 conflicts that stood here this morning were resolved on dev in #3019 and #3020, where staging can validate them, rather than inside this release.

What production gains

8 bench pages, all status: live, none gated:

  • the 6 WebSocket races: ethereum, base, bnb, polygon, arc, hyperliquid (-rpc-ws)
  • rpc-free-tier (283)
  • perp-max-leverage-terms (295)

3 routes: /tools, /tools/perp-cost, /hyperliquid/traders.

0 pages lost. 380 → 388 bench URLs.

What changes on pages already published

  • Validation Cloud on the 9 chain RPC pages, 18 provider rows, warm and cold.
  • Papertrade joins 4 perp boards: perp-active-markets, perp-asset-breadth, perp-capital-efficiency, perp-volume-oi-ratio. Nothing is removed from any of them.
  • p95: a fifth quantile, 106 declarations across the private cohort, a new column on those 9 pages and on /rpc.
  • /rpc gains the geo selector, the WebSocket cohort, and loses the Archive column where nothing measures it.

What it fixes that is invisible

main's recording rules aggregate the call counters without tier, so every keyed spec's success and sample_size query matches nothing and the loader falls back to a success rate of exactly 100%. The private cohort on production has been publishing an assumption. This release replaces those rules with the versioned set (23 against 11).

The one open decision

Validation Cloud reaches production with this merge — 18 rows on 9 public pages. The instruction standing until now was staging only. There is no mechanism to hold a single provider back: DEV_ONLY_BENCH_SLUGS gates whole benches, and no keyed bench can be gated without taking Alchemy, Chainstack, GetBlock, QuickNode and Dwellir down with it.

Two ways forward, both a day or less:

  1. accept it in production and merge as is;
  2. build the per-provider gate first, applied at spec load exactly where the bench gate is, and merge after.

Gates run on the merged tree

253 specs valid · tsc clean · eslint 0 errors · 594 tests · promtool 23 rules · go build and go vet on rpc-keyed-latency, rpc-capabilities, hl-fills-reduce and hyperliquid-frontends · next build generating 293 static pages.

After merging, prod-deploy.yml runs vercel --prod automatically. The four Railway services build from dev and are unaffected: this is a site release only.

Flotapponnier and others added 30 commits September 30, 2026 14:52
…ve step is 2x, not 1.5x (#2732)

* the plan-band doc said the opposite of the code, and GetBlock's archive step is 2x, not 1.5x

Two comments in the rpc-cost harness teach the next reader the belief the
code was changed to remove.

profiles.go carried two doc paragraphs on PlanTiers. The upper one said
`all` is "the cheapest plan of any tier" and that "Free is scored here
too"; the lower one, added with the paid-plan default, says `all` is the
cheapest PAID plan because free tiers would hand the leaderboard to the
$0 rows. The code is the lower one: model.go sets
`paidOnly := tier == "all" && !p.isReference()` and skips TierFree, and
benchmarks/rpc-cost.yml labels the tab "Cheapest paid plan". A reader
scanning top-down got the stale answer, which is the exact defect the
paid-plan default exists to prevent. The stale paragraph goes.

model.go and catalogue.go both gave GetBlock's archive ratio as 1.5x on
one method family. Read off the full_cu / archive_cu columns in
pricing/catalogue.yml, the Ethereum catch-all reads are 20 -> 40 (2x) and
the enumerated debug, trace and txpool methods are 40 -> 120 (3x); Solana
is 50 -> 100 throughout. No ratio in the table is 1.5x, and the spec
already states 2x and 3x. model.go was also naming the same 3x method set
twice ("debug/trace" and "the enumerated trace family") and never
mentioning the plain reads that are the real second case.

The 1.5x is a real number in the wrong place: it is what GetBlock's
published archive_multiplier ceiling of 6 implies against the 4x method
family, which is why that formula is unusable. That is recorded on the
getblock archive_rule note, and model.go now points there instead of
repeating the figure as if it described these columns.

Comments only. No behaviour change, no figure on the page moves.

* the 90-day staleness rule these two comments describe does not exist

catalogue.go said "A number nobody has re-verified in 90 days is reported
as such on the page instead of quietly passing for current", and
refresh.go said "a figure nobody has re-read in 90 days is the thing this
bench most needs to admit".

There is no 90-day constant in the harness. grep over the package finds
the number only in these two comments and in a NOWNodes plan note about a
package valid 90 days; benchmarks/rpc-cost.yml carries no threshold
either. What the harness does is publish rpc_pricing_artifact_age_seconds
per provider per artifact (metrics.go:120) and compare it to nothing.

The distinction matters to anyone reading this before touching the
freshness path: there is no cutoff past which a figure is withheld or
relabelled, so adding one is a feature, not a bug fix, and removing the
gauge would remove the only staleness signal there is. The spec already
words it correctly at line 87, "Age is published either way, so a figure
nobody has re-read is visible as such", so both comments now say that and
name the gauge.

Comments only. go build and go vet clean.
…s saying "Access" (#2733)

`dimension_labels` landed with the zero-is-a-value work and every axis
reads it except the one that motivated it: venue, bucket, kind, chain and
region all do `dimensionLabels?.<key> ?? "<stock>"`, while the tier row
hardcoded "Access".

benchmarks/rpc-cost.yml declares `tier: Billing model` and its tier axis
is usage / dedicated / reference ("Billed per request", "Dedicated
capacity", "Free public, not ranked"). "Access" is the wrong word for
that axis: on rpc-cost the plan band is the `venue` axis, not this one.
The page rendered "Access" sixteen times against a spec that had already
written the right label.

One line, same shape as the five axes beside it. rpc-cost is the only
spec in the tree that declares `dimension_labels.tier`, so the nine chain
RPC benches keep "Access", which is correct for them: there the tier axis
really is public against API-key.

typecheck and eslint clean.
…2734)

src/lib/chains.ts carried two `cosmos-hub` entries, identical but for
`description`. Nothing threw, because CHAIN_BY_SLUG is
`new Map(CHAINS.map(...))` and the later entry simply wins, so the
duplicate only showed in the places that iterate the array:

 - /chains rendered two identical Cosmos Hub cards (verified on
   production: two `href="/chains/cosmos-hub"` on the page);
 - the sitemap emitted the URL twice, 1,211 <loc> entries for 1,210
   unique URLs (verified on an uncached fetch of the live sitemap);
 - generateStaticParams in src/app/chains/[slug]/page.tsx returned the
   slug twice, so every build prerendered the route twice;
 - the stratified smoke test spent one of its 120 tail slots per rotation
   re-checking a URL it had already checked.

The entry removed is the first one, which is the one CHAIN_BY_SLUG was
already discarding, so the rendered description does not move: the page
serves "The original Cosmos SDK chain and IBC hub..." before and after,
checked against the live page.

Guard added rather than only the deletion. A duplicate here is invisible
to typecheck, to lint and to every Map-based consumer, which is why this
one survived; src/lib/chains.test.ts asserts slug uniqueness and that
CHAIN_BY_SLUG.size equals CHAINS.length. Checked both ways: the test
fails on the pre-fix registry (147 entries, 146 in the Map) and passes
after.

typecheck, lint and the full suite (371 tests) clean.
…ur dead exports (#2736)

Found by an audit of today's diff. Each of these was checked against the
code beside it; nothing here changes behaviour.

Cache windows that moved and left their prose behind (#2724 widened the
routes, these were not resurveyed):
 - api/stat/[slug] said the worst case went "from about 360 s to about
   600 s" and called that parity with the bench page's 600 s ISR. 360
   counts stale-while-revalidate on the old header (60 + 300); the
   matching figure for 300 + 900 is 1200, not 600, so it is about twice
   the page's ISR, not parity. The comment now says so and records where
   it shows: /rwa is revalidate = 300 and its FAQ, which becomes FAQPage
   JSON-LD, tells readers this endpoint carries "the same values and
   timestamp" while advertising it as a DataDownload.
 - spec.ts said loadAllBenchmarksCached shares its result "with 60s
   revalidate" three lines above `revalidate: 300`. Several API routes
   size their own windows against this number.
 - benchmark-body.tsx said the variant API "dedupes across users via its
   60s cache"; that cache is 300 s.
 - api/freshness said its edge cache is "5 s s-maxage, 20 s swr" and that
   the LiveIndicator polls "every 8 s". The response sets s-maxage=30,
   swr=60 and the widget polls every 30 s.
 - live-indicator.tsx said it re-fetches /api/citable, edge-cached 60 s.
   It fetches /api/freshness, and that is 30 s. Both now note the
   asymmetry this creates: at 30 s the widget is the freshest clock on the
   page and can read newer than the table under it.
 - api/aggregate said "The ISR revalidate=60 above handles freshness
   instead". There is no revalidate on that route; it is force-dynamic.

Wrong reason, right conclusion:
 - next.config.ts said the sitemap sets its own Cache-Control "because
   metadata routes ignore next.config headers()". sitemap.xml/route.ts is
   a Route Handler, not a metadata route, and the same comment already
   gives the real reason two lines earlier.

Docs separated from their fields by today's zero_is_a_value insertion, so
each described its neighbour. Reordered, text verbatim:
 - spec-schema.ts: three docs stacked in the reverse order of the three
   fields, so zero_is_a_value showed ledger_reliability's doc and
   ledger_columns' doc was stranded two fields up.
 - types/benchmark.ts: dimensionLabels' doc sat on zeroIsAValue.

Counts that no longer hold:
 - site-footer.tsx named /llms.txt and /api/citable as the links absent
   from the sitemap, which reads as a complete list. Four of the ten are
   absent: those two plus /api/openapi.json and /api/llm-context. The
   next person pruning that row needs all four.
 - capital-hub.ts and capital/page.tsx both list five benches; eight feed
   the hub. Both now point at CAPITAL_BENCHES instead of carrying a copy,
   because the copy went stale the first time the list grew.
 - capital-hub.ts also credited bench 265 for perp open interest while
   correctly naming 265 as perp-pf-ratio two lines later. 265 stopped
   feeding that column today; the block above perpOi explains why.
 - capital-hub-rules.ts said "n/a" is the missing value on "the other 37
   places the site uses it". Not derivable: the literal appears 63 times
   in src/ across 21 files. The claim keeps its substance and drops the
   number rather than standing as a fact-shaped guess.
 - the COLUMN_COVERAGE note quotes the owner's "two rows of seventy-nine"
   from the first hub draft. Kept as the quotation it is, with the current
   cohort size (63) named beside it so the two figures stop reading as a
   contradiction.

One reader-facing string, not a comment: the categoryTooSmall reason said
a category under five ranked protocols "compares the token with one or
two others". Below a floor of five it is up to three others.

Dead exports removed from capital-hub-rules.ts, zero callers across src/,
scripts/ and worker/:
 - change7dFromDays and HISTORY_FRESH_MS (its only reader). Today's perp
   open-interest change removed the single call site, and the doc still
   named the data path the hub stopped reading.
 - change7dFromSeries, a one-line wrapper over change7dOfSeries. Its test
   covers change7dOfSeries' real behaviour, so the test was retargeted at
   that function rather than deleted: every assertion is kept.
 - the NaReason type, one reference in the tree and it was its own
   declaration. Using it instead of deleting it was the alternative, and
   it is a refactor rather than a cleanup: CCTP_SCOPE_LABEL values are
   also assigned to the notApplicable fields and are not in NA_REASON, so
   tightening those to NaReason needs a decision about the union first.

typecheck, lint, validate (241 specs) and the suite (367 tests) clean.

Deliberately not in this sweep: the warm-up and smoke comments in
prod-deploy.yml, staging-deploy.yml and scripts/sitemap-smoke.mjs, which
belong to whoever is holding that change; page-metadata.ts's "fallback"
comment, which is only wrong because of a behaviour bug fixed separately;
and benchmarks/rpc-cost.yml's "one plan in seven", which is page copy
whose denominator two passes disagreed on.
pageMetadata pinned openGraph.images and twitter.images to the ROOT
/opengraph-image and /twitter-image. The comment called it a fallback for
hubs without a dedicated card. It was an unconditional override, and
config-based images beats the file convention, so every page built through
this helper shared one card.

28 of the 30 callers ship their own opengraph-image.tsx. Every one of them
was built, is served at its own URL and was named in no meta tag: checked
on production, where /perps/opengraph-image, /rpc/opengraph-image and
/team/opengraph-image all answer 200 with a distinct PNG while the pages
themselves advertise the root one. /capital shipped a 78 KB card this week
that nothing has ever pointed at.

Dropping the two images keys lets Next resolve the card from the route's
own file, and it fills twitter:image from that same file when the route has
no twitter-image.tsx, so both tags land on the right card with its real alt
text and dimensions.

Two things this turned up that the plan had wrong, both found by rendering
the pages rather than by reading the precedence rules:

There is no ancestor cascade. A route with no opengraph-image.tsx of its
own emits NO og:image once the override is gone, the root file
notwithstanding. /rwa, /rpc-map and /perps/[asset] are the three callers in
that position, so each gains a file. /rwa and /rpc-map re-export the site
card, which is byte for byte what they served before (62,240 B), so those
two are dedupe with no visible change; /perps/[asset] takes the perps hub
card, which is the right subject and an upgrade on the generic one.

Route segment config cannot be re-exported. `export { revalidate } from ...`
fails with "can't recognize the exported `revalidate` field in route. It
mustn't be reexported", and it does not fail locally: it 500s every image
route in the app, the root card included. runtime and revalidate are
declared literally in the three new files, with a note saying why.

Verified in dev after the change: /rwa, /rpc-map, /perps/eth, /capital,
/team and /rpc each emit og:image and twitter:image pointing at their own
segment, and all five image routes answer 200 image/png (/capital 78,895 B,
/perps/eth 66,985 B, /rwa and /rpc-map 62,240 B).

typecheck, lint and the suite (368 tests) clean.
…e token matches in the singular (#2739)

The perp open-interest section was taught to say so instead of vanishing,
and the comment above it states the rule: a table that disappears with
nothing said is the same defect as a cell, one level up. Five sections on
the same page still vanished: the stablecoin flow bar, the CCTP block,
prediction-market open interest, the perp token table, and the protocols
block, which takes the divergence heading and the whole reading layer with
it. If bench 274 fails to load, the Valuation tab loses its H2, its reading
and its table in silence.

The data to say it was already there. hub.benches carries live and failed
per bench, so sectionState (now in capital-hub-rules.ts with the other
shared rules, and tested) tells three cases apart:

  failed    the load threw; the reading exists, this render missed it
  unserved  a bench this deployment does not carry
  empty     served, ran, ranked nobody: the only one that is a measurement

and returns null when the bench is not in the list at all, which is the
gated case. Benches 280 and 281 are only listed where they are served, and
a reading the deployment never promised is not a reading that is missing.
Without that clause prod would print a note for the CCTP block it is not
supposed to have.

MissingSection is one implementation, because the point of the rule is that
every section says it the same way; each caller passes what is missing and
why the nearest other reading is not a stand-in. The perp OI paragraph
keeps its own text: its source is the perp venue cohort snapshot, which has
no entry in hub.benches and so no flag to read. The comment there now says
that rather than implying it is the only section with the treatment.

Separately, the divergence FAQ answer had a zero branch and a plural branch
and no singular one, so at one match it read "1 of 67 tokens match today,
Moonwell among them". Wrong agreement, and "among them" for one row.
Singular is the common case now: the rule selected 11 rows before the two
size floors and the category floor, and 1 today. This answer is in the
FAQPage JSON-LD, so it is the sentence an answer engine quotes.

Verified by rendering. With data present every section is unchanged and the
FAQ reads "1 of 67 tokens matches today, Moonwell". With bench 277 forced
to failed and its rows emptied, the section renders its heading plus "is
unavailable here: the bench behind it did not answer on this render, which
is transient and not a retirement", and the stub was reverted.

typecheck, lint, validate (241 specs) and the suite (372 tests, 5 new on
sectionState) clean.
An audit found that /api/aggregate and /api/sitemap-data answer 200 on
production carrying every dev-only bench, bench 208 included, with its full
editorial fields and an seo_title naming a leader for a page that 404s.
Eleven gated slugs in the aggregate, nine in the sitemap blob.

The obvious fix is wrong and would cause an outage. Both libraries point
every Vercel deployment at these routes ON THE PRODUCTION DOMAIN
(aggregate-blob.ts:66, sitemap-blob.ts:4), because the alternative is a
12 s transatlantic fetch of a 7.5 MB blob from the Paris VPS. Staging is
the only place the dev-only benches render, and it reads its data through
production's proxy, so filtering on VERCEL_ENV in either route would strip
all nine benches from staging. Verified: chain-bridged-tvl and
perp-liq-rate both answer 200 on openchainbench-staging.vercel.app today.

So the routes stay verbatim and now say why, at the top of each, addressed
to whoever finds this next. The gate that matters is the loader
(materialize/load.ts) and sitemap-builder.ts, and production's own surfaces
are correct: sitemap.xml, llms.txt, RSS, /api/citable, /api/stat, MCP, the
Markdown views and every page all drop bench 208, each checked live.

What bounds the exposure, recorded with the warning: both routes are
noindex via the /api/:path* X-Robots-Tag, neither is linked from any page,
neither appears in llms.txt or the OpenAPI document, and the payload holds
no secret. Scanned for keyed URL shapes (quiknode, alchemy, chainstack,
getblock, api-key, token): none.

While in that file, the leading docblock was wrong in three more ways. It
described the three sets as one, so it gave 410 as the answer for a
dev-only slug when the middleware only handles REMOVED_BENCH_SLUGS and a
dev-only page 404s. It credited src/app/sitemap.ts, which does not exist
since the sitemap became a Route Handler. And its list of protected
surfaces was the list that omitted these two routes. It is now a map of
which set does what, with the status code each produces and why a 404 is
right for a URL that never published.

Documentation only. typecheck, lint and the suite (368 tests) clean.
Every windowed number on the dashboard was a fixed "last 7 days against
the 7 before it". That is the right default for a site read by a few
hundred people a week and the wrong one on the day something ships: a
launch that triples today's traffic moves a 7-day average by a seventh,
so the page meant to show the spike flattens it.

The window is a parameter now. The header carries a 7 d / 24 h toggle,
`?w=24h` switches the whole dashboard, and the comparison window is
always the same length as the window itself, which is what the deltas on
every card already claimed to be.

The refresh runs the 20 windowed sections a second time for 24 h and
stores them beside the first pass. The 8 long sections (the 28-day and
90-day series, the weekly cohorts, the kiosk list) mean the same thing
whatever window the reader picked, so they are fetched once and a 24 h
page reads them from the 7 d slice. That is 48 queries a pass against 28
before, 192 an hour at the 15 min cadence, 8 % of PostHog's
organisation-wide 2,400; the local budget default goes from 300 to 500 so
the manual Refresh button still fits beside it.

Three things that had to follow the window and would otherwise have
mislabelled a number:

- every "vs previous 7 d" under a delta, which is why Metric and Kpi take
  a prevLabel instead of hardcoding a week
- the manual refresh, which kept only the referer's pathname and so
  returned a 24 h reader to the 7 d view
- every in-app link, so switching to 24 h and then clicking through does
  not silently drop back

Two things deliberately do NOT follow it: the bench staleness thresholds
on /health (Stale > 24 h, Expired > 7 d) describe the data, not the
reader's window, and the daily history table reads history.jsonl, which
records the 7 d headline numbers once a day whatever anyone is looking
at. Both are commented where they sit.

Tests cover that the window sets both intervals, that the fixed sections
render identical SQL in either window, and that the windowed list is
exactly the set whose SQL actually moves, so a section cannot be stored
once and shown under two labels.
Four tables carry a delta column headed "w/w". Under the 24 h window the
comparison is the day before, not the week before, so the header named a
period the number was not measured over. Spotted on the referring-domains
table, where t.co read "+27000% w/w" on a delta that was against
yesterday.

The header follows the window now, "w/w" at 7 d and "d/d" at 24 h.

One "w/w" is deliberately left alone: the AI delta on the overview is
computed from lastFull and prevFull, two complete weeks off the weekly
series, and stays week over week whatever window the reader picked.
… crawler (#2750)

The audience page answered "who reads the site" from PostHog, and PostHog
cannot answer it. Its SDK is client-side JavaScript, so nothing that skips JS
is ever in it, and the server capture in src/lib/analytics-server.ts is wired
to three routes, two of which sit behind a 60 s and a one hour edge cache and
therefore emit one event per cache fill rather than per read. Every HTML page
is ISR-cached, so a crawler fetching a bench page runs no code of ours at all.

What the page showed on 2026-09-30: 317 server reads over its whole history,
of which 19 from an identified AI agent, and the top three "agent families"
were curl, browser and other, 205 of the 317 being our own curl probes and the
Blackbox exporter. Read as adoption, that says agents ignore the site.

Vercel, over the same three days, counted 75,429 requests from AI crawlers,
8,001 from search engine crawlers and 2,005 from AI assistants. The two
sources are not close because they are not measuring the same thing.

So this does not duplicate the collection. Vercel already logs every edge
request with a classified bot, cache hits included, and lib/vercel-obs.ts
queries it as a new snapshot section beside benches, harness and dune. The
PostHog panels stay, now saying what they do and do not count.

The API, /v2/observability/query, is the one `vercel metrics` uses and is not
in the public REST docs, so the header of vercel-obs.ts keeps what was learned
the hard way: the filter dialect is OData with single quotes, orderBy must
name a rollup column, grouped dimensions come back snake_cased, granularity
accepts only 5m/15m/1h/1d so whole-window totals are summed in code, and the
per-path query 408s over 30 days and runs over 7.

The AI total is published twice, with and without the single largest AI bot.
One crawler, meta-externalagent, was 73,192 of the 75,429 and was looping over
fifteen static pages rather than indexing, so a single number would read as
adoption when it is one misbehaving bot.

Retention starts when Observability was enabled on the team, 2026-09-28, so
earlier days are real zeros; firstDayWithData is carried so the page can say
so instead of implying the site was quiet.

Without the three VERCEL_* variables the section says it is not configured and
every other section is unaffected.
Providers ask how much traffic we send them, and the honest answer was
"look in your referrer report". For anyone arriving through a browser
that strips the referrer, there was no answer at all. Outbound links now
carry utm_source=openchainbench.

Applied at render time, never stored. The registry and the specs keep
clean URLs, so the JSON the API serves, the citations, the structured
data and anything a reader copies stay canonical; only the href a human
clicks is tagged. 40 links across 25 files: provider sites, docs, repos
and X profiles, harness sources, venue and market pages, block explorer
transactions, our own GitHub and X.

Four kinds are deliberately left clean, and withUtm enforces each:

  - anything that is not http(s). mailto:, tel:, #anchors, relative
    paths, and the cursor:// and vscode: MCP deeplinks on /mcp.
  - our own site, subdomains included. Tagging an internal link starts a
    new campaign mid-visit and corrupts session attribution in PostHog.
  - a URL that already carries utm_source, so a hand-written campaign
    link keeps its own attribution.
  - identifiers that happen to be URLs: the Zenodo DOI and the Creative
    Commons deed are cited verbatim and appear in JSON-LD as well as in
    prose, where a tracking parameter is wrong in both places.

RPC endpoint URLs are the case worth stating twice. They are printed so a
reader can paste them into a config, so a utm_source in one would be
copied into production. They never reach the helper, and a test asserts
that the two modules that render them do not import it.

The machine-readable surfaces are excluded too: /api/*, llms.txt, the
sitemap and the JSON-LD builders hand URLs to agents and to other
people's pages, where a tracking parameter ends up inside a citation or
somebody else's HTML.

Missing one of these is invisible: the link works and simply reports
nothing. So the test greps the source rather than sampling behaviour. It
fails on any literal or template-literal external href that is not
wrapped, which is what caught the 28 the first pass had left, and it will
fail on the next one somebody adds.
…does not serve (#2741)

* the unfiltered view selected every workload at once, so the page showed almost nothing

injectLabels treats a selector pinned to `="all"` as the pooled form and
replaces it when a reader picks a tab; a selector with no such matcher
spans every combination instead. The queries here carried neither, so the
default view asked for one provider across five profiles, three volumes
and seven plan tiers at once, and the providers whose cheapest plan
differs per slice fell out of the table entirely.

Pooling a bill across workloads would be meaningless, averaging a $24
plan with a $10,500 one, so the harness publishes the headline slice
under `kind="all"` / `bucket="all"` instead: a dapp method mix at a
hundred million requests a month. The dimension labels name that slice
rather than claiming to be an average.

* a provider that cannot serve the workload was reading as free

Seven rows showed $0 on the rpc-cost leaderboard: Infura, whose daily
allowance hard-stops well below the workload, and six providers with no
product on the chain being priced. $0 says "free" where the truth is
"unavailable", which is the one mistake this bench exists to avoid. The
spec now carries a `ranked` query wired to rpc_cost_eligible, so those
rows move to the unranked block with their reason instead of posting a
price nobody offers.

The selector labels were named after the dimension KEY, so the page
offered "Venue", "Trade size" and "Kind" for what are really plan tiers,
monthly request volume and workload profile. `dimension_labels` lets a
spec name its own axes; every bench that sets nothing keeps the label it
has today.

Default view moves to 10M requests a month, the volume most readers
arrive with, and the pinned `venue="all"` now carries the cheapest PAID
plan: at 10M several free tiers are eligible and would otherwise take
every top row, hiding the comparison. The Free tier tab answers that
question on its own axis.

Also: product-page entries for the seven providers that had none, and
logos for four of them. BlockPI, Coinbase CDP and Instanodes publish
only an .ico or a JS-rendered page, so they keep the initials fallback.

* an adversarial audit found the numbers sound and the claims around them not

Eight agents re-checked the bench against primary sources, the code, and a
third independent recomputation. The money holds: 1,155 of 1,194 cells
agreed first try, no cell is non-monotonic, and the winner table reproduces
dollar for dollar. What did not hold was the prose, and six model bugs.

The worst bug: Chainstack's archive rule was recorded and never applied.
`block_age` was treated as a no-op on the theory it was already in the
weights; it was not, so the indexer profile billed 1 Request Unit per
request instead of 2 and the page published $3,990 for a bill of $8,990.
An unknown archive rule now fails closed instead of pricing archive at par.

Also fixed: the throughput gate read the billing unit when Alchemy meters
the rate limit in a different one (25x apart on debug_traceTransaction),
so the check now uses the throughput table and reproduces Alchemy's own
61,728 and 234,568 CU/s exactly; the dedicated cohort had no weights and
rendered nine empty rows; the `cohort` label did not match the `tier`
dimension key, so that selector matched nothing; reference rows were
filtered out by the paid-only default; and nothing was ever Reset, so a
provider that stopped being eligible kept exporting its last price.

Nineteen published claims were false or unsupportable and are corrected or
withdrawn, the largest being "71x" (7.6x), "five winners" (six), "the
lowest per-request price in the market" (three providers beat it), "the
page names the reason" (it does not, yet), and four separate promises that
freshness was being checked when no baseline hash had ever been recorded.
Google Blockchain Node Engine was published as live capacity; Google shuts
it down on 15 December 2026. And a product page described InstantNodes,
which is dead, under the slug of Instanodes, which is trading.

* keep the promises instead of withdrawing them

The audit found four things the page claimed and the code did not do. The
last commit made the prose honest by dropping the claims; this one does the
work instead, for three of the four.

Drift detection is armed. 21 artifacts now carry a baseline hash recorded
from their live body, so a provider editing its pricing surfaces as drift
rather than as a stale figure that still reads as current. The remaining
artifacts stay on manual re-reading on purpose: Chainstack's pricing JSON
regenerates every fifteen minutes and the big marketing pages churn on any
unrelated edit, so hashing them would alarm every cycle until everyone
learned to ignore the alarm. Age is published either way, so a figure
nobody has re-read is visible as one.

An artifact we deliberately never fetch no longer reports ok=0. That gauge
read as "the provider's page is down" for 38 artifacts nobody had asked
about, which is a different and untrue claim; it is now simply absent.

The free-tier allowance, the effective cost per million, the billing units
per request and the dedicated cohort's break-even volume were all being
emitted and rendered nowhere. Each now has a panel. The ledger's p50/p90/p99
columns are relabelled: the bill is a constant read from a committed
catalogue, so three percentiles of it are the same number three times, and
the success-rate columns are off because this bench sends no RPC traffic
and had no business showing 100%.

Still not done: the reason a provider is absent from a cell reaches the
harness log and not the page. The prose says so rather than implying
otherwise.

* a volume tab showed another volume's numbers, and a free provider was pruned as missing

Two bugs in shared code, both found by auditing what the page rendered
rather than what the spec said.

The variant fetch never sent the bucket filter, and the all-selection test
that short-circuits it never considered the bucket either. So picking only
a volume fetched nothing, any other fetch came back with the default
volume's figures, and the result was cached under a key that DOES include
the volume. Every bucket tab rendered the default one. This also broke
bench 268's trade-size tabs, which is the dimension the key was added for.

The ledger and the summary strip prune all-zero rows because on a latency
bench a provider emitting 0 ms is dead, not instant. On a cost bench $0 is
the answer: dRPC and OnFinality genuinely serve a ten-million-request month
for nothing, and pruning them made the page name a paid provider as the
cheapest option. Rather than change that rule for 240 benches, a spec can
now declare `zero_is_a_value`, defaulting to false so nothing else moves.

* a dedicated node priced every chain tab, including the ones it does not serve

The audit caught two blockers, both introduced this afternoon while fixing
something else.

Making the dedicated cohort unmetered so it would stop rendering nine empty
rows also skipped unitsPerRequest, which held the only chain check. So AWS,
whose three plans are Ethereum nodes, led the Solana tab at $97.82; Allnodes
showed $320 for its Ethereum plan where its Solana plan is about $1,920; and
Helius's Solana node carried an Ethereum bill. Every plan now declares the
chains it serves, taken from its own published name rather than inferred
from its id, and the catalogue refuses to load dedicated capacity that
declares none. That hole cannot reopen quietly.

The free-tier panel ranked dRPC first on the trace tab at 10.5M requests
while its own free plan is archive: false, trace: false and the ledger
directly below refused it. The panel now applies the same eligibility gates
as the table, so an allowance a plan cannot spend on a workload is not
published as one.

Fixing the first surfaced a third: the break-even denominator was the
cheapest simple-read rate, and simple-read is an Ethereum profile, so every
Solana node was dropped for want of a denominator. The floor is now computed
per chain from that chain's own baseline workload, and a break-even is
labelled with the chain it was actually computed against.

* the sentences describing the numbers went stale every time the numbers moved

Four published claims were hand-written about values the model computes, so
correcting Chainstack's archive rule, the throughput meter and the dedicated
cohort's chain gate silently falsified them. The audit caught it; the text
had been corrected once already and had rotted again by the time the model
settled.

Recomputed from the harness's own 1,485 series: five providers win a cell
outright rather than six, and two more share a cell on a tie; the trace
profile at a hundred million spans 6.7x from NOWNodes at $567.75, not 7.6x
from Chainstack at $499; Alchemy and QuickNode run 1.7x to 10.6x above the
cell leader, not 2.5x; and Chainstack, which the copy still named as a trace
winner, wins no cell outright.

The durable half of the fix is a per-bench plugin in the audit skill that
recomputes all four from Prometheus and fails when the prose disagrees, plus
a check that understands `dimensions.tier` partitions the cohort so the
generic row count stops reporting 20 of 31 as a fault.

* the quotable sentence crowned a provider on a slice it never named

The SEO audit found the page handing answer engines the exact claim its own
intro says cannot be made. "BlockPI posts the lowest monthly cost at $29.50
across 15 ranked providers" appears on the TL;DR, the infobox, the results
heading, the table header, the StatisticalReport JSON-LD, /api/stat,
/api/citable and llms.txt. It holds for the dapp workload, on Ethereum, at
ten million requests a month, on the cheapest paid plan, and for none of the
other fourteen cells. None of those four conditions appeared anywhere in the
served HTML, and /api/stat returned filters: null.

A bench that declares dimensions answers a different question per tab, so
the headline now names the slice it was measured on. A dimension whose first
value reads "All ..." is a genuine aggregate and is skipped, which leaves
every chain- and region-dimensioned bench untouched; tier is skipped too,
since it partitions the provider list rather than the measurement and the
cohort size is already in the sentence.

Also: the meta description rendered at 65 characters because the spec's was
207, over the cap, and the render kept only its first sentence. Rewritten to
139, inside the window.
There was no contact page. The address lived in the footer and on three
other pages, so every kind of request arrived in one inbox with no
context, and the pages carrying it drew about 1 % of the traffic.

The page routes a request to the channel that answers it. A wrong number
goes to the data-quality template, which asks for the bench slug and the
value and is therefore checkable. A provider correcting their own entry
goes to the provider template, which asks who they are so the change can
be attributed. A benchmark proposal goes through an issue before anyone
writes a harness. An integrity problem goes to a private advisory rather
than a public issue, which is the one case where filing in the open
makes things worse.

Email stays last and unconditional. A contact page that only offers forms
turns people away, and the ones most worth hearing from are often the
ones who will not open a GitHub account to tell us we are wrong. No form
on the page either: it would put a database between the reader and us for
no gain.

A section says what we cannot do, because those three requests arrive
anyway and a page that stays silent about them invites them.

The second half is why this was worth doing at all. The outbound-click
listener returns early on any protocol that is not http(s), which is
correct for the cursor:// and vscode: deeplinks on /mcp and silently
dropped every mailto. The one contact channel the site had was the one
surface it never measured, so "nobody wrote to us" and "we never looked"
were the same number: zero. A mailto now fires contact_click with the
page it was clicked from, and no address, since there is one and it is
already public.

Also: the sitemap gained a duplicate /contact while this was being
written, because a grep for "/contact" does not match a template literal
spelled ${SITE.url}/contact. That is the same defect that put
/chains/cosmos-hub in the production sitemap twice yesterday, so the
static route list now has a test that fails on any repeat.

The footer comment counted its own links and named which ones the sitemap
lacks. Adding one made both halves wrong, so both are corrected.
* a docs page, so the Docs group in the nav finally has one

The left rail had a group labelled Docs holding Methodology, API & MCP,
Contribute, About and Contact. There was no page that said what this
site exposes to a machine, so the answer was spread across /mcp, the
OpenAPI document, llms.txt and whatever a reader happened to find.

/docs is that index. The MCP server and its four tools, the eleven REST
endpoints the OpenAPI document declares, the paths that answer
Accept: text/markdown, the files written for agents, the four citation
formats, the badges, the feeds, and the same data on Hugging Face.

It does not repeat /mcp. That page is the install flow for Claude
Desktop, Cursor and VS Code with the deeplinks and the JSON blocks; this
one says what the server is and links there.

Nothing here was written from memory. Every endpoint, transport, format
and path was probed against production while the page was written:

  - the four tools come from tools/list on the live server, not from the
    route source
  - the eleven endpoints are the ones /api/openapi.json declares, each
    probed, including /api/og/{slug}, which is served through a rewrite
    and so does not appear as a route file
  - the citation formats are bib, ris, apa and txt. Not bibtex, which
    answers 400 with the allowed list
  - Markdown is served for /benchmarks/{slug}, /products/{slug}, /perps,
    /rwa and /capital, and the page says so rather than implying every
    page, because /rpc and /chains/{slug} answer HTML whatever the Accept
    header asks for
  - the MCP rate limit is rateLimit(key, 60, 60), which is 60 a minute
  - both Zenodo DOIs are named and distinguished, because the record at
    ...312 declares ...311 as its conceptdoi and the two are not
    interchangeable: the concept DOI follows the newest version, the
    version DOI is the one that keeps a result reproducible

The two facts most likely to rot say where the live truth is: tools/list
for the tools, and the OpenAPI document for the endpoints, which is
generated from the routes rather than hand-written.

* docs: /api/citable goes through Link, like the footer already does

next/no-html-link-for-pages resolves /api/citable/ as a known route
because /api/citable/[date] exists next to it, so the bare <a> was an
error rather than a warning. The footer has used <Link> for the /api
surfaces since it was written; this matches it.
An SEO pass over the pages shipped today found both of them missing
og:image. 39 routes carry their own opengraph-image.tsx; these two were
the exception, and the root one does not cascade, so a link to either
unfurled on X, Slack or LinkedIn as a bare URL with no picture.

Both now render through the same renderHubOG template as every other
hub, with the same 24 h revalidate: the card changes slowly and
unfurlers fetch it constantly, so regenerating ~1-2 s of satori CPU per
request is what the revalidate exists to avoid.

The /docs meta description was 254 characters, which Google truncates at
roughly 160. Rewritten at 148, which is one line a search result can
actually show.
It shipped as one column of prose and bullets, which is fine for a page
read top to bottom and wrong for this one: nobody reads an API index in
order, they arrive mid-task looking for one endpoint.

Same shell as /methodology now. The wider max-w-5xl, the large hero,
numbered Section I to VIII headers, and card-soft panels instead of
loose lists. The four MCP tools and the eleven endpoints are hover-ruled
definition rows, the same component /methodology uses for its
statistical conventions, which gives the endpoint path and its
description a stable two-column relationship instead of running them
together.

The contents list is the one thing /methodology does not have and this
page needs. Eight sections, each with an anchor, in a two-column card
under the hero. Section headers carry scroll-mt-24 so an anchor does not
land under the sticky header.

Two things improved while moving:

The Markdown section listed its five paths inline and mentioned the
exclusions in a trailing clause. The paths are chips now and the
exclusion is its own line, because "/rpc and /chains/{slug} are not among
these" is the sentence that saves someone an afternoon.

The citation section had the two Zenodo DOIs in one paragraph with the
index endpoints. They are different decisions. Which DOI you cite depends
on whether the reference should follow the dataset or freeze with your
result; which index endpoint you point at depends on whether the page
will move. Two cards, one each.

Every fact is unchanged and still the probed one.
…#2759)

* the app directory needs a privacy policy and terms, and both were 404

Listing the MCP server at /api/mcp/mcp in the ChatGPT app directory requires
four reachable URLs in the submission form: website, support, privacy policy
and terms of service. The first two exist. The other two answered 404 on
production, so the submission could not be filled in at all.

The privacy policy is read off the code rather than drafted from a template,
because overstating what we collect is as wrong as understating it. Every
claim traces to a line: autocapture is off and person profiles are disabled
in posthog-provider.tsx and nothing ever calls identify; analytics are
proxied through our own origin so the browser never contacts the analytics
host; the server-side id in analytics-server.ts is a daily hash of the user
agent with no IP stored; rate-limit buckets live in an in-memory Map that
never reaches disk. If any of those change this page changes in the same
commit.

The terms carry two clauses that are not boilerplate. "Not financial advice,
we never execute transactions, never send us a key" is what the three
negative test cases the review requires will point at. The
conflict-of-interest disclosure repeats what publisher.json and /partners
already say, and all three must stay identical.

The domain challenge route is the third required piece: verification fetches
a token as plain text from /.well-known/openai-apps-challenge and the body
must be the token alone. The token stays in an env var rather than the repo,
because committing it would let anyone prove ownership of this domain for
their own submission; until it is set the route 404s, which is the honest
state. force-dynamic so rotating it needs no rebuild.

Both pages are in the sitemap and linked from the footer: neither is reachable
from the rail, and a reviewer follows those URLs.

* the MCP server published a thousandfold error and 35k tokens of catalogue

Three things, all found by calling the live endpoint rather than by reading
the code.

The values were wrong. Latency benches declaring unit "s" store milliseconds
by the fmtUnit convention, and this endpoint published the stored number
beside the declared unit: {"value":443.494,"unit":"s"} for a head lag the
site and /api/stat both render as 0.44 s. An agent citing value and unit
together was off by a thousand, under provider names, on three benches
including number 001. valueInDeclaredUnit already existed for exactly this
reason, its docstring names this same bench, and /api/stat already called it;
the MCP server never did. Every value crossing the boundary now goes through
it, and rankings carry both the stored `ms` and a `value` in the declared
unit so either read is right.

list_benchmarks returned the catalogue. All 229 live benches in one response,
142,555 characters, about 35,638 tokens, for a question as broad as "what do
you measure". A model handed that spends its context on JSON instead of on
the answer. It is now compact, capped and filterable: 25 rows of nine short
fields, about 2,400 tokens, 94% less, and it says how many it held back.

The tools were named for the REST surface they mirrored, not for the
questions people ask, and a client routes on the description text. Nobody
thinks "list benchmarks"; they ask which Solana RPC is fastest, or whether
Alchemy beats QuickNode. So: search_benchmarks takes their words and ranks
the catalogue against them, including by the provider names a bench measures,
so "is Helius faster than Triton" finds the Solana bench without anyone
knowing its slug. compare_providers takes the names they said and returns a
provider it does not measure as `missing` rather than as a loser, because
those are different answers. recommend_provider takes the goal they stated
and returns the caveat with it, since "read p99 not p50 for a bot" is usually
worth more than the ranking.

query_prom is gone. Behind its allowlist it was safe enough for a developer
tool, but no end user asks for a PromQL passthrough, it was attack surface
with no reader, and every extra tool dilutes the routing the useful ones
depend on. Its allowlist and validator went with it.

Shaping lives in src/lib/mcp-tools.ts so it can be tested without a server:
13 tests cover the unit conversion in both directions, provider-name search,
the cap, the longest-phrase use-case match, and the missing-vs-loser rule.
Read-only annotations and structuredContent on every tool.

* the resource path carried the same unit bug the tools did

Found by auditing the diff rather than by a test. I converted every value on
the tool path and left the one on the resource path, which serves the same
benchmark to the same clients under the same field names: an agent pinning
openchainbench://benchmark/aggregator-head-lag still read 443.494 next to
unit "s".

Both its value and its leader now go through valueInDeclaredUnit, and its
rankings carry a declared-unit `value` beside the stored `ms` like the tool
response does. Nine conversions in the file now, one for every raw read.
Google shows its default globe next to every openchainbench.com result
instead of the logo. The cause is ours: /icon and /apple-icon are served
with `X-Robots-Tag: noindex`.

They go through ogResponse, the helper that stamps that header on every
generated image. Its own comment listed "icons" among them, so this was
deliberate and simply wrong about one case. The header shipped on
2026-09-19 in #2515, which is when the mark would have stopped being
eligible.

The intent was sound. About 200 generated image URLs sat in Search
Console as "Crawled, currently not indexed" because Googlebot fetched
each one from an og:image meta tag and then had to decide about it, and
the header settles that at fetch time. But a favicon is not a share
image. It is the one asset Google has to store and serve beside the
listing, so telling it not to index that asset removes the only thing it
needs. Opengraph and twitter images keep the header; the two icons opt
out through an explicit `indexable: true`.

Verified with the production Host header, since the staging-noindex rule
in next.config.ts fires on any other host and masks this locally:
/icon and /apple-icon now answer with no X-Robots-Tag, while
/opengraph-image and /twitter-image still answer noindex.

Two things this does not fix. /favicon.ico still 404s; Google accepts
the rel="icon" link so it is not required, and changing which URL serves
the mark right now would reset a clock that is about to start. And
Google re-crawls favicons on its own schedule, "several days to several
weeks" by its own documentation, so the globe will persist for a while
after this ships.
Thirdweb is declared on 31 chain RPC benches and ranked on none of them.
It answers 0.3 % to 1.5 % of our probes across 29 chains while returning
25 of 25 calls from a consumer connection: it serves browsers and refuses
our probe hosts.

The exclusion was right. A median drawn from a handful of replies is not
a measurement, and the 50 % ranking floor and the 5 % display floor both
exist for that. What was wrong is that the reader never learned it. Below
5 % a provider left the table, left {{count}}, and left the headline, so
a page naming one endpoint read as a chain that has one endpoint.

That is the flattering half of the fact. Our readers deploy on servers
and would meet the same wall, so the refusal is the most useful thing we
know about that endpoint.

Three changes, all derived from the measurement rather than authored, so
an endpoint that recovers rejoins the table on its own:

- belowDisplayFloor() in provider-filters, the exact complement of
  displayResults over the live set, with a test that the two partition it
  so nothing can fall into neither again.
- A "Declared, but not answering our probes" block under the endpoints
  list, naming each one with its URL and its real success rate, and
  saying that a provider can serve a browser and refuse a datacenter.
  This is separate from the authored excludedProviders block, which is
  for a provider we chose not to probe.
- The one-endpoint headline said "the only free public IoTeX RPC endpoint
  measured" on a chain where three were measured and two answered under
  2 % of the time. It now says "the only one of the N endpoints answering
  our probes", which is what the number behind it actually holds.

The section used to return null below two usable endpoints, so the pages
that needed the explanation most were the ones bailing out before it. It
renders when a silent endpoint exists, and still returns null when there
is nothing to paste at all.
The FAQ endpoint sentence paired an authored list of endpoints with
{{count}}, which is the display cohort behind a 5 % success floor. The
two drifted: twenty-nine specs disagreed with themselves, the worst
reading "2 endpoints sustain continuous keyless probing today" above
three names, one of which answers 1.4 % of the time.

The verb was the other half of it. "Sustain continuous keyless probing"
asserts that every named endpoint answers, which is the claim the
measurement contradicts: Thirdweb is declared on 31 chain RPC benches and
answers 0.3 % to 1.5 % of our probes. An endpoint is probed whether or
not it replies, so the sentence says that instead, and the block shipped
in #2779 names the ones that do not reply, with their success rate.

{{declared_count}} is derived from the spec's own provider list, so
neither number is authored any more. Only the names are, and a test pins
them: no spec may claim continuous probing again, none may pair the
display count with that sentence, and every spec using the sentence must
use the derived placeholder.

The validator keeps an allow-list of placeholders and rejected all 88
files on the first run. That is the check working: the token is
registered now rather than the list being bypassed.

An earlier attempt rewrote the whole sentence, mangled the punctuation on
55 files and covered only 55 of 89 because the tails vary. Reverted. This
one replaces the faulty segment and leaves every tail alone.
An audit of all 161 chain RPC benches found seven with no provider
producing a number. Each declared endpoint was re-tested by hand, with
the right protocol per chain rather than eth_blockNumber everywhere,
which is what separated the dead from the merely non-EVM.

Six are retired:

  quicksilver   0 of 3 endpoints alive, last sample 29 days old
  thundercore   0 of 3 (two answer 503), 24 days
  canto         0 of 1 (connection times out), 24 days
  neon          0 of 3, 8 days
  plume         0 of 1 (drpc answers 400), probe fresh
  zetachain     1 endpoint alive, 0.37 % success, probe fresh

The last two are the pair worth reading twice. Their probes run and
their data is current, and what the data says is that nobody answers.
zetachain's single endpoint is Thirdweb, which serves browsers and
refuses our probe hosts on every chain we measure it (0.3 % to 1.5 %
across 29, see #2779).

The seventh, starknet-rpc, is NOT retired. Its probe has produced
unbroken data for at least seven days, OnFinality clears the 50 % floor
at 50.69 % with a 339 ms p50, and every query the spec declares resolves
against Prometheus right now. The published blob is 585 hours old
regardless, so the bench is a publication bug and not a dead chain.
Removing it would have destroyed working measurement.

Specs deleted, slugs added to REMOVED_BENCH_SLUGS so indexed URLs answer
410 Gone rather than 404, matcher kept in sync (its test caught the
first attempt, which had the wrong indentation and inserted nothing).

The six chains leave the harness too, so three probe regions stop
spending calls on endpoints that time out. Five leave the chain registry
with their bench, which removes an empty /chains page: each linked the
RPC bench and nothing else, and all six were already out of the sitemap.
plume stays in the registry because chain-bridged-tvl and
chain-fees-revenue measure it.

The harness edit took three attempts. A regex over the Go composite
literal left an orphan brace twice; `go build` caught both. The version
here cuts on exact line boundaries and passes build and vet.
The noindex on /icon was the cause of Google's globe and is fixed
(#2771). This is the gap that survived it: /favicon.ico, the
conventional location every crawler and tool probes directly, returned
404. The rel="icon" link is what Google documents as sufficient, so this
is unlikely to be what finally flips the listing, but a 404 on that path
is strictly worse than a file, and it is the single most-requested URL on
any site.

A real multi-size ICO: 16, 32, 48 and 64 px, each a PNG payload, on the
same #f8f3eb background as icon.tsx with 10 % padding so the mark is not
flush to the edge at 16 px.

Two things cost an attempt each and are written into the generator so
they cost nobody else one:

Next's ICO decoder rejects non-RGBA payloads. flatten() produces RGB, so
the first build died on "Format error decoding Ico: The PNG is not in
RGBA format!". ensureAlpha() after flatten() is required, and the script
asserts four channels per size rather than trusting it.

The file is binary and nobody would remember how it was made, so
scripts/build-favicon.mjs regenerates it from public/logo.png and
explains both constraints.

What this does NOT change: the existing <link rel="icon" href="/icon">
stays exactly as it is. Google wants a stable favicon URL and that one
has already been reset once this morning when icon.tsx was edited; it is
not being touched again.
…#2789)

isExpiredBench exists to keep a page that renders noindex out of the
sitemap. It read `b.status === "live"`, and the only rows that ever reach
it are the sitemap rows the worker publishes: slug, lastRunAt, category,
perChainSlugs. No status. So the comparison was false for every row and
the guard did nothing from the day it was written.

The RPC variant beside it has no status check, which is exactly why chain
pages expired correctly and nothing else did. Its own comment describes
perp-asset-breadth sitting in the sitemap with index, follow and
month-old data "because every expiry check downstream was RPC-shaped":
the fix for that was this function, and it was inert.

Found because it blocked a deploy. bridge-execution-latency and
bridge-realized-cost render noindex, both were listed, and the sitemap
smoke gate failed the production deploy and tried to roll back. Their
lastRunAt is the Unix epoch, which materialize/load.ts writes
deliberately when a bench declares a freshness_timestamp_metric and
Prometheus reports no run at all, "so say so rather than pretending". A
56-year-old page must not be advertised to crawlers.

An absent status now counts as live. A draft bench has no business in the
sitemap either, so the default loses nothing, and the worker publishes
the real status from here on so the row carries the fact instead of
relying on a default.

Seven tests pin it, including the case that was broken: a row with no
status at all.
…ion (#2791)

Their documented trades channel, wss://wsdex.okx.com/ws/v6/dex, answers
"Please log in" to every subscribe including the docs' own verbatim example,
and their fee table says why: Free reads "WebSocket not supported", cheapest
WS-capable plan $99/month.

Their web frontend holds a different socket that needs no credential. Two
things hid it, both of which fail silently:

  - the channel is dex-market-trade-history, not any `trades` variant. Twenty
    other names were rejected before this one;
  - the params go in an extraParams JSON STRING. With flat keys the gateway
    still answers {"event":"subscribe"} and then pushes nothing, which is
    indistinguishable from a dead channel. An ack proves nothing here, only
    frames do.

Base and Solana only, and both exclusions are structural. BNB publishes
head_lag_seconds as receiveTime minus the provider's own timestamp, and every
OKX timestamp is a whole second (1,800 sampled across three chains, all
ts%1000 == 0), so measuring it there would publish our arithmetic as OKX's
latency. Robinhood Chain is not in OKX's supported set.

OKX cannot scope a subscription to a pool: six ways of passing the pool
address are accepted and silently ignored, and the paid channel is
token-scoped too, so this is the shape of the product rather than a level of
access. It does not change what is scored. Only emissions matched to the
reference by transaction hash are, and the reference is the bench pool, so the
scored set is identical to the other four providers'. Verified before shipping
rather than assumed: over 20 minutes on the Base pool, 213 of 213 pool swaps
appeared in OKX's stream, p50 636 ms behind our node, OKX never first.

The one cost is bookkeeping. An off-pool emission never matches and would
expire as a miss, which would read as a 96%-miss provider and drown the signal
that counter exists for: whether the reference itself is healthy.
tokenScopedAggregators exempts it, and a test keeps the exemption to providers
that genuinely cannot scope to a pool.

The feed is undocumented frontend infrastructure with no stability guarantee.
That is the bet geckoterminal_monitor.go already makes here, and the failure
mode is the same: the flow watchdog purges the gauge and the column reads
"Feed down" rather than a wrong number. Tests pin the socket and channel
constants so a silent move fails loudly.

okx.svg replaces the opaque JPEG, which drew a white box around the mark on
dark backgrounds.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
head_lag_seconds on bnb published receiveTime minus the provider's OWN
timestamp, because bnb was in neither referenceChains nor raceChains. The
spec justified that by claiming BNB's chain-supplied timestamps sat
"within roughly 60 ms of the moment a trade is observable".

The gap between that series and the reference-matched one IS that
quantity, and it is 569 ms.

So the dominant term in the published column was block time, which every
provider shares, and the column barely separated them. Measured over 24 h,
8,604 samples per provider, eu-west p50:

              published    against the reference
  mobula        0.621 s          0.052 s
  serialized    0.657 s          0.102 s
  codex         0.676 s          0.215 s
  geckoterminal 33.881 s        31.196 s

Within 9% of each other before, spread over a factor of four after, and
the ranking is unchanged. This removes a common offset rather than
reshuffling anyone. p90 moves the same way (1.0-1.2 s to 0.12-0.33 s).

The gate in pending_match.go is that the reference must arrive before
every provider. Our node is beaten 6.6% of the time by mobula on bnb,
4.7% by serialized, 3.7% by codex. Base, already on the reference and
shipping, sits at 4.2% / 3.1% / 0%. Same order of magnitude, so this
clears the bar the project already applies to itself.

The reference subscription this reads already existed and already
populated head_lag_ref_seconds; nothing new is collected. Only which
series the headline reads changes.

Published numbers on the bnb column will drop by roughly 5-10x the day
this deploys. That is a methodology correction, not providers getting
faster, and the spec now says so where it used to carry the 60 ms claim.

TestReferenceChainsScope asserted bnb must NOT be here. Updated
deliberately rather than deleted, with the reason, and a new test pins
why bnb belongs now and why robinhood and solana still do not.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flotapponnier and others added 13 commits October 16, 2026 22:50
Where the zero came from, exactly: QuickNode's median on the block-push
cohort is 0.3 ms, and every formatter on this page rounded to the nearest
millisecond below one second. A third of a millisecond printed as "0 ms",
which tells the reader a provider answered instantly.

It was not confined to the race. The same rounding flattens 0.6 and 1.4 into
"1 ms" across the p90 and p95 columns, and on the HTTP cohorts it turns 2.1
against 5.8 into 2 against 6, erasing the gap the board exists to show.

So below 10 ms the figure keeps one decimal, in all three formatters: the
chain table, the provider pivot, and the summary card, which carried the
same zero under "Smallest push lag". Above 10 ms the tenths are noise and
the rounding stands.

The pivot also gets what the chain table got earlier today: on the race
cohort its "Median p50" is a median LAG behind whichever provider pushed the
block first, so it is headed "Median lag" there, with a tooltip saying the
leader sits near zero because it is zero behind itself and that the number
to read is the trailers' distance from it.

Gates: tsc clean, eslint clean, 561 tests.
Reported as "the speedtest is bugged and will not start". It is not bugged.
Brave Shields blocks requests to RPC hosts by default, the page detected
Brave through navigator.brave.isBrave(), and it said so twice: once before
the run and once after, with the exact two-click fix.

Both were set in `label-mono text-[10px] text-ink-faint text-center`, the
footnote style this site uses for methodology captions. The most actionable
sentence on the page was rendered in the least prominent style the design
system has, so it read as boilerplate and a reader concluded the tool was
broken. An error that carries its own fix but looks like a caption has
failed at the only job it had.

Both become one shared notice: bordered, amber-tinted, body size, with the
consequence as a short lead and the action in its own sentence. Shared so
the before-run and after-run messages cannot drift apart. The wording is
almost untouched, because it was already right; it was only invisible.

The detection logic is left exactly as it is. It earns its keep: the
precheck calls eth_chainId on every endpoint and, when all of them fail,
fails open and keeps the full list rather than blaming six providers for one
local cause. That reasoning is what produced the correct diagnosis here.

Gates: tsc clean, eslint clean, 561 tests.
…3017)

I drove the page headlessly in Brave itself, on the machine that reported
the failure, with default Shields and a clean profile. It prefilled
Ethereum, dropped the one endpoint that sends no CORS header, ran 29 probes
against each of the other four and ranked them: Tenderly 74 ms p50, dRPC 91,
PublicNode 98, 100% ok. The banner reading "Brave Shields will block this
test" sat above that run. It was false, and I had just made it louder.

A false warning is worse than none. It hands the reader a culprit, so they
stop looking, which is exactly what it did to me: an hour spent on Shields,
then on CORS, then on DNS, while the only question that mattered went
unasked, which was whether the thing works at all.

The page already knew. The prefill POSTs eth_chainId to every endpoint
before the run, and when all of them fail it keeps the list and says
nothing. That silence is the gap. The warning now fires on that measurement
instead of on the browser's name, and Shields appears as one candidate among
the local causes rather than as the verdict. The post-run message loses the
same certainty.

What I checked before touching the copy, all from this Mac: DNS resolves
through the router with no filter, no proxy, certificates issued by Google
Trust Services and Let's Encrypt with no interception, the five Ethereum
endpoints answering POSTs in 77 to 101 ms, access-control-allow-origin: *
on both the preflight and the real response for the production and the
staging origin, a CSP whose connect-src allows all https, and a sweep of the
whole directory finding 0 of 81 chains without a browser-usable endpoint.

Gates: tsc clean, eslint clean, 561 tests.
…#3018)

QuickNode wrote to say the monthly limit was reached. Measured before
touching anything, over 24h: QuickNode took 4,598,911 pushes, 138M a month,
against 583,140 HTTP probe calls. The arm is 237 times the HTTP probe by
event count.

The guard we had was looking the other way. `rpc_keyed_quota_used_ratio`
reads 1.17% for QuickNode because it counts HTTP calls and knows nothing
about sockets, which is why nothing warned before the vendor did.

And this is not one vendor's problem. Chainstack 124M, Validation Cloud
124M, Dwellir 112M, GetBlock 99M a month. QuickNode is simply the first to
have written.

Two cuts, measured rather than guessed.

The three fastest chains leave the arm. Robinhood alone is 40% of all
pushes, Solana 19%, Arbitrum 14%, against Ethereum's 0.4%: a chain producing
a block every 100 ms pushes 120 times what a 12 s chain does, to answer the
same question. That is 73% of the volume for a third of the pages. Their
three specs go with them; none was ever on main, so no redirect is owed.

What remains listens ten minutes in every hour. The window is derived from
the wall clock, and that detail is the whole correctness of it: if two
providers computed different boundaries they would listen to different
blocks, each would be its own leader, every lead would read zero and the
board would report that every provider is fastest. A silent failure worse
than an outage, so `wsWindowAt` is pinned by a test that asks several cells
at skewed instants and requires them to open together.

A close on the window deadline is not a disconnect. `rpc_ws_disconnect_total`
is a published result about a provider, and our own scheduler must not write
to it; the read goroutine racing the deadline is covered too.

The skip list is compiled in rather than read from the environment, because
it exists only because a bill was reached, and a lost variable must not be
able to restore one in silence. RPC_KEYED_WS_SKIP_CHAINS overrides it,
"none" empties it, and a duty window as long as its period restores the
continuous behaviour without a code change.

Expected effect per provider: 138M -> about 6M a month, a 95% cut, with six
of the nine benches intact.

Gates: gofmt clean on the file touched, go build, go vet, 4 new Go tests,
253 specs valid, 561 site tests.
…3019)

Preparing the dev to main merge, the conflict triage turned up one file
group where dev is not ahead but behind, and behind on a leak.

`src/lib/mcp-tools.ts` and the MCP route exist on both branches and were
written independently, so they conflict as add/add. main's copies carry five
commits dev never received: a thousandfold error and a 35k-token catalogue
(#2764), the search endpoint handing back an API key it had been asked to
search for (#2766), two tools that answered nothing (#2765), a third
annotation the submission scan reads (#2768), and a chain name outranking
the subject of the question (#2770).

Measured rather than inferred from the titles: `redact` appears 7 times in
main's mcp-tools.ts and 3 times in its route, and 0 times in either of dev's.
Resolving those conflicts in dev's favour, which a bulk resolution would do,
reintroduces the leak.

Checked before overwriting: the dev-to-main diff is additive. main adds
category-aware scoring, a minimum score that cuts prefix-only noise, and
redactSecrets(); the only lines it removes are the ones it replaces. dev
holds nothing in these files that main lacks.

The privacy page is the same shape without the security edge: main's is 219
lines against dev's 140 and is alone in documenting retention and the Vercel
dependency. dev's predates it, from when the page existed only because the
app directory requires a policy URL.

Doing this now, in its own change, means the merge no longer has a
resolution that can silently restore a credential leak.

Gates: tsc clean, eslint clean, 567 tests (up from 561: main's test file
carries six more).
main and dev had diverged both ways: 180 commits on dev, 87 on main, and no
fast-forward since October. A dev to main pull request would have opened
with 32 conflicts and no reviewer able to say which side was right without
reading all of them. This merges main into dev first, where the resolution
can be validated on staging, and leaves the release itself trivial.

Every file resolved in dev's favour was checked rather than assumed:

  the nine keyed RPC specs   main's corrections are already there (the
                             Alchemy figures match on all nine), dev adds
                             Validation Cloud, the p95 and the WebSocket arm
  the recording rules        dev holds all 11 of main's and 12 more
  rpc-capabilities config    dev carries the repair main never got, #2935,
                             which replaced four endpoints that had stopped
                             answering; taking main would restore them
  the Hyperliquid family     10 commits on dev against 5, every file larger,
                             and 34 metric names queried against 32
  katana, xrplevm            dev has one more provider each
  the three registries       no entry exists only on main
  stakeangle.png             dev's is the optimised one from #2938

Two were not a side to take:

  bench-published.json       the union. It records when a page first
                             appeared, so main's six dates for benches dev
                             has since removed are as true as dev's eleven
                             new ones. Earliest date wins where both know a
                             slug.
  provider-registry.ts       dev's entries, main's types. The triage had
                             compared entries and called them identical,
                             which they were; the module's exports were not.
                             main also landed ProviderClaim and an optional
                             claims field, and lib/provider-claims.ts, its
                             test and a product-page call site came through
                             the merge and failed to compile against dev's
                             registry. Caught by tsc, not by reading.

Gates on the merged tree: 253 specs valid, tsc clean, eslint 0 errors,
594 tests, promtool 23 rules, go build and vet on rpc-keyed-latency,
rpc-capabilities, hl-fills-reduce and hyperliquid-frontends, and a full
next build generating 293 static pages.
catch dev up with main, so the release is a fast-forward and not a fight
…3022)

The SEO audit on the release scope flagged bnb-rpc-ws and
hyperliquid-rpc-ws at 62 characters, where the target is 110 to 160. The
specs are not the cause: all six WebSocket benches declare the same
two-sentence description, and ethereum-rpc-ws renders it whole at 152.

capSnippet cuts at the last sentence end inside the budget. When the chain's
name is a few characters longer the whole text crosses 155, the only
sentence end inside the budget is the first one at 62, and the page ships
that: the second sentence, the one naming Chainstack, GetBlock, QuickNode,
Dwellir and Validation Cloud, is dropped whole.

The sentence boundary is the right cut when it lands near the cap and the
wrong one when it lands far below it. It now has to keep 60% of the budget
to be taken; under that the existing clause and word fallbacks already
produce a snippet that uses the space.

This is not specific to the release. Any description whose first sentence is
short and whose total sits just over the cap loses everything after it, on
every page type.

Three tests pin it: the shape that shipped 62 characters now keeps the
providers, a sentence end near the cap is still preferred, and no cut
exceeds the cap or ends on punctuation noise.

Gates: tsc clean, eslint clean, 597 tests.
…owns, (#3023)

and a leader that is not on its board

**The block-push pages quoted the number their own methodology calls an
artifact.** Every surface that cites these six pages said "GetBlock posts
the lowest push lag at 0.4 ms", while the spec two screens below says the
winner reads zero by construction and a coarse first bucket turns that into
an interpolation artifact. On hyperliquid-rpc-ws it was not merely weak but
false: "Chainstack pushes HyperEVM blocks first" with Chainstack at 60.6 ms,
a figure no provider that actually arrived first could post, since it would
sit inside the first histogram bucket. The board under the claim contradicts
it.

One string feeds the TL;DR, the StatisticalReport, /api/stat, /api/citable
and llms.txt, so one branch in citation.ts fixes five surfaces. It says what
the measurement supports: who has the lowest median lag, whether that lands
inside the first bucket, and how far the runner-up trails. The gap is the
figure a reader can act on, which is the same conclusion the hub reached
yesterday for the same reason.

The copy follows. Three sentences per spec told the reader to "read the win
rate": the table has p50, p90, p99 and Success, the harness emits only the
lag histogram, and no placeholder exists for a win share. Pointing at a
column that does not exist is worse than saying nothing, and FAQ 1 went
further and asserted the leader "wins most per-block races". Both now point
at the gap and at p99. A win share can be built from the bucket rates later,
and only then may the copy claim one.

The new descriptions are 136 to 149 characters for every chain and leader,
so the snippet cap never reaches them.

**perp-capital-efficiency named a leader with no row.** The intro read
"Gains supports over 40 active markets per $1M of trading TVL" and called a
"400x gap", while the TL;DR, the meta description and /api/stat all name
Ostium at 19.95 and Gains is absent from the twelve ranked venues. Two
hand-typed figures, neither checkable, contradicting the page above the
fold. It now reads the board through placeholders and keeps the
architectural point it was making. `{{ranked_count}}` is deliberately not
used: it is not a supported placeholder and would print raw.

Still open from the audit, deliberately not in this change: the six WS pages
are near-identical (Jaccard 0.98 on the first 300 words) and nothing links
to them from /rpc or the chain pages. That is differentiation work, not a
correctness fix, and it does not block the release.

Gates: 253 specs valid, tsc clean, 597 tests.
…3024)

**A bench that ranks nobody was telling machines it had never run.**
perp-max-leverage-terms shows four venues and a timestamp from this
morning, while /api/stat, /api/citable and llms.txt all said "Awaiting first
run". headlineParts read "nobody is ranked" as "nothing ran", and on a board
that ranks nobody by design that never corrects itself. It now says what is
true: how many venues were measured, and that none is ranked here.

**Two sibling perp benches contradicted each other.** perp-active-markets
rendered "Hyperliquid at 178 is crypto only" while perp-asset-breadth's own
title says Hyperliquid lists 156 non-crypto markets. Both are right about
different universes: the core book is crypto, the HIP-3 deployer markets are
not. The sentence now says which one it describes and points at the bench
that counts the other.

The same page called market count "the fastest proxy for breadth". "Fastest"
is the measured claim of a latency page; spending it on an unmeasured one is
the rule this repository keeps. It is "simplest" now, in both places.

And it pointed the reader at a findings text that ships only behind
/api/bench/<slug>/variant, which sends x-robots-tag: noindex: a winner named
on evidence no page renders. The clause is gone, replaced by the fact it was
reaching for, that raw count can outrank breadth of asset class.

**Figures typed into prose go stale in silence**, so rpc-free-tier's "a
factor of eight: 3.3M requests against 392k" and perp-asset-breadth's "the
other 175 coins" are now claims that cannot rot.

**Five titles ran 61 to 64 characters** with the brand suffix, and the hub's
ran 64. Dropping one word keeps the entity, the two cohorts and the metric.
The hub's description also named four keyed providers where the chain pages
list six, and the snippet cut landed inside that list, shipping "...plus
Alchemy, Chainstack." It now names all six and fits.

**/hyperliquid/traders invited indexing on an empty page.** It is new, has
no snapshot, sits in no sitemap and is linked from nowhere, and its static
metadata carried no robots rule. It switches to generateMetadata so the rule
follows the data: noindex while the page says "Awaiting the first run",
indexable once a snapshot exists, follow throughout.

**And the hub linked neither new family.** /rpc is the page that ranks for
the head term and it pointed at neither rpc-free-tier, which answers "free
RPC plan", nor any block-push race. Both join the pills.

Not in this change, and deliberately: the six WebSocket pages are still
near-identical (Jaccard 0.98 on the first 300 words) and still have no
inbound link from the chain pages. That is differentiation work with a real
editorial cost, not a correctness fix.

Gates: 253 specs valid, tsc clean, eslint 0 errors, 597 tests.
… is (#3026)

not persistent, and a sample floor copied six times

**Validation Cloud leaves the block-push benches.** The bench audit put it
plainly: it sat fourth at 91% "Success" while Prometheus showed the socket
closed in every region. Over the last three hours, across six chains and
three regions:

    chainstack       35,482 messages   34,936 blocks    3 disconnects
    dwellir          35,478            34,936           0
    quicknode        35,504            34,961           0
    getblock         28,202            27,751           0
    validationcloud       0                 0         542

542 reconnection attempts and not one frame. The logs say `websocket: bad
handshake` in every region, so the endpoint refuses the upgrade: wrong URLs,
or a key without WebSocket rights. The 91% came from the HTTP call counters,
which keep ticking for a provider whose socket never opens, so the row was
publishing a reliability figure about a measurement that does not exist.

Same treatment as Alchemy two days ago, for a stronger reason: Alchemy
worked and was spending its own account, this has never worked at all. A
methodology bullet says so, with the figures, and the row returns when the
vendor gives us an endpoint that opens.

**The specs still promised a persistent socket.** I changed the harness this
morning to open for ten minutes in every hour and updated one bullet,
leaving seven sentences per spec saying "persistent" - including every
provider's formula, which is the line a reader checks a number against. The
first attempt at fixing that pushed all five formulas past the 240 character
cap, which is the same cap that caught me once before, so the formula drops
the adjective and the schedule is stated where there is room to explain it.

**expected_n was one number copied six times.** The schema computes
health = sampleSize / expected_n per provider, and these chains differ by 27
times: Ethereum produces a block every 12 seconds, BNB one every 750 ms. I
had set 1166 everywhere, which was the old value divided by the duty factor
rather than a measurement. They now come from Prometheus, head arrivals over
three hours of the new regime scaled to 24h and divided by the four
providers that remain, rounded down to two significant figures so the badge
does not fire on ordinary variance:

    bnb 96000 · arc 78000 · hyperliquid 37000 · polygon 29000 ·
    base 22000 · ethereum 3600

Gates: 253 specs valid, tsc clean, 597 tests.
/compare/fomo-vs-pump-fun says pump.fun's memecoin trading fee is 63.9%,
and declares a winner on that column. Two cells to its left the same row
reads $41.09M of daily volume and $910.86 of fee revenue, which works out to
0.0022%. Three numbers, side by side, that cannot all describe the same
thing. A Community Note seen 11,400 times now cites the page.

The harness arithmetic was honest. The all-chains rate divides the fees it
trusts by the volume of the chains those fees came from, and for pump.fun
that is Base alone: $910.86 on $1,424.96 really is 63.9%. The dishonesty was
in the publishing. A rate computed over $1,425 was printed as the take rate
of a platform that routed $41,089,797 through chains the rate never saw, a
denominator 28,800 times larger and nowhere on the page.

Measured across the cohort on 2026-10-17:

    pump-fun   63.922%   fee coverage   0.003%
    padre       1.160%                100%
    maestro     1.135%                100%
    trojan      1.086%                100%
    gmgn        1.031%                100%
    photon      0.995%                100%
    axiom       0.918%                 95.9%
    fomo        0.494%                100%

One platform is broken and seven are not, with three orders of magnitude
between them, so the fix is a coverage floor at half. Far below every honest
platform, far above the broken one, tuned to neither.

Below the floor the all-chains rate is withheld the way a chain cell with an
unexplained zero already is: the row says withheld rather than going silent,
because pump.fun may well charge a fee and the honest statement is that we
cannot measure it over a denominator worth printing. The volume and the
transaction totals are unaffected; they are totals, not ratios.

`terminal_fee_coverage_pct` is published for every platform, so a page can
show the denominator instead of leaving a blank, and so the next time this
drifts it is visible before a reader finds it.

A Go test pins the exact shape, with the live figures in its comment, and
checks that a platform at full coverage keeps its rate.

Gates: gofmt clean, go build, go vet, harness tests pass, 253 specs valid.
…rage (#3025)

Adds Robinhood Chain (0.1 ETH -> USDG) to the evm-swap-quoting basket,
probed by Route, KyberSwap and LI.FI, and a Route adapter to
token-quote-coverage. Route adapters only register when ROUTE_API_KEY
is set.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to Dashboard OpenChainBench / production October 17, 2026 20:58 Inactive
…#3029)

pump.fun's take rate reads 63.9% on the live compare page and on the
Robinhood and Base chain tabs. The number comes from one cell: the source
reports $910.86 of fees against $1,424.96 of volume on Base, which is
0.0035% of pump.fun's $41.09M day. That cell is also the platform's only
fee-bearing chain, so it set the all-chains headline too.

The coverage gate added in #3027 withholds the headline, because 0.0035%
of volume is far below the 50% floor. It cannot see the per-chain cell,
which kept publishing 63.9% on its own. This adds the second half:

- A cell whose fees exceed 10% of the volume they sit on is withheld.
  Measured across the cohort, every honest take rate sits between 0.39%
  and 2.20%, the highest being maestro on HyperEVM at 4.87%. Ten percent
  clears that spread by more than double and catches the broken cell by a
  factor of six. A terminal keeping a tenth of every ticket would have no
  users, so this is a statement about trading, not a tuned threshold.

- The withheld flag on the all-chains row now keys off whether the source
  reported a fee for the platform at all, not off the totals. With its
  only fee cell rejected, pump.fun had no totals left and the row went
  silent, which reads as "charges nothing" on the page. It charges
  something; we cannot say what.

Volume and transaction counts for the rejected cell are untouched. Only
the ratio built on an unusable numerator goes away.

The methodology now states the ceiling, and the findings bullet that
named a day's withheld cells is rewritten as the rule: it claimed
pump.fun charges on four chains, which the source stopped reporting.
…ows (#3030)

#3029 cleared the take-rate bench. The compare page still showed 63.9%,
because the leaderboard board reads a different metric family:
terminal_window_take_rate_pct, summed over 1d, 7d and 30d in windows.go,
which had neither gate. pump.fun published 63.9% / 11.4% / 0.13% there.

The arithmetic is the same defect over three windows. $910.86 of Base
fees divided by $1,424.96 of Base volume, while the $40.6M Solana leg
reports nothing and is correctly excluded from the ratio. Excluding it
is right; using what remains as the platform's rate is not.

Both gates now apply to the windows as well as the daily cells:

- A day whose fee exceeds 10% of its own volume never enters a window,
  numerator or denominator. It is the source attributing a fee to the
  wrong denominator.

- The all-chains window needs at least half the platform's volume on
  measurable days. Per-chain windows are whole by construction, so the
  floor is not applied there.

Both constants move to package scope, since two files now share them and
a second copy would be a second thing to forget.

Volume, transactions and share are untouched: a dropped day still counts
everywhere it is a measurement rather than a ratio.

The methodology now states both gates on the leaderboard spec, which
previously described only the zero-fee-day rule.
/products/route has resolved since #3025, because the slug appears in two
specs, but it rendered as an "RO" initials chip with no description, no
official link and no docs. This fills it in.

The mark Route supplied is a 400x400 opaque PNG whose glyph covers 9.1% of
the canvas, the rest white. Dropped in as delivered it would render as a
pale tile with a small dot in it next to tightly cropped neighbours, and
could not sit on a dark page at all. It is cropped to the glyph and
re-padded to a square with a proportional margin, so the glyph now fills
43%, and the white is converted to alpha from the original grey ramp so the
anti-aliased edge survives instead of becoming a 1-bit cutout. No
resampling: every output pixel is an input pixel. 184x184, 781 bytes, in
line with hyperliquid at 180 and paraswap at 200.

Being monochrome on transparent, the slug is opted into the light chip in
ProviderLogo, the same treatment openai and anthropic get.

The registry text is from Route's own docs and homepage: one chain only
(Robinhood Chain, 4663), route.fun as the swap interface, a key-gated
partner API that quotes and builds in one request. The buyback and
liquidity sentence is attributed to Route rather than stated as fact,
because nothing here measures it.
The bench has run from three Railway replicas since it was built, the
spec declares all four region values, and ?region=us-east has always
worked on the variant API. The page never showed a region selector.

regionOptions is not built from the declared dimension. It is built from
extras.regions and extras.seriesByRegion24h, which the loader only fills
when providers declare per-region queries. This spec had none, so
regionsWithData came back empty, regionOptions came back [], and the row
of tabs never rendered.

Two lines of published copy already described the thing that was missing:

  methodology: "...so the page can filter to a single edge or aggregate
                across all three."
  faq:         "The bench page filter on region selects which slice of
                the histogram is queried."

Both are now true rather than aspirational, so neither changes.

Eight providers each get a regions block, shaped like rpc-capabilities:
the region key is the canonical ap-southeast while the PromQL selector
uses the label the harness actually stamps, sgp.

Every query was run against Prometheus before being written. Six of the
eight report in all three regions; openocean (403) and enso (429) report
in none, which is their documented state and not a consequence of this.

What the selector exposes, measured while writing this:

  provider     us-east   eu-west       sgp
  route          117       189        460
  kyberswap      233       279        168
  mobula         454       346        650

The leader flips by region and the all-regions average hides it. Route
is three to four times slower from Singapore than from Virginia, and
KyberSwap is fastest from the one edge where Route is slowest.

Cost, stated rather than discovered later: this adds 48 queries to an
aggregate load of this bench, the same shape of cost rpc-capabilities
already carries for 13 providers.
Route went to main as a cherry-pick (#3028, 45faeaf) so it could ship
without waiting for the release. dev carries the same change under its own
SHA, so git sees two unrelated commits touching the same lines and #3021
went from clean to CONFLICTING.

One file conflicts, benchmarks/evm-quote-latency.yml, and it is not a
disagreement. Checked line by line before resolving: every non-blank line
on main is present on dev, and dev adds 80 that main does not have, which
is exactly the eight providers times ten lines of regions blocks from
#3032. dev is a strict superset, so the resolution takes dev's side whole.

token-quote-coverage.yml auto-merged with no help.

Gates on the merge result: 253 specs valid, tsc clean, 597 tests pass.
Catch up dev with main, so the release PR is mergeable again
…3034)

The release would have published nine dead rows on public chain pages.
Validation Cloud is in the keyed cohort of all nine chain RPC specs and
has never produced a single measurement:

  rpc_health        0 on all 27 (chain, region) series
  rpc_call_total    result="http_err"  22018
                    result="timeout"       5
                    successes              0
  latency series    none exist

The harness log names the cause, identically on every chain and region:
`http_err err=status 401`, plus `websocket: bad handshake` on the WS arm.
The key is invalid, expired, or was never provisioned. The probe runs at
full cadence, 19,437 calls in 24h, the same rate as QuickNode.

Removed from the nine chain specs: 18 provider blocks (warm and cold), 9
disclosure bullets, and 31 prose mentions. The prose matters as much as
the blocks: leaving "Alchemy, Chainstack, GetBlock, QuickNode, Dwellir
and Validation Cloud" in a subtitle would promise a provider the table
does not have. Each enumeration is rewritten rather than left dangling,
and no copy stated a cohort size, so no count goes stale.

Kept, deliberately:

  rpc-cost        Validation Cloud reads 167 and has been live in
                  production since #3012. It is a pricing bench, not a
                  probe, and the figure is real.
  rpc-free-tier   reads 1,219,510, a documented plan allocation.

Neither depends on the keyed endpoints, so neither is affected.

The Railway URLs are backed up to the secrets directory and removed from
the three keyed services, so we stop sending 19,400 authenticated-looking
requests a day to an endpoint that rejects every one of them. Restoring
is one command per variable once a working key arrives.

Gates: 253 specs valid, 597 tests pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants