Hyperliquid MCP server and API: ask in plain words, or call any route
Connect Claude, ChatGPT, Cursor or your own AI trading agent to Hyperliquid data and ask in plain words: 28 MCP tools, 47 REST routes, 12 live WebSocket channels, the vike-data CLI and alerts, on one free key; in Claude Desktop no key at all.
“Which Hyperliquid whales are long ETH right now, and how close are they to liquidation?”
hl_market hl_liquidations“Send me a webhook when a Hyperliquid liquidation over $1M hits BTC.”
alerts_createConnect your client in a minute
Pick your client; the first tab needs no key.
https://data.vike.io/mcp1. Settings → Connectors → Add custom connector
2. Name: Vike URL: https://data.vike.io/mcp
3. Leave OAuth Client ID and Secret empty, click Add
4. Log into Vike in the tab that opens, click Allow
5. The Vike tools appear under the hammer icon
claude mcp add --transport http vike https://data.vike.io/mcp \
--header "X-API-KEY: $VIKE_API_KEY"
{
"mcpServers": {
"vike": {
"url": "https://data.vike.io/mcp",
"headers": { "X-API-KEY": "YOUR_KEY" }
}
}
}
Add a remote MCP server in ChatGPT's connector settings:
Server URL https://data.vike.io/mcp
Name vike
Credential OAuth sign-in, or X-API-KEY: vk_…
28 tools, grouped by the job they do
Each card: a question to copy, and the tools the agent calls for it, with their credits.
Follow a wallet
“What does this Hyperliquid wallet hold right now, what has it made, and what did it trade this week? (paste an address)”
Top traders
“Who are the top Hyperliquid traders this month by realized PnL, and how consistent are they?”
Whale positions
“Which whales are long BTC on Hyperliquid right now, and how big are their positions?”
Liquidations and leverage
“Where would BTC longs on Hyperliquid get liquidated, and who raised leverage today?”
Orders, TWAPs and money moves
“Which TWAPs are running on Hyperliquid now, and who deposited more than $1M today?”
The venue
“What were Hyperliquid's open interest and volume this week, and who holds the most HYPE?”
Builder codes and vaults
“Which builder codes earned the most fees this month, and how is the HLP vault doing?”
History for a backtest
“Find the Tesla market on Hyperliquid, give me its hourly candles for the last week, and a link to yesterday's file of every fill.”
Watch it live
“What did hl.liquidations send in the last hour?”
Alerts
“E-mail me every time this wallet opens, closes or flips a position on Hyperliquid.”
| Tool | Credits | What it does | Runs | CLI |
|---|---|---|---|---|
hl_wallet | 1 | One Hyperliquid wallet, by view. | summary: /v1/hyperliquid/wallets/{address}trades_summary: /v1/hyperliquid/wallets/{address}/trades/summary |
vike-data hl wallet |
hl_wallet_activity | 1 | A Hyperliquid wallet's event history, by view. | fills: /v1/hyperliquid/wallets/{address}/fillsfunding: /v1/hyperliquid/wallets/{address}/fundingledger: /v1/hyperliquid/wallets/{address}/ledgerbuilders: /v1/hyperliquid/wallets/{address}/builders |
vike-data hl wallet-activity |
hl_wallet_trades | 1 | One Hyperliquid wallet's CLOSED round-trip trades, newest close first, keyset-paged: per trade the symbol, direction, open_ts / close_ts, entry and exit price, the largest size held (max_size_usd), traded volume_usd, realized_pnl and total_fee in USD and the… | /v1/hyperliquid/wallets/{address}/trades |
vike-data hl wallet-trades |
hl_leaderboard | 1 | Rank Hyperliquid wallets with the full risk set, by view. | ranking: /v1/hyperliquid/leaderboardflow_adjusted: /v1/hyperliquid/leaderboard/flow-adjusted |
vike-data hl leaderboard |
hl_perp_top_traders | 2 | Rank top Hyperliquid perp wallets by PnL / ROI / win rate / volume / trade count within a time window. | /v1/hyperliquid/traders |
vike-data perp top-traders |
hl_market | 1 | Hyperliquid market positioning, by view. | positions: /v1/hyperliquid/positionsopen_interest: /v1/hyperliquid/positions/symbolshourly: /v1/hyperliquid/symbols/hourlyfunding_rates: /v1/hyperliquid/funding-rates |
vike-data hl market |
hl_position_match | 2 | Reverse-lookup a Hyperliquid open position from share-card details (symbol + side + leverage + entry price). | /v1/hyperliquid/positions/match |
vike-data perp find-position |
hl_liquidations | 1 | Hyperliquid liquidations, by view. | fills: /v1/hyperliquid/liquidationsrisk: /v1/hyperliquid/liquidations/riskheatmap: /v1/hyperliquid/symbols/{symbol}/liquidation-heatmap |
vike-data hl liquidations |
hl_leverage_changes | 1 | Hyperliquid positions whose leverage or margin mode differs between two consecutive complete position snapshots, newest first: one row per wallet and symbol with the previous and the new leverage, margin mode, side, size and entry price. | /v1/hyperliquid/leverage-changes |
vike-data hl leverage-changes |
hl_orders | 1 | Hyperliquid standing orders, by view. | wallet: /v1/hyperliquid/wallets/{address}/ordersopen: /v1/hyperliquid/ordersclusters: /v1/hyperliquid/symbols/{symbol}/order-clusters |
vike-data hl orders |
hl_twaps | 1 | Hyperliquid TWAP orders, one row per status line, newest first. | /v1/hyperliquid/wallets/{address}/twaps/v1/hyperliquid/twaps |
vike-data hl twaps |
hl_ledger | 1 | Hyperliquid deposits, withdrawals, transfers and vault events ACROSS wallets, newest first, with wallet and amount_usd (USD) on every row; min_usd keeps the large ones (most events are small transfers). | /v1/hyperliquid/ledger |
vike-data hl ledger |
hl_stats | 1 | Hyperliquid venue-wide hourly series, newest hour first, by view. | ecosystem: /v1/hyperliquid/statsvolume: /v1/hyperliquid/stats/volume |
vike-data hl stats |
hl_hype_holders | 1 | The HYPE holder leaderboard: each wallet's spot HYPE balance plus staked HYPE, largest first, valued at one current HYPE price, with its pnl_cohort resolved at query time (label_basis is current; balances are last-known, so a stale balance can sit beside a… | /v1/hyperliquid/hype/holders |
vike-data hl hype-holders |
hl_builders | 1 | Hyperliquid builder codes (front-ends and bots that attach their address to an order and take a slice of the taker fee), by view. | leaderboard: /v1/hyperliquid/buildersstats: /v1/hyperliquid/builders/statsprofile: /v1/hyperliquid/builders/{builder}users: /v1/hyperliquid/builders/{builder}/usersfills: /v1/hyperliquid/builders/{builder}/fillsseries: /v1/hyperliquid/builders/{builder}/series |
vike-data hl builders |
hl_vaults | 1 | Hyperliquid vaults across the platform, by view. | list: /v1/hyperliquid/vaultsstats: /v1/hyperliquid/vaults/statsflows: /v1/hyperliquid/vaults/flows |
vike-data hl vaults |
hl_vault | 1 | One Hyperliquid vault by address, by view. | profile: /v1/hyperliquid/vaults/{vault}depositors: /v1/hyperliquid/vaults/{vault}/depositorsflows: /v1/hyperliquid/vaults/{vault}/flowsseries: /v1/hyperliquid/vaults/{vault}/series |
vike-data hl vault |
search | 1 | Search everything vike knows by name, symbol or address in one call, the same search as the vike.io search box. | — | — |
hl_ohlcv | 1 | Historical OHLCV candles for a Hyperliquid market, aggregated from HL's own executed fills, by view. | candles: /v1/hyperliquid/ohlcvlatest: /v1/hyperliquid/ohlcv/latestexport: /v1/hyperliquid/ohlcv/export |
vike-data candles hl |
hl_archive | 1 | Index of the Hyperliquid Parquet archive and download links to its files. | datasets: /v1/hyperliquid/archive/datasetsurl: /v1/hyperliquid/archive/url |
vike-data hl archive |
stream_info | 0 | How to use the vike WebSocket stream (wss://stream.vike.io/v1/stream/<path>), read off the server's own registry: every path (hl, hl-cohorts, onchain, polymarket) with its channels; per channel its filter keys, each a list (with its maximum length), one… | — | vike-data stream info |
stream_check_filter | 0 | Validate a stream subscribe message (or a channel + filter) with the SAME validator the stream server runs, without connecting. | — | vike-data stream check |
stream_events | 1 | Stored events of ONE stream channel after a cursor, oldest first, with the stream's own filters: the REST catch-up of wss://stream.vike.io, for what a socket missed or a quick look without a socket. | /v1/hyperliquid/stream-events |
vike-data stream events |
alerts_create | 3 | Create an alert rule for the current user: an on-chain transfer alert (the default), a Hyperliquid or a Polymarket alert, chosen by alert_type. | — | vike-data alerts create |
alerts_edit | 2 | Partial update of one of the user's alerts. | — | vike-data alerts edit |
alerts_list | 0 | List the current user's alert rules. | — | vike-data alerts list |
alerts_events | 1 | Read the events the current user's alert rules produced, oldest first (the feed behind GET /v1/alerts/events). | — | vike-data alerts events |
alerts_list_channels | 0 | List the user's enabled email + webhook notification channels. | — | vike-data alerts channels |
HL Cohorts has 2 tools of its own, hl_cohort_metrics and hl_segments: whales against smart money, cohort by cohort, on HL Cohorts.
Hyperliquid from the command line: vike-data
The same data from a terminal, and the command your coding agent runs. Open source, on npm.
npm install -g @vike-io/cli # Node.js 22.12 or newer
vike-data init # paste your key once
vike-data doctor # key, endpoint and tools in one check
vike-data hl wallet 0x25773676902085225ae5751075e4161369334897
vike-data hl leaderboard --window 7d --limit 10
vike-data hl market --view positions --symbol BTC --sort size --limit 10
vike-data hl liquidations --view heatmap --symbol BTC
vike-data candles hl BTC --interval 1h
vike-data hl archive --view url --dataset positions --date latest
vike-data stream hl --channel hl.liquidations --filter min_usd=100000
vike-data alerts create --type hl_wallet --wallet 0x25773676902085225ae5751075e4161369334897 --actions open close flip --channel email
vike-data install --claude # the vike skill for Claude Code
vike-data install --all # Claude, Cursor, Copilot, AGENTS.md
| Command | Runs |
|---|---|
vike-data hl wallet | hl_wallet |
vike-data hl wallet-activity | hl_wallet_activity |
vike-data hl wallet-trades | hl_wallet_trades |
vike-data hl leaderboard | hl_leaderboard |
vike-data hl liquidations | hl_liquidations |
vike-data hl market | hl_market |
vike-data hl builders | hl_builders |
vike-data hl vaults | hl_vaults |
vike-data hl vault | hl_vault |
vike-data hl orders | hl_orders |
vike-data hl twaps | hl_twaps |
vike-data hl ledger | hl_ledger |
vike-data hl leverage-changes | hl_leverage_changes |
vike-data hl stats | hl_stats |
vike-data hl hype-holders | hl_hype_holders |
vike-data hl archive | hl_archive |
vike-data perp top-traders | hl_perp_top_traders |
vike-data perp find-position | hl_position_match |
vike-data candles hl | hl_ohlcv |
vike-data stream hl | the live channels |
vike-data alerts | the alert tools |
vike-data call <tool> --args '{…}'runs any MCP tool by name.vike-data install --claudeadds the skill that teaches Claude Code to use the CLI.- Package
@vike-io/cli0.17.0.
12 live Hyperliquid channels
At wss://stream.vike.io/v1/stream/hl: every fill, liquidation and whale move as it lands, filtered on the server.
- Connect
wss://stream.vike.io/v1/stream/hlwithX-API-KEYon the handshake. - Subscribe
Send
{"action": "subscribe", "id", "channel", "filter"}; up to 100 on one connection. - Resume
Keep the last
cursor; subscribe with it after a drop and get what you missed, up to 3 days back.
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.liquidations",
"filter": {"min_usd": 100000}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["subject"], e["data"])
vike-data stream hl --channel hl.liquidations --filter min_usd=100000 # NDJSON, reconnects and resumes by itself
- Every event has the same envelope:
schema,event_id,source,kind,ts,cursor,chain,subject,data,labels. Keep each frame'scursorand dedupe onevent.event_id: delivery is at least once. - The server sends
{"type": "ping"}every 15 s; answer{"action": "pong"}. 3 missed pongs close the connection. - 1 connection per key on this path, 3 per account, 100 subscriptions on one connection; a newer connection wins and the older one is closed with
4410and a resume cursor. - Close codes:
4401key_revoked;4408slow_consumer;4409server_catching_up;4410replaced;4411memory_backstop;4412ping_timeout;1011a server fault;1012a server restart. - A list is OR inside a key, keys are AND across, an empty filter matches everything. A cohort or tier value
unknownalso matches an event with no label. - A channel is produced only while someone subscribes to it (and 15 minutes after).
- Dropped? Subscribe again with your last
cursorand get what you missed, up to 3 days back. Without a socket:GET /v1/hyperliquid/stream-events(its card) or thestream_eventstool. - Connecting, subscribing and the frames you receive are not counted against the hourly limit.
Build a subscription
Pick a channel and fill the filters: the subscribe message and the CLI command update as you type.
hl.fills
Every perp fill as it lands: the wallet, its cohort labels, the coin, side, size in USD and, where the node says it, whether it opened, closed, flipped, increased or reduced the position. A subscribe without min_usd gets the fills of $1,000 or more; min_usd 0 is every fill.
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- action, coin, min_usd, pnl_cohort, side, size_cohort, wallet
- Default
- min_usd 1,000
Filters
| Field | Type | Description |
|---|---|---|
action | list ≤ 50 | close, flip, increase, open, reduce |
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
min_usd | number | must be a finite number >= 0 |
pnl_cohort | list ≤ 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner |
side | one value | buy, sell |
size_cohort | list ≤ 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale |
wallet | list ≤ 1,000 | wallets are 0x followed by 40 hex characters |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.fills:0x6131f8e5d4eb73de6d030bf8e349efcc432eba7b:1793463688185:332800189911381",
"source": "hl.fills",
"kind": "fill",
"ts": "2026-10-31T16:21:28.185Z",
"cursor": "1793463688185-00000332800189911381",
"chain": "hyperliquid",
"subject": {
"wallet": "0x6131f8e5d4eb73de6d030bf8e349efcc432eba7b",
"symbol": "xyz:CL"
},
"data": {
"action": "reduce",
"side": "buy",
"size_usd": 61781.937000000005,
"size_token": 603.575,
"price": 102.36,
"closed_pnl": 21.7287,
"start_position": -4925.28,
"position_after": -4321.705,
"crossed": true,
"trade_id": 332800189911381,
"dex": "XYZ"
},
"labels": {
"pnl_cohort": "3x rekt",
"pnl_cohort_basis": "live",
"pnl_cum_usd": -43439.253754530044,
"size_cohort": "fish"
}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.fills",
"filter": {"coin": ["BTC"], "min_usd": 100000}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.fills --filter coin=BTC --filter min_usd=100000 # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.fills",
"filter": {
"coin": [
"BTC"
],
"min_usd": 100000
}
}
hl.positions
A whale's position change, sent beside the fill that made it: what the fill did to the position (open, increase, reduce, close or flip), the position before and after, and the wallet's labels; filter it by the wallet's tags too.
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- action, coin, min_leverage, min_usd, pnl_cohort, side, size_cohort, tag, wallet
Filters
| Field | Type | Description |
|---|---|---|
action | list ≤ 50 | close, flip, increase, open, reduce |
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
min_leverage | number | must be a finite number >= 0 |
min_usd | number | must be a finite number >= 0 |
pnl_cohort | list ≤ 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner |
side | one value | buy, sell |
size_cohort | list ≤ 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale |
tag | list ≤ 50 | must be a printable name of 1 to 64 characters |
wallet | list ≤ 1,000 | wallets are 0x followed by 40 hex characters |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.positions:0x6131f8e5d4eb73de6d030bf8e349efcc432eba7b:1793463688185:332800189911381",
"source": "hl.positions",
"kind": "position",
"ts": "2026-10-31T16:21:28.185Z",
"cursor": "1793463688185-00000332800189911381",
"chain": "hyperliquid",
"subject": {
"wallet": "0x6131f8e5d4eb73de6d030bf8e349efcc432eba7b",
"symbol": "xyz:CL"
},
"data": {
"action": "reduce",
"side": "buy",
"size_usd": 61781.937000000005,
"size_token": 603.575,
"price": 102.36,
"closed_pnl": 21.7287,
"start_position": -4925.28,
"position_after": -4321.705,
"crossed": true,
"trade_id": 332800189911381,
"dex": "XYZ"
},
"labels": {
"pnl_cohort": "3x rekt",
"pnl_cohort_basis": "live",
"pnl_cum_usd": -43439.253754530044,
"size_cohort": "fish"
}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.positions",
"filter": {"action": ["open", "flip"]}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.positions --filter action=open,flip # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.positions",
"filter": {
"action": [
"open",
"flip"
]
}
}
hl.closed_trades
A round trip that closed: when it opened and closed, entry and exit price, size, realized PnL and fees.
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- coin, direction, min_usd, pnl_cohort, size_cohort, wallet
Filters
| Field | Type | Description |
|---|---|---|
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
direction | one value | long, short |
min_usd | number | must be a finite number >= 0 |
pnl_cohort | list ≤ 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner |
size_cohort | list ≤ 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale |
wallet | list ≤ 1,000 | wallets are 0x followed by 40 hex characters |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.closed_trades:0x17c3c8fdbcb7d1b240ce08965e09b1fc91cba868:BTC:short:473018254659257",
"source": "hl.closed_trades",
"kind": "closed_trade",
"ts": "2026-11-02T14:11:24.151Z",
"cursor": "1793630999000000-0x17c3c8fdbcb7d1b240ce08965e09b1fc91cba868-BTC-short-473018254659257",
"chain": "hyperliquid",
"subject": {
"wallet": "0x17c3c8fdbcb7d1b240ce08965e09b1fc91cba868",
"symbol": "BTC"
},
"data": {
"direction": "short",
"open_ts": "2026-10-05T23:08:45.141000Z",
"close_ts": "2026-11-02T14:11:24.151000Z",
"duration_seconds": 2386959,
"entry_price": 82742.48610952242,
"exit_price": 0.0,
"max_size_token": 65.89715999999954,
"max_size_usd": 5589001.728239961,
"volume_usd": 35491053.94947996,
"realized_pnl": 203357.7737329998,
"total_fee": 2926.3286200000007,
"fill_count": 27212,
"open_tid": 473018254659257,
"baseline_adopted": false,
"size_usd": 5589001.728239961
},
"labels": {
"pnl_cohort": "3x smart",
"pnl_cohort_basis": "live",
"pnl_cum_usd": 821833.0928032473,
"size_cohort": "3x whale"
}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.closed_trades",
"filter": {"wallet": ["0x25773676902085225ae5751075e4161369334897"]}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.closed_trades --filter wallet=0x25773676902085225ae5751075e4161369334897 # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.closed_trades",
"filter": {
"wallet": [
"0x25773676902085225ae5751075e4161369334897"
]
}
}
hl.liquidations
Every liquidation: the wallet, its cohort labels, the coin, the side that was closed and its size in USD.
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- coin, direction, min_usd, pnl_cohort, size_cohort, wallet
Filters
| Field | Type | Description |
|---|---|---|
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
direction | one value | long, short |
min_usd | number | must be a finite number >= 0 |
pnl_cohort | list ≤ 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner |
size_cohort | list ≤ 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale |
wallet | list ≤ 1,000 | wallets are 0x followed by 40 hex characters |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.liquidations:0xc0c7aa447bbc40a4a487dad6343b20f8e844cff1:SOL:1793463720000",
"source": "hl.liquidations",
"kind": "liquidation",
"ts": "2026-10-31T16:22:00.000Z",
"cursor": "1793463720000-0xc0c7aa447bbc40a4a487dad6343b20f8e844cff1-SOL",
"chain": "hyperliquid",
"subject": {
"wallet": "0xc0c7aa447bbc40a4a487dad6343b20f8e844cff1",
"symbol": "SOL"
},
"data": {
"direction": "long",
"size_usd": 410608.1475,
"size_token": 4276.5,
"mark_price": 96.015,
"inferred": true
},
"labels": {
"pnl_cohort": "3x rekt",
"pnl_cohort_basis": "live",
"pnl_cum_usd": -25940.84974099999,
"size_cohort": null
}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.liquidations",
"filter": {"min_usd": 100000}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.liquidations --filter min_usd=100000 # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.liquidations",
"filter": {
"min_usd": 100000
}
}
hl.liq_risk
An open position whose price came close to its liquidation price, announced once when it enters the zone; it re-arms when it moves away.
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- coin, direction, min_usd, pnl_cohort, size_cohort, wallet
Filters
| Field | Type | Description |
|---|---|---|
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
direction | one value | long, short |
min_usd | number | must be a finite number >= 0 |
pnl_cohort | list ≤ 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner |
size_cohort | list ≤ 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale |
wallet | list ≤ 1,000 | wallets are 0x followed by 40 hex characters |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.liq_risk:0x082e843a431aef031264dc232693dd710aedca88:HYPE:long:1793616060",
"source": "hl.liq_risk",
"kind": "liq_risk",
"ts": "2026-11-02T10:41:00.000Z",
"cursor": "1793616060",
"chain": "hyperliquid",
"subject": {
"wallet": "0x082e843a431aef031264dc232693dd710aedca88",
"symbol": "HYPE"
},
"data": {
"asset": "HYPE",
"direction": "long",
"liq_ratio": 0.8703577332750073,
"enter": 0.8,
"prev": null,
"mark_price": 85.725,
"entry_price": 38.675565324008176,
"liquidation_price": 74.611416685,
"size_usd": 118304157.02849999,
"leverage": 5.0,
"snapshot_ts": "2026-11-02T10:41:00Z"
},
"labels": {
"pnl_cohort": null,
"pnl_cohort_basis": null,
"pnl_cum_usd": null,
"size_cohort": "4x whale"
}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.liq_risk",
"filter": {"coin": ["BTC", "ETH"]}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.liq_risk --filter coin=BTC,ETH # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.liq_risk",
"filter": {
"coin": [
"BTC",
"ETH"
]
}
}
hl.top_positions
Each big coin's largest positions: one event when a position enters the top, leaves it or moves rank (event entered | left | moved).
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- coin, direction, event, min_usd, pnl_cohort, size_cohort, wallet
Filters
| Field | Type | Description |
|---|---|---|
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
direction | one value | long, short |
event | one value | entered, left, moved |
min_usd | number | must be a finite number >= 0 |
pnl_cohort | list ≤ 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner |
size_cohort | list ≤ 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale |
wallet | list ≤ 1,000 | wallets are 0x followed by 40 hex characters |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.top_positions:ETH:0x5b5d51203a0f9079f8aeb098a6523a13f298c060:entered:1793629560",
"source": "hl.top_positions",
"kind": "top_position",
"ts": "2026-11-02T14:26:00.000Z",
"cursor": "1793629560",
"chain": "hyperliquid",
"subject": {
"wallet": "0x5b5d51203a0f9079f8aeb098a6523a13f298c060",
"symbol": "ETH"
},
"data": {
"event": "entered",
"rank": 1,
"prev_rank": null,
"top_n": 10,
"direction": "short",
"size_usd": 222141532.18031597,
"size_token": 96925.8868,
"entry_price": 2322.723237249773,
"mark_price": 2291.87,
"liquidation_price": 3700.6952196025,
"leverage": 5.0,
"unrealized_pnl": 2990477.381085038,
"snapshot_ts": "2026-11-02T14:26:00Z",
"first": true
},
"labels": {
"pnl_cohort": "3x smart",
"pnl_cohort_basis": "live",
"pnl_cum_usd": 38550482.93637243,
"size_cohort": "4x whale"
}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.top_positions",
"filter": {"coin": ["BTC"]}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.top_positions --filter coin=BTC # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.top_positions",
"filter": {
"coin": [
"BTC"
]
}
}
hl.order_walls
A large resting order that appeared on the book or disappeared from it.
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- coin, event, min_usd, pnl_cohort, side, size_cohort, wallet
Filters
| Field | Type | Description |
|---|---|---|
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
event | one value | appeared, disappeared |
min_usd | number | must be a finite number >= 0 |
pnl_cohort | list ≤ 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner |
side | one value | buy, sell |
size_cohort | list ≤ 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale |
wallet | list ≤ 1,000 | wallets are 0x followed by 40 hex characters |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.order_walls:592426159180:appeared",
"source": "hl.order_walls",
"kind": "order_wall",
"ts": "2026-11-02T10:25:00.000Z",
"cursor": "1793615100-00000000592426159180",
"chain": "hyperliquid",
"subject": {
"wallet": "0x31dea2516beee92135b96f464eeec3cf292a13f2",
"symbol": "BTC"
},
"data": {
"event": "appeared",
"coin": "BTC",
"side": "sell",
"limit_px": 79407.0,
"size_usd": 8328956.56,
"remaining_usd": 8328956.56,
"ts_placed": "2026-11-02T10:24:32.865Z",
"first_seen": "2026-11-02T10:25:00.000Z",
"last_seen": "2026-11-02T10:25:00.000Z",
"oid": 592426159180
},
"labels": {
"pnl_cohort": "3x smart",
"pnl_cohort_basis": "live",
"pnl_cum_usd": 5362809.680522572,
"size_cohort": "4x whale"
}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.order_walls",
"filter": {"coin": ["BTC"], "event": "appeared"}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.order_walls --filter coin=BTC --filter event=appeared # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.order_walls",
"filter": {
"coin": [
"BTC"
],
"event": "appeared"
}
}
hl.twaps
TWAP status lines: a TWAP order activated, error, finished, stopped, terminated, waitingForTrigger.
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- coin, min_usd, pnl_cohort, side, size_cohort, status, wallet
Filters
| Field | Type | Description |
|---|---|---|
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
min_usd | number | must be a finite number >= 0 |
pnl_cohort | list ≤ 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner |
side | one value | buy, sell |
size_cohort | list ≤ 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale |
status | list ≤ 50 | activated, error, finished, stopped, terminated, waitingForTrigger |
wallet | list ≤ 1,000 | wallets are 0x followed by 40 hex characters |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.twaps:2494610:1793614010669043:terminated",
"source": "hl.twaps",
"kind": "twap",
"ts": "2026-11-02T10:06:50.669Z",
"cursor": "1793614010669043-2494610-1793614010669043-terminated",
"chain": "hyperliquid",
"subject": {
"wallet": "0x40f20b734d7d25ba0b6219e921382fa992a56a9e",
"symbol": "xyz:CL"
},
"data": {
"twap_id": 2494610,
"side": "buy",
"status": "terminated",
"size": 15945.587,
"executed_size": 9158.657,
"executed_notional_usd": 912873.590198,
"duration_minutes": 30,
"reduce_only": false,
"randomize": true,
"placed_at": "2026-11-02T09:49:28.250000+00:00",
"trigger_px": null,
"trigger_above": null,
"stop_px": null,
"error": null,
"size_usd": 1587622.37
},
"labels": {
"pnl_cohort": "3x rekt",
"pnl_cohort_basis": "live",
"pnl_cum_usd": -630786.4677088035,
"size_cohort": "2x dolphin"
}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.twaps",
"filter": {"status": ["activated"]}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.twaps --filter status=activated # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.twaps",
"filter": {
"status": [
"activated"
]
}
}
hl.ledger
Deposits, withdrawals, transfers and vault events of every wallet (event_type accountClassTransfer, borrowLend, deposit, internalTransfer, rewardsClaim, send, spotTransfer, subAccountTransfer, vaultCreate, vaultDeposit, vaultDistribution, vaultLeaderCommission, vaultWithdraw, withdraw).
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- event_type, min_usd, pnl_cohort, size_cohort, wallet
Filters
| Field | Type | Description |
|---|---|---|
event_type | list ≤ 50 | accountClassTransfer, borrowLend, deposit, internalTransfer, rewardsClaim, send, spotTransfer, subAccountTransfer, vaultCreate, vaultDeposit, vaultDistribution, vaultLeaderCommission, vaultWithdraw, withdraw |
min_usd | number | must be a finite number >= 0 |
pnl_cohort | list ≤ 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner |
size_cohort | list ≤ 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale |
wallet | list ≤ 1,000 | wallets are 0x followed by 40 hex characters |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.ledger:0x57dd78cd36e76e2011e8f6dc25cabbaba994494b:1793613669773:0x6569de99a42617cb66e30447e622be020134007f3f29369d093289ec6329f1b6",
"source": "hl.ledger",
"kind": "ledger",
"ts": "2026-11-02T10:01:09.773Z",
"cursor": "1793613669773000-0x57dd78cd36e76e2011e8f6dc25cabbaba994494b-1793613669773-0x6569de99a42617cb66e30447e622be020134007f3f29369d093289ec6329f1b6",
"chain": "hyperliquid",
"subject": {
"wallet": "0x57dd78cd36e76e2011e8f6dc25cabbaba994494b"
},
"data": {
"event_type": "send",
"amount_usd": -500119.0,
"fee": 0.0,
"token": "USDC",
"counterparty": "0x71d0e11ebb6150cebe20cf62f798be1a50108c26",
"hash": "0x6569de99a42617cb66e30447e622be020134007f3f29369d093289ec6329f1b6",
"size_usd": 500119.0
},
"labels": {
"pnl_cohort": "3x smart",
"pnl_cohort_basis": "live",
"pnl_cum_usd": 5809892.161684301,
"size_cohort": "3x whale"
}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.ledger",
"filter": {"min_usd": 1000000}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.ledger --filter min_usd=1000000 # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.ledger",
"filter": {
"min_usd": 1000000
}
}
hl.leverage
A position whose leverage or margin mode changed between two snapshots.
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- coin, direction, min_usd, pnl_cohort, size_cohort, wallet
Filters
| Field | Type | Description |
|---|---|---|
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
direction | one value | long, short |
min_usd | number | must be a finite number >= 0 |
pnl_cohort | list ≤ 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner |
size_cohort | list ≤ 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale |
wallet | list ≤ 1,000 | wallets are 0x followed by 40 hex characters |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.leverage:0x01e273fc08012db1b75e8ca93f8139bfac2ee977:BTC:1793614860",
"source": "hl.leverage",
"kind": "leverage",
"ts": "2026-11-02T10:21:00.000Z",
"cursor": "1793614860000000-0x01e273fc08012db1b75e8ca93f8139bfac2ee977-BTC-1793614860",
"chain": "hyperliquid",
"subject": {
"wallet": "0x01e273fc08012db1b75e8ca93f8139bfac2ee977",
"symbol": "BTC"
},
"data": {
"prev_ts": "2026-11-02T10:16:00+00:00",
"captured_at": "2026-11-02T10:19:41+00:00",
"prev_captured_at": "2026-11-02T10:07:43+00:00",
"dex_name": "HL",
"change": "up",
"leverage_prev": 7.0,
"leverage": 8.0,
"mode_prev": "cross",
"mode": "cross",
"position_side_prev": "long",
"position_side": "long",
"size_usd_prev": 14434371.116759999,
"size_usd": 15722429.26275,
"entry_price_prev": 78128.55916020906,
"entry_price": 78117.7483785485,
"direction": "long"
},
"labels": {
"pnl_cohort": "3x smart",
"pnl_cohort_basis": "live",
"pnl_cum_usd": 828767.6863311598,
"size_cohort": "whale"
}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.leverage",
"filter": {"coin": ["BTC"]}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.leverage --filter coin=BTC # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.leverage",
"filter": {
"coin": [
"BTC"
]
}
}
hl.funding
Every perp's funding rate, premium, mid price and open interest at each 5-minute market snapshot.
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- coin
Filters
| Field | Type | Description |
|---|---|---|
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.funding:BTC:1793614860",
"source": "hl.funding",
"kind": "funding",
"ts": "2026-11-02T10:21:00.000Z",
"cursor": "1793614860000000-BTC-1793614860",
"chain": "hyperliquid",
"subject": {
"symbol": "BTC"
},
"data": {
"snapshot_ts": "2026-11-02T10:21:00Z",
"funding_rate": 1.25e-05,
"premium": -0.0002409614,
"mid_price": 78001.5,
"open_interest": 40353.10018,
"open_interest_usd": 3147602343.69
},
"labels": {}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.funding",
"filter": {"coin": ["BTC", "ETH"]}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.funding --filter coin=BTC,ETH # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.funding",
"filter": {
"coin": [
"BTC",
"ETH"
]
}
}
hl.candles
Closed candles: one event each time a bar closes, intervals 1m, 5m, 15m, 30m, 1h, 4h, 1d, perp or spot, on the core venue or a HIP-3 dex.
- Path
- wss://stream.vike.io/v1/stream/hl
- Filters
- coin, dex, interval, market
- Intervals
1m5m15m30m1h4h1d, closed bars only. A spot pair matches its token (HYPE) and its Hyperliquid id (@107);marketisperporspot,dexHLfor the core venue or a HIP-3 dex. - Each bar is
event.data: open, high, low, close, volume,volume_usd,n_tradesandclosed: true;data.assetkeeps the Hyperliquid id (@107for a spot pair).event.tsis the bar's open. History of the same bars:GET /v1/hyperliquid/ohlcv. - Closed bars moved here from
wss://data.vike.io/v1/stream, which answers HTTP 410 since 2026-11-02.
Filters
| Field | Type | Description |
|---|---|---|
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
dex | list ≤ 50 | not a dex name (A-Z and 0-9, at most 16 characters; HL is the core venue) |
interval | list ≤ 50 | 15m, 1d, 1h, 1m, 30m, 4h, 5m |
market | list ≤ 50 | perp, spot |
A real event captured 2026-11-03
{
"schema": "vike.event.v1",
"event_id": "hl.candles:5m:xyz:SNDK:1793613000",
"source": "hl.candles",
"kind": "candle",
"ts": "2026-11-02T09:50:00.000Z",
"cursor": "1793613300-00300",
"chain": "hyperliquid",
"subject": {
"symbol": "xyz:SNDK"
},
"data": {
"interval": "5m",
"market": "perp",
"dex": "XYZ",
"asset": "xyz:SNDK",
"open": 1183.3,
"high": 1186.1,
"low": 1183.3,
"close": 1185.2,
"volume": 2102.737,
"volume_usd": 2491744.5026,
"n_trades": 631,
"funding": null,
"closed": true
},
"labels": {}
}
Subscribe
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.candles",
"filter": {"coin": ["BTC"], "interval": ["1m"]}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(e["ts"], e["data"]) # keep frame["cursor"] to resume
vike-data stream hl --channel hl.candles --filter coin=BTC --filter interval=1m # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.candles",
"filter": {
"coin": [
"BTC"
],
"interval": [
"1m"
]
}
}
47 routes under /v1/hyperliquid
Base URL https://data.vike.io/v1/hyperliquid, one header. Every route is also an MCP tool, named on its card.
Authentication
- Sign in at vike.io
Any account. No card, no plan to pick.
- Create a key
On https://vike.io/account?tab=api-keys. Every key starts vk_; you can read it back later from the same page.
- Send the header
X-API-KEY: vk_… on every request; OAuth clients send Authorization: Bearer.
| Surface | Credential |
|---|---|
https://data.vike.io/v1/hyperliquid/… | X-API-KEY, or an OAuth bearer token |
https://data.vike.io/mcp | OAuth sign-in for desktop clients (no key to copy), X-API-KEY for everything else |
wss://stream.vike.io/v1/stream/hl | Either header on the handshake, or ?token=vk_… for a browser, which cannot set WebSocket headers: a key in a URL can be logged, so use a header wherever you can |
https://data.vike.io/v1/hyperliquid/archive/url | Your key asks for the file; the answer is a signed link, valid 7 days, and the link is the credential |
- Header names are case-insensitive: X-API-KEY and X-API-Key are the same header.
- A key in a query string is never read on the REST API or MCP.
- Every key has the same reach: there are no scopes.
Limits
| Requests | 20,000 per hour per key, a rolling hour; MCP calls share it |
|---|---|
| Per minute | 200 per minute per key, on /v1 only |
| Keys | up to 3 active per account; each key is its own budget |
| Cost of a call | 1 per REST request (/ohlcv/export: 2); an MCP tool 0 to 3 credits |
| WebSocket | not counted: connecting, subscribing and the frames you receive |
| Archive | downloads not counted; asking for a link is one request, and the link is valid 7 days for any number of reads, range reads included |
| Price | $0, nothing is billed |
Paging is keyset, not offset
A list answer carries nextCursor; send it back verbatim as cursor until it is null or a page comes back empty. There is no offset parameter: a keyset cursor makes every page cost the same and keeps rows from shifting between pages.
A cursor is bound to the query that issued it: reuse it after changing start / end, or on another route, and you get a 400 telling you to start again without one. A malformed cursor is a 400 too, never a silent page 1.
import os, requests
H = {"X-API-KEY": os.environ["VIKE_API_KEY"]}
url = "https://data.vike.io/v1/hyperliquid/positions"
cursor = None
while True:
params = {"symbol": "BTC", "limit": 5000} | ({"cursor": cursor} if cursor else {})
page = requests.get(url, params=params, headers=H, timeout=60).json()
rows = page["positions"] # the list key differs per route
if not rows:
break
handle(rows)
cursor = page.get("nextCursor") # null on the last page
if cursor is None:
break
Try it: Position snapshots
Send replays an answer the API really returned for this exact URL; its nextCursor follows the rows.
/v1/hyperliquid/positions?symbol={symbol}&limit={limit}Errors
Branch on the status code, not the body: the body depends on which hop answered.
| Status | Meaning | Body |
|---|---|---|
| 400 | The input was rejected: an unknown symbol or dex, a malformed identifier, a cursor from another query. | {"detail": "…"} |
| 401 | A missing, invalid or deactivated key. | Turned away before the API runs, so the body can be an HTML page. Do not assume JSON. |
| 404 | No such route or archive file. | {"detail": "Not Found"} |
| 410 | The stream catch-up: a cursor older than the events kept; the answer names the oldest cursor it accepts. | {"detail": "…"} |
| 422 | A parameter failed validation: the wrong type, or outside the bounds the route declares. | {"detail": [ … ]} |
| 429 | Over the hourly limit, or over 200 requests a minute on one key: wait the Retry-After seconds. | Retry-After: 60 (hourly) or Retry-After: 1 (per-minute) header, JSON error body |
| 503 | The data store failed. Not your request: retry with back-off. | {"detail": "upstream unavailable"} |
- A failed query is never a 200 with an empty body. An empty list means there were no rows.
/tradersand/positions/matchrun through the shared tool layer, which reports its own failures in-band, so an internal error there can arrive as a 400 carrying the error text.- MCP answers a bad credential with HTTP 401, a
WWW-Authenticate: Bearer resource_metadata=...header (where an OAuth client starts discovery) and a JSON-RPC error (-32000, Invalid or missing API key). A rate-limit trip there is a real 429. Check the body, not just the status.
Conventions
Every time is UTC.
Timestamps
| Where | Field | Format |
|---|---|---|
| Rows: positions, fills, liquidations, the feeds | ts, snapshot_ts, liq_ts | ISO-8601 string with offset, 2026-10-12T09:00:00+00:00 |
| /ohlcv | ts (the bar's open) | epoch milliseconds |
| /symbols/hourly | ts | epoch seconds |
| Query bounds | start, end | ISO-8601 UTC (or epoch ms on /ohlcv); start inclusive, end exclusive |
Identifiers
| Field | Means |
|---|---|
| wallet, address | 0x and 40 hex characters |
| symbol | the instrument: BTC; a HIP-3 market with its dex, xyz:TSLA; a spot pair @107 |
| cohort | a rung of the chosen axis; the labels and ladders are HL Cohorts' |
Paging envelopes
| Routes | How they page |
|---|---|
| /v1/hyperliquid list routes | keyset: nextCursor, a string or null, opaque and bound to the query that issued it; pass it back verbatim as cursor |
| /ohlcv | no cursor: one page per request; move start and end |
Before you rely on it
What the field names do not tell you.
/liquidations direction is the side of the position that was closed: long means longs were liquidated, so the price fell. size_usd is the size of the closed position in USD, not what the trader lost.nextCursor. Pass it back verbatim as cursor; do not build one. A cursor resumes after the last row's sort key, so rows written meanwhile do not shift the pages. Paging →axis and cohort narrow positions, the leaderboard and liquidation risk to one cohort. The ladders and their exact label strings are on HL Cohorts.pnl_cohort_basis (or the answer's label_basis) says whether its cohort label is point_in_time, as it was that hour, or current, today's label on an older row: right for "who is smart now", look-ahead in a backtest.min_win_rate on the leaderboard is a fraction (0.6 = 60%); /traders takes and returns a percent.symbol is the instrument parameter: BTC, a HIP-3 market xyz:TSLA. coin, its old name, is still accepted; do not send both.Request builder: every route
Pick a route; the parameters come filled with real ids. The call is rebuilt as you type, and a captured route shows its real answer.
All 47 routes
Click a route to jump to its parameters and a call you can copy.
One wallet
Wallet Summary
- Route
GET /v1/hyperliquid/wallets/{address}- CLI
vike-data hl wallet- MCP
hl_wallet· 1 credit- Cost
- 1 request of the hourly limit
One wallet: equity, cohorts, risk metrics, lifetime external flow.
openedWithin bounds the open-positions block only — the equity, lifetime and metrics blocks are wallet-level totals with nothing per-position to filter, and bounding them would return a number that looks bounded and is not. Positions with no known open time are excluded from every bounded bucket, matching what /v1/hl-cohorts/segments and /v1/hl-cohorts/heatmap do, so cohort comparisons stay like-for-like.
open_positions.perp_bias = (long - short) / equity.account_value and open_positions.equity_exposure = (long + short) / equity.account_value, native perp account only: positions on the main dex over the native perp account value; builder-DEX positions are not included. Unbounded ratios, null with no main-dex position or no positive equity.
A wallet we hold nothing for (no equity snapshot, open position, trade, fill or ledger event) is a 400 naming it, not a block of zeros.
Parameters
| Parameter | In | Description |
|---|---|---|
addressrequired | path | The wallet |
window | query | 1d | 7d | 30d | all. Default all |
openedWithin | query | 24h | 7d | 30d | all. Default all |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897?window=30d"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897?window=30d",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl wallet 0x25773676902085225ae5751075e4161369334897 --window 30d
# any MCP client; or from the terminal:
vike-data call hl_wallet --args '{"view": "summary", "address": "0x25773676902085225ae5751075e4161369334897", "window": "30d"}'
Wallet Trades
- Route
GET /v1/hyperliquid/wallets/{address}/trades- CLI
vike-data hl wallet-trades- MCP
hl_wallet_trades· 1 credit- Cost
- 1 request of the hourly limit
Closed round-trip trades for one wallet.
Parameters
| Parameter | In | Description |
|---|---|---|
addressrequired | path | The wallet |
symbol | query | HL ticker, e.g. BTC. Omit for all |
start | query | ISO-8601 UTC bounds |
end | query | ISO-8601 UTC bounds |
limit | query | Default 100, max 5000 |
cursor | query | nextCursor from the previous page, verbatim |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/trades?limit=20"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/trades?limit=20",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl wallet-trades 0x25773676902085225ae5751075e4161369334897 --limit 20
# any MCP client; or from the terminal:
vike-data call hl_wallet_trades --args '{"address": "0x25773676902085225ae5751075e4161369334897", "limit": 20}'
Wallet Trades Summary
- Route
GET /v1/hyperliquid/wallets/{address}/trades/summary- CLI
vike-data hl wallet- MCP
hl_wallet· 1 credit- Cost
- 1 request of the hourly limit
Trade statistics for one wallet, overall and per coin.
Medians as well as means: one $4M outlier drags the mean trade size far from anything the wallet typically does, and the mean is where most leaderboards stop.
A wallet with no trade at all is a 400 naming it, not zeros. A wallet that has traded and was quiet in the requested window gets the zeros, which are true.
Parameters
| Parameter | In | Description |
|---|---|---|
addressrequired | path | The wallet |
window | query | 1d | 7d | 30d | all. Default all |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/trades/summary?window=all"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/trades/summary?window=all",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl wallet 0x25773676902085225ae5751075e4161369334897 --view trades_summary --window all
# any MCP client; or from the terminal:
vike-data call hl_wallet --args '{"view": "trades_summary", "address": "0x25773676902085225ae5751075e4161369334897", "window": "all"}'
Wallet Fills
- Route
GET /v1/hyperliquid/wallets/{address}/fills- CLI
vike-data hl wallet-activity- MCP
hl_wallet_activity· 1 credit- Cost
- 1 request of the hourly limit
Raw fills for one wallet.
Parameters
| Parameter | In | Description |
|---|---|---|
addressrequired | path | The wallet |
symbol | query | HL ticker |
start | query | ISO-8601 UTC bounds |
end | query | ISO-8601 UTC bounds |
side | query | buy | sell |
limit | query | Default 100, max 5000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/fills?limit=50"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/fills?limit=50",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl wallet-activity 0x25773676902085225ae5751075e4161369334897 --limit 50
# any MCP client; or from the terminal:
vike-data call hl_wallet_activity --args '{"view": "fills", "address": "0x25773676902085225ae5751075e4161369334897", "limit": 50}'
Wallet Funding
- Route
GET /v1/hyperliquid/wallets/{address}/funding- CLI
vike-data hl wallet-activity- MCP
hl_wallet_activity· 1 credit- Cost
- 1 request of the hourly limit
Funding paid and received by one wallet.
Parameters
| Parameter | In | Description |
|---|---|---|
addressrequired | path | The wallet |
symbol | query | HL ticker |
start | query | ISO-8601 UTC bounds |
end | query | ISO-8601 UTC bounds |
limit | query | Default 100, max 5000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/funding?limit=50"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/funding?limit=50",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl wallet-activity 0x25773676902085225ae5751075e4161369334897 --view funding --limit 50
# any MCP client; or from the terminal:
vike-data call hl_wallet_activity --args '{"view": "funding", "address": "0x25773676902085225ae5751075e4161369334897", "limit": 50}'
Wallet Ledger
- Route
GET /v1/hyperliquid/wallets/{address}/ledger- CLI
vike-data hl wallet-activity- MCP
hl_wallet_activity· 1 credit- Cost
- 1 request of the hourly limit
Deposits, withdrawals, transfers and vault events for one wallet.
Parameters
| Parameter | In | Description |
|---|---|---|
addressrequired | path | The wallet |
event_type | query | One of accountClassTransfer, borrowLend, deposit, internalTransfer, rewardsClaim, send, spotTransfer, subAccountTransfer, vaultCreate, vaultDeposit, vaultDistribution, vaultLeaderCommission, vaultWithdraw, withdraw. Validated against this fixed set |
start | query | ISO-8601 UTC bounds |
end | query | ISO-8601 UTC bounds |
limit | query | Default 100, max 5000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/ledger?limit=50"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/ledger?limit=50",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl wallet-activity 0x25773676902085225ae5751075e4161369334897 --view ledger --limit 50
# any MCP client; or from the terminal:
vike-data call hl_wallet_activity --args '{"view": "ledger", "address": "0x25773676902085225ae5751075e4161369334897", "limit": 50}'
Wallet Orders
- Route
GET /v1/hyperliquid/wallets/{address}/orders- CLI
vike-data hl orders- MCP
hl_orders· 1 credit- Cost
- 1 request of the hourly limit
What became of one wallet's orders in a window: each order seen resting in it or filled in it, with its fills in the window and a status. Newest placement first; notes says what a snapshot cannot tell.
Parameters
| Parameter | In | Description |
|---|---|---|
addressrequired | path | The wallet |
from | query | ISO-8601 UTC. Default: 7 days before to |
to | query | ISO-8601 UTC. Default: the newest order snapshot. At most 30 days after from |
symbol | query | HL perp ticker |
status | query | open | filled | partially_filled | canceled_or_expired |
limit | query | Default 100, max 1000 |
cursor | query | nextCursor from the previous page, verbatim |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/orders?limit=20"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/orders?limit=20",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl orders --view wallet --address 0x25773676902085225ae5751075e4161369334897 --limit 20
# any MCP client; or from the terminal:
vike-data call hl_orders --args '{"view": "wallet", "address": "0x25773676902085225ae5751075e4161369334897", "limit": 20}'
Wallet TWAPs
- Route
GET /v1/hyperliquid/wallets/{address}/twaps- CLI
vike-data hl twaps- MCP
hl_twaps· 1 credit- Cost
- 1 request of the hourly limit
One wallet's TWAP orders: /twaps with the wallet pinned, wrapped as wallet, items, nextCursor.
Parameters
| Parameter | In | Description |
|---|---|---|
addressrequired | path | The wallet |
twap_id | query | One TWAP id: every status line of that TWAP |
symbol | query | HL ticker, e.g. BTC or xyz:AAPL |
status | query | One of activated, waitingForTrigger, finished, terminated, stopped, error. Default: every status except error, a rejected placement rather than an order; status=error shows those rows |
start | query | ISO-8601 UTC. Default: no lower bound |
end | query | ISO-8601 UTC. Default: now. The window has no 31-day cap |
order | query | asc | desc. Default desc, newest first. order=asc returns the oldest events first and is the mode for following a feed: pass each nextCursor back verbatim. It orders by key, so a row the writer inserts late with an older key is not seen by a poller already past it: re-read a small overlap. An asc page with rows always carries a cursor; an empty one returns the cursor you sent, or on a first call null (the wallet already pins the place). So stop on an empty page, not on a null cursor. A desc page that comes back short carries no cursor. Anything else is a 400 |
limit | query | Default 100, max 5000 |
cursor | query | nextCursor from the previous page, verbatim. It pins the call that returned it: the feed, order, every filter and the start and end you sent. Changing or dropping any of them, start included, is a 400. It is also valid on /twaps?wallet= with the same parameters |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/twaps?limit=20"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/twaps?limit=20",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl twaps --wallet 0x25773676902085225ae5751075e4161369334897 --limit 20
# any MCP client; or from the terminal:
vike-data call hl_twaps --args '{"wallet": "0x25773676902085225ae5751075e4161369334897", "limit": 20}'
Wallet Builders
- Route
GET /v1/hyperliquid/wallets/{address}/builders- CLI
vike-data hl wallet-activity- MCP
hl_wallet_activity· 1 credit- Cost
- 1 request of the hourly limit
Reverse lookup — which builders this wallet's flow is routed through.
share_of_volume is the share of the wallet's builder-routed volume. Fills placed without a builder code never reach this table, so it is not a share of the wallet's total trading.
Parameters
| Parameter | In | Description |
|---|---|---|
addressrequired | path | The wallet |
window | query | 1d | 7d | 30d | all. Default all |
limit | query | Default 50, max 500 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/builders?window=30d"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/wallets/0x25773676902085225ae5751075e4161369334897/builders?window=30d",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl wallet-activity 0x25773676902085225ae5751075e4161369334897 --view builders --window 30d
# any MCP client; or from the terminal:
vike-data call hl_wallet_activity --args '{"view": "builders", "address": "0x25773676902085225ae5751075e4161369334897", "window": "30d"}'
Leaderboards
Leaderboard
- Route
GET /v1/hyperliquid/leaderboard- CLI
vike-data hl leaderboard- MCP
hl_leaderboard· 1 credit- Cost
- 1 request of the hourly limit
Trader leaderboard carrying the full risk-metric set.
Drawdown, Calmar, recovery factor, profit factor, expectancy and streaks sit beside PnL: a wallet up 300% through a 70% drawdown is not the same find as one up 300% without, and a PnL-only board cannot tell them apart.
calmar_ratio and recovery_factor divide by CURRENT account value, so they are null under $1 of it, and sort=calmar, recovery_factor or roi skips those wallets (an emptied account used to lead with a ratio in the billions). profit_factor is capped at 999: 999 means no losing trade closed, or only a negligible one, not a measured ratio.
Parameters
| Parameter | In | Description |
|---|---|---|
window | query | 1d | 7d | 30d | all. Default 30d. The trades CLOSED in the current UTC day and the 1, 7, 30 or 365 days before it, so PnL, trade counts, win rates and tags are what was realized in the window |
sort | query | One of pnl, realized_pnl, roi, volume, trades, win_rate, long_win_rate, short_win_rate, account_value, profit_factor, expectancy, calmar, recovery_factor, max_drawdown, current_drawdown, avg_leverage, avg_duration, fees, win_streak, loss_streak. Default pnl. An unknown key is a 400, not a default |
order | query | asc | desc. Default desc |
cohort | query | One label on axis |
axis | query | size | pnl | pnl_abs. Default size |
min_trades | query | Minimum closed trades in the window. Default 3, the floor /traders applies; pass 0 for no floor |
min_win_rate | query | Minimum win rate |
min_volume | query | Minimum total volume |
limit | query | Default 25, max 500 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/leaderboard?limit=10"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/leaderboard?limit=10",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl leaderboard --limit 10
# any MCP client; or from the terminal:
vike-data call hl_leaderboard --args '{"view": "ranking", "limit": 10}'
Leaderboard Flow Adjusted
- Route
GET /v1/hyperliquid/leaderboard/flow-adjusted- CLI
vike-data hl leaderboard- MCP
hl_leaderboard· 1 credit- Cost
- 1 request of the hourly limit
Leaderboard ranked on trading PnL, with external flow reported beside it.
An account-value delta is not a trading result: a wallet that deposits $1M and loses $50k reads as +$950k on any equity-delta ranking. This ranks on fills-derived realized PnL, which excludes deposits by construction, and returns the deposits, withdrawals and the daily attribution figures in the same row so a caller can see what was excluded rather than trust that something was.
Each row carries flow_unbalanced. It is true when the wallet had a send in the window, which is when the *attributed* block beside the rank is unreliable — ledger_events records send one-sided, so an outflow is seen while the matching inflow is not. The rank itself does not depend on attribution and is unaffected. The flag is reported, never used to filter: active traders move money constantly, and excluding them would empty the board rather than clean it.
Parameters
| Parameter | In | Description |
|---|---|---|
window | query | 1d | 7d | 30d | all. Default 30d. The trades CLOSED in the current UTC day and the 1, 7, 30 or 365 days before it, so PnL, trade counts, win rates and tags are what was realized in the window |
limit | query | Default 25, max 500 |
cursor | query | Keyset cursor |
min_trades | query | Minimum closed trades in the window. Default 3; 0 keeps any wallet with at least one |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/leaderboard/flow-adjusted?window=30d&limit=10"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/leaderboard/flow-adjusted?window=30d&limit=10",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl leaderboard --view flow_adjusted --window 30d --limit 10
# any MCP client; or from the terminal:
vike-data call hl_leaderboard --args '{"view": "flow_adjusted", "window": "30d", "limit": 10}'
Top Traders
- Route
GET /v1/hyperliquid/traders- CLI
vike-data perp top-traders- MCP
hl_perp_top_traders· 2 credits- Cost
- 1 request of the hourly limit
Top HL perp traders by the requested metric over the window.
Kept on the tool implementation rather than moved to the new engine: MCP and the AI chat call the same code, and a second ranking that disagreed with the one the assistant quotes would be worse than a near-duplicate route. /leaderboard is the richer, cursor-paged version.
Parameters
| Parameter | In | Description |
|---|---|---|
window | query | 1d | 7d | 30d | all. Effective default 30d |
sort | query | pnl | roi | win_rate | volume | trade_count | drawdown | account_value | long_pnl | short_pnl. Default pnl |
order | query | asc | desc. Default desc |
min_trades | query | Minimum closed trades. Floored at 3: omitting it or passing 0, 1 or 2 gives 3 |
min_win_rate | query | Percent, 0–100, not a fraction |
min_volume | query | Minimum total volume |
size | query | Number of traders, 1–100 |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/traders?size=10"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/traders?size=10",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data perp top-traders --size 10
# any MCP client; or from the terminal:
vike-data call hl_perp_top_traders --args '{"size": 10}'
Positions and open interest
List Positions
- Route
GET /v1/hyperliquid/positions- CLI
vike-data hl market- MCP
hl_market· 1 credit- Cost
- 1 request of the hourly limit
Position rows, current or historical.
open_only=true reads the latest-state table. Passing start/end switches to the historical snapshot table, which is 8.7 billion rows — bound it by symbol or wallet, or expect it to be slow.
Rows come in key order, not by size. The cursor pages on the table's primary key, so current positions are ordered by wallet, symbol, direction and historical ones by symbol, snapshot_ts, wallet (resolution=1h with wallet: by snapshot_ts, then symbol): limit=10 is the first ten wallets in address order, never the ten biggest positions. For the biggest, pass sort=size (below), or read /v1/hl-cohorts/symbols/{symbol}/size-breakdown (positioning by position-size bucket), /positions/symbols (open interest per symbol) or /v1/hl-cohorts/symbols/{symbol}/cohort-metrics (by cohort).
sort=size returns the limit biggest current positions of one symbol, ordered by size_usd descending, then wallet, then direction, from the same latest-state read as the default. It is a top-N, not a page: symbol is required, limit is at most 500, there is no cursor (nextCursor is always null), and start/end are refused; each violation is a 400 with a one-line reason. Every row additionally carries account_value: the wallet's NATIVE perp account value in USD at the newest snapshot (the same figure as equity.account_value on /wallets/{address}, so it leaves out a builder DEX's separate account), or null when it is unknown (a repaired value, or no row in the newest snapshot). It is never 0 for unknown. No per-position cumulative funding is served, because no per-position source exists that avoids scanning the funding table.
Every row declares its pnl-label provenance: pnl_cohort_basis (point_in_time | current, the same vocabulary labelBasis uses elsewhere) and pnl_cohort_as_of (the ISO hour the label was computed at, or null). Historical rows carry the point-in-time label from the hourly label table — not today's dictionary standing — with the current unlabelled tail (the newest hour or two) falling back to the dictionary and saying so per row; the envelope's label_computed block states the mode's source once. A historical pnl/pnl_abs cohort filter is answered from the same label table and therefore requires symbol or wallet alongside it.
A historical row's own label counts fills up to the end of its hour (label_computed.lookahead is true). For a backtest read label_causal instead: an object with the same keys (pnl_cohort, pnl_abs_cohort, pnl_cohort_as_of, pnl_cohort_basis) computed at the PREVIOUS hour, so it holds no later fill. It is null where that hour has no label for the wallet and is absent on current rows. The row's own label values are unchanged.
Parameters
| Parameter | In | Description |
|---|---|---|
symbol | query | HL ticker, e.g. BTC |
wallet | query | One wallet |
cohort | query | One label on axis |
axis | query | size | pnl | pnl_abs. Default size |
open_only | query | Boolean, default true. Latest open positions only |
start | query | ISO-8601 UTC. Setting either switches to history. Not before 2026-05-01 |
end | query | ISO-8601 UTC. Setting either switches to history |
limit | query | Default 100, max 5000 (500 with sort=size) |
cursor | query | Keyset cursor. Refused with sort=size |
sort | query | size = the biggest positions of one symbol first. Default: key order. Current mode only; needs symbol; limit at most 500; no cursor |
resolution | query | History only (needs start). 5m = every snapshot, ~5 minutes apart. 1h = the last snapshot of each hour, ~12x fewer rows. Default: 1h when wallet is given (served from a wallet-sorted copy), 5m otherwise. A 5m read of one wallet is limited to 48 hours per request. |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/positions?symbol=BTC&limit=10"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/positions?symbol=BTC&limit=10",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl market --view positions --symbol BTC --limit 10
# any MCP client; or from the terminal:
vike-data call hl_market --args '{"view": "positions", "symbol": "BTC", "limit": 10}'
OI Snapshot
- Route
GET /v1/hyperliquid/positions/symbols- CLI
vike-data hl market- MCP
hl_market· 1 credit- Cost
- 1 request of the hourly limit
Market-wide open interest per coin at the latest snapshot, with the venue's own OI alongside our aggregate so the two can be reconciled.
Parameters
None.
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/positions/symbols"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/positions/symbols",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl market
# any MCP client; or from the terminal:
vike-data call hl_market --args '{"view": "open_interest"}'
Position Match
- Route
GET /v1/hyperliquid/positions/match- CLI
vike-data perp find-position- MCP
hl_position_match· 2 credits- Cost
- 1 request of the hourly limit
Find open positions matching a symbol/side/leverage/entry profile — the 'who else is in this trade' query.
A dated sample: its entry price matched an open BTC short on 2026-11-03. Once that position closes the same call answers an empty list; send the coin, side, leverage and entry price of a position open now (/positions lists them).
Parameters
| Parameter | In | Description |
|---|---|---|
symbolrequired | query | HL ticker as listed, e.g. BTC, kPEPE, xyz:TSLA; case-insensitive for those spellings |
siderequired | query | long | short |
leveragerequired | query | Numeric, 0 or more. Matched within ±0.5 absolute. 0 finds cross-margin positions with no leverage recorded |
entry_pricerequired | query | Numeric, positive |
mark_price | query | Tie-break among equally close entries (matches are ranked by entry-price distance first). Defaults to entry_price when omitted |
limit | query | Number of matches, 1–500 |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/positions/match?symbol=BTC&side=short&leverage=5&entry_price=76327.4&limit=10"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/positions/match?symbol=BTC&side=short&leverage=5&entry_price=76327.4&limit=10",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data perp find-position --coin BTC --side short --leverage 5 --entry 76327.4 --limit 10
# any MCP client; or from the terminal:
vike-data call hl_position_match --args '{"symbol": "BTC", "side": "short", "leverage": 5, "entry_price": 76327.4, "limit": 10}'
Venue stats, funding and HYPE holders
Stats
- Route
GET /v1/hyperliquid/stats- CLI
vike-data hl stats- MCP
hl_stats· 1 credit- Cost
- 1 request of the hourly limit
Ecosystem time series: perp equity, open interest, position counts.
Parameters
| Parameter | In | Description |
|---|---|---|
start | query | ISO-8601 UTC bounds |
end | query | ISO-8601 UTC bounds |
limit | query | Default 500, max 5000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/stats"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/stats",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl stats
# any MCP client; or from the terminal:
vike-data call hl_stats --args '{"view": "ecosystem"}'
Stats Volume
- Route
GET /v1/hyperliquid/stats/volume- CLI
vike-data hl stats- MCP
hl_stats· 1 credit- Cost
- 1 request of the hourly limit
Hourly perp volume, whole venue or one coin.
volume is the traded notional counted once per trade (one side), the figure the candles report; until 2026-10-25 it counted both sides of every trade and was exactly twice that. The venue total (no symbol) is perp volume, builder-dex perps included, and leaves out spot and outcome markets; naming one of those as symbol still returns it. n_fills counts fill records, two per trade (one for each side).
Parameters
| Parameter | In | Description |
|---|---|---|
symbol | query | HL ticker. Omit for the whole venue |
start | query | ISO-8601 UTC bounds |
end | query | ISO-8601 UTC bounds |
limit | query | Default 500, max 5000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/stats/volume?symbol=BTC&limit=24"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/stats/volume?symbol=BTC&limit=24",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl stats --view volume --symbol BTC --limit 24
# any MCP client; or from the terminal:
vike-data call hl_stats --args '{"view": "volume", "symbol": "BTC", "limit": 24}'
Assets Hourly
- Route
GET /v1/hyperliquid/symbols/hourly- CLI
vike-data hl market- MCP
hl_market· 1 credit- Cost
- 1 request of the hourly limit
Hourly close + open interest + funding rate + premium, many assets at once.
Rows are arrays in columns order, not objects — a 120-day 10-asset window is 28,800 of them.
Parameters
| Parameter | In | Description |
|---|---|---|
symbolsrequired | query | Comma-separated perp tickers, e.g. BTC,ETH,SOL. Max 64 |
days | query | Window length back from end. Default 120, range 1–400 |
start | query | ISO-8601 UTC or epoch s/ms, inclusive. Overrides days |
end | query | ISO-8601 UTC or epoch s/ms, exclusive. Defaults to now |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/symbols/hourly?symbols=BTC&days=2"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/symbols/hourly?symbols=BTC&days=2",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl market --view hourly --symbols BTC --days 2
# any MCP client; or from the terminal:
vike-data call hl_market --args '{"view": "hourly", "symbols": "BTC", "days": 2}'
HYPE Holders
- Route
GET /v1/hyperliquid/hype/holders- CLI
vike-data hl hype-holders- MCP
hl_hype_holders· 1 credit- Cost
- 1 request of the hourly limit
HYPE holder leaderboard — spot balance plus staked, per wallet.
pnl_cohort is resolved at query time and the envelope declares it (label_basis='current' + label_computed). Correct here, not a fallback: the leaderboard has no time axis — every row is last-known state, so a current label has no earlier row to contaminate. The caveat runs the other way: balances are last-known, so a stale balance can sit beside a fresh label. account_value is the wallet's perp account value and is null for a holder with no perp account on record (it used to read 0.0, the same as an account that really holds nothing).
Parameters
| Parameter | In | Description |
|---|---|---|
order | query | asc | desc. Default desc |
limit | query | Default 100, max 500 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/hype/holders?limit=10"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/hype/holders?limit=10",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl hype-holders --limit 10
# any MCP client; or from the terminal:
vike-data call hl_hype_holders --args '{"limit": 10}'
Funding rates
Funding Rates
- Route
GET /v1/hyperliquid/funding-rates- CLI
- —
- MCP
hl_market· 1 credit- Cost
- 1 request of the hourly limit
Hyperliquid funding rates: the latest of every perp, or one perp's hourly history.
funding_rate is the venue's hourly rate as a fraction (0.0000125 = 0.00125% an hour; Hyperliquid settles funding every hour, longs paying shorts when it is positive) and funding_rate_annualized is it x 8,760 (24 x 365), simple, not compounded (0.1095 = 10.95% a year). premium is the venue's premium of the perp over its oracle price, also a fraction. open_interest is one side, in coins, and open_interest_usd it x mid_price.
Without symbol, the board: one row per perp, native and builder-DEX (HIP-3, xyz:TSLA), its newest reading (taken every 5 minutes; ts says when), ranked by sort. as_of is the newest reading's time.
With symbol, the history: one row per UTC hour (ts is the hour's start), from data_from, the perp's first recorded hour (2026-04-02 for the oldest). funding_rate, premium, mid_price and open_interest are the means of the hour's readings (n_snapshots, 12 in a full hour), not the rate settled at the end of the hour, and the newest hour may still be filling. A symbol we hold no hour of is a 404.
Builder-DEX (HIP-3) perps: from 2026-04-22 13:00 to 2026-11-03 their hours are the rate the venue SETTLED at the end of the hour, rebuilt from its funding history, not a mean of readings; a value the venue never published (a market already delisted) is null.
Parameters
| Parameter | In | Description |
|---|---|---|
symbol | query | Hyperliquid perp ticker, e.g. BTC, or a builder-DEX (HIP-3) perp such as xyz:TSLA. Omit for the board: the latest funding of every perp. Given, that perp's hourly history. A spot or outcome market is a 400 |
sort | query | Board only: oi | rate | abs_rate. Default oi, open interest in USD; abs_rate ranks by the size of the rate whatever its sign. A 400 with symbol |
order | query | asc | desc. Default desc: the board largest first, a history newest hour first |
start | query | History only (needs symbol): ISO-8601 UTC, inclusive. Default: no lower bound (data_from) |
end | query | History only (needs symbol): ISO-8601 UTC, inclusive. Default: now |
limit | query | Default 500, max 5000. The board holds about 350 perps, so the default is all of it |
cursor | query | nextCursor from the previous page, verbatim. It pins the call that returned it: resend the same symbol, sort and order, or the call is a 400. A board cursor also pins that board's time, so every page reads the same one |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/funding-rates"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/funding-rates",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
# any MCP client; or from the terminal:
vike-data call hl_market --args '{"view": "funding_rates"}'
Liquidations and leverage
Liquidations
- Route
GET /v1/hyperliquid/liquidations- CLI
vike-data hl liquidations- MCP
hl_liquidations· 1 credit- Cost
- 1 request of the hourly limit
Liquidation events, newest first: exact, from the node's fills since 2026-08-03; older rows inferred from position snapshots.
Since 2026-08-03 a row is the liquidated account's own fill, as the venue marked it: liq_ts is the fill's millisecond time, size_token and size_usd the size it closed (fills of one liquidation in the same millisecond summed) and mark_price the mark price the venue liquidated at. An older row is a position worth over $100 whose mark was within 5% of its liquidation price in one snapshot (taken about every 5 minutes) and that is gone from the next: liq_ts is the time of that snapshot, size and mark are the last values seen, a position closed by hand that close to its liquidation price is counted and a partial liquidation is not.
Each row's pnl_cohort is point-in-time — the label as it stood at the liquidation's own hour — where the hourly label table covers it, and the query-time dictionary label where it cannot; pnl_cohort_basis says which per row, and the envelope's label_computed declares the provenance and the two windows with no point-in-time label (rows before 2026-04-07, and the newest hours the label job has not yet published).
Parameters
| Parameter | In | Description |
|---|---|---|
symbol | query | HL ticker |
wallet | query | One wallet |
start | query | ISO-8601 UTC bounds |
end | query | ISO-8601 UTC bounds |
direction | query | long | short |
limit | query | Default 100, max 5000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/liquidations?limit=20"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/liquidations?limit=20",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl liquidations --limit 20
# any MCP client; or from the terminal:
vike-data call hl_liquidations --args '{"view": "fills", "limit": 20}'
Liquidation Risk
- Route
GET /v1/hyperliquid/liquidations/risk- CLI
vike-data hl liquidations- MCP
hl_liquidations· 1 credit- Cost
- 1 request of the hourly limit
Share of each coin's open interest sitting close to liquidation.
Parameters
| Parameter | In | Description |
|---|---|---|
axis | query | size | pnl | pnl_abs. Default size |
cohort | query | One label on that axis |
limit | query | Default 100, max 500 |
min_value_usd | query | Smallest total open notional (USD) a coin needs to be ranked. Default 0 (no floor): the ranking is by percent_risk, so small markets with a few thousand dollars of interest lead it; raise this to keep only markets big enough for a cascade to matter. |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/liquidations/risk"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/liquidations/risk",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl liquidations --view risk
# any MCP client; or from the terminal:
vike-data call hl_liquidations --args '{"view": "risk"}'
Liquidation Heatmap
- Route
GET /v1/hyperliquid/symbols/{symbol}/liquidation-heatmap- CLI
vike-data hl liquidations- MCP
hl_liquidations· 1 credit- Cost
- 1 request of the hourly limit
Where liquidations would cluster if price moved — every open position's own liquidation price, binned and weighted by notional.
Parameters
| Parameter | In | Description |
|---|---|---|
symbolrequired | path | HL ticker |
bins | query | Number of price bins. Default 50, min 5, max 500 |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/symbols/BTC/liquidation-heatmap?bins=50"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/symbols/BTC/liquidation-heatmap?bins=50",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl liquidations --view heatmap --symbol BTC --bins 50
# any MCP client; or from the terminal:
vike-data call hl_liquidations --args '{"view": "heatmap", "symbol": "BTC", "bins": 50}'
Leverage Changes
- Route
GET /v1/hyperliquid/leverage-changes- CLI
vike-data hl leverage-changes- MCP
hl_leverage_changes· 1 credit- Cost
- 1 request of the hourly limit
Positions whose leverage or margin mode differs between two consecutive complete position snapshots, newest first.
One row per wallet and symbol, with the previous and the new value of leverage, margin mode, side, size and entry price. ts is when the later snapshot was written, not when the venue changed the setting. Snapshots are stamped every 5 minutes, but the positions behind them are captured about every 12 minutes, so changes arrive in batches.
Parameters
| Parameter | In | Description |
|---|---|---|
wallet | query | One wallet, 0x plus 40 hex, any case |
symbol | query | HL ticker, e.g. BTC or xyz:TSLA |
change | query | up (leverage rose) | down (leverage fell) | mode (only the margin mode, cross or isolated, changed). Validated against this fixed set |
min_leverage | query | Minimum new leverage, 0 or more. Default 0: no floor |
start | query | ISO-8601 UTC. Default: the 24 hours before end (before now when end is empty), unless wallet is given. Resuming an order=asc cursor starts from the cursor instead; a desc cursor keeps the 24-hour floor |
end | query | ISO-8601 UTC. Default: now. A window wider than 31 days is a 400 unless wallet is given |
order | query | asc | desc. Default desc, newest first. order=asc returns the oldest events first and is the mode for following a feed: pass each nextCursor back verbatim. It orders by key, so a row the writer inserts late with an older key is not seen by a poller already past it: re-read a small overlap. An asc page with rows always carries a cursor; an empty one returns the cursor you sent, or on a first call with no start or wallet, a cursor at the lower bound of the default window (null when one of them already pins the place). So stop on an empty page, not on a null cursor. A desc page that comes back short carries no cursor. Anything else is a 400 |
limit | query | Default 100, max 5000 |
cursor | query | nextCursor from the previous page, verbatim. It pins the call that returned it: the feed, order, every filter and the start and end you sent. Changing or dropping any of them, start included, is a 400. Without wallet, an asc cursor more than 31 days before end (before now when end is empty) is a 400 too: restart with start |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/leverage-changes?symbol=BTC&limit=20"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/leverage-changes?symbol=BTC&limit=20",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl leverage-changes --symbol BTC --limit 20
# any MCP client; or from the terminal:
vike-data call hl_leverage_changes --args '{"symbol": "BTC", "limit": 20}'
Standing orders, TWAPs and the ledger
Open Orders
- Route
GET /v1/hyperliquid/orders- CLI
vike-data hl orders- MCP
hl_orders· 1 credit- Cost
- 1 request of the hourly limit
The orders resting at one instant: every order in a 5-minute snapshot, or a rebuild from the sightings around it when no complete 5-minute snapshot exists.
Returns snapshotTs, source (l4_5m | state_12m), orders, nextCursor. The 5-minute snapshots start at 2026-10-23 21:45 UTC; before that, and after a stalled feed's last snapshot, the answer is rebuilt from the 12-minute checkpoints. A rebuilt answer for a past at lists only the orders seen before the instant and again after it, so short-lived orders can be missing, and its snapshotTs is the at floored to 5 minutes; an at inside an outage of either feed is answered the same way. Rows in key order (dex, symbol, side, oid); the cursor pins the snapshot.
Parameters
| Parameter | In | Description |
|---|---|---|
symbol | query | HL perp ticker, e.g. BTC or xyz:TSLA. symbol and/or wallet is required |
wallet | query | One wallet. symbol and/or wallet is required |
at | query | ISO-8601 UTC, floored to 5 minutes. Default: the newest snapshot. Not before 2026-07-29, not in the future |
oid | query | One order id |
order_type | query | Limit | Stop Market | Stop Limit | Take Profit Market | Take Profit Limit | Trailing Stop Market |
created_from | query | ISO-8601 UTC. Placed at or after |
created_to | query | ISO-8601 UTC. Placed at or before |
limit | query | Default 100, max 1000 |
cursor | query | nextCursor from the previous page, verbatim |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/orders?symbol=BTC&limit=10"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/orders?symbol=BTC&limit=10",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl orders --symbol BTC --limit 10
# any MCP client; or from the terminal:
vike-data call hl_orders --args '{"view": "open", "symbol": "BTC", "limit": 10}'
Order Clusters
- Route
GET /v1/hyperliquid/symbols/{symbol}/order-clusters- CLI
vike-data hl orders- MCP
hl_orders· 1 credit- Cost
- 1 request of the hourly limit
One symbol's standing orders within range_pct of the mid of the best resting bid and ask, bucketed by price: bid / ask by limit price, tp / sl by trigger price.
Trailing stops are not bucketed. For an at with no complete 5-minute snapshot at that instant (before 2026-10-23 21:45 UTC, or inside an outage) the book is rebuilt from the sightings on both sides of it: short-lived orders can be missing and reference_price then reflects the long-lived orders only. A call without at is never rebuilt: it is the newest snapshot, or the newest checkpoint while the 5-minute feed is stalled. For a past at, sizes can be later, smaller remaining sizes.
Parameters
| Parameter | In | Description |
|---|---|---|
symbolrequired | path | HL perp ticker |
at | query | ISO-8601 UTC, floored to 5 minutes. Default: the newest snapshot |
step_bps | query | Bucket width in basis points of the reference price. Default 10 |
range_pct | query | Half-width of the price range, percent. Default 10 |
cohort_axis | query | size | pnl | pnl_abs. Default size |
cohort | query | One label on cohort_axis, by CURRENT labels: refused with at |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/symbols/BTC/order-clusters"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/symbols/BTC/order-clusters",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl orders --view clusters --symbol BTC
# any MCP client; or from the terminal:
vike-data call hl_orders --args '{"view": "clusters", "symbol": "BTC"}'
TWAPs
- Route
GET /v1/hyperliquid/twaps- CLI
vike-data hl twaps- MCP
hl_twaps· 1 credit- Cost
- 1 request of the hourly limit
TWAP orders across wallets, one row per status line, newest first.
A line is a status CHANGE (activated, waitingForTrigger, then finished, terminated or stopped), not a slice fill: executed_size and executed_notional_usd are the cumulative totals when the line was written. Rejected placements (error) are hidden unless status=error. Rows sharing a ts keep a stable order, so a cursor never skips or repeats one.
Parameters
| Parameter | In | Description |
|---|---|---|
wallet | query | One wallet, 0x plus 40 hex, any case |
twap_id | query | One TWAP id: every status line of that TWAP |
symbol | query | HL ticker, e.g. BTC or xyz:AAPL |
status | query | One of activated, waitingForTrigger, finished, terminated, stopped, error. Default: every status except error, a rejected placement rather than an order; status=error shows those rows |
start | query | ISO-8601 UTC. Default: the 24 hours before end (before now when end is empty), unless wallet or twap_id is given. Resuming an order=asc cursor starts from the cursor instead; a desc cursor keeps the 24-hour floor |
end | query | ISO-8601 UTC. Default: now. A window wider than 31 days is a 400 unless wallet or twap_id is given |
order | query | asc | desc. Default desc, newest first. order=asc returns the oldest events first and is the mode for following a feed: pass each nextCursor back verbatim. It orders by key, so a row the writer inserts late with an older key is not seen by a poller already past it: re-read a small overlap. An asc page with rows always carries a cursor; an empty one returns the cursor you sent, or on a first call with no start or wallet or twap_id, a cursor at the lower bound of the default window (null when one of them already pins the place). So stop on an empty page, not on a null cursor. A desc page that comes back short carries no cursor. Anything else is a 400 |
limit | query | Default 100, max 5000 |
cursor | query | nextCursor from the previous page, verbatim. It pins the call that returned it: the feed, order, every filter and the start and end you sent. Changing or dropping any of them, start included, is a 400. Without wallet or twap_id, an asc cursor more than 31 days before end (before now when end is empty) is a 400 too: restart with start. /twaps?wallet= and the per-wallet TWAP route are one feed: with the same parameters their cursors are interchangeable |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/twaps?limit=20"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/twaps?limit=20",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl twaps --limit 20
# any MCP client; or from the terminal:
vike-data call hl_twaps --args '{"limit": 20}'
Ledger Feed
- Route
GET /v1/hyperliquid/ledger- CLI
vike-data hl ledger- MCP
hl_ledger· 1 credit- Cost
- 1 request of the hourly limit
Deposits, withdrawals, transfers and vault events across wallets, newest first.
The cross-wallet twin of /wallets/{address}/ledger, with wallet on every row. A transfer between two wallets is one row per affected wallet with the same hash.
Parameters
| Parameter | In | Description |
|---|---|---|
wallet | query | One wallet, 0x plus 40 hex, any case |
event_type | query | One of accountClassTransfer, borrowLend, deposit, internalTransfer, rewardsClaim, send, spotTransfer, subAccountTransfer, vaultCreate, vaultDeposit, vaultDistribution, vaultLeaderCommission, vaultWithdraw, withdraw. Validated against this fixed set |
min_usd | query | Minimum absolute amount_usd, 0 or more. Default 0: no floor |
start | query | ISO-8601 UTC. Default: the 24 hours before end (before now when end is empty), unless wallet is given. Resuming an order=asc cursor starts from the cursor instead; a desc cursor keeps the 24-hour floor |
end | query | ISO-8601 UTC. Default: now. A window wider than 31 days is a 400 unless wallet is given |
order | query | asc | desc. Default desc, newest first. order=asc returns the oldest events first and is the mode for following a feed: pass each nextCursor back verbatim. It orders by key, so a row the writer inserts late with an older key is not seen by a poller already past it: re-read a small overlap. An asc page with rows always carries a cursor; an empty one returns the cursor you sent, or on a first call with no start or wallet, a cursor at the lower bound of the default window (null when one of them already pins the place). So stop on an empty page, not on a null cursor. A desc page the read finds short carries no cursor, but duplicate rows are dropped from a page after it is read, so a page with fewer than limit rows can still carry one: stop on an empty page. Anything else is a 400 |
limit | query | Default 100, max 5000 |
cursor | query | nextCursor from the previous page, verbatim. It pins the call that returned it: the feed, order, every filter and the start and end you sent. Changing or dropping any of them, start included, is a 400. Without wallet, an asc cursor more than 31 days before end (before now when end is empty) is a 400 too: restart with start |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/ledger?min_usd=1000000&limit=20"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/ledger?min_usd=1000000&limit=20",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl ledger --min-usd 1000000 --limit 20
# any MCP client; or from the terminal:
vike-data call hl_ledger --args '{"min_usd": 1000000, "limit": 20}'
Builder codes
Builder Leaderboard
- Route
GET /v1/hyperliquid/builders- CLI
vike-data hl builders- MCP
hl_builders· 1 credit- Cost
- 1 request of the hourly limit
Builder leaderboard — fee revenue, routed volume, unique traders, fills.
sort=fee ranks the builder's own cut, not the trader's total cost; the two orderings differ materially because take rates range from ~0.2 bps to ~3.4 bps across the top of the board.
fee_usd and volume_usd cover every builder fill on record, older history included; trader_fee_usd covers only the newer, dollar-paid fills, so fee_usd / trader_fee_usd is not a take rate (it can pass 100%). Use fee_usd_node / trader_fee_usd: fee_usd_node is the builder's revenue over exactly the fills trader_fee_usd covers.
Every row carries active_days for the window. The history is still being filled in, so a long window can currently cover fewer days than it names — active_days beside the resolved window.start / window.end is how a client tells a quiet builder from an unfilled window instead of guessing.
market_share_pct is the row's routed notional over all Hyperliquid fill notional, measured over market_share_window_start → window.end — which can start LATER than window.start. Venue-wide volume exists only from the node's own retained fills, while the builder history reaches months further back via Hyperliquid's published CSVs, and that gap is permanent rather than a backlog. The share is therefore taken over the overlap; a number covering less than the requested window says so instead of quietly meaning something else.
Auditing it: divide share_volume_usd (the clamped numerator, shipped per row) by venue_volume_usd (top level). Do NOT use volume_usd — that is the row's full unclamped notional for the requested window, so on window=all it can exceed the numerator by more than 2x.
first_seen_lifetime is NOT window-scoped (first_seen is) — it is the builder's genuine first fill, which is the only one of the two that answers "how established is this front-end". Every *_pct here is percentage points, 0-100.
Parameters
| Parameter | In | Description |
|---|---|---|
window | query | 1d | 7d | 30d | all. Default 30d |
sort | query | fee | volume | users | fills. Default fee |
order | query | asc | desc. Default desc |
limit | query | Default 50, max 500 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/builders?limit=10"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/builders?limit=10",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl builders --limit 10
# any MCP client; or from the terminal:
vike-data call hl_builders --args '{"view": "leaderboard", "limit": 10}'
Builder Ecosystem Stats
- Route
GET /v1/hyperliquid/builders/stats- CLI
vike-data hl builders- MCP
hl_builders· 1 credit- Cost
- 1 request of the hourly limit
Builder-code economics for the whole venue — the summary-card surface.
Answers what no per-builder route can: how many builders are active, how many distinct traders route through one, what share of all Hyperliquid fill notional carries a builder code, and how that moved day by day. lifetime.fee_usd is all-time and unwindowed. No fixed figure or start date is quoted here: builder history is imported from Hyperliquid's published per-builder fee CSVs, which reach back to the launch of builder codes, so both move as that import proceeds. Read lifetime.first_seen for the actual coverage start. (This line previously read "$21.7M since 2026-04-05" and was 40% low and eight months late within a day.)
Declared before /builders/{builder} because Starlette matches in declaration order and compiles {builder} to [^/]+: the other way round, this path returns a 400 for a builder literally named "stats".
Parameters
| Parameter | In | Description |
|---|---|---|
window | query | 1d | 7d | 30d | all. Default 30d |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/builders/stats"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/builders/stats",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl builders --view stats
# any MCP client; or from the terminal:
vike-data call hl_builders --args '{"view": "stats"}'
Builder Profile
- Route
GET /v1/hyperliquid/builders/{builder}- CLI
vike-data hl builders- MCP
hl_builders· 1 credit- Cost
- 1 request of the hourly limit
One builder: window totals plus per-asset and per-dex breakdowns.
fee_rate_bps is the realized effective rate (fee / volume); the venue's configured per-order rate is not in the data. fee_rate_bps_p50 is the median across fills that actually paid a builder fee, which is the closest observable thing to an advertised rate — 21% of rows pay the builder nothing and drag the mean.
user_volume_distribution is the concentration answer: users split into volume deciles, so "19k users" and "one whale plus a tail" stop looking the same. profitable_users is a per-wallet test (sum(closed_pnl) > 0), not a per-fill one — a wallet that won once and lost once is one loser. lifetime is unwindowed and is what makes a 30-day window interpretable.
Parameters
| Parameter | In | Description |
|---|---|---|
builderrequired | path | Builder address |
window | query | 1d | 7d | 30d | all. Default 30d |
breakdown_limit | query | Rows per breakdown. Default 100, max 1000 |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/builders/0xb84168cf3be63c6b8dad05ff5d755e97432ff80b?window=30d&breakdown_limit=10"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/builders/0xb84168cf3be63c6b8dad05ff5d755e97432ff80b?window=30d&breakdown_limit=10",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl builders --view profile --builder 0xb84168cf3be63c6b8dad05ff5d755e97432ff80b --window 30d --breakdown-limit 10
# any MCP client; or from the terminal:
vike-data call hl_builders --args '{"view": "profile", "builder": "0xb84168cf3be63c6b8dad05ff5d755e97432ff80b", "window": "30d", "breakdown_limit": 10}'
Builder Users
- Route
GET /v1/hyperliquid/builders/{builder}/users- CLI
vike-data hl builders- MCP
hl_builders· 1 credit- Cost
- 1 request of the hourly limit
The wallets routing through one builder, ranked.
sort=pnl ranks sum(closed_pnl) — the traders' realized result, not the builder's. It is signed, and a builder whose users lose money is a different business from one whose users win.
Parameters
| Parameter | In | Description |
|---|---|---|
builderrequired | path | Builder address |
window | query | 1d | 7d | 30d | all. Default 30d |
sort | query | volume | fees | pnl. Default volume |
order | query | asc | desc. Default desc |
limit | query | Default 100, max 1000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/builders/0xb84168cf3be63c6b8dad05ff5d755e97432ff80b/users?window=30d&limit=20"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/builders/0xb84168cf3be63c6b8dad05ff5d755e97432ff80b/users?window=30d&limit=20",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl builders --view users --builder 0xb84168cf3be63c6b8dad05ff5d755e97432ff80b --window 30d --limit 20
# any MCP client; or from the terminal:
vike-data call hl_builders --args '{"view": "users", "builder": "0xb84168cf3be63c6b8dad05ff5d755e97432ff80b", "window": "30d", "limit": 20}'
Builder Fills
- Route
GET /v1/hyperliquid/builders/{builder}/fills- CLI
vike-data hl builders- MCP
hl_builders· 1 credit- Cost
- 1 request of the hourly limit
Raw builder-routed fills, newest first.
fee ships raw beside its own fee_token here, unlike every aggregate in this namespace: 0.95% of fills pay in a non-dollar token, so summing this column client-side without grouping by fee_token produces nonsense. fee_usd (the builder's slice) is dollars on every row.
Parameters
| Parameter | In | Description |
|---|---|---|
builderrequired | path | Builder address |
symbol | query | HL ticker |
side | query | buy | sell |
start | query | ISO-8601 UTC bounds |
end | query | ISO-8601 UTC bounds |
limit | query | Default 100, max 5000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/builders/0xb84168cf3be63c6b8dad05ff5d755e97432ff80b/fills?limit=50"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/builders/0xb84168cf3be63c6b8dad05ff5d755e97432ff80b/fills?limit=50",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl builders --view fills --builder 0xb84168cf3be63c6b8dad05ff5d755e97432ff80b --limit 50
# any MCP client; or from the terminal:
vike-data call hl_builders --args '{"view": "fills", "builder": "0xb84168cf3be63c6b8dad05ff5d755e97432ff80b", "limit": 50}'
Builder Series
- Route
GET /v1/hyperliquid/builders/{builder}/series- CLI
vike-data hl builders- MCP
hl_builders· 1 credit- Cost
- 1 request of the hourly limit
Fee, volume, users and fills per bucket for one builder, newest first.
users is a distinct count per bucket and is not additive — summing the daily column double-counts anyone who traded on two days. A period total comes from /builders/{builder} over that period.
Parameters
| Parameter | In | Description |
|---|---|---|
builderrequired | path | Builder address |
bucket | query | 1h | 1d. Default 1d |
start | query | ISO-8601 UTC bounds |
end | query | ISO-8601 UTC bounds |
limit | query | Default 500, max 2000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/builders/0xb84168cf3be63c6b8dad05ff5d755e97432ff80b/series?bucket=1d&limit=30"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/builders/0xb84168cf3be63c6b8dad05ff5d755e97432ff80b/series?bucket=1d&limit=30",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl builders --view series --builder 0xb84168cf3be63c6b8dad05ff5d755e97432ff80b --bucket 1d --limit 30
# any MCP client; or from the terminal:
vike-data call hl_builders --args '{"view": "series", "builder": "0xb84168cf3be63c6b8dad05ff5d755e97432ff80b", "bucket": "1d", "limit": 30}'
Vaults and HLP
Vault List
- Route
GET /v1/hyperliquid/vaults- CLI
vike-data hl vaults- MCP
hl_vaults· 1 credit- Cost
- 1 request of the hourly limit
Every Hyperliquid vault, one row per vault family: current TVL, APR, the window's PnL, return, net flow, volume and fills, and the risk columns.
HLP and its sub-vaults are ONE row (child_vaults lists them), every dollar counted once. A window ends on the newest recorded day and reaches back 7, 30, 90 or 365 days; all is all recorded history. Windows are whole UTC days, from the close of the start day to the newest reading, so a figure can differ from the venue's own rolling week or month by up to one day's movement; tvl_usd is within a few percent of the venue's own figure. The top-level window states the resolved bounds, and each row its own start and partial: a vault younger than the window starts later. A family is measured from the first day inside the window on which all its open vaults are recorded (its start; partial true when that is later than the window's), every vault over that same span; if one of them has no record that day, pnl_usd, return_pct and apr_pct are null. A family's flows are the deposits into and withdrawals from its parent only: a sub-vault's own deposits and withdrawals are the parent moving money between its vaults, and pnl_usd nets them out. A closed sub-vault holds nothing and never holds a family's figures back.
pnl_usd is the equity change minus net deposits, null until a day after start is recorded (as return_pct is). return_pct is the chained share-price return in percentage points over the days with a measurable share-price step; days without one are skipped, and coverage (a fraction) says how much of the window was measured. It is null below 95% coverage, and null when a rebuilt day before 2026-10-27 holds a share-price jump of more than 2x either way that is not one of a pair (a jump and an equally large reversal on the next day, with no other such jump beside them, are both skipped). apr_pct annualises the vault's own 30-day return and is null for a vault, or a family, not recorded for all 30 days. net_flow_usd counts withdrawals and distributions as outflow. leverage is gross open notional over equity, leader_share_pct the leader's own share and top5_share_pct the five largest depositors' share, in percentage points, from the newest hourly reading (null for a vault not read in the last day, e.g. a closed one).
Parameters
| Parameter | In | Description |
|---|---|---|
window | query | 7d | 30d | 90d | 365d | all, whole UTC days ending on the newest recorded day. Default 30d |
sort | query | tvl | apr | pnl | return | net_flow | volume | age | leverage | top5_share. Default tvl |
order | query | asc | desc. Default desc |
status | query | open | closed | all. Default open |
deposits | query | any | open | closed: whether the vault accepts deposits. Default any |
min_tvl | query | Minimum current TVL in USD. Default 0 |
q | query | Search, 2 to 64 characters: a case-insensitive part of the vault name, or an address prefix when it starts with 0x (a sub-vault's address finds its family). Matched literally: % and _ are not wildcards |
limit | query | Default 50, max 500 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/vaults?sort=tvl&limit=10"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/vaults?sort=tvl&limit=10",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl vaults --sort tvl --limit 10
# any MCP client; or from the terminal:
vike-data call hl_vaults --args '{"view": "list", "sort": "tvl", "limit": 10}'
Vault Stats
- Route
GET /v1/hyperliquid/vaults/stats- CLI
vike-data hl vaults- MCP
hl_vaults· 1 credit- Cost
- 1 request of the hourly limit
Vault platform totals: TVL with every vault family counted once, flows over the window, vaults created per month.
user_vault_tvl_usd is the TVL outside the HLP family and hlp_net_flow_usd HLP's own net flow. withdrawals_usd includes distributions (a vault paying its depositors out), as net_flow_usd does. HLP moving money between its own sub-vaults is not a flow. vaults and open_vaults count vault families. With no recorded history the money totals and window are null.
Parameters
| Parameter | In | Description |
|---|---|---|
window | query | 7d | 30d | 90d | 365d | all, whole UTC days ending on the newest recorded day. Default 30d |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/vaults/stats?window=30d"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/vaults/stats?window=30d",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl vaults --view stats --window 30d
# any MCP client; or from the terminal:
vike-data call hl_vaults --args '{"view": "stats", "window": "30d"}'
Vault Flows Feed
- Route
GET /v1/hyperliquid/vaults/flows- CLI
vike-data hl vaults- MCP
hl_vaults· 1 credit- Cost
- 1 request of the hourly limit
Deposits into and withdrawals from every vault, newest first, from the depositor's side.
Each row names the depositor, the vault and direction (in | out; amount_usd is unsigned). pnl_rank is the depositor's rank in the top 1,000 of /v1/hyperliquid/leaderboard?window=all, else null. vault is null on rows recorded before 2026-10-25 06:31 UTC.
Parameters
| Parameter | In | Description |
|---|---|---|
window | query | 7d | 30d | 90d | 365d | all, whole UTC days ending on the newest recorded day. Default 7d |
min_usd | query | Smallest flow in USD. Default 10000 |
type | query | all | deposit | withdraw | distribution | commission. Default all |
smart_only | query | Only depositors in the top 1,000 of /v1/hyperliquid/leaderboard?window=all. Default false |
limit | query | Default 100, max 1000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/vaults/flows?window=7d&min_usd=100000"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/vaults/flows?window=7d&min_usd=100000",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl vaults --view flows --window 7d --min-usd 100000
# any MCP client; or from the terminal:
vike-data call hl_vaults --args '{"view": "flows", "window": "7d", "min_usd": 100000}'
Vault Profile
- Route
GET /v1/hyperliquid/vaults/{vault}- CLI
vike-data hl vault- MCP
hl_vault· 1 credit- Cost
- 1 request of the hourly limit
One vault: registry fields, current TVL, APR, leverage, depositor concentration and all five windows.
windows holds 7d, 30d, 90d, 365d and all, each with start, end, partial, pnl_usd, return_pct, net_flow_usd, volume_usd, fills and coverage as on /vaults. leverage is open notional over equity; leader_share_pct and top5_share_pct are the leader's and the five largest depositors' shares in percentage points; active_depositors counts depositors holding at least $1. A parent vault's figures include its sub-vaults (child_vaults). With no recorded history windows, tvl_usd and apr_pct are null; an unknown address is a 404.
The vault's fills, trades and positions are served by /v1/hyperliquid/wallets/{address}/… with the vault's address.
Parameters
| Parameter | In | Description |
|---|---|---|
vaultrequired | path | Vault address |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/vaults/0xdfc24b077bc1425ad1dea75bcb6f8158e10df303"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/vaults/0xdfc24b077bc1425ad1dea75bcb6f8158e10df303",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl vault 0xdfc24b077bc1425ad1dea75bcb6f8158e10df303
# any MCP client; or from the terminal:
vike-data call hl_vault --args '{"view": "profile", "vault": "0xdfc24b077bc1425ad1dea75bcb6f8158e10df303"}'
Vault Depositors
- Route
GET /v1/hyperliquid/vaults/{vault}/depositors- CLI
vike-data hl vault- MCP
hl_vault· 1 credit- Cost
- 1 request of the hourly limit
A vault's current depositors, largest share first.
share_pct is the depositor's share of the vault in percentage points and equity_usd its value at as_of; a parent vault's depositors own shares of the parent and its sub-vaults together, so the top-level equity_usd is theirs combined. pnl_usd is that value minus the cost basis of the depositor's current position (unrealised, as the venue reports it), all_time_pnl_usd that value minus everything the depositor ever put in net of what it took out. entered_at is the depositor's entry into the vault and lockup_until the end of the lockup after its last deposit (null when the vault has no lockup). pnl_rank is the depositor's rank in the top 1,000 of /v1/hyperliquid/leaderboard?window=all, else null. The list is complete: every depositor holding at least min_equity_usd, not only the largest 100; a wallet that has left the vault is never listed.
Parameters
| Parameter | In | Description |
|---|---|---|
vaultrequired | path | Vault address |
sort | query | share | pnl | all_time_pnl | entered. Default share |
order | query | asc | desc. Default desc |
smart_only | query | Only depositors in the top 1,000 of /v1/hyperliquid/leaderboard?window=all. Default false |
min_equity_usd | query | Smallest depositor equity in USD, at least 1. Default 1 |
limit | query | Default 100, max 1000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/vaults/0xdfc24b077bc1425ad1dea75bcb6f8158e10df303/depositors?limit=20"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/vaults/0xdfc24b077bc1425ad1dea75bcb6f8158e10df303/depositors?limit=20",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl vault 0xdfc24b077bc1425ad1dea75bcb6f8158e10df303 --view depositors --limit 20
# any MCP client; or from the terminal:
vike-data call hl_vault --args '{"view": "depositors", "vault": "0xdfc24b077bc1425ad1dea75bcb6f8158e10df303", "limit": 20}'
Vault Flows
- Route
GET /v1/hyperliquid/vaults/{vault}/flows- CLI
vike-data hl vault- MCP
hl_vault· 1 credit- Cost
- 1 request of the hourly limit
One vault's own deposits, withdrawals, distributions and leader commissions, newest first.
counterparty is the depositor on deposits and withdrawals (on a deposit it is read from the depositor's own row of the same event; null when that row is missing or ambiguous). amount_usd is unsigned and direction is in or out. On a parent vault (HLP), the parent moving money into or out of its own sub-vaults is not a row.
Parameters
| Parameter | In | Description |
|---|---|---|
vaultrequired | path | Vault address |
window | query | 7d | 30d | 90d | 365d | all, whole UTC days ending on the newest recorded day. Default 30d |
type | query | all | deposit | withdraw | distribution | commission. Default all |
limit | query | Default 100, max 1000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/vaults/0xdfc24b077bc1425ad1dea75bcb6f8158e10df303/flows?window=30d&limit=20"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/vaults/0xdfc24b077bc1425ad1dea75bcb6f8158e10df303/flows?window=30d&limit=20",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl vault 0xdfc24b077bc1425ad1dea75bcb6f8158e10df303 --view flows --window 30d --limit 20
# any MCP client; or from the terminal:
vike-data call hl_vault --args '{"view": "flows", "vault": "0xdfc24b077bc1425ad1dea75bcb6f8158e10df303", "window": "30d", "limit": 20}'
Vault Series
- Route
GET /v1/hyperliquid/vaults/{vault}/series- CLI
vike-data hl vault- MCP
hl_vault· 1 credit- Cost
- 1 request of the hourly limit
Equity, share price, net flow and PnL of one vault per hour or day, oldest first.
nav is the chained share price, 1.0 at the first point of the first page; a missing step ends the chain (nav null from there), it is never read as 1.0. pnl_usd is the equity change minus the point's net flow. The cursor carries the previous point, so nav and pnl_usd continue across pages. A parent vault's (HLP's) series is its family's: equity_usd sums the parent and the sub-vaults read at that hour, or for a day at the parent's last reading of it (a closed one while it existed, a new one from its first reading), and net_flow_usd is the parent's depositors' deposits and withdrawals, so money moved between the parent and its sub-vaults is not PnL.
Parameters
| Parameter | In | Description |
|---|---|---|
vaultrequired | path | Vault address |
bucket | query | 1d | 1h. Default 1d |
start | query | ISO-8601 UTC bounds |
end | query | ISO-8601 UTC bounds |
limit | query | Default 500, max 5000 |
cursor | query | Keyset cursor |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/vaults/0xdfc24b077bc1425ad1dea75bcb6f8158e10df303/series?bucket=1d&limit=30"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/vaults/0xdfc24b077bc1425ad1dea75bcb6f8158e10df303/series?bucket=1d&limit=30",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl vault 0xdfc24b077bc1425ad1dea75bcb6f8158e10df303 --view series --bucket 1d --limit 30
# any MCP client; or from the terminal:
vike-data call hl_vault --args '{"view": "series", "vault": "0xdfc24b077bc1425ad1dea75bcb6f8158e10df303", "bucket": "1d", "limit": 30}'
Candles and the Parquet archive
OHLCV
- Route
GET /v1/hyperliquid/ohlcv- CLI
vike-data candles hl- MCP
hl_ohlcv· 1 credit- Cost
- 1 request of the hourly limit
Hyperliquid candles, derived from executed fills.
Parameters
| Parameter | In | Description |
|---|---|---|
symbolrequired | query | Perp ticker (BTC), spot token (HYPE) or raw index (@107) |
interval | query | 1m | 5m | 15m | 30m | 1h | 4h | 1d, or Nt for N-trade bars. Default 1h |
market | query | perp | spot. Default perp |
dex | query | HL core or a HIP-3 perp DEX, e.g. XYZ. Default HL. Builder DEXs are added over time, so do not hard-code a list |
start | query | ISO-8601 UTC or epoch ms, inclusive |
end | query | ISO-8601 UTC or epoch ms, exclusive |
limit | query | Default 500, clamped to 5000 |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/ohlcv?symbol=BTC&interval=1h&market=perp&limit=48"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/ohlcv?symbol=BTC&interval=1h&market=perp&limit=48",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data candles hl BTC --interval 1h --market perp --limit 48
# any MCP client; or from the terminal:
vike-data call hl_ohlcv --args '{"view": "candles", "symbol": "BTC", "interval": "1h", "market": "perp", "limit": 48}'
OHLCV Latest
- Route
GET /v1/hyperliquid/ohlcv/latest- CLI
vike-data candles hl- MCP
hl_ohlcv· 1 credit- Cost
- 1 request of the hourly limit
The newest 1-minute candle of every symbol that traded in the last hour, in one request.
Parameters
| Parameter | In | Description |
|---|---|---|
market | query | perp | spot. Default perp |
dex | query | HL core or a HIP-3 perp DEX, e.g. XYZ. Omit for every DEX; each row names its own |
interval | query | Only 1m, the default. Any other value is a 400 |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/ohlcv/latest"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/ohlcv/latest",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data candles hl --view latest
# any MCP client; or from the terminal:
vike-data call hl_ohlcv --args '{"view": "latest"}'
OHLCV Export
- Route
GET /v1/hyperliquid/ohlcv/export- CLI
vike-data candles hl- MCP
hl_ohlcv· 1 credit- Cost
- 2 requests of the hourly limit
Stream a window as NDJSON. Billed 2 credits up front, because the response is a stream and its row count is unknown when the usage is recorded.
Parameters
| Parameter | In | Description |
|---|---|---|
symbolrequired | query | As on /ohlcv |
interval | query | As on /ohlcv. Default 1h |
market | query | As on /ohlcv. Default perp |
dex | query | As on /ohlcv. Default HL |
start | query | As on /ohlcv |
end | query | As on /ohlcv |
limit | query | Defaults to the maximum, 5000 |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/ohlcv/export?symbol=BTC&interval=1m&limit=1000"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/ohlcv/export?symbol=BTC&interval=1m&limit=1000",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data candles hl BTC --view export --interval 1m --limit 1000
# any MCP client; or from the terminal:
vike-data call hl_ohlcv --args '{"view": "export", "symbol": "BTC", "interval": "1m", "limit": 1000}'
Archive file index
- Route
GET /v1/hyperliquid/archive/datasets- CLI
vike-data hl archive- MCP
hl_archive· 1 credit- Cost
- 1 request of the hourly limit
Index of the Hyperliquid Parquet file archive: which UTC days exist and what is in each file.
One Parquet file per UTC day for fills (every fill) and positions (every 5-minute snapshot of every open position), from 2026-05-01. dates is newest first, each {"date": "YYYY-MM-DD", "streams": {"fills": {"rows", "bytes"}, "positions": {"rows", "bytes", "snapshots"}}}; snapshots is how many distinct 5-minute snapshots that day holds. schema lists every column of both datasets with its type and meaning. A day appears once it is complete (yesterday's files are published in the early UTC morning).
latest.positions is ONE file with the current open positions of every asset, replaced every 5 minutes (snapshot_ts, generated_at, rows, bytes, assets, note).
No daily or latest file has a direct URL and the folder cannot be listed: url_endpoint (/v1/hyperliquid/archive/url) answers with a 7-day link for exactly one file. The one exception is the free samples: samples_base is their public folder, and its files (manifest.json names them) are read with a plain GET or HEAD and no API key.
Parameters
None.
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/archive/datasets"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/archive/datasets",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl archive
# any MCP client; or from the terminal:
vike-data call hl_archive --args '{"view": "datasets"}'
Download link for one archive file
- Route
GET /v1/hyperliquid/archive/url- CLI
vike-data hl archive- MCP
hl_archive· 1 credit- Cost
- 1 request of the hourly limit
A download link to ONE archive file, valid 7 days.
{"url", "expires_at", "expires_in", "dataset", "date", "bytes", "rows"}. The url is a presigned Cloudflare R2 link (host <account>.r2.cloudflarestorage.com): it needs no API key (the link is the credential), names exactly this file, and works for expires_in seconds (604800, 7 days) for any number of requests, so a reader that fetches byte ranges over HTTP (DuckDB, pyarrow) can use it directly. Once it expires, R2 answers 403: ask again. The call costs one credit and one request of the hourly budget like any route; the download itself spends none, and there is no download quota.
Parameters
| Parameter | In | Description |
|---|---|---|
datasetrequired | query | fills or positions. Anything else returns 400 naming both. |
daterequired | query | YYYY-MM-DD of a published UTC day, or latest (positions only: the file of the current open positions, replaced every 5 minutes). A day that is not published returns 400 naming the range that is. |
redirect | query | 1 or true answers 302 with the link in Location; the JSON body is still sent, so a client that does not follow redirects keeps the answer. |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/archive/url?dataset=positions&date=latest"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/archive/url?dataset=positions&date=latest",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl archive --view url --dataset positions --date latest
# any MCP client; or from the terminal:
vike-data call hl_archive --args '{"view": "url", "dataset": "positions", "date": "latest"}'
Stream catch-up
Stream catch-up
- Route
GET /v1/hyperliquid/stream-events- CLI
- —
- MCP
stream_events· 1 credit- Cost
- 1 request of the hourly limit
Stored events of one live-stream channel after a cursor, oldest first: the REST catch-up of wss://stream.vike.io/v1/stream/hl, for what a client missed while it was disconnected (events are kept 3 days). Pass the channel's filters as query parameters, as on the socket: a list as a comma list (wallet=0x..,0x..), a number as a number (min_usd=1000); an unknown filter, or one the channel does not take, is a 400 naming it.
Channels and their filters:
- hl.candles: coin, dex, interval, market - hl.closed_trades: coin, direction, min_usd, pnl_cohort, size_cohort, wallet - hl.fills: action, coin, min_usd, pnl_cohort, side, size_cohort, wallet (no min_usd means min_usd=1000, as on the socket) - hl.funding: coin - hl.ledger: event_type, min_usd, pnl_cohort, size_cohort, wallet - hl.leverage: coin, direction, min_usd, pnl_cohort, size_cohort, wallet - hl.liq_risk: coin, direction, min_usd, pnl_cohort, size_cohort, wallet - hl.liquidations: coin, direction, min_usd, pnl_cohort, size_cohort, wallet - hl.order_walls: coin, event, min_usd, pnl_cohort, side, size_cohort, wallet - hl.positions: action, coin, min_leverage, min_usd, pnl_cohort, side, size_cohort, tag, wallet - hl.top_positions: coin, direction, event, min_usd, pnl_cohort, size_cohort, wallet - hl.twaps: coin, min_usd, pnl_cohort, side, size_cohort, status, wallet
Returns {events: [{cursor, event}], next_cursor, has_more}. event is exactly what the socket sent, so dedupe on event.event_id. Pass next_cursor back as cursor while has_more is true. A window in which the channel was idle (nobody subscribed) comes as an item {cursor, gap} in its place. A cursor older than the retention is a 410 naming the oldest cursor accepted; omit cursor to start there.
The HL Cohorts channels have their own catch-up: /v1/hl-cohorts/stream-events.
Live, the same events arrive on the socket: vike-data stream hl --channel hl.liquidations. Channels, filters and real events: WebSocket.
Parameters
| Parameter | In | Description |
|---|---|---|
channelrequired | query | hl.candles, hl.closed_trades, hl.fills, hl.funding, hl.ledger, hl.leverage, hl.liq_risk, hl.liquidations, hl.order_walls, hl.positions, hl.top_positions, hl.twaps |
cursor | query | next_cursor of the previous page or the cursor of a stream frame. Empty: the oldest retained event |
limit | query | Default 200, clamped to 1-1000 |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/stream-events?channel=hl.liquidations&limit=50"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hyperliquid/stream-events?channel=hl.liquidations&limit=50",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
# any MCP client; or from the terminal:
vike-data call stream_events --args '{"channel": "hl.liquidations", "limit": 50}'
Hyperliquid alerts by e-mail or webhook
No code: make one on your account page, ask your AI (alerts_create), or run the CLI. A webhook gets a signed POST.
A wallet opens, closes or flips
- Type
hl_wallet
hl_wallet = every fill of wallet (required), narrowed by actions, token_symbol (the coin) and min_value_usd.
Create it on your account, no code →
{
"label": "Whale fills",
"alert_type": "hl_wallet",
"wallet": "0x25773676902085225ae5751075e4161369334897",
"actions": [
"open",
"close",
"flip"
],
"channels": [
"webhook"
]
}
vike-data alerts create --type hl_wallet --wallet 0x25773676902085225ae5751075e4161369334897 --actions open close flip --channel webhook
vike-data call alerts_create --args '{"label": "Whale fills", "alert_type": "hl_wallet", "wallet": "0x25773676902085225ae5751075e4161369334897", "actions": ["open", "close", "flip"], "channels": ["webhook"]}'
Whales fill big
- Type
hl_cohort_fill
hl_cohort_fill = every fill by wallets in pnl_cohorts / size_cohorts (at least one cohort is required, or it would match every large fill on Hyperliquid), requires min_value_usd >= 10000.
Create it on your account, no code →
{
"label": "Whale fills over $1M",
"alert_type": "hl_cohort_fill",
"size_cohorts": [
"4x whale"
],
"min_value_usd": 1000000,
"channels": [
"email"
]
}
vike-data alerts create --type hl_cohort_fill --size-cohorts "4x whale" --min-usd 1000000 --channel email
vike-data call alerts_create --args '{"label": "Whale fills over $1M", "alert_type": "hl_cohort_fill", "size_cohorts": ["4x whale"], "min_value_usd": 1000000, "channels": ["email"]}'
A big liquidation
- Type
hl_liquidation
hl_liquidation = every liquidation, optionally of one wallet, of cohorts, of one coin, above a min_value_usd floor.
Create it on your account, no code →
{
"label": "BTC liquidations over $1M",
"alert_type": "hl_liquidation",
"token_symbol": "BTC",
"min_value_usd": 1000000,
"channels": [
"webhook"
]
}
vike-data alerts create --type hl_liquidation --token BTC --min-usd 1000000 --channel webhook
vike-data call alerts_create --args '{"label": "BTC liquidations over $1M", "alert_type": "hl_liquidation", "token_symbol": "BTC", "min_value_usd": 1000000, "channels": ["webhook"]}'
A wallet nears liquidation
- Type
hl_liq_proximity
hl_liq_proximity = wallet (required) getting threshold of the way from entry to liquidation price.
Create it on your account, no code →
{
"label": "Whale near liquidation",
"alert_type": "hl_liq_proximity",
"wallet": "0x25773676902085225ae5751075e4161369334897",
"threshold": 0.8,
"channels": [
"email"
]
}
vike-data alerts create --type hl_liq_proximity --wallet 0x25773676902085225ae5751075e4161369334897 --threshold 0.8 --channel email
vike-data call alerts_create --args '{"label": "Whale near liquidation", "alert_type": "hl_liq_proximity", "wallet": "0x25773676902085225ae5751075e4161369334897", "threshold": 0.8, "channels": ["email"]}'
hl_cohort_bias =the cohort index of token_symbol (required) crossing threshold by band, filtered by direction. HL Cohorts →Fill and liquidation alerts notify on every event; a wallet alert once per coin, action and side of each evaluator pass (about every 10 s): the fills merged into one event, fills their count, min_value_usd applied to the merged total. Bias and proximity alerts notify once per crossing and re-arm after the value retreats by band.
Webhooks and the alert feed
Register a webhook once and point any alert at it: the body, verifying the signature, testing it, retries, and the feed every alert fills.
alerts_list_channels
- Cost
- 0 credits a call
Your enabled notification channels. No parameters.
Returns {"channels": [...]} with {id, channel_type, is_verified, label} per row. channel_type is email or webhook; a Discord connection is not listed, because Discord alerts are off. A webhook is is_verified: true once a delivery to it got a 2xx answer: an alert, or a test (at registration, or with alerts_test_webhook); webhooks registered before this check existed (early November 2026) show true regardless. An unverified webhook still receives its alerts.
Call it before creating an alert. A transfer alert that names a channel you have not set up is still created, and because it then has no channel of its own its notifications go to your account's email address. A Hyperliquid or Polymarket alert that names a webhook you have not registered is refused, so a rule is never left silent by accident.
alerts_register_webhook
- Cost
- 2 credits a call
Registers an HTTPS webhook as a delivery channel. Vike POSTs signed JSON to the URL when your alerts fire, and once right away: a signed test event, whose result comes back in the answer.
| Parameter | Type | Default | Notes |
|---|---|---|---|
url | string | required | Webhook URL, https:// only, reachable from Vike's servers |
secret | string | — | Your own HMAC secret, 16 characters or more. Leave it out and Vike generates one |
Returns {"ok": true, "channel_id": ..., "channel_type": "webhook", "url_preview": "...", "signed": true, "test": {...}}, plus secret and secret_note when Vike generated the secret.
- The secret can be shown again. A generated
secretis in this answer, and you can show it again at any time on Account → Alerts → Channels (the Show secret button), so losing it does not require a rotation.alerts_rotate_webhook_secretissues a new one when you want one. A secret you passed yourself is not repeated back in this answer. - The test send. Right after the channel is stored, Vike POSTs one
testevent to the URL, signed and built like a real delivery:alert_id0,alert_labelWebhook test,alert_typeandevent.kindtest, and anevent.event_idoftest:and 32 hex characters, new for every test.testis{"ok": true, "status": 200}for a 2xx answer,{"ok": false, "status": 401}for any other answer, or{"ok": false, "error": "timeout"}(alsonetwork,ssrf_refused, …) when none came. A failing test does not refuse the channel. Because the test is signed with the new secret, a receiver that checks signatures answers 401 or 403 until it has it: install the secret, then callalerts_test_webhook. The whole call, address check and test included, takes at most about 20 seconds. https://only for a new URL. A webhook registered overhttp://before keeps working.- On the MCP server and over REST (
POST /v1/alerts/channels/webhooks) only. The vike.io Ask AI chat does not register, rotate or test webhooks: a signing secret must not pass through a chat that stores the conversation.
The channel is yours and works for every rule you point at it.
The body
Every rule type sends one envelope: schema (vike.alert.v1), alert_id, alert_label, alert_type, triggered_at and event, the event that matched. A transfer also carries the flat keys the first webhook format had (chain, token, value_usd, from_address, to_address, tx_hash), so an old receiver keeps working. The envelope, a complete example and the shape of every event kind are on Alert feed and webhooks.
Verifying a delivery
The signing is Stripe-style. Each POST carries:
Content-Type: application/json
User-Agent: vike-alerts/1.0
X-Vike-Timestamp: <unix epoch seconds>
X-Vike-Signature: t=<ts>,sig=<hex hmac>[,sig=<hex hmac>]
X-Vike-Event-Id: <event_id>
X-Vike-Attempt: <1 for the first try>
X-Vike-Delivery-Id: <this delivery's id, the same on every try>
Compute HMAC-SHA256(secret, "<t>.<raw body>") with the t= of the header (the same value as X-Vike-Timestamp), hex-encode it, and compare it in constant time against every sig= value: accept the delivery if any of them matches. The header carries one sig= per valid secret: one normally, two for the 24 hours after a secret rotation (the new secret's first). A receiver that compares only one value rejects deliveries during those 24 hours. Check the raw bytes you received, before any JSON parser has touched them. Reject the delivery if the timestamp is more than about five minutes old — that is what stops a captured request being replayed.
Verify a delivery
import hashlib
import hmac
import time
def verify(secret: str, raw_body: bytes, signature_header: str, tolerance_s: int = 300) -> bool:
"""raw_body is the request body bytes exactly as received (do not re-serialise the JSON).
Hostile input (a non-ASCII sig, `t=²`, 5000 digits, a lone surrogate, no header) is a False, never an exception."""
try:
parts = [p.split("=", 1) for p in signature_header.split(",") if "=" in p]
ts = next((v for k, v in parts if k == "t"), "")
if not ts.isdecimal() or len(ts) > 12 or abs(time.time() - int(ts)) > tolerance_s: # replay window
return False
want = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest().encode()
return any(hmac.compare_digest(want, v.encode()) for k, v in parts if k == "sig") # ANY sig= may match
except (ValueError, TypeError, AttributeError, OverflowError): # UnicodeError is a ValueError
return False
Every webhook registered now has a secret. A channel registered earlier without one is signed with an empty key, which proves nothing: give it a secret with alerts_rotate_webhook_secret.
Test webhooks on your own computer
To try a receiver before it is deployed, run it on localhost and expose it with a tunnel such as ngrok or cloudflared. Register the tunnel's https:// address as a webhook with alerts_register_webhook, and deliveries arrive on your machine. A tunnel's address is public for as long as it runs, so when you are done remove the channel under Account → Alerts → Channels, or rotate its secret.
Or skip the tunnel and send yourself the events you already have. This script reads your own alert feed, GET /v1/alerts/events, and POSTs each event to a local URL with the body and the headers of a real delivery, signed with your webhook's secret, so the verifier above accepts it. Set VIKE_API_KEY and VIKE_WEBHOOK_SECRET; TARGET_URL is http://localhost:8000/webhook unless you set it.
Forward your alerts to localhost
import hashlib
import hmac
import json
import os
import sys
import time
from datetime import datetime
import requests
API = os.environ.get("VIKE_API_BASE", "https://data.vike.io")
KEY, SECRET = os.environ["VIKE_API_KEY"], os.environ["VIKE_WEBHOOK_SECRET"]
TARGET = os.environ.get("TARGET_URL", "http://localhost:8000/webhook")
STATE, EVERY = "vike_forward.cursor", 5 # the cursor file; seconds between polls (720 requests an hour)
def delivery(row):
"""The body and the headers a real webhook delivery of this feed row carries."""
ev = row["event"]
body = {"schema": "vike.alert.v1", "alert_id": row["rule_id"], "alert_type": row["alert_type"], "event": ev,
"alert_label": row["alert_label"] or f"alert #{row['rule_id']}",
"triggered_at": datetime.fromisoformat(ev["ts"].replace("Z", "+00:00")).isoformat()}
if ev["kind"] == "transfer": # the flat keys of the earlier webhook format
who, data = ev["subject"], ev["data"]
bare = lambda v: v.lower().removeprefix("0x") if isinstance(v, str) else v
usd = data.get("value_usd")
body.update(chain=ev["chain"], token=who.get("token_symbol") or "", value_usd=None if usd is None else float(usd),
from_address=bare(who.get("from")), to_address=bare(who.get("to")),
tx_hash=bare(who.get("tx_hash") or data.get("tx_hash")))
raw = json.dumps(body, sort_keys=True, separators=(",", ":")).encode()
ts = int(time.time())
sig = hmac.new(SECRET.encode(), f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
hook = next((d["id"] for d in row["delivery"] if d["channel"] == "webhook"), 0)
return raw, {"Content-Type": "application/json", "User-Agent": "vike-alerts/1.0", "X-Vike-Timestamp": str(ts),
"X-Vike-Signature": f"t={ts},sig={sig}", "X-Vike-Event-Id": ev["event_id"],
"X-Vike-Attempt": "1", "X-Vike-Delivery-Id": str(hook)}
cursor = open(STATE).read().strip() if os.path.exists(STATE) else ""
sent = set()
while True:
try:
more = True
while more: # a page that says there is more is read at once
r = requests.get(f"{API}/v1/alerts/events", headers={"X-API-Key": KEY},
params={"cursor": cursor, "limit": 500}, timeout=20)
r.raise_for_status()
page = r.json()
for row in page["events"]:
if row["id"] not in sent:
raw, headers = delivery(row)
status = requests.post(TARGET, data=raw, headers=headers, timeout=20).status_code
sent.add(row["id"])
print(row["event"]["event_id"], status, flush=True)
cursor, more = page["next_cursor"], page["has_more"]
open(STATE, "w").write(cursor)
except (requests.RequestException, KeyError, ValueError) as e:
print("error", type(e).__name__, file=sys.stderr, flush=True) # the next poll tries again
time.sleep(EVERY)
- Steady and inside the limits. It asks the feed every 5 seconds: 720 requests an hour and 12 a minute, against the 20,000 an hour and 200 a minute the API publishes. Each call is one credit.
- It keeps its cursor in
vike_forward.cursor, so a restart continues where it stopped. The first run forwards everything the feed still holds, up to 15 days; delete the file to forward it all again. Within one run nothing is forwarded twice. After a restart the page it was on can be sent again, which a receiver that dedupes onalert_idandevent.event_id(see below) drops. - It prints each forwarded event's id and the status your receiver answered, and nothing else; a failed call prints only the error's class name. The key and the secret are read from the environment and never printed.
- Two headers are filled in as best they can be.
X-Vike-Delivery-Idis the id of the event's webhook delivery in the feed (0when the rule has no webhook channel), andX-Vike-Attemptis always1.
Retries and duplicates
- Answer with any 2xx within 20 seconds. That is one budget for the whole try: the address lookup, the connection, TLS, the request and your response's status line. After it the try is a
timeout. A redirect is not followed and counts as a failure. - One delivery at a time per webhook. Vike never has two POSTs open to the same channel, and a slow or hanging endpoint delays only its own deliveries, never another channel's.
- A failed delivery is tried again after 30 seconds, then after twice as long each time, up to an hour between tries, and given up on after 72 hours. A webhook that keeps failing is paused for a growing interval, at most 5 minutes, so one dead URL does not slow your other channels, and after three failures in a row Vike emails you once a day about it.
- A
429with aRetry-Afterheader (seconds or an HTTP date) pauses that webhook for at least that long; the next try is never sooner, and never later than the 72 hours. - Delivery is at least once. A send that was cut off can arrive twice, so dedupe on the rule and the event:
alert_idandevent.event_idfrom the body (one event can match two of your rules, and each sends its own delivery). That pair is the replay-safe key, because the body is what the signature covers.X-Vike-Delivery-Ididentifies the delivery too, but no header is signed: a captured request replayed inside the 5-minute window with a changed header still verifies, so dedupe on the header only alongside the body pair. A delivery sent again withalerts_resend_deliverykeeps itsX-Vike-Delivery-Idand counts on inX-Vike-Attempt. Events are not guaranteed to arrive in the order they happened. - Every delivery to a webhook, with its status, tries, last error and a Send again button, is listed under Account → Alerts → Webhook log for 15 days.
alerts_test_webhook
- Cost
- 1 credit a call
Sends the signed test event to one of your webhook channels again — for example after your receiver installed a new secret.
| Parameter | Type | Default | Notes |
|---|---|---|---|
channel_id | integer | required | The webhook channel, its id from alerts_list_channels |
Returns {"ok": true, "channel_id": ..., "test": {...}}, with test as at registration: {"ok", "status"} when the endpoint answered (ok for a 2xx), else {"ok": false, "error": ...}. The event is signed like a real delivery, with the current secret and, inside a rotation's 24 hours, the previous one too. Its X-Vike-Delivery-Id is its test: event id, new for every test. Nothing is queued, retried or stored. The same webhook channel not found as for a rotation.
alerts_rotate_webhook_secret
- Cost
- 2 credits a call
Issues a new signing secret for one of your webhook channels.
| Parameter | Type | Default | Notes |
|---|---|---|---|
channel_id | integer | required | The webhook channel, its id from alerts_list_channels |
Returns {"ok": true, "channel_id": ..., "secret": "...", "secret_note": "...", "previous_secret_valid_until": "..."}. The new secret is in this answer, like a generated one at registration, and can be shown again on Account → Alerts → Channels (Show secret).
For 24 hours after a rotation the old secret keeps signing as well: every delivery carries two sig= values, the new secret's first, until previous_secret_valid_until. A receiver that accepts a delivery when any sig= matches (the verifier above) keeps working while you swap the secret; one that compares a single value rejects deliveries for those 24 hours. Rotating again inside them ends the first overlap. A channel that had no secret rotates without an overlap (previous_secret_valid_until is null, one sig=). Install the new secret, then check it with alerts_test_webhook.
Another user's channel, an email channel and an id that does not exist all answer webhook channel not found.
alerts_events
- Cost
- 1 credit a call
Reads the events your alert rules have produced, oldest first. To follow the feed, pass the returned next_cursor back as cursor.
| Parameter | Type | Default | Notes |
|---|---|---|---|
cursor | string | — | The previous page's next_cursor, passed back unchanged. Omit it for the oldest retained event |
limit | integer | 100 | Events per page, 1 to 500 |
rule_id | integer | — | Only the events of this alert (its id from alerts_list). It must be a positive id; anything else is an error rather than a read of every rule |
Returns {"events": [...], "next_cursor": "...", "has_more": false}. Each event carries id, rule_id, alert_label, alert_type, created_at, event (what happened) and delivery: one item per channel it went to, with the delivery's id (what alerts_resend_delivery takes), channel, channel_key, status, attempts, last_attempt_at, next_attempt_at (while it waits) and last_error as a code: http 500, timeout, network, tls, blocked, expired or error. An empty page returns the same cursor, so a poller keeps it and asks again. An event is in the feed as soon as it is recorded, before its email or webhook goes out. Events and their deliveries are kept for 15 days, and only your own alerts appear.
It is the same feed as GET /v1/alerts/events; the fields, the paging rules and every event shape are on Alert feed and webhooks.
alerts_resend_delivery
- Cost
- 1 credit a call
Sends one of your deliveries again: the webhook or email of an event that failed, expired, or was lost on your side.
| Parameter | Type | Default | Notes |
|---|---|---|---|
delivery_id | integer | required | The delivery's id, from an event's delivery list in alerts_events |
Returns {"id": ..., "status": "retry", "result": "queued"}. result is queued (it will be tried at once), already_queued (it was already waiting for a try; nothing changed) or sending (a try is under way right now).
- Any status, up to 15 days after the event. A delivery that had already closed (sent, expired, skipped) gets a fresh 72 hours of retries.
- It goes out with the same event, the same
X-Vike-Delivery-Id, a new signature and the nextX-Vike-Attempt, and is then retried like any delivery. queuedis a request, not a promise: the sender checks the rule and the channel again, so a delivery whose rule is paused or whose channel is disabled or deleted is skipped again, and a paused webhook delays the try.- An error, and no credit, for a delivery that is not yours or does not exist (the same answer for both), one whose event is older than 15 days (and, once the prune removed it, the same "does not exist"), and an
unsupportedone (its channel cannot carry that kind of event, so it would fail the same way). - Over REST it is
POST /v1/alerts/deliveries/{id}/resend, and on the website the Send again button of Account → Alerts → Webhook log.