# Hyperliquid Vaults and Leaderboard Data via API 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](https://github.com/dwellir-public/dwellir-examples/tree/main/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 the `user` field 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 `batchClearinghouseStates` request
- Show a wallet's vault deposits, APR and lockups with `userVaultEquities`

## Prerequisites

- Python 3.10 or newer. Run `python3 --version` to check.
- A Dwellir API key from the [dashboard](https://dashboard.dwellir.com/register). The free plan allows [20 responses per second](https://www.dwellir.com/docs/getting-started/rate-limits), which is more than this guide needs. No gRPC or Order Book add-on is required.
- One package:

```bash
pip install httpx
```

- Comfort with `argparse` and 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:

```bash
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-key
```

**Note:** 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:

```python
"""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 data
```

`info()` 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:

```python
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:

```python
>>> 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:

```python
"""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 rows
```

Two 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:

```python
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:

```python
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:

```bash
python vaults.py top --by tvl --n 10
```

```
3062 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:

```bash
python vaults.py top --by apr --min-tvl 250000 --n 5
```

```
48 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  0x9e02aca9865e1859bb7865f6f64801e804a173df
```

**Note:** `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`:

```python
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`:

```python
    # 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:

```bash
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,543
```

That 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,378
```

To 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`.

```python
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:

```bash
python vaults.py leader 0x2b804617c6f63c040377e95bb276811747006f4b
```

```
[ Systemic Strategies ] L/S Grids  0x07fd993f0fa3a185f7207adccd29f7a87404689d
📉 Inverse Andrew Tate  0x7f6ce756bdee0db22a8fbda15aa48023aae5a8be
[ Systemic Strategies ] ♾️ HyperGrowth ♾️  0xd6e56265890b76413d1d527eb9b75e334c0c5b42
```

This 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`:

```python
"""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 rows
```

The 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:

```python
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:

```python
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:

```bash
python leaderboard.py top --window month --by roi --min-value 100000 --n 2
```

```
14386 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%               $0
```

Volume 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.

```python
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:

```bash
python leaderboard.py top --window week --by pnl --n 5 --profile
```

```
48192 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`:

```python
"""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):

```bash
python my_vaults.py 0x0293eb1f9ec324555629d38f97ffbaaaa43770f5
```

```
Vault                                    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,297
```

## Putting 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.

```bash
python leaderboard.py top --window month --by pnl --vault-leaders --min-volume 1000000 --n 5
```

```
9076 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.

```bash
python leaderboard.py addresses --out addresses.txt
```

```
wrote 48192 addresses to addresses.txt
```

Feed 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](https://www.dwellir.com/docs/hyperliquid/info-endpoint) 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 `vaultTransfer` action is decoded in Dwellir's [gRPC block stream](https://www.dwellir.com/docs/hyperliquid/vaultTransfer), so a flow monitor for the vaults you follow is a filter on `StreamBlocks`.
- Replay a leader's trades. The [trade post-mortem guide](https://www.dwellir.com/guides/hyperliquid-trade-postmortem) rebuilds any wallet's closed trades from `userFillsByTime` on 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](https://www.dwellir.com/guides/hyperliquid-whale-tracking) streams large fills over gRPC and enriches them with the same `clearinghouseState` call used here, and the [copy-trading bot post](https://www.dwellir.com/blog/hyperliquid-copy-trading-bot-python) 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](https://dashboard.dwellir.com/register) and set `DWELLIR_API_KEY`.

### References

- [dwellir-examples/hyperliquid-vaults-leaderboard](https://github.com/dwellir-public/dwellir-examples/tree/main/examples/hyperliquid-vaults-leaderboard)
- [Dwellir Hyperliquid Info endpoint](https://www.dwellir.com/docs/hyperliquid/info-endpoint)
- [userVaultEquities](https://www.dwellir.com/docs/hyperliquid/user-vault-equities), [leadingVaults](https://www.dwellir.com/docs/hyperliquid/leading-vaults), [batchClearinghouseStates](https://www.dwellir.com/docs/hyperliquid/batch-clearinghouse-states)
- [Hyperliquid Info endpoint documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint)

*Trading cryptocurrencies involves significant risk. This content is for educational purposes only and does not constitute financial advice.*
