
Hyperliquid Vaults and Leaderboard Data via API in Python
Rank every Hyperliquid vault, inspect one before you deposit, pull the trader leaderboard and profile the top wallets live, all in Python.
Hyperliquid's vault page and leaderboard are two of the most visited screens on Hyperliquid, and neither has a documented API that returns the data you see on screen. vaultSummaries comes back as []. leadingVaults returns a 422 error if you copy the request from the docs. The leaderboard has no Info query at all. People end up scraping the website, or they give up and read screenshots.
In this guide we build three small Python programs that get the real data: a vault ranker that sorts every open vault by TVL, APR or 30-day PnL, a vault inspector that shows a vault's leader, fees, returns, biggest depositors and live positions before we put money in, and a leaderboard tool that ranks 48,000 traders and then profiles the top five with one Dwellir call. We also add a short script that shows a wallet's vault deposits and when they unlock.
The interesting part is the routing. Some queries work through Dwellir's Info proxy, two only work on Hyperliquid's public endpoint, and the two big lists live in JSON feeds the app itself loads. One helper sends each request to the right place, and everything else is sorting and printing.
The code is in dwellir-examples/hyperliquid-vaults-leaderboard. Every number in this guide comes from running it on 5 and 6 November 2026.
What You Will Learn
- Download and cache the two feeds behind the Hyperliquid vault list and leaderboard
- Rank vaults by TVL, APR or window PnL, and filter out the 6,400 closed ones
- Read a vault's leader, commission, window returns, largest followers and open positions
- List the vaults an address leads with
leadingVaults, with theuserfield it silently requires - Rank traders by PnL, ROI or volume over a day, week, month or all time, and filter the noise out of ROI
- Profile the top wallets' live positions in one
batchClearinghouseStatesrequest - Show a wallet's vault deposits, APR and lockups with
userVaultEquities
Prerequisites
- Python 3.10 or newer. Run
python3 --versionto check. - A Dwellir API key from the dashboard. The free plan allows 20 responses per second, which is more than this guide needs. No gRPC or Order Book add-on is required.
- One package:
pip install httpx- Comfort with
argparseand dict comprehensions. There is no async code here; every call is a plain POST.
We build the four files from scratch below. Clone the finished example to compare against, and set the key:
git clone https://github.com/dwellir-public/dwellir-examples
cd dwellir-examples/examples/hyperliquid-vaults-leaderboard
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export DWELLIR_API_KEY=your-api-keyNote: vault names are set by their leaders, and some of the real names in the outputs below include emoji. We print them as they are.
Where the Vault and Leaderboard Data Actually Lives
Before writing code, we need to know which request returns what. We tested every candidate on 5 November 2026 against both Hyperliquid's public endpoint and Dwellir's Info proxy at https://api-hyperliquid-mainnet-info.n.dwellir.com/YOUR_API_KEY/info.
| Query | Public endpoint | Dwellir Info proxy | What it returns |
|---|---|---|---|
vaultSummaries | [] | [] | Nothing useful right now |
leadingVaults without user | 422 | 422 | Failed to deserialize the JSON body into the target type |
leadingVaults with user | 200 | 200 | Vaults that address leads: [{address, name}] |
vaultDetails | 200, 56 KB | 403 type not allowed | One vault: leader, fees, returns, followers, child relationship |
portfolio | 200 | 403 type not allowed | Equity, PnL and volume history for any address |
userVaultEquities | 200 | 200 | A wallet's vault deposits, equity and lockup |
clearinghouseState | 200 | 200 | Open positions for any address, vaults included |
batchClearinghouseStates | not available | 200 | Many wallets' positions in one call, Dwellir only |
GET stats-data.hyperliquid.xyz/Mainnet/vaults | 200, 14 MB | Every vault ever created, 9,478 on the day | |
GET stats-data.hyperliquid.xyz/Mainnet/leaderboard | 200, 40 MB | Every ranked trader, 48,192 on the day |
Three things follow from that table. The per-wallet queries go through Dwellir, where the rate limit is the plan's own rather than a shared per-IP cap. vaultDetails and portfolio go to the public endpoint. The two lists come from stats-data.hyperliquid.xyz, which is unauthenticated, undocumented, and large enough that we download each file once per run and cache it on disk.
Step 1: One Helper That Routes Each Query
Create hl_api.py. It holds the three hosts, the routing rule and the feed cache, and nothing else:
"""Read helpers for Hyperliquid vault and leaderboard data.
Three sources, one module:
- Dwellir's Hyperliquid Info proxy for the per-user queries it serves
(clearinghouseState, batchClearinghouseStates, userVaultEquities, leadingVaults, ...).
- Hyperliquid's public Info endpoint for the two vault queries the proxy does not serve
(vaultDetails, portfolio). Checked on 5 November 2026.
- The stats-data feeds behind app.hyperliquid.xyz: /Mainnet/vaults (every vault, about
14 MB) and /Mainnet/leaderboard (every ranked trader, about 40 MB). Both are cached on
disk because you want to download them once per run, not once per lookup.
"""
import json
import os
import time
from pathlib import Path
import httpx
API_KEY = os.environ.get("DWELLIR_API_KEY", "")
DWELLIR_INFO = f"https://api-hyperliquid-mainnet-info.n.dwellir.com/{API_KEY}/info"
PUBLIC_INFO = "https://api.hyperliquid.xyz/info"
STATS_BASE = "https://stats-data.hyperliquid.xyz/Mainnet"
# Info types the Dwellir proxy answers with 403 "type not allowed". Route these
# to the public endpoint. Everything else goes through Dwellir.
PUBLIC_ONLY = {"vaultDetails", "portfolio"}
CACHE_DIR = Path(__file__).parent / ".cache"
def info(query: dict, timeout: float = 30.0):
"""POST one Info query to the right host and return the parsed JSON."""
if query["type"] in PUBLIC_ONLY:
url = PUBLIC_INFO
elif API_KEY:
url = DWELLIR_INFO
else:
raise RuntimeError("Set DWELLIR_API_KEY to query the Dwellir Hyperliquid Info endpoint")
resp = httpx.post(url, json=query, timeout=timeout)
if resp.status_code >= 400:
# The key sits in the Dwellir URL, so never let httpx print it.
raise RuntimeError(f"{query['type']} failed with HTTP {resp.status_code}: {resp.text[:200]}")
return resp.json()
def feed(name: str, max_age_s: int = 600):
"""Download /Mainnet/<name> once and reuse it for max_age_s seconds."""
CACHE_DIR.mkdir(exist_ok=True)
path = CACHE_DIR / f"{name}.json"
if path.exists() and time.time() - path.stat().st_mtime < max_age_s:
return json.loads(path.read_text())
resp = httpx.get(f"{STATS_BASE}/{name}", timeout=120.0)
resp.raise_for_status()
data = resp.json() # parse before caching, so a bad body is never reused
path.write_bytes(resp.content)
return datainfo() is the only place a host name appears. If Dwellir adds vaultDetails to the proxy later, we delete one string from PUBLIC_ONLY and nothing else changes. Leaving DWELLIR_API_KEY unset raises an error for the Dwellir queries instead of quietly sending them to the public endpoint, where batchClearinghouseStates does not exist. The error path re-raises without the URL, because the key is part of it.
feed() writes the raw bytes to .cache/ and reuses them for ten minutes. The leaderboard file downloaded in about 1.2 seconds on our connection, so the cache is less about speed than about not fetching 40 MB every time we change a sort flag.
Add three formatting helpers at the bottom of the same file. Hyperliquid returns every number as a string, and the window data comes as lists of [name, value] pairs rather than objects:
def windows(pairs):
"""Turn Hyperliquid's [["day", {...}], ["week", {...}]] lists into a dict."""
return {name: value for name, value in pairs}
def usd(value) -> str:
"""Format a numeric string as a dollar amount with thousands separators."""
return f"${float(value):,.0f}"
def pct(value, signed: bool = True) -> str:
"""Format a fraction (0.12 = 12%) as a percentage, signed for returns."""
return f"{float(value) * 100:{'+' if signed else ''}.1f}%"Verify the routing from a Python shell:
>>> from hl_api import info
>>> info({"type": "userRole", "user": "0xdfc24b077bc1425ad1dea75bcb6f8158e10df303"})
{'role': 'vault'}That ran through Dwellir and confirms the HLP address is a vault.
Step 2: Rank Every Vault
The vaults feed is a list of entries, each with a summary object and two extras: apr as a fraction, and pnls, a short cumulative PnL series for each of day, week, month and allTime. The last point in each series is the PnL over that window.
Create vaults.py and start with a loader that flattens the feed. The leader example in the docstring is HLP's leader address:
"""Rank Hyperliquid vaults, inspect one, or list the vaults an address leads.
python vaults.py top --by tvl --n 20
python vaults.py top --by apr --min-tvl 250000
python vaults.py show 0xdfc24b077bc1425ad1dea75bcb6f8158e10df303
python vaults.py leader 0x677d831aef5328190852e24f13c46cac05f984e7
"""
import argparse
from datetime import datetime, timezone
from hl_api import feed, info, pct, usd, windows
def last(series: list) -> float:
"""Last value of a cumulative PnL series, or 0 when the window has no points yet."""
return float(series[-1]) if series else 0.0
def load_vaults(open_only: bool = True, min_tvl: float = 0.0) -> list[dict]:
"""Flatten the /Mainnet/vaults feed into one dict per vault."""
rows = []
for entry in feed("vaults"):
summary = entry["summary"]
if open_only and summary["isClosed"]:
continue
tvl = float(summary["tvl"])
if tvl < min_tvl:
continue
pnls = windows(entry["pnls"])
rows.append({
"name": summary["name"],
"address": summary["vaultAddress"],
"leader": summary["leader"],
"tvl": tvl,
"apr": float(entry["apr"]),
# Each window is a short series of cumulative PnL points. The last
# point is the PnL over that window; a brand-new vault has none yet.
"pnl_day": last(pnls["day"]),
"pnl_week": last(pnls["week"]),
"pnl_month": last(pnls["month"]),
"pnl_all": last(pnls["allTime"]),
"created": datetime.fromtimestamp(summary["createTimeMillis"] / 1000, timezone.utc),
"kind": summary["relationship"]["type"],
})
return rowsTwo filters do most of the work. open_only drops the 6,416 closed vaults, which still sit in the feed with a TVL of a few cents. min_tvl drops the long tail: only 48 open vaults held more than $250,000.
Now the ranking and the table:
SORT_KEYS = {"tvl": "tvl", "apr": "apr", "day": "pnl_day", "week": "pnl_week",
"month": "pnl_month", "all": "pnl_all"}
def print_top(rows: list[dict], by: str, n: int) -> None:
rows = sorted(rows, key=lambda r: r[SORT_KEYS[by]], reverse=True)[:n]
print(f"{'#':>3} {'Vault':<34} {'TVL':>14} {'APR':>9} {'30d PnL':>13} Address")
for i, r in enumerate(rows, 1):
print(f"{i:>3} {r['name'][:34]:<34} {usd(r['tvl']):>14} {pct(r['apr']):>9} "
f"{usd(r['pnl_month']):>13} {r['address']}")Add the command-line entry point at the bottom of the file. The show and leader subcommands call show_vault and show_leader, which we write in Steps 3 and 4; define the functions above main() as we go:
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="command", required=True)
top = sub.add_parser("top", help="rank vaults")
top.add_argument("--by", choices=SORT_KEYS, default="tvl")
top.add_argument("--n", type=int, default=20)
top.add_argument("--min-tvl", type=float, default=0.0)
top.add_argument("--include-closed", action="store_true")
sub.add_parser("show", help="inspect one vault").add_argument("address")
sub.add_parser("leader", help="vaults an address leads").add_argument("address")
args = parser.parse_args()
if args.command == "top":
rows = load_vaults(open_only=not args.include_closed, min_tvl=args.min_tvl)
print(f"{len(rows)} vaults after filters")
print_top(rows, args.by, args.n)
elif args.command == "show":
show_vault(args.address)
else:
show_leader(args.address)
if __name__ == "__main__":
main()Run the ranking:
python vaults.py top --by tvl --n 103062 vaults after filters # Vault TVL APR 30d PnL Address 1 Hyperliquidity Provider (HLP) $151,014,571 -0.0% $122,470 0xdfc24b077bc1425ad1dea75bcb6f8158e10df303 2 HLP Strategy X $100,778,255 +4.3% $356,963 0x469f690213c467c39a23efacfd2816896009d7d8 3 HLP Liquidator 2 $30,000,002 +0.0% $2 0xb0a55f13d22f66e6d495ac98113841b2326e9540 4 Growi HF $12,496,442 -0.6% $-82,557 0x1e37a337ed460039d1b15bd3bc489de789768d5e 5 [ Systemic Strategies ] L/S Grids $11,103,636 +137.0% $1,246,919 0x07fd993f0fa3a185f7207adccd29f7a87404689d 6 drkmttr $7,213,316 -36.2% $-4,388,417 0xc179e03922afe8fa9533d3f896338b9fb87ce0c8 7 DOUBLETOP Vault $7,182,580 -11.8% $-960,705 0x53f8f390fd4f70941c5d160a964f6893c8dbceff 8 [ Systemic Strategies ] ♾️ HyperGr $6,132,482 +169.4% $902,959 0xd6e56265890b76413d1d527eb9b75e334c0c5b42 9 BredoStrategy $5,124,118 -39.2% $-3,299,440 0x654016a8c9fcf0c4cb7ed6078aba21f7f399f7b7 10 HLP Strategy A $3,060,457 -2.1% $-107,633 0x010461c14e146ac35fe42271bdc1134ee31c703a
TVL ranks size, not skill. Sort by APR with a TVL floor to find vaults that are both large enough to matter and actually making money:
python vaults.py top --by apr --min-tvl 250000 --n 548 vaults after filters
# Vault TVL APR 30d PnL Address
1 Elsewhere $808,791 +208.4% $119,663 0x8fc7c0442e582bca195978c5a4fdec2e7c5bb0f7
2 [ Systemic Strategies ] ♾️ HyperGr $6,132,482 +169.4% $902,959 0xd6e56265890b76413d1d527eb9b75e334c0c5b42
3 [ Systemic Strategies ] L/S Grids $11,103,636 +137.0% $1,246,919 0x07fd993f0fa3a185f7207adccd29f7a87404689d
4 Orbit Value Strategies $1,512,052 +117.0% $134,293 0x115849ce84370f25cadcf0d348510d73837e1aa5
5 AceVault Hyper01 $900,790 +94.5% $73,641 0x9e02aca9865e1859bb7865f6f64801e804a173dfNote: apr in the feed is annualised from recent performance, so a vault that had one good month shows a triple-digit APR. The --by month sort uses realised 30-day PnL in dollars and is harder to flatter.
Step 3: Inspect One Vault Before Depositing
The ranking gives us candidates. vaultDetails tells us what we would be buying: who runs it, what they take, how it has done over each window, who else is in it, and what it holds right now. The first four come from one public call. The positions come from clearinghouseState through Dwellir, because a vault is just an address.
Add this to vaults.py:
def show_vault(address: str) -> None:
"""Everything the vault page shows, from vaultDetails plus live positions."""
details = info({"type": "vaultDetails", "vaultAddress": address})
portfolio = windows(details["portfolio"])
print(f"{details['name']} ({address})")
print(f" leader {details['leader']}")
print(f" APR {pct(details['apr'])}")
print(f" leader share {pct(details['leaderFraction'], signed=False)} of equity, "
f"{pct(details['leaderCommission'], signed=False)} commission")
print(f" deposits {'open' if details['allowDeposits'] else 'closed'}"
f"{', vault closed' if details['isClosed'] else ''}")
print(f" followers {len(details['followers'])} listed, "
f"max distributable {usd(details['maxDistributable'])}")
print(" returns")
for window in ("day", "week", "month", "allTime"):
history = portfolio[window]["accountValueHistory"]
if not history:
print(f" {window:<8} no history yet")
continue
pnl = portfolio[window]["pnlHistory"][-1][1]
start, end = float(history[0][1]), float(history[-1][1])
print(f" {window:<8} equity {usd(start):>14} -> {usd(end):>14} "
f"pnl {usd(pnl):>13} volume {usd(portfolio[window]['vlm'])}")
print(" largest followers")
followers = sorted(details["followers"], key=lambda f: float(f["vaultEquity"]), reverse=True)
for f in followers[:5]:
print(f" {f['user']} equity {usd(f['vaultEquity']):>12} "
f"all-time pnl {usd(f['allTimePnl']):>12} {f['daysFollowing']} days")portfolio carries eight windows: day, week, month, allTime and a perp-prefixed copy of each. Each window has accountValueHistory and pnlHistory as lists of [timestamp_ms, value] pairs plus a vlm total. The first and last equity points show how much money came and went, which pnl alone hides: a vault can lose depositors while making money.
followers is capped at 100 entries, sorted by the server, so for a vault with thousands of depositors we see the biggest ones. Each follower carries vaultEquity, pnl, allTimePnl, daysFollowing and lockupUntil.
Now the positions. Append this to the end of show_vault:
# HLP is a parent vault: its capital sits in child vaults, which show up in
# the vaults feed with the parent as their leader.
children = [v for v in load_vaults(open_only=False) if v["leader"].lower() == address.lower()]
if children:
print(f" child vaults ({len(children)})")
for c in sorted(children, key=lambda c: c["tvl"], reverse=True):
print(f" {c['name']:<24} TVL {usd(c['tvl']):>13} APR {pct(c['apr']):>7} {c['address']}")
# A vault is an address, so its open positions come from clearinghouseState
# like any other wallet. This call runs through Dwellir.
state = info({"type": "clearinghouseState", "user": address})
positions = [p["position"] for p in state["assetPositions"] if float(p["position"]["szi"]) != 0]
print(f" open positions ({len(positions)}) account value {usd(state['marginSummary']['accountValue'])}")
for p in sorted(positions, key=lambda p: abs(float(p["positionValue"])), reverse=True)[:10]:
side = "LONG " if float(p["szi"]) > 0 else "SHORT"
print(f" {side} {p['coin']:<8} {usd(p['positionValue']):>13} "
f"{p['leverage']['value']}x {p['leverage']['type']} "
f"upnl {usd(p['unrealizedPnl'])}")Run it on the Systemic Strategies grid vault from the ranking:
python vaults.py show 0x07fd993f0fa3a185f7207adccd29f7a87404689d[ Systemic Strategies ] L/S Grids (0x07fd993f0fa3a185f7207adccd29f7a87404689d)
leader 0x2b804617c6f63c040377e95bb276811747006f4b
APR +137.8%
leader share 7.9% of equity, 10.0% commission
deposits open
followers 100 listed, max distributable $5,496,930
returns
day equity $11,240,855 -> $11,111,113 pnl $-161,380 volume $6,961,193
week equity $11,048,770 -> $11,111,113 pnl $444,003 volume $20,822,926
month equity $10,797,571 -> $11,111,113 pnl $1,254,395 volume $82,254,487
allTime equity $0 -> $11,111,113 pnl $2,666,403 volume $1,072,362,168
largest followers
0xa9ff08af55b24bb5d064d776a078e8a292b8dfe2 equity $3,401,808 all-time pnl $391,981 641 days
Leader equity $877,263 all-time pnl $720,683 651 days
0x8fec806c9e94ff7ab2af3d7e4875c2381413f98e equity $534,659 all-time pnl $84,659 146 days
0xbdfa4f4492dd7b7cf211209c4791af8d52bf5c50 equity $483,231 all-time pnl $19,210 83 days
0xa69d62573ff153390148bf41b233e65901993b48 equity $386,911 all-time pnl $158,769 504 days
open positions (60) account value $11,111,113
LONG XMR $3,074,524 5x cross upnl $167,158
LONG ETH $2,649,361 25x cross upnl $-174,937
LONG AAVE $1,840,490 10x cross upnl $-197,783
LONG BTC $1,400,222 40x cross upnl $-18,806
LONG POL $1,264,704 5x cross upnl $-88,820
LONG BNB $1,233,521 10x cross upnl $-36,679
LONG PENDLE $1,158,061 5x cross upnl $-293,465
LONG LDO $1,155,351 5x cross upnl $-224,220
LONG PAXG $947,295 5x cross upnl $-79,053
LONG HYPE $930,902 10x cross upnl $-70,543That is the whole due-diligence screen in one command. The leader has $877,000 of their own money in the vault and takes 10% of profits. The vault made $1.25M over the month on $82M of volume. One depositor holds 31% of the equity. And right now it holds 60 positions on cross margin, and the ten largest are all longs, which is a different risk from what the name "L/S Grids" suggests.
Run the same command on HLP and the output is almost empty: 0 open positions and an account value of $11.3M against $151M of TVL. That is the child vaults block earning its place. HLP's relationship.type is parent, and seven child vaults list HLP's address as their leader:
child vaults (7)
HLP Strategy X TVL $100,778,255 APR +4.3% 0x469f690213c467c39a23efacfd2816896009d7d8
HLP Liquidator 2 TVL $30,000,002 APR +0.0% 0xb0a55f13d22f66e6d495ac98113841b2326e9540
HLP Strategy A TVL $3,060,457 APR -2.1% 0x010461c14e146ac35fe42271bdc1134ee31c703a
HLP Strategy B TVL $3,027,313 APR -0.7% 0x31ca8395cf837de08b24da3f660e77761dfb974b
HLP Liquidator 4 TVL $1,000,000 APR +0.0% 0x2ed5c4484ea3ff8b57d5f2fb152a40d9f2b68308
HLP Liquidator TVL $999,999 APR +0.0% 0x2e3d94f0562703b25c83308a05046ddaf9a8dd14
HLP Liquidator 3 TVL $844,213 APR -39.5% 0x5e177e5e39c0f4e421f5865a6d8beed8d921cb70
open positions (0) account value $11,257,378To see what HLP is actually trading, run show on HLP Strategy X.
Step 4: The Vaults an Address Leads
leadingVaults has the most misleading name in the Info API. It does not rank vaults. It returns the vaults that one address leads, and it needs that address in a user field. Send it without one and both the public endpoint and Dwellir return HTTP 422 with Failed to deserialize the JSON body into the target type.
def show_leader(address: str) -> None:
"""Vaults this address leads. leadingVaults needs a user; without one it returns 422."""
vaults = info({"type": "leadingVaults", "user": address})
if not vaults:
print(f"{address} leads no vaults")
return
for v in vaults:
print(f"{v['name']} {v['address']}")The leader of the grid vault from Step 3 (the leader address on its screen) runs three:
python vaults.py leader 0x2b804617c6f63c040377e95bb276811747006f4b[ Systemic Strategies ] L/S Grids 0x07fd993f0fa3a185f7207adccd29f7a87404689d
📉 Inverse Andrew Tate 0x7f6ce756bdee0db22a8fbda15aa48023aae5a8be
[ Systemic Strategies ] ♾️ HyperGrowth ♾️ 0xd6e56265890b76413d1d527eb9b75e334c0c5b42This is the query to run on any leaderboard address before copying it. A top trader who leads a vault can be followed with a deposit instead of a bot.
Step 5: Rank the Leaderboard
The leaderboard feed is one object with a leaderboardRows list. Each row has ethAddress, accountValue, an optional displayName (1,482 of 48,192 rows had one) and windowPerformances, a list of [window, {pnl, roi, vlm}] pairs for day, week, month and allTime.
Create leaderboard.py:
"""Rank Hyperliquid traders from the leaderboard feed and profile the top ones live.
python leaderboard.py top --window week --by pnl --n 20
python leaderboard.py top --window month --by roi --min-value 100000 --profile
python leaderboard.py top --vault-leaders --n 10
python leaderboard.py addresses --out addresses.txt
"""
import argparse
from hl_api import feed, info, pct, usd, windows
from vaults import load_vaults
WINDOWS = ("day", "week", "month", "allTime")
METRICS = ("pnl", "roi", "vlm")
def load_leaderboard(min_value: float = 0.0, min_volume: float = 0.0, window: str = "month") -> list[dict]:
"""Flatten /Mainnet/leaderboard into one dict per trader.
min_value drops small accounts. min_volume drops accounts that did not trade
in the window: deposits and transfers can show up as PnL on a tiny starting
balance, which makes their ROI meaningless.
"""
rows = []
for row in feed("leaderboard")["leaderboardRows"]:
value = float(row["accountValue"])
if value < min_value:
continue
perf = windows(row["windowPerformances"])
if float(perf[window]["vlm"]) < min_volume:
continue
rows.append({
"address": row["ethAddress"],
"name": row.get("displayName") or "",
"value": value,
# perf[window] is {"pnl": ..., "roi": ..., "vlm": ...} as strings. Flattened
# to pnl_day, roi_day, vlm_day, pnl_week, ... pnl_allTime, roi_allTime, vlm_allTime.
**{f"{m}_{w}": float(perf[w][m]) for w in WINDOWS for m in METRICS},
})
return rowsThe dict comprehension flattens twelve numbers per trader into keys like pnl_week and roi_allTime, so sorting is one sorted() call with a composed key.
Add the printer. It takes a live flag we will wire up in Step 6:
def print_top(rows: list[dict], window: str, by: str, n: int, live: bool) -> None:
key = f"{by}_{window}"
rows = sorted(rows, key=lambda r: r[key], reverse=True)[:n]
states = profile([r["address"] for r in rows]) if live else {}
print(f"{'#':>3} {'Trader':<42} {'Account':>13} {'PnL':>13} {'ROI':>8} {'Volume':>16}")
for i, r in enumerate(rows, 1):
print(f"{i:>3} {r['address']:<42} {usd(r['value']):>13} {usd(r[f'pnl_{window}']):>13} "
f"{pct(r[f'roi_{window}']):>8} {usd(r[f'vlm_{window}']):>16}")
notes = [r["name"]] if r["name"] else []
if r["address"] in states:
notes.append(describe(states[r["address"]]))
if notes:
print(f" {' | '.join(notes)}")Add the entry point at the bottom of the file. The --profile flag calls profile() and describe(), which we write in Step 6:
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="command", required=True)
top = sub.add_parser("top", help="rank traders")
top.add_argument("--window", choices=WINDOWS, default="week")
top.add_argument("--by", choices=METRICS, default="pnl")
top.add_argument("--n", type=int, default=20)
top.add_argument("--min-value", type=float, default=0.0,
help="ignore accounts below this value in USD")
top.add_argument("--min-volume", type=float, default=0.0,
help="ignore accounts that traded less than this in the window (ROI on untraded deposits is noise)")
top.add_argument("--vault-leaders", action="store_true",
help="only traders who lead at least one open vault")
top.add_argument("--profile", action="store_true",
help="add live positions from Dwellir batchClearinghouseStates")
dump = sub.add_parser("addresses", help="write every leaderboard address to a file")
dump.add_argument("--out", default="addresses.txt")
args = parser.parse_args()
if args.command == "addresses":
rows = load_leaderboard()
with open(args.out, "w") as fh:
fh.write("\n".join(r["address"] for r in rows) + "\n")
print(f"wrote {len(rows)} addresses to {args.out}")
return
rows = load_leaderboard(min_value=args.min_value, min_volume=args.min_volume, window=args.window)
print(f"{len(rows)} traders after filters")
if args.vault_leaders:
# A leader can run several vaults (HLP's leader runs eight). Name the
# largest and count the rest.
vaults_by_leader: dict[str, list[dict]] = {}
for v in sorted(load_vaults(open_only=True), key=lambda v: v["tvl"], reverse=True):
vaults_by_leader.setdefault(v["leader"], []).append(v)
rows = [dict(r, name=f"leads {vs[0]['name']} ({usd(vs[0]['tvl'])} TVL"
+ (f", and {len(vs) - 1} more)" if len(vs) > 1 else ")"))
for r in rows if (vs := vaults_by_leader.get(r["address"]))]
print(f"{len(rows)} of them lead an open vault")
print_top(rows, args.window, args.by, args.n, live=args.profile)
if __name__ == "__main__":
main()Rank by ROI over a month with only an account-value floor and the result is useless:
python leaderboard.py top --window month --by roi --min-value 100000 --n 214386 traders after filters
# Trader Account PnL ROI Volume
1 0x03a66ab07ef5851ab95ce65aaf7f35226a6c9111 $848,500 $848,000 +169600.0% $0
2 0xbfce143acf73a34a1f8b2e2da64736146035c732 $1,697,000 $1,696,000 +169600.0% $0Volume is zero. These accounts started the window with $500 or $1,000 and received a transfer, which the feed counts as PnL against the starting balance. --min-volume removes them: adding --min-volume 1000000 to the same command leaves 3,791 traders and a top of the table made of people who traded.
Step 6: Profile the Top Wallets in One Call
A leaderboard row is history. What the trader holds now is a different question, and asking it one wallet at a time for twenty wallets is twenty round trips. Dwellir's Info proxy adds batchClearinghouseStates, which is not in Hyperliquid's own API: it takes a list of addresses and returns every state in one response.
def profile(addresses: list[str]) -> dict[str, dict]:
"""Live account state for many wallets in one Dwellir request.
batchClearinghouseStates is a Dwellir-only Info type. It is billed per wallet,
so 20 addresses cost 20 responses, the same as 20 single calls, in one round trip.
"""
result = info({"type": "batchClearinghouseStates", "users": addresses}, timeout=60.0)
return {address: state for address, state in result["successful_states"]}
def describe(state: dict) -> str:
"""One line: biggest position, leverage, margin use."""
summary = state["marginSummary"]
value = float(summary["accountValue"])
used = float(summary["totalMarginUsed"])
positions = [p["position"] for p in state["assetPositions"] if float(p["position"]["szi"]) != 0]
if not positions:
return f"flat, margin used {used / value * 100 if value else 0:.0f}%"
big = max(positions, key=lambda p: abs(float(p["positionValue"])))
side = "long" if float(big["szi"]) > 0 else "short"
return (f"{len(positions)} positions, largest {side} {big['coin']} "
f"{usd(big['positionValue'])} at {big['leverage']['value']}x, "
f"margin used {used / value * 100 if value else 0:.0f}%")The response has successful_states, a list of [address, state] pairs in input order, and failed_wallets for any upstream lookups that timed out. Each state is the same object clearinghouseState returns, so describe() works on either.
The week's biggest earners, with their live books:
python leaderboard.py top --window week --by pnl --n 5 --profile48192 traders after filters
# Trader Account PnL ROI Volume
1 0x5b5d51203a0f9079f8aeb098a6523a13f298c060 $220,434,589 $58,999,938 +35.1% $145,461,895
23 positions, largest short ETH $184,268,026 at 5x, margin used 100%
2 0xb83de012dba672c76a7dbbbf3e459cb59d7d6e36 $196,167,837 $51,772,639 +35.4% $229,779,403
30 positions, largest short BTC $147,935,229 at 5x, margin used 100%
3 0x024294c53a69c1e6b39810b3fac87385752fffa1 $46,014,702 $26,138,317 +183.1% $370,469,635
flat, margin used 0%
4 0xf5d81a135f756ca16544e53c20fc20643ec3ad53 $97,862,218 $20,575,626 +24.1% $13,401,831,903
29 positions, largest long BTC $279,299 at 3x, margin used 9%
5 0x7fdafde5cfb5465924316eced2d3715494c517d1 $44,531,145 $16,380,053 +28.8% $766,542,921
BobbyBigSize | 35 positions, largest short BTC $37,965,921 at 30x, margin used 53%Five wallets, five different stories. The top two made $111M between them in a week and are both fully margined short, one in ETH and one in BTC. Number three made $26M and is sitting in cash. Number four turned over $13.4B for a 24% return and runs 9% margin, which looks like a market maker rather than a directional trader. Those distinctions are invisible in the leaderboard and obvious in the batch call.
The whole command, including parsing 40 MB of cached JSON and the batch request, took 2.2 seconds.
Step 7: Your Own Vault Deposits
userVaultEquities runs through Dwellir and returns a wallet's vault positions. It only carries the vault address, the equity and lockedUntilTimestamp, so we join the vaults feed for names and APR.
Create my_vaults.py:
"""Show a wallet's vault deposits with names, lockups and vault performance.
python my_vaults.py 0x677d831aef5328190852e24f13c46cac05f984e7
"""
import sys
from datetime import datetime, timezone
from hl_api import info, pct, usd
from vaults import load_vaults
def main(address: str) -> None:
# userVaultEquities runs through Dwellir. It returns address, equity and
# lockup only, so we join the vaults feed for names and APR.
equities = info({"type": "userVaultEquities", "user": address})
if not equities:
print(f"{address} has no vault deposits")
return
vaults = {v["address"]: v for v in load_vaults(open_only=False)}
now = datetime.now(timezone.utc)
total = 0.0
print(f"{'Vault':<34} {'Equity':>12} {'APR':>9} {'TVL':>14} Lockup")
for e in equities:
v = vaults.get(e["vaultAddress"])
name = v["name"] if v else e["vaultAddress"]
apr = pct(v["apr"]) if v else "n/a"
tvl = usd(v["tvl"]) if v else "n/a"
unlock = datetime.fromtimestamp(e["lockedUntilTimestamp"] / 1000, timezone.utc)
lock = "unlocked" if unlock <= now else f"locked until {unlock:%Y-%m-%d %H:%M} UTC"
total += float(e["equity"])
print(f"{name[:34]:<34} {usd(e['equity']):>12} {apr:>9} {tvl:>14} {lock}")
print(f"{'total':<34} {usd(total):>12}")
if __name__ == "__main__":
if len(sys.argv) != 2:
sys.exit(__doc__)
main(sys.argv[1])load_vaults(open_only=False) matters here. A deposit in a vault that has since closed still shows up in userVaultEquities, and we want its name. Run it on a wallet from the grid vault's follower list (it sits below the five printed in Step 3):
python my_vaults.py 0x0293eb1f9ec324555629d38f97ffbaaaa43770f5Vault Equity APR TVL Lockup
[ Systemic Strategies ] L/S Grids $159,699 +137.0% $11,103,636 unlocked
Growi HF $240,598 -0.6% $12,496,442 unlocked
total $400,297Putting It All Together
The question this guide set out to answer is "who is worth following, and can I follow them with a deposit?" One flag joins the two feeds: keep only leaderboard rows whose address leads an open vault.
python leaderboard.py top --window month --by pnl --vault-leaders --min-volume 1000000 --n 59076 traders after filters
103 of them lead an open vault
# Trader Account PnL ROI Volume
1 0x49e96e255ba418d08e66c35b588e2f2f3766e1d0 $30,940,980 $14,356,501 +36.7% $1,410,528,403
leads TEST VAULT DO NOT SEND FUNDS HERE ($100 TVL)
2 0xf517639a8872e756ac98d3c65507d2ebc25cc032 $11,304,504 $1,579,753 +17.0% $755,212,366
leads 100x degen Vault ($5 TVL)
3 0x94dac1facdc4afd2614c3119447187b4143bc449 $10,354,584 $636,752 +50.0% $14,828,128
leads Big Brother Capital ($1,345 TVL)
4 0x7facb3ec0415d6605e0cf5dff744f1108224ff4d $951,789 $415,872 +137.1% $2,352,282
leads Long HYPE | Short Garbage ($1,192 TVL)
5 0xf04ac903c91a028c9a6de6dcb86875920f02206c $97,468 $88,102 +158.0% $3,451,895
leads Special Sits 50x ($0 TVL)The TVL in brackets is the point of the join. The five best monthly PnLs among vault leaders all run vaults holding under $1,500, so this flag finds traders who opened a vault, not vaults worth a deposit. For candidates, go the other way: python vaults.py top --by month --min-tvl 250000 ranks funded vaults by 30-day PnL, and python vaults.py show <address> on one of them gives the fee, lockup, follower and position screen from Step 3 before any money moves.
The last tool answers a question that shows up in our Search Console queries: how to get every address on the leaderboard.
python leaderboard.py addresses --out addresses.txtwrote 48192 addresses to addresses.txtFeed that file to batchClearinghouseStates in chunks and you have a snapshot of every ranked trader's book.
Going to Production
Cache the feeds, and decide how stale is acceptable. The two stats-data files are rebuilt by Hyperliquid on their own schedule, not on request, and the vault feed's 30-day PnL for the grid vault was $1,246,919 while vaultDetails said $1,254,395 a minute later. Treat the feeds as a few minutes behind. Ten minutes of cache is right for a dashboard; a copy-trading signal should use vaultDetails and clearinghouseState for the vaults it cares about and the feed only for discovery.
Keep the per-wallet calls on Dwellir. The public Info endpoint applies a shared rate limit per IP address, and every service behind one NAT gateway shares it. Through Dwellir the limit is the plan's responses per second, counted per account, and batchClearinghouseStates keeps the round trips down. It is billed per distinct wallet in the batch, so profiling 1,000 addresses costs 1,000 responses, the same as 1,000 single calls, in one request. See the Info endpoint docs for the per-plan limits.
Expect PUBLIC_ONLY to shrink. vaultDetails and portfolio return 403 on the proxy today. If you want them routed through Dwellir, ask support@dwellir.com; the routing table in hl_api.py is the only line to change when they land.
Handle the two feed failures. A 40 MB download will occasionally fail halfway. feed() only writes the cache after raise_for_status(), so a failed download leaves the old file in place, but on a first run there is no old file. Wrap the call in a retry with backoff before you schedule it.
Treat ROI as a screen, not a score. ROI in the feed is PnL over the window's starting equity. Deposits inflate PnL, small starting balances inflate ROI, and neither is a trading result. Rank by PnL with an account-value floor, or by ROI with --min-volume, and then look at the positions.
Vault terms are in vaultDetails, not the feed. leaderCommission, leaderFraction, allowDeposits and each follower's lockupUntil only come from the detail call. Deposits lock for one day in user vaults and four days in HLP, measured as lockupUntil minus vaultEntryTime across the followers of both on 6 November 2026, so read lockupUntil before assuming you can withdraw today.
This is not investment advice. The vaults and wallets shown here were picked because they topped a sort on one afternoon. A vault holding 60 cross-margin positions can lose a year of APR in a day.
Next Steps
- Stream vault deposits and withdrawals as they happen. Every
vaultTransferaction is decoded in Dwellir's gRPC block stream, so a flow monitor for the vaults you follow is a filter onStreamBlocks. - Replay a leader's trades. The trade post-mortem guide rebuilds any wallet's closed trades from
userFillsByTimeon the Hyperliquid Index, which works on a vault leader's address as well as your own. - Turn a leaderboard wallet into a signal. The whale tracking guide streams large fills over gRPC and enriches them with the same
clearinghouseStatecall used here, and the copy-trading bot post takes it from signal to order.
This guide ran every per-wallet query through Dwellir's Hyperliquid Info endpoint. To run it yourself, create a free account and set DWELLIR_API_KEY.
References
- dwellir-examples/hyperliquid-vaults-leaderboard
- Dwellir Hyperliquid Info endpoint
- userVaultEquities, leadingVaults, batchClearinghouseStates
- Hyperliquid Info endpoint documentation
Trading cryptocurrencies involves significant risk. This content is for educational purposes only and does not constitute financial advice.
Frequently Asked Questions
Is there a Hyperliquid leaderboard API?
Not in the documented Info API. The leaderboard on app.hyperliquid.xyz loads one JSON file, https://stats-data.hyperliquid.xyz/Mainnet/leaderboard, with every ranked trader (48,192 rows and about 40 MB on 5 November 2026). Each row has the address, account value and PnL, ROI and volume for day, week, month and all time. Download it once, cache it, and rank it locally. Then use Dwellir's batchClearinghouseStates to pull live positions for the top wallets in one call.
How do I get a list of all Hyperliquid vaults?
vaultSummaries is the documented query, but it returned an empty array on both the public endpoint and Dwellir on 5 November 2026. The vault list the app shows comes from https://stats-data.hyperliquid.xyz/Mainnet/vaults, about 14 MB with 9,478 vaults, 3,062 of them open. Each entry has the name, leader, TVL, APR, open or closed flag and a short cumulative PnL series per window.
What does leadingVaults return?
The vaults an address leads, not a ranking of the best vaults. It needs a user field. Without one the endpoint returns HTTP 422 'Failed to deserialize the JSON body into the target type', which is why it looks broken if you copy the bare request from older docs.
How do I see what a Hyperliquid vault is holding?
A vault is an address. Query clearinghouseState with the vault address as the user and you get its open positions, leverage, unrealized PnL and margin use, the same as any wallet. For HLP that returns almost nothing, because HLP is a parent vault whose capital sits in child vaults such as HLP Strategy X; query the children instead.
Which vault queries work through Dwellir's Info proxy?
clearinghouseState, batchClearinghouseStates, userVaultEquities, leadingVaults and userRole all run through api-hyperliquid-mainnet-info.n.dwellir.com with your API key. vaultDetails and portfolio return 403 'type not allowed' there, so send those two to the public api.hyperliquid.xyz/info endpoint. The guide's helper routes each query automatically.