Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions benchmarks/hyperliquid-frontends.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: |
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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?"
Expand Down
82 changes: 67 additions & 15 deletions src/components/hl-feed-coverage-note.tsx
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand Down Expand Up @@ -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";
Expand All @@ -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"}
</p>
{!truncation ? null : !feed ? (
<p>
Expand All @@ -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(" ")}
</p>
) : nodeLead ? (
<p>
{[
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(" ")}
</p>
) : (
<p>
Expand All @@ -118,7 +166,11 @@ export function HlFeedCoverageNote({
{rebuilt > 0 && (
<p className="mt-2">
{[
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",
Expand Down
57 changes: 57 additions & 0 deletions src/lib/hl-feed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
feedNeedsDisclosure,
HL_WINDOWS,
HL_WINDOW_LABEL,
hlDaySources,
} from "@/lib/hl-feed";

describe("feedNeedsDisclosure", () => {
Expand Down Expand Up @@ -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,
});
});
});
37 changes: 37 additions & 0 deletions src/lib/hl-feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
Loading