Repository navigation
release: dev to main, 8 new pages and the keyed cohort's real success rate - #3021
Merged
Merged
Conversation
…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>
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
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.
This was referenced Oct 18, 2026
Closed
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Not ready to merge until the Validation Cloud question below is answered. Everything else is prepared and verified.
mainis now 0 commits ahead ofdevand the merge reports 0 conflicts: this is a fast-forward. The 32 conflicts that stood here this morning were resolved ondevin #3019 and #3020, where staging can validate them, rather than inside this release.What production gains
8 bench pages, all
status: live, none gated: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
perp-active-markets,perp-asset-breadth,perp-capital-efficiency,perp-volume-oi-ratio. Nothing is removed from any of them./rpc./rpcgains 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 withouttier, 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_SLUGSgates 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:
Gates run on the merged tree
253 specs valid · tsc clean · eslint 0 errors · 594 tests · promtool 23 rules ·
go buildandgo veton rpc-keyed-latency, rpc-capabilities, hl-fills-reduce and hyperliquid-frontends ·next buildgenerating 293 static pages.After merging,
prod-deploy.ymlrunsvercel --prodautomatically. The four Railway services build fromdevand are unaffected: this is a site release only.