From 012018edc77b247774c33e75a59fdcf659a9a0ff Mon Sep 17 00:00:00 2001 From: Flotapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Sun, 25 Oct 2026 13:51:47 +0100 Subject: [PATCH] bench 030 claimed it used no node, on the day the node actually arrived The node substitution merged on 2026-10-07 (#2872) and never ran. The hyperliquid-frontends image was rebuilt on 2026-10-21 from ~/ocb-build/harness-src/hlf-build, a source tree whose Go files are frozen at 2026-10-07 18:09, hours before that merge: no nodesource.go, and no `a.node` anywhere in its agg.go. That rebuild refreshed only builders.json, which is why Nova appeared on the site and the node figures did not. The gauges are promauto-registered and Set in both branches of `if a.node != nil` inside publish(), so their absence from /metrics was the proof the binary had no node code rather than a node that was declining every day. Rebuilt today from current dev, verified by symbol before the container was touched, and the swap moved every figure on the bench: FOMO volume 7d $263.1M -> $780.8M (2.97x) cohort volume 7d $1.811B -> $4.783B (2.64x) node days / 30 0 -> 18, plus 10 reconstructions $780.8M is the figure the node's own seven complete days give, read independently through the reducer, so the bench was publishing roughly a third of the real notional for eighteen days. That makes four statements on the page false as of today, where they had been accidentally true for as long as the binary was stale: the subtitle, the abstract, the reproducibility bullet and the "where does the data come from" FAQ answer all said no node. The FAQ answer is also FAQPage JSON-LD, so it was a machine-readable claim. They now name both sources, say that a whole node day replaces the export's copy outright rather than being averaged with it, and say plainly that the days our node measured are not reproducible from the public feed alone. The coverage note had a second defect the same reading exposed. It took the export's share of the window as windowDays - nodeDays and then described truncatedDaysWindow as a subset of it, which the gauges cannot support: a rebuilt day sits in that remainder, and the truncated count includes days the node replaced. On today's reading that rendered as "the remaining 12 fall back to the export, and 11 of those stop around 12:10" where two days rest on the export. hlDaySources splits the window into the three sources the page can name, clamped so a scrape landing mid-pass cannot publish a negative day count, and the amber branch now names the node share instead of handing all thirty days to the upstream feed. The nodeDays == 0 wording is kept and tested. A node outage or a rollback to an older image reproduces exactly the state the page was in until today, and the note has to describe that one correctly too. --- benchmarks/hyperliquid-frontends.yml | 26 ++++++-- src/components/hl-feed-coverage-note.tsx | 82 +++++++++++++++++++----- src/lib/hl-feed.test.ts | 57 ++++++++++++++++ src/lib/hl-feed.ts | 37 +++++++++++ 4 files changed, 181 insertions(+), 21 deletions(-) diff --git a/benchmarks/hyperliquid-frontends.yml b/benchmarks/hyperliquid-frontends.yml index 78815aa24..e63ab73ef 100644 --- a/benchmarks/hyperliquid-frontends.yml +++ b/benchmarks/hyperliquid-frontends.yml @@ -5,7 +5,7 @@ number: "030" title: Hyperliquid frontends builder revenue leaderboard seo_title: "Hyperliquid frontends: {{count}} apps ranked by builder fees" seo_description: "{{best_name}} collected {{best_p50}} in builder fees on the last complete UTC day. Tracked Hyperliquid frontends ranked by fees, volume, users and fee bps." -subtitle: How much USD builder fee revenue each Hyperliquid frontend collected on the last complete UTC day, and over the 7 and 30 days ending on it. Source is the per-builder daily fills feed Hyperliquid publishes, no node and no API key. +subtitle: How much USD builder fee revenue each Hyperliquid frontend collected on the last complete UTC day, and over the 7 and 30 days ending on it. A day comes from our own Hyperliquid node where it holds that day end to end, and from Hyperliquid's public per-builder feed otherwise. category: Trading status: live metric: Builder fees collected @@ -17,7 +17,7 @@ disclaimer: | seo_intro: | - This page answers one question. Which Hyperliquid frontend collected the most builder fee revenue in USD over the last 24 hours, 7 days and 30 days. Every Hyperliquid order can attach an optional on-chain builder field. Frontends like Phantom Perps, MetaMask, Rabby, Insilico, based.app, OneKey, Axiom and pvp.trade write their wallet address into this field on every fill they route, then collect a share of the taker fee as their own rebate (capped at 100 basis points on perps, 1000 on spot). The bench ranks {{count}} frontends by the dollar amount each one collected through this mechanism, surfacing the real revenue scale of the Hyperliquid frontend ecosystem rather than a percentage ratio that hides the actual numbers. Data comes from the per-builder fills CSV that Hyperliquid publishes for every UTC day on its public stats feed. A Go harness on OCB infrastructure downloads each builder's file once the day is published, sums fees, notional and distinct wallets per builder, keeps 30 days of files for the 7 day and 30 day windows, and exposes Prometheus gauges that this page consumes. Day D lands on the feed early on D+1, so every number here describes the last complete UTC day and every builder is measured on the same day. Companion tabs surface volume routed, unique users and the trader perspective effective fee in basis points. + This page answers one question. Which Hyperliquid frontend collected the most builder fee revenue in USD over the last 24 hours, 7 days and 30 days. Every Hyperliquid order can attach an optional on-chain builder field. Frontends like Phantom Perps, MetaMask, Rabby, Insilico, based.app, OneKey, Axiom and pvp.trade write their wallet address into this field on every fill they route, then collect a share of the taker fee as their own rebate (capped at 100 basis points on perps, 1000 on spot). The bench ranks {{count}} frontends by the dollar amount each one collected through this mechanism, surfacing the real revenue scale of the Hyperliquid frontend ecosystem rather than a percentage ratio that hides the actual numbers. Data comes from the per-builder fills CSV that Hyperliquid publishes for every UTC day on its public stats feed, and from our own Hyperliquid node where it holds a day end to end: that export stops around 12:10 UTC on most days, so a day the node carries in full replaces the export's copy of it. A Go harness on OCB infrastructure downloads each builder's file once the day is published, sums fees, notional and distinct wallets per builder, keeps 30 days of files for the 7 day and 30 day windows, and exposes Prometheus gauges that this page consumes, including a count of how many days of the window were measured by the node rather than read from the export. Day D lands on the feed early on D+1, so every number here describes the last complete UTC day and every builder is measured on the same day. Companion tabs surface volume routed, unique users and the trader perspective effective fee in basis points. abstract: | @@ -36,8 +36,21 @@ abstract: | lives at `harnesses/hyperliquid-frontends/builders.json` in this repo, seeded from DefiLlama dimension adapters and HyperTracker plus a cross reference pass on the top unidentified addresses. New builders - ship via public PR. The bench does not place trades, does not touch - private keys, and does not depend on any node or internal service. + ship via public PR. The bench does not place trades and does not touch + private keys. + + Two sources supply the days. Hyperliquid's public export has been cutting + most days off around 12:10 UTC since 2026-09-08, and on a day it did carry + in full the hours before 13:00 hold roughly 43 per cent of the notional, so + a window summed from it alone lands near half of itself. Our own Hyperliquid + node writes every hour of a day it was up, and where it holds a day end to + end that day replaces the export's copy outright. The two are never averaged: + two readings of one quantity that differ by a factor of two are not two + readings. A day the node cannot replay may instead be reconstructed from the + export's own morning spliced to an afternoon rebuilt wallet by wallet from + the info API, and a reconstruction is counted and reported apart from a + measurement. The coverage note on the Hyperliquid hub states how the current + window divides between the three. methodology: @@ -49,7 +62,8 @@ methodology: - "Effective fee bps. Builder fees divided by notional, times ten thousand, on day D. The trader perspective on what a representative dollar of flow paid the frontend." - "Builder registry. builders.json in the harness tree is a hand-curated array of slug, name and address for every tracked frontend. A frontend routing through several builder addresses can list them all under one entry and they are merged into one row; MetaMask's documented secondary address stays its own row (MetaMask alt) for continuity with the recorded history. New addresses ship via public PR. Addresses outside the registry are not fetched." - "Exclusions. Native Hyperliquid UI orders carry no builder code and are not attributable; roughly 95 percent of Hyperliquid fills land without one. HIP-3 market fills routed through a frontend count toward that frontend; deployer fees are not part of this feed." - - "Reproducibility. Harness source is open in the OCB harness tree. Anyone can run it against the same public feed with the same registry and reproduce every number, no node required." + - "Node substitution. Hyperliquid's public export cuts most days off around 12:10 UTC, so where our own Hyperliquid node holds a day end to end that day replaces the export's copy of it outright, never an average of the two. A day the node cannot replay may be reconstructed from the export's morning plus an afternoon rebuilt per wallet from the info API; reconstructions are counted and published apart from measured days, because close is not measured." + - "Reproducibility. Harness source is open in the OCB harness tree. Anyone can run it against the same public feed with the same registry, which reproduces every number for the days that rest on the export. The days our node measured cannot be reproduced from the public feed alone, and the coverage gauges say which days those are." - "Failures. A transport error leaves the previous file and gauge values in place and is retried on the next pass. The cohort never switches to a day before its batch has settled, and the choice of day only moves forward. Restarts re-read the local mirror before touching the feed." findings: @@ -68,7 +82,7 @@ faq: - q: "Why USD revenue instead of effective fee in basis points?" a: "The previous version of this bench ranked frontends by effective fee in basis points, which answered the trader question of cost per dollar routed. The dollar revenue framing answers the operator question of size and revenue scale, the same way CoinMarketMan and ASXN dashboards present builder leaderboards. Both views are useful. The effective fee bps view is still available as a companion tab." - q: "Where does the data come from?" - a: "From the per-builder daily fills feed Hyperliquid publishes on its public stats CDN: one CSV per builder address and UTC day, with every fill that carried that builder code. A Go harness on OCB infrastructure downloads each file once the day is published and aggregates it locally. No node, no API key, no third party indexer, no internal Mobula service. Source code is in `harnesses/hyperliquid-frontends` in this repo." + a: "From two sources. The first is the per-builder daily fills feed Hyperliquid publishes on its public stats CDN: one CSV per builder address and UTC day, with every fill that carried that builder code. The second is our own Hyperliquid node, whose fill stream carries every hour of a day it was up; the public export has been stopping around 12:10 UTC on most days since 2026-09-08, so where the node holds a day end to end that day replaces the export's copy of it. A Go harness on OCB infrastructure aggregates both locally. No API key, no third party indexer. Source code is in `harnesses/hyperliquid-frontends` in this repo." - q: "How are the 7 day and 30 day numbers built?" a: "The harness keeps the last 30 daily files per builder in a local mirror. The 7 day figure sums builder fees over the files of the 7 UTC days ending on the current feed day, the 30 day figure over 30 days. Wallet counts for those windows are unions of the daily wallet sets. Files older than the window are deleted; a small ledger of daily totals is kept for the revenue history on the product pages." - q: "Why does the page say 24h when the data is a full UTC day?" diff --git a/src/components/hl-feed-coverage-note.tsx b/src/components/hl-feed-coverage-note.tsx index e2d4918e1..dceefecaa 100644 --- a/src/components/hl-feed-coverage-note.tsx +++ b/src/components/hl-feed-coverage-note.tsx @@ -1,4 +1,8 @@ -import { feedIsMostlyWhole, feedNeedsDisclosure } from "@/lib/hl-feed"; +import { + feedIsMostlyWhole, + feedNeedsDisclosure, + hlDaySources, +} from "@/lib/hl-feed"; import type { HlFeedCoverage } from "@/lib/hl-feed"; /** @@ -47,6 +51,29 @@ export function HlFeedCoverageNote({ if (!truncation && rebuilt === 0) return null; const mostlyWhole = feedIsMostlyWhole(feed); + // Which source each day of the window came from. Until 2026-10-25 the + // deployed harness carried no node code, so nodeDays was 0 and the only + // honest lead was the export's. Both leads are kept: a node outage, or a + // rollback to an older image, puts the page back in that state and the + // note has to describe it correctly too. + const src = feed ? hlDaySources(feed) : null; + const nodeLead = + feed && src && src.nodeDays > 0 + ? [ + `${src.nodeDays} of the last ${feed.windowDays} days are read from`, + "our own Hyperliquid node, which publishes every hour of the day.", + src.exportDays > 0 + ? [ + `${src.exportDays} ${src.exportDays === 1 ? "day is" : "days are"} left on Hyperliquid's`, + "public per-builder export, which stopped before midnight UTC", + feed.truncatedDaysWindow > 0 + ? `on ${feed.truncatedDaysWindow} of the window's days` + : "on part of the window", + "and sums such a day at roughly 43% of its real size.", + ].join(" ") + : "No day in the window rests on that export alone.", + ].join(" ") + : null; const tone = mostlyWhole ? "border-ink/15 bg-paper-soft/40 text-ink-soft" : "border-amber-500/30 bg-amber-500/5 text-ink-soft"; @@ -63,7 +90,14 @@ export function HlFeedCoverageNote({ ? "Some days are rebuilt, not measured" : mostlyWhole ? "A few days come from a partial upstream feed" - : "Upstream feed is publishing partial days"} + : // A window the node supplies most of still earns the amber + // tone while a tenth of it is reconstruction, but the label + // may not hand the whole window to the upstream feed: on + // 2026-10-25 that read "upstream feed is publishing partial + // days" over a window with two such days in thirty. + nodeLead + ? "Part of this window is rebuilt or partial" + : "Upstream feed is publishing partial days"}
{!truncation ? null : !feed ? (@@ -80,18 +114,32 @@ export function HlFeedCoverageNote({ text after it is trimmed by the transform, which is how this first shipped reading "The remaining 4fall back to". */} {[ - `${feed.nodeDays} of the last ${feed.windowDays} days are read from`, - "our own Hyperliquid node, which publishes every hour of the day.", - `The remaining ${feed.windowDays - feed.nodeDays} fall back to`, - "Hyperliquid's public per-builder export because the node was down", - "for part of them, and", - feed.truncatedDaysWindow > 0 - ? `${feed.truncatedDaysWindow} of those stop around 12:10 UTC.` - : "some of those stop before midnight UTC.", - "Those days are summed at roughly 43% of their real size, so the", - "window totals read a few per cent low. Figures are published as", - "measured rather than scaled to an estimate.", - ].join(" ")} + // The old sentence took the remainder as windowDays - nodeDays + // and then said the truncated count was "of those", which the + // gauges cannot support: a rebuilt day is in that remainder, and + // truncatedDaysWindow counts every day the export cut short + // including the ones the node replaced. On 2026-10-25 that read + // "the remaining 12 fall back to the export, and 11 of those + // stop around 12:10", where only 2 days rested on the export. + nodeLead, + "Window totals read a few per cent low as a result. Figures are", + "published as measured rather than scaled to an estimate.", + ] + .filter(Boolean) + .join(" ")} +
+ ) : nodeLead ? ( ++ {[ + nodeLead, + feed.coverageHours > 0 + ? `The latest day carries ${feed.coverageHours.toFixed(1)} of 24 hours.` + : "", + "Figures are published as measured rather than scaled to an", + "estimate.", + ] + .filter(Boolean) + .join(" ")}
) : (@@ -118,7 +166,11 @@ export function HlFeedCoverageNote({ {rebuilt > 0 && (
{[ - truncation + // "Of those" only has a referent when the paragraph above it + // ended on the export's short days. Once the lead names node + // days and export days separately, "those" points at the export + // remainder, which is the one set the rebuilt days are not in. + truncation && !nodeLead ? `${rebuilt} of those ${rebuilt === 1 ? "days is" : "days are"} rebuilt rather than measured:` : `${rebuilt} ${rebuilt === 1 ? "day in this window is" : "days in this window are"} rebuilt rather than measured:`, "the export's own hours up to its cutoff, spliced to an afternoon", diff --git a/src/lib/hl-feed.test.ts b/src/lib/hl-feed.test.ts index 2e8264751..ffb4b1c55 100644 --- a/src/lib/hl-feed.test.ts +++ b/src/lib/hl-feed.test.ts @@ -4,6 +4,7 @@ import { feedNeedsDisclosure, HL_WINDOWS, HL_WINDOW_LABEL, + hlDaySources, } from "@/lib/hl-feed"; describe("feedNeedsDisclosure", () => { @@ -154,3 +155,59 @@ describe("rebuilt days are not covered by the truncation trigger", () => { expect(whole.nodeDays + whole.rebuiltDays).toBe(whole.windowDays); }); }); + +describe("hlDaySources", () => { + // The live shape at 12:40 UTC on 2026-10-25, the first pass of a harness + // binary that actually carried the node substitution. The note's old + // sentence took the export's share as windowDays - nodeDays, which is 12 + // here, and then described 11 truncated days as a subset of it. Two days + // rest on the export. Ten are reconstructions, which the note reports on + // their own line precisely so they are not read as measurements. + const live = { + coverageHours: 23.9, + latestDayTruncated: false, + truncatedDaysWindow: 11, + daysMeasured: 30, + windowDays: 30, + nodeDays: 18, + rebuiltDays: 10, + rebuiltWorstErrorPct: -35.2, + }; + + it("keeps rebuilt days out of the export's share", () => { + expect(hlDaySources(live)).toEqual({ + nodeDays: 18, + rebuiltDays: 10, + exportDays: 2, + }); + }); + + it("splits the whole window and nothing more", () => { + const s = hlDaySources(live); + expect(s.nodeDays + s.rebuiltDays + s.exportDays).toBe(live.windowDays); + }); + + // A scrape landing mid-pass can pair a node count from the new window with + // a rebuilt count from the old. The remainder is clamped rather than + // allowed to go negative, because "-3 days are left on the export" is a + // worse thing to publish than a conservative zero. + it("never returns a negative remainder", () => { + const s = hlDaySources({ ...live, nodeDays: 28, rebuiltDays: 9 }); + expect(s.exportDays).toBe(0); + expect(s.rebuiltDays).toBe(2); + expect(s.nodeDays + s.rebuiltDays + s.exportDays).toBe(30); + }); + + // The state the page was in from 2026-10-07 to 2026-10-25: the harness + // image was rebuilt from a source tree frozen before the node code landed, + // so the deployed binary never set the gauge and every day did rest on the + // export. That reading has to stay describable, because a rollback or a + // node outage reproduces it. + it("hands the whole window to the export when the node reports nothing", () => { + expect(hlDaySources({ ...live, nodeDays: 0, rebuiltDays: 0 })).toEqual({ + nodeDays: 0, + rebuiltDays: 0, + exportDays: 30, + }); + }); +}); diff --git a/src/lib/hl-feed.ts b/src/lib/hl-feed.ts index ec6d1baaf..8b59fe057 100644 --- a/src/lib/hl-feed.ts +++ b/src/lib/hl-feed.ts @@ -105,3 +105,40 @@ export function feedIsMostlyWhole(feed: HlFeedCoverage | null): boolean { if (feed.windowDays <= 0) return false; return feed.nodeDays >= feed.windowDays - 5; } + +/** + * How the window's days divide between the three sources the page can name. + * + * The disclosure note used to describe the whole window as the public + * export's. That was true for as long as it was written, because the node + * substitution never reached the running binary: the harness image was + * rebuilt on 2026-10-21 from a source tree frozen on 2026-10-07, hours + * before the substitution merged, so the deployed process had no node code + * at all. Once a current binary shipped on 2026-10-25 the same sentence + * became wrong in two directions at once. It named the export as the source + * of days the node had measured, and it described a window reading 43% low + * when most of it no longer did, which understates our own figures. + * + * exportDays is the remainder, floored at 0 and clamped so the three counts + * sum to windowDays. The gauges are set in one pass of one process, but a + * scrape that lands mid-pass can pair a node count from the new window with + * a rebuilt count from the old, and a negative day count in prose is a + * worse failure than a conservative zero. + */ +export function hlDaySources(feed: HlFeedCoverage): { + nodeDays: number; + rebuiltDays: number; + exportDays: number; +} { + const windowDays = Math.max(0, feed.windowDays); + const nodeDays = Math.max(0, Math.min(windowDays, feed.nodeDays)); + const rebuiltDays = Math.max( + 0, + Math.min(windowDays - nodeDays, feed.rebuiltDays), + ); + return { + nodeDays, + rebuiltDays, + exportDays: windowDays - nodeDays - rebuiltDays, + }; +}