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.
“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_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_…
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.
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?”
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?”
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?”
Watch it live
“Which cohorts flipped their BTC bias on Hyperliquid in the last hour?”
Alerts
“E-mail me when BTC's smart money index on Hyperliquid rises above 0.3.”
| Tool | Credits | What it does | Runs | CLI |
|---|---|---|---|---|
hl_cohort_metrics | 1 | Hourly 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_segments | 1 | The Hyperliquid cohort vocabulary and its latest-snapshot positioning, by view. | cohorts: /v1/hl-cohorts/cohortssegments: /v1/hl-cohorts/segmentssummary: /v1/hl-cohorts/segments/{cohort}/summaryheatmap: /v1/hl-cohorts/heatmaphistory: /v1/hl-cohorts/segments/{cohort}/history |
vike-data hl segments |
hl_leaderboard | 1 | Rank Hyperliquid wallets with the full risk set, by view. | its cohort filter | vike-data hl leaderboard |
hl_liquidations | 1 | Hyperliquid liquidations, by view. | its cohort filter | vike-data hl liquidations |
hl_market | 1 | Hyperliquid market positioning, by view. | symbol_metrics: /v1/hl-cohorts/symbols/{symbol}/metricsindices: /v1/hl-cohorts/symbols/{symbol}/indicessize_breakdown: /v1/hl-cohorts/symbols/{symbol}/size-breakdownlarge_fills: /v1/hl-cohorts/symbols/{symbol}/large-fills |
vike-data hl market |
hl_orders | 1 | Hyperliquid standing orders, by view. | its cohort filter | vike-data hl orders |
hl_wallet | 1 | One Hyperliquid wallet, by view. | its cohort filter | vike-data hl wallet |
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/hl-cohorts/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 |
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 · view | Cohort parameters | Route | A call |
|---|---|---|---|
hl_leaderboard · ranking1 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 · risk1 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_metrics1 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 · positions1 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 · clusters1 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 · summary1 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 |
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.
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 cohort-metrics BTC --axis size --hours 720
vike-data hl cohort-metrics BTC --axis pnl --label-basis point_in_time --hours 720
vike-data hl segments --view cohorts
vike-data hl segments --view summary --cohort whale --axis size
vike-data hl segments --view heatmap --axis pnl
vike-data hl segments --view history --cohort whale --axis size --hours 720 --include leverage
vike-data hl market --view indices --symbol BTC --hours 720
vike-data hl market --view large_fills --symbol BTC --min-usd 100000
vike-data stream hl-cohorts --channel hl.cohort_bias --filter coin=BTC --filter axis=size
vike-data alerts create --type hl_cohort_bias --token BTC --index smi --threshold 0.3 --direction up --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 leaderboard | hl_leaderboard |
vike-data hl liquidations | hl_liquidations |
vike-data hl market | hl_market |
vike-data hl orders | hl_orders |
vike-data hl wallet | hl_wallet |
vike-data hl cohort-metrics | hl_cohort_metrics |
vike-data hl segments | hl_segments |
vike-data stream info | stream_info |
vike-data stream check | stream_check_filter |
vike-data stream events | stream_events |
vike-data alerts create | alerts_create |
vike-data alerts edit | alerts_edit |
vike-data stream hl-cohorts | the live channels |
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.
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.
- Connect
wss://stream.vike.io/v1/stream/hl-cohortswithX-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-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"])
vike-data stream hl-cohorts --channel hl.cohort_bias --filter coin=BTC --filter axis=size # NDJSON, reconnects and resumes by itself
- Every event has the same envelope (
schema,event_id,source,kind,ts,cursor,chain,subject,data,labels); the cohort sits indata. 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): the first events of an idle channel come with its source's next pass, and an idle window is one
gapframe in cursor order. - Dropped? Subscribe again with your last
cursorand get what you missed, up to 3 days back. Without a socket:GET /v1/hl-cohorts/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.
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.
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
Filters
| Field | Type | Description |
|---|---|---|
axis | one value | pnl, size |
cohort | list ≤ 50 | 2x 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 |
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
A real event captured 2026-11-04T07:38Z
{
"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
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
vike-data stream hl-cohorts --channel hl.cohort_bias --filter coin=BTC --filter axis=size --filter cohort=whale # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.cohort_bias",
"filter": {
"coin": [
"BTC"
],
"axis": "size",
"cohort": [
"whale"
]
}
}
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
Filters
| Field | Type | Description |
|---|---|---|
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
index | list ≤ 50 | smart_bias, smi, smi_wsi_gap, whale_bias, wsi |
A real event captured 2026-11-04T07:38Z
{
"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
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
vike-data stream hl-cohorts --channel hl.indices --filter coin=BTC --filter index=smi,wsi # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.indices",
"filter": {
"coin": [
"BTC"
],
"index": [
"smi",
"wsi"
]
}
}
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
Filters
| Field | Type | Description |
|---|---|---|
axis | one value | pnl, size |
cohort | list ≤ 50 | 2x 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 |
coin | list ≤ 200 | not a coin name (A-Z, 0-9 and : @ . _ / -, at most 32 characters) |
A real event captured 2026-11-04T07:38Z
{
"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
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
vike-data stream hl-cohorts --channel hl.cohort_flow --filter coin=BTC,ETH --filter axis=pnl --filter 'cohort=3x smart,2x smart' # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.cohort_flow",
"filter": {
"coin": [
"BTC",
"ETH"
],
"axis": "pnl",
"cohort": [
"3x smart",
"2x smart"
]
}
}
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
Filters
| Field | Type | Description |
|---|---|---|
axis | one value | pnl, size |
cohort | list ≤ 50 | 2x 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
{
"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
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
vike-data stream hl-cohorts --channel hl.cohort_leverage --filter axis=size # prints NDJSON, reconnects by itself
{
"action": "subscribe",
"id": "s1",
"channel": "hl.cohort_leverage",
"filter": {
"axis": "size"
}
}
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.
The cohort ladders
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
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
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hl-cohorts/cohorts"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/cohorts",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl segments --view cohorts
# any MCP client; or from the terminal:
vike-data call hl_segments --args '{"view": "cohorts"}'
Try it
/v1/hl-cohorts/cohortsList Segments
- Route
GET /v1/hl-cohorts/segments- CLI
vike-data hl segments- MCP
hl_segments· 1 credit- Cost
- 1 request of the hourly limit
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
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hl-cohorts/segments"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/segments",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl segments
# any MCP client; or from the terminal:
vike-data call hl_segments --args '{"view": "segments"}'
Try it
/v1/hl-cohorts/segmentsOne cohort and the whole board
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
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
| Parameter | In | Description |
|---|---|---|
cohortrequired | path | A label or slug from /cohorts / /segments |
axis | query | size | pnl | pnl_abs. Default size. Per-wallet ladders only — trade_size is per-position and is rejected here; see the groupable field on /segments. |
openedWithin | query | 24h | 7d | 30d | all. Default all |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hl-cohorts/segments/whale/summary?axis=size"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/segments/whale/summary?axis=size",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl segments --view summary --cohort whale --axis size
# any MCP client; or from the terminal:
vike-data call hl_segments --args '{"view": "summary", "cohort": "whale", "axis": "size"}'
Try it
/v1/hl-cohorts/segments/{cohort}/summary?axis={axis}&openedWithin={openedWithin}Heatmap
- Route
GET /v1/hl-cohorts/heatmap- CLI
vike-data hl segments- MCP
hl_segments· 1 credit- Cost
- 1 request of the hourly limit
Coin x cohort positioning matrix — the whole board in one call.
Parameters
| Parameter | In | Description |
|---|---|---|
axis | query | size | pnl | pnl_abs. Default size. Per-wallet ladders only — trade_size is per-position and is rejected here; see the groupable field on /segments. |
openedWithin | query | 24h | 7d | 30d | all. Default all |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hl-cohorts/heatmap?axis=size"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/heatmap?axis=size",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl segments --view heatmap --axis size
# any MCP client; or from the terminal:
vike-data call hl_segments --args '{"view": "heatmap", "axis": "size"}'
Try it
/v1/hl-cohorts/heatmap?axis={axis}&openedWithin={openedWithin}One coin, hour by hour
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
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
| Parameter | In | Description |
|---|---|---|
symbolrequired | path | HL ticker |
axis | query | size | 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 |
start | query | ISO-8601 UTC. Oldest edge of the window. Omit it and the window is the hours newest hourly buckets at or before end. |
end | query | ISO-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. |
openedWithin | query | 24h | 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. |
hours | query | Number 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). |
cursor | query | Cursor over hours |
grading | query | realized | 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. |
labelBasis | query | current | 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. |
include | query | Comma 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
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/cohort-metrics?axis=size&hours=720"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/cohort-metrics?axis=size&hours=720",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl cohort-metrics BTC --axis size --hours 720
# any MCP client; or from the terminal:
vike-data call hl_cohort_metrics --args '{"symbol": "BTC", "axis": "size", "hours": 720}'
Try it
/v1/hl-cohorts/symbols/{symbol}/cohort-metrics?axis={axis}&hours={hours}&labelBasis={labelBasis}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
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
| Parameter | In | Description |
|---|---|---|
symbolrequired | path | HL ticker |
start | query | ISO-8601 UTC. Oldest edge of the window. Omit it and the window is the limit newest hourly rows at or before end. |
end | query | ISO-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. |
axis | query | size | pnl | pnl_abs. Omit for all cohorts combined |
cohort | query | One label on that axis |
openedWithin | query | 24h | 7d | 30d | all. Default all. Rolling per bucket, not relative to now — each hour counts positions opened in the window before THAT hour. |
limit | query | Default 500, max 5000. One row per hour, so also the window length in hours when start is omitted |
cursor | query | Keyset cursor |
labelBasis | query | current 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
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/metrics?axis=size&cohort=whale&limit=720"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/metrics?axis=size&cohort=whale&limit=720",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl market --view symbol_metrics --symbol BTC --axis size --cohort whale --limit 720
# any MCP client; or from the terminal:
vike-data call hl_market --args '{"view": "symbol_metrics", "symbol": "BTC", "axis": "size", "cohort": "whale", "limit": 720}'
Try it
/v1/hl-cohorts/symbols/{symbol}/metrics?axis={axis}&cohort={cohort}&limit={limit}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
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
| Parameter | In | Description |
|---|---|---|
symbolrequired | path | HL ticker |
start | query | ISO-8601 UTC. Oldest edge. Omit it for the hours newest hours. |
end | query | ISO-8601 UTC. Newest edge; with no start, the window is the hours newest hours at or before it. |
hours | query | Window length in hours when start is omitted. Default 168, max 8760. |
limit | query | Max 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
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/indices?hours=720"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/indices?hours=720",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl market --view indices --symbol BTC --hours 720
# any MCP client; or from the terminal:
vike-data call hl_market --args '{"view": "indices", "symbol": "BTC", "hours": 720}'
Try it
/v1/hl-cohorts/symbols/{symbol}/indices?hours={hours}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
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
| Parameter | In | Description |
|---|---|---|
symbolrequired | path | HL ticker |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/size-breakdown"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/size-breakdown",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl market --view size_breakdown --symbol BTC
# any MCP client; or from the terminal:
vike-data call hl_market --args '{"view": "size_breakdown", "symbol": "BTC"}'
Try it
/v1/hl-cohorts/symbols/{symbol}/size-breakdownSymbol 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
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
| Parameter | In | Description |
|---|---|---|
symbolrequired | path | HL ticker |
min_usd | query | Smallest fill in USD. Default and MINIMUM 50000: the feed behind this route holds nothing smaller. |
hours | query | How far back, in hours. Default and max 24: the feed holds a day. |
limit | query | Max rows, newest first. Default 200 (all the feed holds per symbol), max 1000; has_more says a lower limit cut rows. |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/large-fills?min_usd=100000&limit=50"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/large-fills?min_usd=100000&limit=50",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl market --view large_fills --symbol BTC --min-usd 100000 --limit 50
# any MCP client; or from the terminal:
vike-data call hl_market --args '{"view": "large_fills", "symbol": "BTC", "min_usd": 100000, "limit": 50}'
Try it
/v1/hl-cohorts/symbols/{symbol}/large-fills?min_usd={min_usd}&limit={limit}One cohort across every coin
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
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
| Parameter | In | Description |
|---|---|---|
cohortrequired | path | A label or slug of axis, from /cohorts / /segments |
axisrequired | query | size | pnl: this endpoint serves history for these two only. Required. |
start | query | ISO-8601 UTC. Oldest edge. Omit it for the hours newest hours. |
end | query | ISO-8601 UTC. Newest edge; with no start, the window is the hours newest hours at or before it. |
hours | query | Window length in hours when start is omitted. Default 168, max 8760. |
limit | query | Max 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). |
include | query | Comma 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
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hl-cohorts/segments/whale/history?axis=size&hours=720"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/segments/whale/history?axis=size&hours=720",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data hl segments --view history --cohort whale --axis size --hours 720
# any MCP client; or from the terminal:
vike-data call hl_segments --args '{"view": "history", "cohort": "whale", "axis": "size", "hours": 720}'
Try it
/v1/hl-cohorts/segments/{cohort}/history?axis={axis}&hours={hours}&include={include}Stream catch-up
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
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
| Parameter | In | Description |
|---|---|---|
channelrequired | query | hl.cohort_bias, hl.cohort_flow, hl.cohort_leverage, hl.indices |
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/hl-cohorts/stream-events?channel=hl.cohort_bias&limit=50"
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/stream-events?channel=hl.cohort_bias&limit=50",
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
)
print(r.json())
vike-data stream events hl.cohort_bias --limit 50
# any MCP client; or from the terminal:
vike-data call stream_events --args '{"channel": "hl.cohort_bias", "limit": 50}'
Try it
/v1/hl-cohorts/stream-events?channel={channel}&limit={limit}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.
A cohort index crosses a level
- Type
hl_cohort_bias
hl_cohort_bias = the cohort index of token_symbol (required) crossing threshold by band, filtered by direction.
- 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).
Create it on your account, no code →
{
"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"
]
}
vike-data alerts create --type hl_cohort_bias --token BTC --index smi --threshold 0.3 --band 0.1 --direction up --channel email
vike-data call alerts_create --args '{"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"]}'
A cohort fills 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 cohort gets liquidated
- 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": "Whale liquidations over $1M",
"alert_type": "hl_liquidation",
"size_cohorts": [
"4x whale",
"3x whale"
],
"min_value_usd": 1000000,
"channels": [
"webhook"
]
}
vike-data alerts create --type hl_liquidation --size-cohorts "4x whale" "3x whale" --min-usd 1000000 --channel webhook
vike-data call alerts_create --args '{"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.
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.
Before you rely on it
Freshness, label basis, gaps and what a null means: what the field names do not tell you.
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./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.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./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.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.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.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 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.
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
The code
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},
)
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# the time of the newest 5-minute snapshot
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/indices?hours=1",
headers={"X-API-Key": API_KEY},
)
# then: jq '.latest.ts'
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# the newest snapshot in the latest positions file and when the file was written
r = requests.get(
"https://data.vike.io/v1/hyperliquid/archive/datasets",
headers={"X-API-Key": API_KEY},
)
# then: jq '.latest.positions | {snapshot_ts, generated_at}'
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# the two newest hourly rows of BTC, the first one provisional
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/metrics?limit=2",
headers={"X-API-Key": API_KEY},
)
Limits
One free key for everything on this page.
| Key | free: sign in at vike.io and create one; every key starts vk_ |
|---|---|
| Requests | 20,000 per hour per key, a rolling hour; MCP calls share it |
| Per minute | 200 per minute per key |
| Keys | up to 3 active per account; each key is its own budget |
| Cost of a call | 1 per REST request; an MCP tool 0 to 3 credits |
| WebSocket | not counted: connecting, subscribing and the frames you receive |
| Where it is stated | x-vike-limits in the OpenAPI document, /data/openapi.json |
| Price | $0, nothing is billed |
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.
- 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.