Get a free key
HL Cohorts · AI & API

Hyperliquid cohort data for AI agents: MCP, API, WebSocket and alerts

Connect Claude, ChatGPT, Cursor or your own agent to Hyperliquid cohort data and ask in plain words who is long and who is short: 2 MCP tools of its own and cohort filters on five more, 11 REST routes, 4 live WebSocket channels, the vike-data CLI and cohort alerts, on one free key; in Claude Desktop no key at all.

Ask your AI · Claude, ChatGPT, CursorConnect in a minute →

“Are Hyperliquid whales net long or short BTC right now, and how has that moved over the last 30 days?”

hl_cohort_metrics

“E-mail me when BTC's smart money index on Hyperliquid rises above 0.3.”

alerts_create
Connect

Connect your client in a minute

Pick your client; the first tab needs no key.

MCP server · Streamable HTTP · 12 tools with cohort data · 0 to 3 credits a call
https://data.vike.io/mcp
text
1. 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
MCP tools

12 tools, grouped by the job they do

Each card: a question to copy, and the tools the agent calls for it, with their credits.

1 tool

Who is long, who is short

“Are Hyperliquid whales net long or short BTC right now, and how has that moved over the last 30 days?”

1 tool

The ladders and the board

“Which Hyperliquid cohorts are the most net short right now, and what does the whale cohort hold across every coin?”

5 tools

Cohorts inside the Hyperliquid tools

“Who are this month's top Hyperliquid traders among whales only, and what is BTC's smart money index doing?”

3 tools

Watch it live

“Which cohorts flipped their BTC bias on Hyperliquid in the last hour?”

2 tools

Alerts

“E-mail me when BTC's smart money index on Hyperliquid rises above 0.3.”

Every tool, the /v1/hl-cohorts routes it runs and its command. A tool with several views takes view; the first is its default.
ToolCreditsWhat it doesRunsCLI
hl_cohort_metrics1Hourly long/short positioning of ONE Hyperliquid perp split by cohort: every bucket of one ladder, every hour, in one call (the matrix behind 'are whales / smart money long or short BTC'). /v1/hl-cohorts/symbols/{symbol}/cohort-metrics vike-data hl cohort-metrics
hl_segments1The Hyperliquid cohort vocabulary and its latest-snapshot positioning, by view. cohorts: /v1/hl-cohorts/cohorts
segments: /v1/hl-cohorts/segments
summary: /v1/hl-cohorts/segments/{cohort}/summary
heatmap: /v1/hl-cohorts/heatmap
history: /v1/hl-cohorts/segments/{cohort}/history
vike-data hl segments
hl_leaderboard1Rank Hyperliquid wallets with the full risk set, by view. its cohort filter vike-data hl leaderboard
hl_liquidations1Hyperliquid liquidations, by view. its cohort filter vike-data hl liquidations
hl_market1Hyperliquid market positioning, by view. symbol_metrics: /v1/hl-cohorts/symbols/{symbol}/metrics
indices: /v1/hl-cohorts/symbols/{symbol}/indices
size_breakdown: /v1/hl-cohorts/symbols/{symbol}/size-breakdown
large_fills: /v1/hl-cohorts/symbols/{symbol}/large-fills
vike-data hl market
hl_orders1Hyperliquid standing orders, by view. its cohort filter vike-data hl orders
hl_wallet1One Hyperliquid wallet, by view. its cohort filter vike-data hl wallet
stream_info0How 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_filter0Validate a stream subscribe message (or a channel + filter) with the SAME validator the stream server runs, without connecting. — vike-data stream check
stream_events1Stored 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/hl-cohorts/stream-events vike-data stream events
alerts_create3Create 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_edit2Partial update of one of the user's alerts. — vike-data alerts edit

Cohort filters in the Hyperliquid tools

These tools belong to Hyperliquid; each view below takes a ladder (axis) and one cohort label, spelled as hl_segments view cohorts lists it.

Tool · viewCohort parametersRouteA call
hl_leaderboard · ranking
1 credit · traders ranked on any of 20 metrics with the full risk set
cohort axis /v1/hyperliquid/leaderboard vike-data call hl_leaderboard --args '{"window": "30d", "axis": "size", "cohort": "whale", "limit": 10}'
vike-data hl leaderboard --window 30d --axis size --cohort whale --limit 10
hl_liquidations · risk
1 credit · per symbol, the share of open notional close to liquidation
axis cohort /v1/hyperliquid/liquidations/risk vike-data call hl_liquidations --args '{"view": "risk", "axis": "pnl", "cohort": "3x smart", "limit": 20}'
vike-data hl liquidations --view risk --axis pnl --cohort "3x smart" --limit 20
hl_market · symbol_metrics
1 credit · the hourly open-interest, position-count and bias series of one symbol
axis cohort label_basis /v1/hl-cohorts/symbols/{symbol}/metrics vike-data call hl_market --args '{"view": "symbol_metrics", "symbol": "BTC", "axis": "size", "cohort": "whale", "limit": 720}'
vike-data hl market --view symbol_metrics --symbol BTC --axis size --cohort whale --limit 720
hl_market · positions
1 credit · individual position rows in key order (not by size; `sort=size` for the biggest of one symbol), current or historical (historical rows carry `label_causal`, the previous hour's label, for a backtest without lookahead)
cohort axis /v1/hyperliquid/positions vike-data call hl_market --args '{"view": "positions", "symbol": "BTC", "axis": "size", "cohort": "4x whale", "limit": 20}'
vike-data hl market --view positions --symbol BTC --axis size --cohort "4x whale" --limit 20
hl_orders · clusters
1 credit · one symbol's standing orders bucketed by price around the mid
cohort_axis cohort /v1/hyperliquid/symbols/{symbol}/order-clusters vike-data call hl_orders --args '{"view": "clusters", "symbol": "BTC", "cohort_axis": "size", "cohort": "whale"}'
vike-data hl orders --view clusters --symbol BTC --cohort-axis size --cohort whale
hl_wallet · summary
1 credit · equity, open positions, cohort labels, risk metrics and lifetime flow
none: it returns the wallet's cohort labels /v1/hyperliquid/wallets/{address} vike-data call hl_wallet --args '{"address": "0x25773676902085225ae5751075e4161369334897"}'
vike-data hl wallet 0x25773676902085225ae5751075e4161369334897
CLI

HL Cohorts from the command line: vike-data

The same data from a terminal, and the command your coding agent runs. Open source, on npm.

bash
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
CommandRuns
vike-data hl leaderboardhl_leaderboard
vike-data hl liquidationshl_liquidations
vike-data hl markethl_market
vike-data hl ordershl_orders
vike-data hl wallethl_wallet
vike-data hl cohort-metricshl_cohort_metrics
vike-data hl segmentshl_segments
vike-data stream infostream_info
vike-data stream checkstream_check_filter
vike-data stream eventsstream_events
vike-data alerts createalerts_create
vike-data alerts editalerts_edit
vike-data stream hl-cohortsthe live channels
  • vike-data call <tool> --args '{…}' runs any MCP tool by name.
  • vike-data install --claude adds the skill that teaches Claude Code to use the CLI.
  • Package @vike-io/cli 0.17.0.
WebSocket

4 live cohort channels

At wss://stream.vike.io/v1/stream/hl-cohorts: cohort bias, flow, leverage and the indices as they change, filtered on the server.

  1. Connect

    wss://stream.vike.io/v1/stream/hl-cohorts with X-API-KEY on the handshake.

  2. Subscribe

    Send {"action": "subscribe", "id", "channel", "filter"}; up to 100 on one connection.

  3. Resume

    Keep the last cursor; subscribe with it after a drop and get what you missed, up to 3 days back.

python
import json, os
from websockets.sync.client import connect   # websockets >= 14

with connect("wss://stream.vike.io/v1/stream/hl-cohorts",
             additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
    ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.cohort_bias",
                        "filter": {"coin": ["BTC"], "axis": "size"}}))
    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"]["cohort"], e["data"]["bias"])
  • Every event has the same envelope (schema, event_id, source, kind, ts, cursor, chain, subject, data, labels); the cohort sits in data. Keep each frame's cursor and dedupe on event.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 4410 and a resume cursor.
  • Close codes: 4401 key_revoked; 4408 slow_consumer; 4409 server_catching_up; 4410 replaced; 4411 memory_backstop; 4412 ping_timeout; 1011 a server fault; 1012 a server restart.
  • A list is OR inside a key, keys are AND across, an empty filter matches everything. A cohort or tier value unknown also matches an event with no label.
  • A channel is produced only while someone subscribes to it (and 15 minutes after): the first events of an idle channel come with its source's next pass, and an idle window is one gap frame in cursor order.
  • Dropped? Subscribe again with your last cursor and get what you missed, up to 3 days back. Without a socket: GET /v1/hl-cohorts/stream-events (its card) or the stream_events tool.
  • 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.

Subscribe message
json
The same from the CLI
bash

The real events below: one subscription to wss://stream.vike.io/v1/stream/hl-cohorts (filter coin BTC; hl.cohort_leverage axis size): the first event of each channel, 2026-11-04T07:38Z.

Hyperliquid · WebSocket

hl.cohort_bias

At every evaluated position capture (about every 12 minutes), for the 50 coins with the largest open notional, one event per cohort of both ladders: long and short USD, net USD, the bias and the number of long and short positions.

Path
wss://stream.vike.io/v1/stream/hl-cohorts
Filters
axis, cohort, coin
Free APIArchiveStreamMCP

Filters

FieldTypeDescription
axisone valuepnl, size
cohortlist ≤ 502x dolphin, 2x fish, 2x loser, 2x rekt, 2x shark, 2x shrimp, 2x smart, 2x whale, 2x winner, 3x loser, 3x rekt, 3x smart, 3x whale, 3x winner, 4x whale, dolphin, fish, loser, rekt, shark, shrimp, smart, unknown, whale, winner
coinlist ≤ 200not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters)

A real event captured 2026-11-04T07:38Z

json
{
 "schema": "vike.event.v1",
 "event_id": "hl.cohort_bias:BTC:pnl:2x_loser:1793777640",
 "source": "hl.cohort_bias",
 "kind": "cohort_bias",
 "ts": "2026-11-04T07:34:00.000Z",
 "cursor": "1793777640",
 "chain": "hyperliquid",
 "subject": {
  "symbol": "BTC"
 },
 "data": {
  "snapshot_ts": "2026-11-04T07:34:00Z",
  "axis": "pnl",
  "cohort": "2x loser",
  "long_usd": 1368821.93,
  "short_usd": 1277746.89,
  "net_usd": 91075.04,
  "bias": 0.034412,
  "positions_long": 1199,
  "positions_short": 597
 },
 "labels": {}
}

Subscribe

python
import json, os
from websockets.sync.client import connect   # websockets >= 14

with connect("wss://stream.vike.io/v1/stream/hl-cohorts",
             additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
    ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.cohort_bias",
                        "filter": {"coin": ["BTC"], "axis": "size", "cohort": ["whale"]}}))
    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
Hyperliquid · WebSocket

hl.indices

At every evaluated capture, for the same 50 coins: the five indices (smi, wsi, smart_bias, whale_bias, smi_wsi_gap), their previous values and `crossed`, the indices that crossed 0 by a band of 0.10 since the last capture. Filter `index` keeps the events where that index crossed.

Path
wss://stream.vike.io/v1/stream/hl-cohorts
Filters
coin, index
Free APIArchiveStreamMCP

Filters

FieldTypeDescription
coinlist ≤ 200not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters)
indexlist ≤ 50smart_bias, smi, smi_wsi_gap, whale_bias, wsi

A real event captured 2026-11-04T07:38Z

json
{
 "schema": "vike.event.v1",
 "event_id": "hl.indices:BTC:1793777640",
 "source": "hl.indices",
 "kind": "index",
 "ts": "2026-11-04T07:34:00.000Z",
 "cursor": "1793777640",
 "chain": "hyperliquid",
 "subject": {
  "symbol": "BTC"
 },
 "data": {
  "values": {
   "smi": -0.160602,
   "wsi": -0.236862,
   "smart_bias": -0.146511,
   "whale_bias": -0.160162,
   "smi_wsi_gap": 0.07626
  },
  "prev": null,
  "crossed": [],
  "snapshot_ts": "2026-11-04T07:34:00Z",
  "positions": 39592,
  "notional_usd": 3101851318.84
 },
 "labels": {}
}

Subscribe

python
import json, os
from websockets.sync.client import connect   # websockets >= 14

with connect("wss://stream.vike.io/v1/stream/hl-cohorts",
             additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
    ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.indices",
                        "filter": {"coin": ["BTC"], "index": ["smi", "wsi"]}}))
    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
Hyperliquid · WebSocket

hl.cohort_flow

Every closed minute, per perp coin and per cohort of both ladders: the buy and sell USD of every fill, not only the large ones, net USD, wallets, fills and realized PnL. A wallet with no label is `unknown`, so each ladder sums to the coin's total. A minute that late fills changed is sent again with rev 1: keep the higher rev.

Path
wss://stream.vike.io/v1/stream/hl-cohorts
Filters
axis, cohort, coin
Free APIArchiveStreamMCP

Filters

FieldTypeDescription
axisone valuepnl, size
cohortlist ≤ 502x dolphin, 2x fish, 2x loser, 2x rekt, 2x shark, 2x shrimp, 2x smart, 2x whale, 2x winner, 3x loser, 3x rekt, 3x smart, 3x whale, 3x winner, 4x whale, dolphin, fish, loser, rekt, shark, shrimp, smart, unknown, whale, winner
coinlist ≤ 200not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters)

A real event captured 2026-11-04T07:38Z

json
{
 "schema": "vike.event.v1",
 "event_id": "hl.cohort_flow:BTC:pnl:2x_loser:1793777760",
 "source": "hl.cohort_flow",
 "kind": "cohort_flow",
 "ts": "2026-11-04T07:36:00.000Z",
 "cursor": "1793777760-0",
 "chain": "hyperliquid",
 "subject": {
  "symbol": "BTC"
 },
 "data": {
  "window_start": "2026-11-04T07:36:00Z",
  "window_s": 60,
  "axis": "pnl",
  "cohort": "2x loser",
  "buy_usd": 0.0,
  "sell_usd": 390.7,
  "net_usd": -390.7,
  "wallets": 2,
  "fills": 2,
  "rev": 0,
  "closed_pnl_usd": -0.44
 },
 "labels": {}
}

Subscribe

python
import json, os
from websockets.sync.client import connect   # websockets >= 14

with connect("wss://stream.vike.io/v1/stream/hl-cohorts",
             additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
    ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.cohort_flow",
                        "filter": {"coin": ["BTC", "ETH"], "axis": "pnl", "cohort": ["3x smart", "2x smart"]}}))
    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
Hyperliquid · WebSocket

hl.cohort_leverage

Each newly written hour (about HH+1:03 for hour HH), one event per cohort of both ladders: perp_bias, equity_exposure and equity_covered_share with the numbers behind them. A new subscriber gets the newest hour at once.

Path
wss://stream.vike.io/v1/stream/hl-cohorts
Filters
axis, cohort
Free APIArchiveStreamMCP

Filters

FieldTypeDescription
axisone valuepnl, size
cohortlist ≤ 502x dolphin, 2x fish, 2x loser, 2x rekt, 2x shark, 2x shrimp, 2x smart, 2x whale, 2x winner, 3x loser, 3x rekt, 3x smart, 3x whale, 3x winner, 4x whale, dolphin, fish, loser, rekt, shark, shrimp, smart, unknown, whale, winner

A real event captured 2026-11-04T07:38Z

json
{
 "schema": "vike.event.v1",
 "event_id": "hl.cohort_leverage:size:2x_dolphin:1793772000",
 "source": "hl.cohort_leverage",
 "kind": "cohort_leverage",
 "ts": "2026-11-04T06:00:00.000Z",
 "cursor": "1793772000000000-size-2x_dolphin-1793772000",
 "chain": "hyperliquid",
 "subject": {},
 "data": {
  "hour": "2026-11-04T06:00:00Z",
  "axis": "size",
  "cohort": "2x dolphin",
  "perp_bias": 1.0425888888860788,
  "equity_exposure": 4.97453280749542,
  "equity_covered_share": 0.9996590520750238,
  "long_cov": 454119607.4343031,
  "short_cov": 296748664.705644,
  "equity_cov": 150942470.619265,
  "notional_main": 751124366.4340819,
  "wallets_cov": 1055,
  "wallets": 1056
 },
 "labels": {}
}

Subscribe

python
import json, os
from websockets.sync.client import connect   # websockets >= 14

with connect("wss://stream.vike.io/v1/stream/hl-cohorts",
             additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
    ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.cohort_leverage",
                        "filter": {"axis": "size"}}))
    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
REST

11 routes under /v1/hl-cohorts

Base URL https://data.vike.io/v1/hl-cohorts, one header (X-API-KEY). Every route is also an MCP tool, named on its card, and has a builder prefilled with real values.

Ladders

The cohort ladders

HL Cohorts · REST · CLI · MCP

List Cohorts

Route
GET /v1/hl-cohorts/cohorts
CLI
vike-data hl segments
MCP
hl_segments · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

The canonical cohort ladders — label sets only, in axis order.

{"size": [...12], "pnl": [...12, "unknown"], "pnl_abs": [...8, "unknown"], "tier": [...10]} — the same sets the axes group by and the cohort filters validate against, so this endpoint cannot disagree with either: a label is served by its axis if and only if it is in this response, and ?cohort= accepts exactly these strings (or their /segments slugs). unknown rides the two PnL sets because it is a real, queryable bucket there (pnl: wallets under 10 closed trades; pnl_abs: wallets with no realized record at all), not a rung of the ladder. tier is the per-position ladder that /segments publishes as trade_size.

/segments remains the detailed catalogue (bounds, slugs, ranks, groupable); this is the flat taxonomy for a client that just needs the exact spellings.

Parameters

None.

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/cohorts"

Try it

no parameters
GET/v1/hl-cohorts/cohorts

    
Press Send ▶ — the captured response appears here

    
HL Cohorts · REST · CLI · MCP

List Segments

Route
GET /v1/hl-cohorts/segments
CLI
vike-data hl segments
MCP
hl_segments · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

The cohort catalogue: every bucket on every axis, with its boundaries.

Self-describing on purpose. A client that hard-codes bucket names learned from an absolute-$ ladder elsewhere will mis-map ours, which is a percentile ladder over realized PnL; this endpoint is how it finds that out.

Each row carries groupable: true for the three per-wallet ladders, false for trade_size, which is a per-position property and is not accepted as ?axis= by /heatmap or /segments/{cohort}/summary. Filter on it rather than assuming every published axis is a valid grouping key.

Parameters

None.

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/segments"

Try it

no parameters
GET/v1/hl-cohorts/segments

    
Press Send ▶ — the captured response appears here

    
Now

One cohort and the whole board

HL Cohorts · REST · CLI · MCP

Segment Summary

Route
GET /v1/hl-cohorts/segments/{cohort}/summary
CLI
vike-data hl segments
MCP
hl_segments · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

Aggregate positioning for one cohort at the latest snapshot.

perp_bias = (long - short) / equity and equity_exposure = (long + short) / equity, native perp account only: positions on the main dex over the native perp account value of the wallets holding them, each wallet counted once; builder-DEX positions are not included, and wallets without a known positive native equity are left out of perp_bias, equity_exposure and the numerator of equity_covered_share (main-dex notional of the counted wallets / main-dex notional of the whole cohort). Unbounded ratios (above 1 = more than 1x of equity), null when that equity <= 0. That equity is not total_equity, so perp_bias x total_equity is not total_long_usd - total_short_usd. Under openedWithin long/short are the qualifying positions and the equity the whole native account of every wallet holding one. exposure_ratio stays notional / margin used.

Parameters

ParameterInDescription
cohortrequiredpathA label or slug from /cohorts / /segments
axisquerysize | pnl | pnl_abs. Default size. Per-wallet ladders only — trade_size is per-position and is rejected here; see the groupable field on /segments.
openedWithinquery24h | 7d | 30d | all. Default all

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/segments/whale/summary?axis=size"

Try it

axis
openedWithin
GET/v1/hl-cohorts/segments/{cohort}/summary?axis={axis}&openedWithin={openedWithin}

    
Press Send ▶ — the captured response appears here

    
HL Cohorts · REST · CLI · MCP

Heatmap

Route
GET /v1/hl-cohorts/heatmap
CLI
vike-data hl segments
MCP
hl_segments · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

Coin x cohort positioning matrix — the whole board in one call.

Parameters

ParameterInDescription
axisquerysize | pnl | pnl_abs. Default size. Per-wallet ladders only — trade_size is per-position and is rejected here; see the groupable field on /segments.
openedWithinquery24h | 7d | 30d | all. Default all

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/heatmap?axis=size"

Try it

axis
openedWithin
GET/v1/hl-cohorts/heatmap?axis={axis}&openedWithin={openedWithin}

    
Press Send ▶ — the captured response appears here

    
One coin

One coin, hour by hour

HL Cohorts · REST · CLI · MCP

Coin Cohort Metrics

Route
GET /v1/hl-cohorts/symbols/{symbol}/cohort-metrics
CLI
vike-data hl cohort-metrics
MCP
hl_cohort_metrics · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

The full cohort matrix for one coin — every bucket on an axis, hourly.

Data from 2026-04-07 (the first hour cohort labels are correct), on every axis: an older start is served from there (never a 400), paging stops there, and the envelope says data_from. Every row carries n_wallets, the wallets in the cohort holding the coin that hour.

/symbols/{symbol}/metrics returns one row per hour, either all cohorts summed or a single cohort. Rebuilding the matrix from it costs one request per cohort (12 for size, 13 for pnl); this returns all of them in one.

axis=tier is the PER-POSITION ladder — each position row's own notional bucket, DERIVED from the row's size_usd: the canonical ten dollar-range buckets, largest first, the website's spellings verbatim (>$2.5m, $1m-$2.5m, $500k-$1m, $250k-$500k, $100k-$250k, $50k-$100k, $25k-$50k, $10k-$25k, $1k-$10k, <$1k — /cohorts and /segments publish the ladder with bounds). Rows have the same shape as axis=size, the bucket in the cohort field. It is a grouping only here: this endpoint aggregates position rows, so each row sits in exactly one bucket, while the per-wallet endpoints (/heatmap, /segments/{cohort}/summary) keep rejecting it — a wallet straddles buckets. (The retired micro/small/medium/large/mega vocabulary is gone; sending one of those as a cohort filter anywhere returns a 400 naming this ladder.)

Paginated by HOUR, not by row — a row limit would cut a bucket in half and return an hour whose cohorts do not sum to its total. hours bounds the buckets; every cohort in a returned bucket comes with it.

No provisional row: this is bulk history, and a partial hour inside a matrix that will be differenced or z-scored produces a fake move at the right edge. Use /symbols/{symbol}/metrics for the current value.

include=volume,median_pnl adds two per-cohort columns to the rows of the newest bucket, volume_24h_usd and median_realized_pnl_usd (size and pnl only; see the parameter). They are computed at request time, so they describe the book now, not the bucket's hour.

⚠️ labelBasis matters for research. By default the pnl cohort is resolved from the dictionary AT QUERY TIME, so a historical row carries TODAY's grading — correct for "who is smart right now", lookahead for a backtest, because a wallet under the 10-closed-trade floor back then may be graded now. Pass labelBasis=point_in_time to get each row's label as it was that hour. Hours from 2026-08-07 on carry every field (unavailable_fields is empty). A window that reaches an earlier hour is served as one aggregate for all of its hours, and its position_count, position_count_long, total_unrealized_pnl, total_position_size and total_position_size_long are null (listed in unavailable_fields); so are those five fields under grading=unrealized and grading=realized-pit. Point-in-time matrices are stored aggregates and refuse openedWithin (400).

axis=size is always point-in-time (a stored column), axis=tier is always point-in-time (a pure function of the stored size_usd), axis=pnl_unrealized is always point-in-time (the HOURLY rollup stores it, see below) and axis=pnl_abs is always current (no frozen source exists); all four reject a conflicting labelBasis rather than silently ignoring it. Every row and the envelope carry label_basis so the answer is self-describing.

⚠️ grading selects WHICH PnL grading; label_computed says WHEN it was computed. The two are independent and the response carries both, on the envelope and on every row. A grading name is neutral by design — realized does not say whether the label is today's or the hour's — so the endpoint refuses to let the name travel alone.

* grading=realized (default on axis=pnl) — the percentile ladder over cumulative realized PnL. With the default labelBasis this is resolved at QUERY time, so every historical hour carries TODAY's grading: label_computed.lookahead is true and says so. With labelBasis=point_in_time it is the frozen ladder, and label_computed names the 2026-08-06 seam — before it the labels come from the 2026-08-17 point-in-time rebuild (daily resolution), from it they come from the row's own pnl_cohort_realized (hourly). Both halves are honest; a series crossing the seam changes label RESOLUTION, which a turnover count will read as a regime change that is an artefact. * grading=realized-pit — the same ladder graded point-in-time across the WHOLE window (day D on the cumulative as of the end of D-1), which is what makes it the control for a study crossing that seam. Rebuilt once a DAY: it is extended just after 00:00 UTC through 23:00 of the last complete UTC day, so a window reaching the present comes back 1 to 25 hours short rather than wrong. * grading=unrealized — "currently onside": hour H's rank over the wallet's last snapshot strictly before H. axis=pnl_unrealized is the legacy spelling of exactly this and keeps working; both reach one branch.

grading is refused on size, tier and pnl_abs, which rank no PnL, and realized-pit/unrealized refuse labelBasis=current rather than silently overriding it.

⚠️ freshness is how a REALTIME caller checks it is not trading a stale book. On any window that reaches the present the envelope carries {newest_ts, behind_hours, settled_lag_ceiling_hours, stale}. stale is the pre-computed verdict, so a consumer does not have to re-derive the rollup's cadence — the rollup writes hour H at H+1:00–H+1:30, so a bucket up to two hours old is healthy and the ceiling sits an hour above that.

It exists because prose was not enough. grading=realized-pit reads a table rebuilt once a DAY; ask it the way a poller does (?grading=realized-pit&hours=1, no start) and it returns its own newest bucket — identical in shape to a current one, and hours old. That gap is not a constant: the newest hour is 23:00 of the last complete UTC day, so it runs from 1 to 25 hours over each day and stale is true for most of it. Read the age off freshness.behind_hours rather than from any number written in prose.

For live trading use grading=realized with labelBasis=point_in_time: honest labels, and one hour behind the clock rather than up to a day.

A window that ends in the PAST gets no freshness — "the newest bar is four months old" answers a question a historical query did not ask, and would make every backtest fetch look broken.

axis=pnl_unrealized grades positioning, not skill. Each wallet is ranked by the unrealized PnL of its open positions as a share of account value — "currently onside" — over its last snapshot STRICTLY BEFORE hour H, and that rank labels hour H. The grading turns over ~44% of wallets per day (the realized ladder: ~2.4%), which is the point: it is a different axis, not a re-spelling. Twelve rungs, no unknown (eligibility is a positive-equity snapshot, not a trade count), the same five null fields as a point-in-time pnl window that reaches before 2026-08-07, and a known three-day hole (2026-06-28..30, graded off days whose equity feed was down).

Data from 7 April 2026: the first hour cohort labels are correct. In the builder below, hours 1 replays the answer the showcase captured for that exact call. 720 hours is 30 days; hours goes up to 8,760 per request. end includes the hour it falls in (end=…T01:00:00Z returns the 01:00 hour): to stop after the last full hour HH, pass HH:59:59.

Parameters

ParameterInDescription
symbolrequiredpathHL ticker
axisquerysize | pnl | pnl_abs | tier | pnl_unrealized. Default size. trade_size (the name /segments gives the ladder) is accepted as an alias of tier; the response still says axis: tier
startqueryISO-8601 UTC. Oldest edge of the window. Omit it and the window is the hours newest hourly buckets at or before end.
endqueryISO-8601 UTC. Newest edge. With no start: the hours newest hourly buckets at or before end, the same buckets as start = end - hours. A bucket counts when its hour starts at or before end, so an end exactly on the hour includes that hour's bucket. An end at or after the newest data (a future one included) is the same as no end; one older than all data returns an empty series.
openedWithinquery24h | 7d | 30d | all. Default all. Refused (400) with axis=pnl and labelBasis=point_in_time, with grading=unrealized or realized-pit and with axis=pnl_unrealized: those matrices are stored aggregates with position age summed away. Use axis=size, tier, pnl_abs or pnl with the default basis.
hoursqueryNumber of hourly buckets. Default 168, max 8760. Sizes the window when start is omitted; with start, it caps how many buckets come back (newest first).
cursorqueryCursor over hours
gradingqueryrealized | realized-pit | unrealized. Which PnL grading labels each wallet, for the axes that carry one (axis=pnl, axis=pnl_unrealized). Default realized on axis=pnl; refused on size, tier, pnl_abs. Every response declares in label_computed WHEN the label was computed — the grading name alone cannot say it.
labelBasisquerycurrent | point_in_time. Default depends on the axis: size, tier and pnl_unrealized are always point_in_time, pnl_abs always current. point_in_time (axis=pnl) returns each row's cohort label AS IT WAS that hour. Hours from 2026-08-07 carry every field; a window with an earlier hour is served as one aggregate whose five position fields are null (listed in unavailable_fields). Use it for backtests. Refused (400) with openedWithin.
includequeryComma list of volume and median_pnl; default none (nothing extra is read). Adds to every row of the NEWEST bucket volume_24h_usd (USD the cohort's wallets filled in this symbol over the last 24 hours; on axis=size only wallets holding the symbol now count; null when they filled nothing or the cohort is empty) and/or median_realized_pnl_usd (median cumulative realized PnL, USD, over the DISTINCT wallets holding the symbol now; null when none does). The median is a sampled quantile: exact up to 8,192 wallets in a cohort, an estimate above it. The asset page's pnl-axis "Med Lifetime" counts positions rather than wallets, so it can differ slightly. Both are CURRENT figures (request time, newest 5-minute snapshot), not the bucket hour's, and null on unknown. axis=size and axis=pnl with grading=realized only; refused (400) on any other axis or grading, with openedWithin, with cursor and with an end in the past.

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/symbols/BTC/cohort-metrics?axis=size&hours=720"

Try it

symbol
axis
hours
labelBasis
GET/v1/hl-cohorts/symbols/{symbol}/cohort-metrics?axis={axis}&hours={hours}&labelBasis={labelBasis}

    
Press Send ▶ — the captured response appears here

    
HL Cohorts · REST · CLI · MCP

Coin Metrics

Route
GET /v1/hl-cohorts/symbols/{symbol}/metrics
CLI
vike-data hl market
MCP
hl_market · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

Hourly open-interest / position-count / bias series for one coin, optionally sliced to a single cohort.

Hourly by construction: the 5-minute snapshot table summed into hour buckets over-counts open interest ~12x, which is what forced the previous cohort aggregate to be rebuilt.

Every row carries provisional. It is True on at most one row — the newest, covering the hour still in progress — because the hourly rollup lands 60-120 minutes late and a "current positioning" read would otherwise be silently that stale. Its ts is a snapshot instant rather than an hour boundary and it changes on every poll; drop it when persisting a series. Present only on the first page of a window reaching the present.

With a cohort: data from 2026-04-07 (the first hour cohort labels are correct); an older start is served from there and the envelope says data_from. Without one (all cohorts combined) the series does not depend on any label and keeps its whole history.

Parameters

ParameterInDescription
symbolrequiredpathHL ticker
startqueryISO-8601 UTC. Oldest edge of the window. Omit it and the window is the limit newest hourly rows at or before end.
endqueryISO-8601 UTC. Newest edge. With no start: the limit newest hourly rows at or before end, the same rows as start = end - limit hours. A row counts when its hour starts at or before end, so an end exactly on the hour includes that hour. An end at or after the newest data (a future one included) is the same as no end; one older than all data returns no rows.
axisquerysize | pnl | pnl_abs. Omit for all cohorts combined
cohortqueryOne label on that axis
openedWithinquery24h | 7d | 30d | all. Default all. Rolling per bucket, not relative to now — each hour counts positions opened in the window before THAT hour.
limitqueryDefault 500, max 5000. One row per hour, so also the window length in hours when start is omitted
cursorqueryKeyset cursor
labelBasisquerycurrent only. This endpoint resolves the cohort label from the wallet dictionary at QUERY time, so on the pnl ladders it can only serve current, and it REFUSES point_in_time by name instead of ignoring it. Every response declares label_basis and, for a pnl axis, label_computed.lookahead.

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/symbols/BTC/metrics?axis=size&cohort=whale&limit=720"

Try it

symbol
axis
cohort
limit
GET/v1/hl-cohorts/symbols/{symbol}/metrics?axis={axis}&cohort={cohort}&limit={limit}

    
Press Send ▶ — the captured response appears here

    
HL Cohorts · REST · CLI · MCP

Symbol Indices

Route
GET /v1/hl-cohorts/symbols/{symbol}/indices
CLI
vike-data hl market
MCP
hl_market · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

Smart Money Index, Whale-vs-Shrimp Index, whale / smart bias: the website's numbers as RATIOS in [-1, +1] (the site multiplies by 100). latest is the newest 5-minute snapshot (its pnl rungs graded by today's PnL grading, latest.label_basis: current), series the settled hourly rows, newest first. smi = long share of the 3x smart / 2x smart / smart wallets minus long share of the rekt / 2x rekt / 3x rekt wallets; wsi = long share of the 4x whale .. whale wallets minus long share of the 2x shrimp / shrimp wallets; whale_bias and smart_bias (latest only) = (long - short) / (long + short) over those whale / smart wallets. A value is null, never 0, when a cohort holds no positions. series: data from 2026-04-07 (the first hour cohort labels are correct); an older start is served from there, and the envelope says data_from.

Parameters

ParameterInDescription
symbolrequiredpathHL ticker
startqueryISO-8601 UTC. Oldest edge. Omit it for the hours newest hours.
endqueryISO-8601 UTC. Newest edge; with no start, the window is the hours newest hours at or before it.
hoursqueryWindow length in hours when start is omitted. Default 168, max 8760.
limitqueryMax series rows, newest first, default and max 5000; has_more says there are older ones (page back with end = the oldest ts minus 1 second; end is inclusive).

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/symbols/BTC/indices?hours=720"

Try it

symbol
hours
GET/v1/hl-cohorts/symbols/{symbol}/indices?hours={hours}

    
Press Send ▶ — the captured response appears here

    
HL Cohorts · REST · CLI · MCP

Coin Size Breakdown

Route
GET /v1/hl-cohorts/symbols/{symbol}/size-breakdown
CLI
vike-data hl market
MCP
hl_market · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

Positioning split by canonical position-size bucket for one coin.

Ten dollar-range buckets (largest first), derived from each row's size_usd — the same ladder, spellings and bounds as axis=tier on /symbols/{symbol}/cohort-metrics and the /cohorts catalogue. Empty buckets come back zero-filled so the table shape is stable.

Parameters

ParameterInDescription
symbolrequiredpathHL ticker

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/symbols/BTC/size-breakdown"

Try it

symbol
GET/v1/hl-cohorts/symbols/{symbol}/size-breakdown

    
Press Send ▶ — the captured response appears here

    
HL Cohorts · REST · CLI · MCP

Symbol Large Fills

Route
GET /v1/hl-cohorts/symbols/{symbol}/large-fills
CLI
vike-data hl market
MCP
hl_market · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

The asset page's Whale Activity feed: a symbol's large fills, newest first. The feed keeps only the newest 200 fills over $50k per symbol of the last 24 hours, so on a busy symbol that is minutes, not a day: covered_from is how far back the answer reaches, truncated says that cap may have cut older fills inside the window, has_more that limit cut rows. Each row has ts, wallet, direction (buy | sell), size_token, size_usd, price, order_type (market = crossed the book, limit = rested), size_cohort (the wallet's size rung on its latest snapshot) and pnl_cohort (graded at the fill's own hour where that hour is labelled, else today; pnl_cohort_basis says which). Liquidations are not flagged.

Parameters

ParameterInDescription
symbolrequiredpathHL ticker
min_usdquerySmallest fill in USD. Default and MINIMUM 50000: the feed behind this route holds nothing smaller.
hoursqueryHow far back, in hours. Default and max 24: the feed holds a day.
limitqueryMax rows, newest first. Default 200 (all the feed holds per symbol), max 1000; has_more says a lower limit cut rows.

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/symbols/BTC/large-fills?min_usd=100000&limit=50"

Try it

symbol
min_usd
limit
GET/v1/hl-cohorts/symbols/{symbol}/large-fills?min_usd={min_usd}&limit={limit}

    
Press Send ▶ — the captured response appears here

    
History

One cohort across every coin

HL Cohorts · REST · CLI · MCP

Segment History

Route
GET /v1/hl-cohorts/segments/{cohort}/history
CLI
vike-data hl segments
MCP
hl_segments · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

One cohort's long / short / bias per hour, summed over every symbol. Ratios in [-1, +1]. A 404 names the axis only. axis=size groups positions by the size cohort stored on each position; axis=pnl by the realized-PnL cohort frozen at that hour (hourly from 2026-08-06, daily before it; see label_computed). Per hour: long_usd and short_usd in USD, bias = (long - short) / (long + short), null when both are 0, and position_count, which counts open positions ((wallet, symbol) pairs), not wallets. Newest first, settled hours only. include=leverage adds the three equity ratios described on that parameter. Data from 2026-04-07 (the first hour cohort labels are correct): an older start is served from there, and the envelope says data_from.

Parameters

ParameterInDescription
cohortrequiredpathA label or slug of axis, from /cohorts / /segments
axisrequiredquerysize | pnl: this endpoint serves history for these two only. Required.
startqueryISO-8601 UTC. Oldest edge. Omit it for the hours newest hours.
endqueryISO-8601 UTC. Newest edge; with no start, the window is the hours newest hours at or before it.
hoursqueryWindow length in hours when start is omitted. Default 168, max 8760.
limitqueryMax rows, newest first, default and max 5000; has_more says there are older ones (page back with end = the oldest ts minus 1 second; end is inclusive).
includequeryComma list; only leverage, default none (nothing extra is read). Adds to every row perp_bias = (long - short) / equity, equity_exposure = (long + short) / equity and equity_covered_share = main-dex notional of the counted wallets / main-dex notional of the whole cohort: unbounded ratios (above 1 = more than 1x of equity), native perp account only (main-dex positions of wallets with a known positive native equity, each wallet once; builder-DEX positions are not included). An hour with no stored row, and any hour before leverage.data_from (2026-08-11), is null on all three (never 0). perp_bias and equity_exposure are also null when no counted wallet has a positive equity; equity_covered_share is null when the cohort holds no main-dex notional and a real 0 when it holds some but no wallet is counted. Hours rebuilt from fills rather than read from snapshots are null. An unknown token is a 400.

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/segments/whale/history?axis=size&hours=720"

Try it

cohort
axis
hours
include
GET/v1/hl-cohorts/segments/{cohort}/history?axis={axis}&hours={hours}&include={include}

    
Press Send ▶ — the captured response appears here

    
Live stream

Stream catch-up

HL Cohorts · REST · CLI · MCP

Stream catch-up

Route
GET /v1/hl-cohorts/stream-events
CLI
vike-data stream events
MCP
stream_events · 1 credit
Cost
1 request of the hourly limit
Free APIArchiveStreamMCP

Stored events of one live-stream channel after a cursor, oldest first: the REST catch-up of wss://stream.vike.io/v1/stream/hl-cohorts, 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.cohort_bias: axis, cohort, coin - hl.cohort_flow: axis, cohort, coin - hl.cohort_leverage: axis, cohort - hl.indices: coin, index

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.

Without a cursor it starts 3 days minus 60 seconds back, the oldest cursor it still accepts; pass next_cursor back while has_more is true. Live, the same events arrive on the socket: WebSocket.

Parameters

ParameterInDescription
channelrequiredqueryhl.cohort_bias, hl.cohort_flow, hl.cohort_leverage, hl.indices
cursorquerynext_cursor of the previous page or the cursor of a stream frame. Empty: the oldest retained event
limitqueryDefault 200, clamped to 1-1000

Call it

bash
curl -H "X-API-KEY: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/stream-events?channel=hl.cohort_bias&limit=50"

Try it

channel
limit
GET/v1/hl-cohorts/stream-events?channel={channel}&limit={limit}

    
Press Send ▶ — the captured response appears here

    
Alerts

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

HL Cohorts · Alert

A cohort index crosses a level

Type
hl_cohort_bias
Free APIArchiveStreamMCP

hl_cohort_bias = the cohort index of token_symbol (required) crossing threshold by band, filtered by direction.

The band stops the flapping. hl_cohort_bias and hl_liq_proximity: hysteresis, greater than 0 (default 0.1). A bias alert fires when the index reaches threshold + band (up) or threshold - band (down); a proximity alert fires when the ratio reaches threshold and re-arms below threshold - band. Default 0.1; |threshold| + band must stay inside the index's range (smi ±1, wsi ±1, smart_bias ±1, whale_bias ±1, smi_wsi_gap ±2).
  • index: hl_cohort_bias only, required: the index watched for token_symbol. smi = long share of smart wallets minus long share of rekt wallets; wsi = long share of whales minus long share of shrimp; smart_bias / whale_bias = (long - short) / (long + short) of that cohort; smi_wsi_gap = smi - wsi. Ratios run -1..1 (smi_wsi_gap -2..2).
  • threshold: hl_cohort_bias: the index level to cross (default 0). hl_liq_proximity: the share of the way from entry price to liquidation price, strictly between 0 and 1 (default 0.8).
Ask: “E-mail me when BTC's smart money index on Hyperliquid rises above 0.3, once per crossing.”

Create it on your account, no code →

json
{
 "label": "BTC smart money above 0.3",
 "alert_type": "hl_cohort_bias",
 "token_symbol": "BTC",
 "index": "smi",
 "threshold": 0.3,
 "band": 0.1,
 "direction": "up",
 "channels": [
  "email"
 ]
}
HL Cohorts · Alert

A cohort fills big

Type
hl_cohort_fill
Free APIArchiveStreamMCP

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.

Ask: “E-mail me when a Hyperliquid whale with $10M or more fills over $1M.”

Create it on your account, no code →

json
{
 "label": "Whale fills over $1M",
 "alert_type": "hl_cohort_fill",
 "size_cohorts": [
  "4x whale"
 ],
 "min_value_usd": 1000000,
 "channels": [
  "email"
 ]
}
HL Cohorts · Alert

A cohort gets liquidated

Type
hl_liquidation
Free APIArchiveStreamMCP

hl_liquidation = every liquidation, optionally of one wallet, of cohorts, of one coin, above a min_value_usd floor.

Ask: “Send a webhook when a Hyperliquid whale is liquidated for over $1M.”

Create it on your account, no code →

json
{
 "label": "Whale liquidations over $1M",
 "alert_type": "hl_liquidation",
 "size_cohorts": [
  "4x whale",
  "3x whale"
 ],
 "min_value_usd": 1000000,
 "channels": [
  "webhook"
 ]
}

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.

Alerts

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.

HL Cohorts · Alerts · MCP tool

alerts_list_channels

Cost
0 credits a call
Free APIArchiveStreamMCP

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.

Ask: “What notification channels do I have set up?”
HL Cohorts · Alerts · MCP tool

alerts_register_webhook

Cost
2 credits a call
Free APIArchiveStreamMCP

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.

ParameterTypeDefaultNotes
urlstringrequiredWebhook URL, https:// only, reachable from Vike's servers
secretstring—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 secret is 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_secret issues 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 test event to the URL, signed and built like a real delivery: alert_id 0, alert_label Webhook test, alert_type and event.kind test, and an event.event_id of test: and 32 hex characters, new for every test. test is {"ok": true, "status": 200} for a 2xx answer, {"ok": false, "status": 401} for any other answer, or {"ok": false, "error": "timeout"} (also network, 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 call alerts_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 over http:// 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:

text
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

python
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

python
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 on alert_id and event.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-Id is the id of the event's webhook delivery in the feed (0 when the rule has no webhook channel), and X-Vike-Attempt is always 1.

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 429 with a Retry-After header (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_id and event.event_id from 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-Id identifies 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 with alerts_resend_delivery keeps its X-Vike-Delivery-Id and counts on in X-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.
Ask: “Send my alerts to https://example.com/hooks/vike as well, with a signing secret.”
HL Cohorts · Alerts · MCP tool

alerts_test_webhook

Cost
1 credit a call
Free APIArchiveStreamMCP

Sends the signed test event to one of your webhook channels again — for example after your receiver installed a new secret.

ParameterTypeDefaultNotes
channel_idintegerrequiredThe 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.

Ask: “Send a test event to my webhook.”
HL Cohorts · Alerts · MCP tool

alerts_rotate_webhook_secret

Cost
2 credits a call
Free APIArchiveStreamMCP

Issues a new signing secret for one of your webhook channels.

ParameterTypeDefaultNotes
channel_idintegerrequiredThe 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.

Ask: “Rotate the signing secret of my webhook.”
HL Cohorts · Alerts · MCP tool

alerts_events

Cost
1 credit a call
Free APIArchiveStreamMCP

Reads the events your alert rules have produced, oldest first. To follow the feed, pass the returned next_cursor back as cursor.

ParameterTypeDefaultNotes
cursorstring—The previous page's next_cursor, passed back unchanged. Omit it for the oldest retained event
limitinteger100Events per page, 1 to 500
rule_idinteger—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.

Ask: “Show me what my alerts have fired since yesterday.”
HL Cohorts · Alerts · MCP tool

alerts_resend_delivery

Cost
1 credit a call
Free APIArchiveStreamMCP

Sends one of your deliveries again: the webhook or email of an event that failed, expired, or was lost on your side.

ParameterTypeDefaultNotes
delivery_idintegerrequiredThe 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 next X-Vike-Attempt, and is then retried like any delivery.
  • queued is 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 unsupported one (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.
Ask: “The webhook of my last USDC alert never arrived: send it again.”
Notes

Before you rely on it

Freshness, label basis, gaps and what a null means: what the field names do not tell you.

The newest hour is not in the matrix. The rollup writes hour H between H+1:00 and H+1:30, so the newest settled hour is one to two hours old. cohort-metrics never returns the hour in progress; a window that reaches the present carries freshness (newest_ts, behind_hours, stale), and stale is true once the newest hour is more than 3 hours old.
The newest row of /metrics is provisional. On /symbols/{symbol}/metrics at most one row has provisional: true: the hour in progress, read from the latest 5-minute snapshot. Its ts moves on every poll; drop it before you store a series.
Point-in-time or today's label. labelBasis=point_in_time grades each hour with the cohort the wallet was in that hour; current, the default on axis=pnl, grades history with today's label (look-ahead in a backtest, and the answer says lookahead: true). size and tier are always point-in-time, pnl_abs always current, and /metrics refuses point_in_time by name. Every answer carries label_basis; the rules are on Backtests.
History since April 2026. Data from 7 April 2026: the first hour cohort labels are correct. /cohort-metrics, /indices, /segments/{cohort}/history and a cohort slice of /metrics return no hour before 2026-04-07: an older start is served from there, and the answer says data_from. PnL labels are daily before 2026-08-06 and hourly from it, and label_computed names that seam, which a turnover count would otherwise read as a regime change.
Gaps are null, never zero. include=leverage starts on 2026-08-11; before it, for an hour with no stored row and for an hour rebuilt from fills, its three ratios are null. Across a hole of more than one hour a window returns fewer hours than asked. An empty heatmap cell is left out, never sent as 0.
What a null means. bias is null when a cohort holds nothing long or short; an index is null (never 0) when its rungs hold no position that hour. Under point_in_time the fields its source does not carry are null and named in unavailable_fields.
unknown is a real cohort. A wallet with fewer than 10 closed trades cannot be ranked on pnl: it is unknown, served and filterable, so the ladders agree on a coin's total. On pnl_abs, unknown is a wallet the PnL dictionary has not seen. On size, /cohort-metrics can also return an unknown row in a few hours: positions whose stored size label is unknown (BTC, the 720 hours to 2026-11-04: 28 of 8,668 rows). It is not one of the twelve size cohorts: leave it out of a ladder.
Large fills cover minutes on a busy coin. /large-fills holds the newest 200 fills of $50,000 or more of the last 24 hours, per coin: on BTC that is minutes. truncated and covered_from say how far back an answer reaches.

Checked with code: every block was run against the live API on 2026-11-04.

HL Cohorts · API

How do I check Hyperliquid cohort data is fresh before I trade on it?

Read the freshness block of /symbols/{symbol}/cohort-metrics; stale is true when the newest hourly bucket is more than 3 hours behind, and the 5-minute snapshot time is latest.ts on /indices.

Uses
/symbols/{…}/cohort-metrics, /symbols/{…}/indices, /symbols/{…}/metrics, /v1/hyperliquid/archive/datasets
Verified
live API, 2026-11-04: 4 calls, all 200
Free APIArchiveStreamMCP
Ask your AI agent: “Is the Hyperliquid cohort data for BTC fresh right now? Show the freshness block”

The code

python
import os
import requests

API_KEY = os.environ["VIKE_API_KEY"]  # your key, read from the environment

# the newest settled hour of BTC's cohort matrix, with the freshness verdict in the envelope
r = requests.get(
    "https://data.vike.io/v1/hl-cohorts/symbols/BTC/cohort-metrics?hours=1",
    headers={"X-API-Key": API_KEY},
)
Limits

Limits

One free key for everything on this page.

Keyfree: sign in at vike.io and create one; every key starts vk_
Requests20,000 per hour per key, a rolling hour; MCP calls share it
Per minute200 per minute per key
Keysup to 3 active per account; each key is its own budget
Cost of a call1 per REST request; an MCP tool 0 to 3 credits
WebSocketnot counted: connecting, subscribing and the frames you receive
Where it is statedx-vike-limits in the OpenAPI document, /data/openapi.json
Price$0, nothing is billed
Errors

Errors

Branch on the status code, not the body: the body depends on which hop answered.

StatusMeaningBody
400The input was rejected: an unknown symbol or dex, a malformed identifier, a cursor from another query.{"detail": "…"}
401A missing, invalid or deactivated key.Turned away before the API runs, so the body can be an HTML page. Do not assume JSON.
404No such route or archive file.{"detail": "Not Found"}
410The stream catch-up: a cursor older than the events kept; the answer names the oldest cursor it accepts.{"detail": "…"}
422A parameter failed validation: the wrong type, or outside the bounds the route declares.{"detail": [ … ]}
429Over 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
503The 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.
  • 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.
FAQ

HL Cohorts MCP and API FAQ

Is there an MCP server for Hyperliquid cohort data?
Yes: https://data.vike.io/mcp. hl_cohort_metrics gives one coin's hourly long and short by cohort, hl_segments the ladders, one cohort now or hour by hour, and the whole coin x cohort board. Claude Desktop signs in with OAuth and needs no key; other clients send a free key in X-API-KEY.
Can my AI agent tell whether whales or smart money are long?
Yes. Ask in plain words; the agent calls hl_cohort_metrics or hl_segments and answers with the cohort's long and short value and its bias. A call of either costs 1 credit; nothing is billed.
Is there a WebSocket for Hyperliquid cohort positioning?
Yes: wss://stream.vike.io/v1/stream/hl-cohorts carries 4 channels: hl.cohort_bias, hl.indices, hl.cohort_flow, hl.cohort_leverage, filtered on the server by coin, ladder and cohort. Drop the connection and resume with your last cursor, up to 3 days back.
Can I get an alert when whales turn short?
Yes, with no code: an hl_cohort_bias alert fires when a coin's cohort index crosses your level by a band, once per crossing; hl_cohort_fill and hl_liquidation watch a cohort's large fills and liquidations. By e-mail or a signed webhook, from your account page, your AI (alerts_create) or vike-data alerts create.
How far back does cohort history go?
All history since April 2026. Data from 7 April 2026: the first hour cohort labels are correct. One cohort-metrics request returns up to 8,760 hours; page older hours with its cursor.