Key takeawaysThe leaderboard is served from stats-data.hyperliquid.xyz/Mainnet/leaderboard as a plain GET and isn’t in the official reference — treat its shape as unstable, cache it hourly, and health-check that host separately from api.hyperliquid.xyz.
- The leaderboard is served from
stats-data.hyperliquid.xyz/Mainnet/leaderboardas a plain GET and isn’t in the official reference — treat its shape as unstable, cache it hourly, and health-check that host separately fromapi.hyperliquid.xyz. clearinghouseStatereturns a wallet’s open positions with no key or signature, at weight 2 — the cheap tier; an emptyassetPositionsmeans flat, not failed.- Per-user WebSocket subscriptions cap at 10 unique users per IP — beyond that, either distribute them across addresses or filter the market-wide trades stream (every fill carries a
usersarray) and reconcile the affected wallets throughclearinghouseState. userFillsreturns at most 2,000 recent fills (10,000 available viauserFillsByTime); a full 2,000-row response costs about 120 weight — roughly 60clearinghouseStatecalls — so page with an overlapping cursor and deduplicate bytid.- There’s no copy-trading endpoint and no dedicated close: you place your own reduce-only order as an aggressive limit with a slippage bound. Vaults are the native alternative — exposure to a leader’s strategy, not mirroring an arbitrary wallet.
- In our August 2026 test, the first 429 appeared at 1,212–1,381 calls per minute, about double the nominal arithmetic — but the rate we sustained, 574/min per datacenter address, sits close to the documented 600. Plan from the docs and treat the headroom as burst tolerance; limits are counted per address rather than per /24, and no response exposes the remaining quota.
This summary was created with AI.
Where the leaderboard actually lives
The main API answers on POST https://api.hyperliquid.xyz/info, and the info API documentation covers it thoroughly — every wallet-level query in this article is there. The leaderboard is the exception: it is served from https://stats-data.hyperliquid.xyz/Mainnet/leaderboard as a plain GET, and you will not find that path in the reference. Public SDKs treat it as a second base URL alongside the main API rather than as an info request, which is the clearest signal of how it’s built: a different host, a different response shape, no type field, no POST body.
Two consequences follow, and both are operational rather than theoretical. First, availability is independent: the two hosts are separate services, so a health check against api.hyperliquid.xyz tells you nothing about whether the stats host is answering. Monitor them separately, or your leaderboard job will fail silently while every other call keeps working. Second, the response is heavy and slow compared with an info call. Measuring straight from our own server, the leaderboard’s median response time was roughly 0.7 seconds against about 0.3 seconds for a clearinghouseState request — and it returns the full ranking rather than a page.
The response is a ranking, one object per wallet, and it’s worth knowing its shape before you build on it:
{
"ethAddress": "0x...",
"accountValue": "1234567.89",
"displayName": "trader name or null",
"prize": 0,
"windowPerformances": [
["day", {"pnl": "12345.6", "roi": "0.0123", "vlm": "9876543.2"}],
["week", {"pnl": "...", "roi": "...", "vlm": "..."}],
["month", {"pnl": "...", "roi": "...", "vlm": "..."}],
["allTime", {"pnl": "...", "roi": "...", "vlm": "..."}]
]
}
ethAddress is the key you carry into every wallet-level call below. Performance arrives as window/metrics pairs rather than flat fields, so “top by weekly PnL” means selecting the right window first, and every number is a string — consistent across the whole API and a routine source of comparison bugs. displayName is optional and frequently null.
That shapes how you should use it. Per Hyperliquid’s API announcements channel, the stats snapshot refreshes on an hourly cadence, so polling it more often normally buys you no fresher data — while the positions behind those wallets change constantly. So fetch the leaderboard rarely, cache it, and build your fast loop around wallet-level calls using the addresses you already extracted:
import json, time, requests
STATS = "https://stats-data.hyperliquid.xyz/Mainnet/leaderboard"
CACHE, TTL = "leaderboard.json", 3600 # matches the snapshot's refresh cadence
def leaderboard():
try: # serve from cache while it's fresh
with open(CACHE) as f:
blob = json.load(f)
if time.time() - blob["fetched"] < TTL:
return blob["rows"]
except (FileNotFoundError, json.JSONDecodeError, KeyError):
pass
r = requests.get(STATS, timeout=30) # generous: this host is slower than /info
r.raise_for_status()
rows = r.json().get("leaderboardRows", [])
with open(CACHE, "w") as f:
json.dump({"fetched": time.time(), "rows": rows}, f)
return rows
rows = leaderboard()
print(len(rows), "wallets")
print(rows[0]["ethAddress"], rows[0]["accountValue"])
What to look at: the timeout is deliberately generous because this host is slower than the main API, and the cache is what keeps a slow, undocumented dependency off your critical path. Validate the fields you depend on rather than assuming them — an endpoint absent from the formal reference carries no compatibility promise, even though its move to this host was announced publicly. What this snippet does not do: it has no fallback if the stats host is down, so production code should keep serving the last good cache and alert instead of failing.
Reading the state of a single wallet
Once you have addresses, everything else is the documented API. The clearinghouseState request returns a wallet’s perpetuals state: margin summary, withdrawable balance, and the open positions for a user in assetPositions. It weighs 2 — the cheap tier, alongside l2Book and allMids — and needs no key, no signature, and no account. One scoping detail that older guides miss: the request accepts an optional dex parameter, and with it omitted you get the first perp DEX. Positions on builder-deployed HIP-3 DEXes have to be queried separately when they matter to you.
import requests
INFO = "https://api.hyperliquid.xyz/info"
def wallet_state(address):
r = requests.post(INFO, json={"type": "clearinghouseState", "user": address}, timeout=10)
r.raise_for_status()
return r.json()
state = wallet_state("0x0000000000000000000000000000000000000000") # user or sub-account address
for item in state["assetPositions"]:
p = item["position"]
print(p["coin"], p["szi"], "entry", p["entryPx"], "uPnL", p["unrealizedPnl"])
Reading positions by address is that direct: szi is signed size, so a negative value is a short; entryPx is the average entry; unrealizedPnl and marginUsed come alongside. For a correctly resolved user or sub-account address, an empty assetPositions list means no open positions rather than an error — which matters when you iterate over hundreds of addresses and need to tell “flat” apart from “failed.” One caveat the documentation calls out: don’t point this at an agent or API-wallet address, since that can return an empty result for a wallet that plainly holds positions.
Two limits define what you can build on this call. It’s a snapshot, not a feed — you learn that a position changed only by asking again, so your resolution equals your polling interval. And per-user WebSocket subscriptions stop scaling on a single address: Hyperliquid’s documented limits allow a maximum of 10 unique users across user-specific WebSocket subscriptions per IP.
That ceiling shapes the architecture rather than closing it. Three patterns work, and they combine:
- Up to ten wallets per address — user-specific subscriptions, the lowest-latency option and the right choice for a dashboard.
- Distribute the subscriptions — ten unique users per egress address, so the watchlist grows with the number of addresses.
- Watch the market instead of the wallets — the market-wide trades subscription carries a
usersarray naming the buyer and seller of each fill, so you can filter every trade against your watchlist for change detection and then reconcile only the affected wallets throughclearinghouseState. Several current wallet trackers are built exactly this way, and it converts a per-wallet subscription problem into a filtering problem.
The third pattern doesn’t remove polling — it retargets it. Instead of asking every wallet on a timer, you ask the wallets that just traded, plus a slow full sweep to catch whatever the stream missed. The question then becomes how fast that reconciliation can run, which is where budgets come in.
Pulling the fills behind a position
A position tells you where a wallet stands; fills tell you how it got there. The userFills endpoint returns up to 2,000 most recent fills for an address, with aggregateByTime available to combine partial fills of one crossing order. For anything time-bounded, userFillsByTime takes startTime in milliseconds — required — plus an optional endTime that defaults to now, and the documentation notes at most 2,000 fills per response with only the 10,000 most recent available.
The startTime behavior on fills is where a first implementation usually goes wrong, and the trap isn’t window size — it’s cursor handling around truncated responses. startTime is inclusive and expressed in milliseconds, and several fills can share the same millisecond, so advancing to last_time + 1 can silently drop rows that sat behind the last one you saw. The safe pattern is an overlapping cursor: restart from the last timestamp rather than past it, deduplicate on a fill identifier such as tid or the transaction hash, and advance only after processing everything the response returned. Mind the horizon too — at most 2,000 fills per response and only the 10,000 most recent overall, so deep history isn’t available here at all, and pretending otherwise produces silently truncated backfills.
Their cost is easy to misjudge in both directions. Both fills endpoints carry weight 20 — the default tier for info requests — plus an additional weight per 20 items returned. A full 2,000-fill response therefore costs about 120 weight under the documented formula, roughly the equivalent of 60 clearinghouseState calls: expensive enough that fill history belongs in a slow background job, cheap enough that a single such call won’t wreck a minute’s budget on its own.
Following a wallet in practice
Two honest notes before any architecture, because the phrase “copy trading” promises more than the platform exposes. There is no copy trading API on Hyperliquid in the sense of an endpoint that mirrors another trader’s actions to your account: what exists is public read access to every wallet’s state and a normal exchange API for your own orders. Any “following” system you build is a pipeline you own — read a wallet, decide, then place your own order — and that means the latency, the slippage, and the decision to trade are all yours.
Vaults are the closest native equivalent, and they work differently: a depositor puts funds into a vault and shares proportionally in the profits and losses of the vault leader’s strategy. That gives you exposure to a strategy natively — it does not let you pick an arbitrary leaderboard wallet and mirror its individual trades.
Closing a position with a market order is the same story from the other side. Hyperliquid’s exchange API has no dedicated close endpoint; you close by submitting an order in the opposite direction with reduce_only set, and market execution is expressed as an aggressive limit order with a slippage bound rather than a MARKET type. The official Python SDK’s market_close helper does exactly that under the hood — read it before writing your own.
So the architecture reduces to a polling budget, and the budget is documented: REST requests share an aggregated weight limit of 1,200 per minute per IP address. With clearinghouseState at weight 2, arithmetic gives 600 wallet checks per minute from one address. Before you plan capacity around that number, three software levers matter more than any of them:
- Cache what doesn’t move. The leaderboard hourly,
metadaily. Those are weight-20 calls with no reason to be in a loop. - Poll by tier, not uniformly. Wallets with open positions deserve seconds; flat wallets deserve minutes. Uniform polling spends most of its budget confirming that nothing happened.
- Back off on the observed signal, not the assumed one. No response carries a remaining-quota header, so track spend yourself and treat 429 as data — our measurements below show the recovery is far shorter than the “per minute” phrasing implies.
Our test: how many wallets one address covers
Hypothesis. The documented budget describes real behavior, and one egress address is not enough to watch a large wallet set.
Environment and profile. PapaProxy.net addresses over SOCKS5 with IP whitelisting, targeting the public POST /info on api.hyperliquid.xyz; no keys and no signing, with wallet addresses taken from the leaderboard response itself. Datacenter and ISP pools were run as separate series and never merged into a single figure. We raised in-flight requests per address in 150-second stages — each covering a full clock minute — and stopped a ladder at the first HTTP 429. Run dated August 6–7, 2026.
The first observed 429 point. Note the wording: a ladder that stops at the first refusal measures where enforcement began, not a confirmed sustained ceiling. Those are different measurements, and this run made the first one.
| Pool | clearinghouseState calls/min at first observed 429 |
Concurrency | Latency p50 |
|---|---|---|---|
| Datacenter | 1,212 | 8 | 284 ms |
| ISP | 1,381 | 12 | 332 ms |
Enforcement started well above the nominal arithmetic. A 1,200 weight/min budget at weight 2 implies 600 calls; the first refusal came at roughly twice that, and the same pattern appeared on every endpoint where we reached a limit:
| Endpoint | Pool | Calls before 429 | Documented weight | Weight spent per the docs |
|---|---|---|---|---|
clearinghouseState |
Datacenter | 1,212 | 2 | 2,424 |
clearinghouseState |
ISP | 1,381 | 2 | 2,762 |
allMids |
ISP | 1,089 | 2 | 2,178 |
meta |
ISP | 131 | 20 | 2,620 |
In all four cases, the consumed budget came out 1.8–2.3× above the documented 1,200. The measured behavior broadly preserved the documented weight tiers, but the first-429 point varied materially between runs and endpoints — around 21% between two weight-2 endpoints on the same pool.
And here is the conclusion that matters more than the headroom. The rate we settled on for sustained multi-address work — 574 calls per minute per datacenter address — lands close to the documented planning figure of 600. So the documentation remains a sensible capacity-planning baseline, and the extra room before the first refusal is best treated as burst tolerance rather than as capacity you can plan to buy less of.
Three findings that change how you write the client. Remaining quota is not exposed: no response carried a rate-limit header, on success or on 429, so maintain a client-side counter from the documented weights — a 429 only tells you that enforcement has already been reached. Recovery was quick in these runs: after a 429, the address answered again within 5.3–5.4 seconds. Treat that as an observation rather than a guaranteed cooldown, and keep backoff plus your own weight accounting in place. And the limit is counted per address, not per subnet — four addresses inside one /24 sustained 2,278 calls/min on datacenter against 2,274 for four addresses in separate /24s (2,052 against 2,044 on ISP), with zero refusals in either group, so there’s nothing to gain from scattering addresses across blocks.
One anomaly we could not explain. Converted to documented weight, three of the four ceilings cluster around 2,500 (±12%). But allMids from datacenter ran to 1,622 calls per minute — 3,244 weight — without a single refusal, while the same call from ISP hit 429 at 1,089. The gap points the opposite way from the clearinghouseState result, so no systematic pool advantage is visible. Plausible explanations, in order: the ceiling isn’t a constant and drifts between runs; the larger allMids payload kept us from reaching the required rate before the ladder ended; the limit is counted differently on that endpoint. Our data can’t separate them. The practical lesson outranks the puzzle: the ceiling isn’t a constant, so don’t plan against the measured maximum — leave margin.
The sustained operating point. This is the number to plan from — the rate we held, not the one that broke:
| Pool | Sustained calls/min per address | Against the documented 600 |
|---|---|---|
| Datacenter | 574 | ~96% |
| ISP | 514 | ~86% |
Interactive capacity planner
How many addresses does your wallet watchlist need?
The calculator uses the sustained rates we held in the test, not the higher and less stable point where the first 429 appeared.
Calls per minute per address. The first-429 values describe observed burst headroom, not a safe operating rate.
Rounded up to whole addresses. This estimate assumes one clearinghouseState call per wallet per refresh and no other REST spend from the same IPs.
Aggregate throughput scaled with address count: exactly linear to five addresses, and close to it beyond. One clearinghouseState call equals one wallet, so the addresses you need are simply wallets divided by refresh interval:
| Wallets watched | Refresh every 10s | every 30s | every 60s |
|---|---|---|---|
| 100 | 2 | 1 | 1 |
| 250 | 3 | 1 | 1 |
| 500 | 6 | 2 | 1 |
| 1,000 | 11 | 4 | 2 |
| 5,000 | 53 | 18 | 9 |
Put plainly: one address covers roughly 574 wallets at a 60-second refresh, 287 at 30 seconds, and 95 at 10 seconds. So a couple of hundred wallets at a half-minute refresh needs exactly one address — while five thousand wallets need nine even at a leisurely one-minute cadence, or more than fifty at ten seconds. And if you use the market-wide trade stream for change detection, these numbers describe your reconciliation sweep rather than a per-wallet timer, which is usually a much smaller bill.
Where does that leave the pairing this article started with? Everything up to a few hundred wallets is a software problem — cache the leaderboard, filter the trade stream instead of polling every wallet, tier whatever polling remains, and back off on your own counter. Past that, the constraint is the documented per-IP budget, and capacity is added the same way you’d add workers: more egress addresses, each with its own budget, sized from the table above. That’s the part our proxies for crypto projects cover — dedicated datacenter and ISP IPv4 addresses, static for the whole plan term, with contiguous blocks carrying no penalty since the limit is counted per address. Check the platform’s current terms of use before scaling a polling fleet, and keep the leaderboard call out of the fast loop regardless of how many addresses you have.