Vike Data API
面向 AI 智能体的加密数据:链上钱包与代币、Hyperliquid、HL Cohorts 和 Polymarket。
托管 MCP 服务器
npx add-mcp 'https://data.vike.io/mcp'可安装到 Claude Code、Codex、Cursor 等客户端
文档
LIVE --:--:-- UTC Hyperliquid positions snapshot on a 5-min UTC grid — last boundary --:-- next in -:--
- Sign in at vike.io Any account. No card, no plan to pick.
- Create a key
On /account?tab=api-keys. Up to 3 active keys per account; every key starts
vk_. You can read a key back later from the same page. - Send the header
X-API-KEY: vk_…on every request. OAuth clients:Authorization: Bearer vk_oat_…. Claude Desktop needs no key at all.
Reference · every section of this page
- Alerts API
- Alert feed
- WebSocket
- Resuming 2. Filters 3. Close codes
- CLI
- MCP
- Clients 2. Alert tools 3. Search and streams 4. Candles 5. Perps
- Glossary
- Not covered
- License
Each product's own routes, channels and tools: On-chain, Hyperliquid, HL Cohorts, Polymarket, Predict.fun. What changed: Changelog.
Your first request
Choose how to connect, make a first call, download a first file.
| You are building | Use | Start at |
|---|---|---|
| A script, a notebook, a backfill | REST: JSON over HTTPS at https://data.vike.io/v1 | First request, then each product's routes |
| An AI client: Claude, ChatGPT, Cursor, Gemini CLI, Copilot | MCP at https://data.vike.io/mcp | Clients |
| A terminal session or a coding agent | the vike-data CLI | CLI |
| Live events: fills, liquidations, transfers, trades | the WebSocket at wss://stream.vike.io | WebSocket |
| A research dataset, or a backtest over whole days | the Parquet archives | First archive file |
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/ohlcv?symbol=BTC&interval=1h&limit=3"
Your first archive file
# the index needs no key: every date and family, row counts, byte sizes, the schema
curl -s https://data.vike.io/archive/manifest.json | jq '.dates[0]'
# a full day takes two requests: ask for the link with your key (one request) ...
URL=$(curl -s -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/polymarket/archive/url?date=2026-09-01&stream=trades" | jq -r '.url')
# ... then download it: the link is the credential, valid 7 days
curl -f -o trades-2026-09-01.parquet "$URL"
- Neither download spends your hourly budget. Asking for a link is one ordinary request.
- The Hyperliquid samples are
positions-sample.parquetandfills-sample.parquet: BTC on one published day, every open position at its first snapshot and every fill of its first hour, rebuilt about once a week;hl-samples/manifest.jsonnames the day. - Download whole files rather than reading them over HTTP: a Parquet reader pointed at a URL makes a Range request per column chunk (one 1.26 GB partition was measured at 20,237 requests). Limits has why.
- The files themselves: Polymarket historical data and Hyperliquid historical data.
Two credentials, tried in this order on every surface: Authorization: Bearer vk_oat_… (an OAuth 2.1 access token) and then X-API-KEY: vk_… (an API key). A bearer that does not validate falls through to the key header, so a request may carry either or both; both reach the same account.
| Surface | Credential |
|---|---|
https://data.vike.io/v1/… | X-API-KEY or an OAuth bearer |
https://data.vike.io/archive/manifest.json, samples/ and hl-samples/ | none: they are open |
| an archive file link (Cloudflare R2, valid 7 days) | none: the link is the credential. The key goes on the call that asks for it, https://data.vike.io/v1/polymarket/archive/url or https://data.vike.io/v1/hyperliquid/archive/url |
https://data.vike.io/mcp | OAuth for desktop clients, X-API-KEY for everything else |
wss://stream.vike.io/v1/stream/… | either header on the handshake, or ?token=vk_… for a browser, which cannot set WebSocket headers (details) |
- Header names are case-insensitive:
X-API-KEYandX-API-Keyare the same header. - The credential always travels in a header. A key in a query string authenticates nothing on REST, the archive or MCP, and no cookie or vike.io session does either.
- Up to 3 active keys per account; one more is refused with a
400. Deactivating or deleting a key frees its slot. Every key startsvk_: match on that prefix, never on the length. - Keys are recoverable. An encrypted copy is stored, so /account?tab=api-keys shows a key again later. Only keys made before that existed cannot be read back (the page answers
410): make a new one. - Every key has the same reach: there are no scopes and nothing to narrow.
- An account whose e-mail address is not confirmed is refused with
403and codeEMAIL_UNCONFIRMEDon/v1, the archive,/mcpand the stream. Open the link in our e-mail, or get a new one at vike.io/account; the same credential works again within 45 seconds.
OAuth 2.1
Claude Desktop, ChatGPT and other clients that support Dynamic Client Registration (RFC 7591) need no key: the client registers itself and you approve in a browser. The server is on data.vike.io, sign-in stays on vike.io: an unauthenticated call to /mcp is answered 401 with WWW-Authenticate: Bearer resource_metadata="https://data.vike.io/.well-known/oauth-protected-resource", and that document names https://vike.io as the authorization server.
| Field | Value |
|---|---|
| Metadata (RFC 8414) | https://vike.io/.well-known/oauth-authorization-server |
| Registration | POST https://vike.io/oauth/register |
| Authorization / token / revocation | /oauth/authorize, /oauth/token, /oauth/revoke on https://vike.io |
| Scope | mcp:tools only; anything else is invalid_scope |
| Grants | authorization_code, refresh_token; response type code |
| PKCE | S256, mandatory |
| Lifetimes | code 10 minutes, access token 1 hour, refresh token 30 days; a refresh rotates the token |
| Access tokens | start vk_oat_, sent as Authorization: Bearer |
| Redirect URIs | claude.ai and claude.com, loopback addresses (localhost, 127.0.0.1, [::1]), and exact vendor callbacks for ChatGPT, Cursor web, VS Code and Perplexity; any other is invalid_redirect_uri, and that client uses a key |
Connected apps are listed, and revocable, under Account → API Keys → Connected Apps. Revoking one revokes every token that client holds for your account and leaves your other clients alone. All of an account's OAuth tokens share one hourly budget; each API key has its own. Tokens, codes and client secrets are stored only as SHA-256 hashes: a plaintext value is shown once, when it is issued.
Revocation takes seconds, not zero
- A valid credential is trusted for up to 45 seconds before it is checked again, so a deleted key or a revoked token can keep working for that long.
- An invalid credential is remembered for up to 8 seconds: after fixing a typo, give it a moment.
- A check that fails for an infrastructure reason is never cached, so an outage cannot turn into a rejection of a valid key.
Keeping a key safe
- Keep it in an environment variable or a secret store, never in source control or a URL.
- Rotate by creating the new key first, moving traffic to it, then deleting the old one.
- If a key leaks, delete it: it stops authenticating within 45 seconds.
# an API key
curl -H "X-API-KEY: $VIKE_API_KEY" "https://data.vike.io/v1/onchain/tokens/search?q=USDC&limit=3"
# an OAuth access token
curl -H "Authorization: Bearer $VIKE_OAUTH_TOKEN" "https://data.vike.io/v1/onchain/tokens/search?q=USDC&limit=3"
Limits
One free key: 20,000 requests an hour, 200 a minute.
| Requests | 20,000 per hour, per key: a rolling hour, so capacity returns continuously |
|---|---|
| Per minute | 200 per minute, per key, on /v1 only: a sustained rate, with room for a short burst above it |
| Keys | Up to 3 active per account. Each key is its own budget. |
| OAuth apps | All of an account's OAuth tokens share one pooled budget |
| Price | $0: nothing is billed |
| Archive downloads | Not counted against the hourly limit. A file needs a key only to ask for its signed link (one ordinary request), and the download itself carries none |
| Archive links | Valid 7 days, for any number of requests (range reads included). No download quota |
| Over the limit | 429 with Retry-After: 60 over the hourly limit, Retry-After: 1 over the per-minute cap |
| Failed requests | Count toward the hourly ceiling like any other: back off on a 429, do not retry in a tight loop |
| Headers | Every /v1 response, the 429 included, carries X-RateLimit-Limit (the hourly budget) and X-RateLimit-Remaining. There is no X-RateLimit-Reset: a rolling hour has no reset instant |
| WebSocket | Connecting, subscribing and the frames you receive are not counted |
| Keyless | /archive/samples/ · /archive/hl-samples/ · /archive/manifest.json |
One ceiling, shared. MCP tool calls and REST calls made with the same key draw on the same 20,000-an-hour counter, and the counter is per key: keys are how you keep a bulk job from starving an interactive client. The per-minute cap, 200 a minute per key, applies to /v1 requests only, never to MCP or archive downloads.
Why the archive is not counted. The ceiling counts HTTP requests, and requests are the wrong unit for bulk files: a Parquet reader pointed at a URL fetches the footer and then every column chunk as its own Range request, thousands for one daily partition. So you ask once for a file's link, one ordinary request, and what you do with the link is charged against nothing.
Download whole files anyway. curl -O pulls a partition once and curl -C - -O resumes an interrupted one; remote-reading re-fetches the footer and column chunks on every query.
A backstop, not a meter. The ceiling is enforced approximately: each server process keeps its own count and re-syncs it on a short interval, and a failed re-sync reuses the last count rather than opening the gate. Design to stay under it, not to spend it to the last request. Over the per-minute cap, wait a second and carry on, or pace a bulk job to under 200 requests a minute (about 3 a second).
Credits
A credit is an accounting figure written to the usage row of each call. Nothing compares it against a balance, so it never blocks a call. A call that fails (a 400, 429 or 503) logs 0 credits, though it still spends one unit of the hourly limit, which counts requests, not credits.
REST: 1 credit per call, except:
| Route | Credits |
|---|---|
GET /v1/alerts/channels | 0 |
POST /v1/alerts/channels/webhooks | 2 |
POST /v1/alerts/channels/{channel_id}/rotate-secret | 2 |
GET /v1/alerts/rules | 0 |
POST /v1/alerts/rules | 3 |
POST /v1/alerts/rules/{alert_id}/delete | 2 |
POST /v1/alerts/rules/{alert_id}/edit | 2 |
GET /v1/hyperliquid/ohlcv/export | 2 |
MCP: each tool has its own price, 0 to 5 credits; 6 tools at 0 credits; 50 tools at 1 credit; 20 tools at 2 credits; 5 tools at 3 credits; 1 tool at 4 credits; 1 tool at 5 credits. Each tool's price is on its card: On-chain, Hyperliquid, HL Cohorts, Polymarket, Predict.fun and, below, the shared tools.
Your usage
Recent calls per key, with each one's credits, status and latency, are under Account → Usage (vike.io/account?tab=usage). Rate-limited calls are recorded with 0 credits, so a client hammering past the ceiling shows up rather than looking as if it stopped.
Each call is listed under a label: mcp for a tool call, api_<namespace>_<resource> for a /v1 route (the first two path segments, api_onchain_wallets for /v1/onchain/wallets/…). An archive file link is not recorded; the request that asked for it is. The labels separate your usage, not your budget: mcp and /v1 draw on the same counter.
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 market or dex, a malformed identifier, a cursor from another query. Polymarket routes also refuse unknown query parameters. | Hyperliquid / on-chain {"detail": "…"} · Polymarket {"error": "…"} |
| 401 | Missing, invalid or deactivated credential, on /v1, the archive and /mcp alike. | the UNAUTHORIZED envelope below (MCP: a JSON-RPC error) |
| 403 | A valid credential of an account whose e-mail address is not confirmed (EMAIL_UNCONFIRMED), or a Hyperliquid download link past its 7 days or altered (answered by Cloudflare R2, an XML body). | the envelope with code EMAIL_UNCONFIRMED |
| 404 | No such route or archive file. An unknown condition_id on a Polymarket detail route is 200 [], not a 404. A path people often guess carries a hint (below). | {"detail": "Not Found"} |
| 414 | The request line is longer than 16 KB: split a long id list (/v1/polymarket/events takes at most 100 token_ids). | the envelope with code URI_TOO_LONG |
| 422 | A parameter failed type or range checking (limit above its maximum, a word where a number goes). Polymarket reads its parameters as text and answers 400 instead. | {"detail": [ … ], "error": "…"} |
| 429 | The hourly limit, or on /v1 the per-minute cap (200 a minute per key). | Retry-After: 60 (hourly) or Retry-After: 1 (per-minute), the RATE_LIMITED envelope |
| 503 | The data store failed. Not your request: retry with back-off. | Hyperliquid / on-chain {"detail": "upstream unavailable"} · Polymarket {"error": "upstream unavailable"} |
{
"error": {
"code": "UNAUTHORIZED",
"message": "invalid or missing credential",
"docs": "https://vike.io/data/api"
}
}
- A failed query is never a 200 with an empty body. An empty list means there were no rows; a store outage is a
503. - Every refusal our servers produce carries a JSON body, the
401and the414included. A hop in front of us can still answer with its own HTML page (a Cloudflare 5xx), so branch on the status code, not the body. - A
429on a route usually called in a loop carries ahintnaming the bulk call that replaces the loop, for example on/v1/polymarket/settlements/{condition_id}. MCP's 429 carries none. - The
/v1/onchainroutes and Hyperliquid/tradersand/positions/matchrun through a shared tool layer that reports its own failures in-band, so an internal error there can arrive as a400carrying the error text. - MCP answers a bad credential with HTTP 401, a
WWW-Authenticate: Bearer resource_metadata=…header and a JSON-RPC error (-32000,Invalid or missing API key); a rate-limit trip there is a real429. A failed tool call is an HTTP200whose result carriesisError: true: check the body, not just the status (MCP).
A 404 on a path you expected
The paths people guess most often, and the call that answers the same question. A 404 on one of them carries the answer in a hint field; detail is unchanged.
| You tried | Use instead |
|---|---|
/v1/hyperliquid/wallets/{address}/positions | /v1/hyperliquid/positions?wallet={address}; add start and end (ISO-8601 UTC) for history, symbol to narrow it |
/v1/hyperliquid/candles | /v1/hyperliquid/ohlcv?symbol=BTC&interval=1h |
/v1/hl-cohorts/cohort-metrics | /v1/hl-cohorts/symbols/BTC/cohort-metrics: the symbol is in the path |
/v1/hl-cohorts/positions | only the cohort routes live under /v1/hl-cohorts; positions, leaderboards, wallets, liquidations, builders and the archive are under /v1/hyperliquid |
/v1/polymarket/settlements | /v1/polymarket/settlements/bulk for many markets, /v1/polymarket/settlements/{condition_id} for one |
/v1/marketdata/funding | not part of the API: centralised-exchange data is not offered |
A 429 names the bulk call
| Route | hint |
|---|---|
/v1/polymarket/settlements/{condition_id} | Many markets? GET /v1/polymarket/settlements/bulk returns up to 5,000 settlements per request (filter with slug_prefix, from_ms, to_ms; page with cursor). |
/v1/polymarket/markets/{condition_id} | Many markets? GET /v1/polymarket/markets returns up to 100 per request (query= filters by slug). |
/v1/polymarket/events | For bulk history download the daily Parquet files: GET /v1/polymarket/archive/datasets lists them, GET /v1/polymarket/archive/url returns one; downloads do not count against the request limits. |
/v1/polymarket/market_stats | For bulk history download the daily Parquet files: GET /v1/polymarket/archive/datasets lists them, GET /v1/polymarket/archive/url returns one; downloads do not count against the request limits. |
/v1/polymarket/ohlcv | For bulk history download the daily Parquet files: GET /v1/polymarket/archive/datasets lists them, GET /v1/polymarket/archive/url returns one; downloads do not count against the request limits. |
/v1/polymarket/orderbook | For bulk history download the daily Parquet files: GET /v1/polymarket/archive/datasets lists them, GET /v1/polymarket/archive/url returns one; downloads do not count against the request limits. |
/v1/hyperliquid/ohlcv | Many symbols? GET /v1/hyperliquid/ohlcv/latest returns the newest 1-minute candle of every symbol that traded in the last hour, in one request. |
/v1/hyperliquid/positions | For bulk history download the daily Parquet files: GET /v1/hyperliquid/archive/url?dataset=positions&date=YYYY-MM-DD returns a link (index: GET /v1/hyperliquid/archive/datasets); downloads do not count against the request limits. |
/v1/hyperliquid/wallets/{address}/fills | For bulk history download the daily Parquet files: GET /v1/hyperliquid/archive/url?dataset=fills&date=YYYY-MM-DD returns a link (index: GET /v1/hyperliquid/archive/datasets); downloads do not count against the request limits. |
Advisory headers
Some answers carry advice in a header; the body never changes, so two identical requests still return identical bytes.
| Header | When |
|---|---|
Deprecation: true and Link: <…>; rel="successor-version" | A /v1/hyperliquid request used a retired spelling of a parameter or a path. The Link is the same request in the current spelling; the answer itself is unchanged. |
X-Vike-Note | An empty answer from /v1/polymarket whose reason the request shows: a window that ends before 2026-07-25, when order-book recording starts (/ohlcv, /events, /orderbook); from after to on /ohlcv; a condition_id that is not 0x and 64 hex digits. |
X-Vike-Hint | About the hundredth call in a minute from one key to /v1/polymarket/settlements/{condition_id} or /v1/polymarket/markets/{condition_id}: names the bulk route, as the 429 hint does. |
Pagination
Four ways a route pages, and the on-chain routes that do not.
Hyperliquid and HL Cohorts page by keyset, never by offset. A list response carries nextCursor; pass it back verbatim as cursor until it is null or a page comes back empty. On most list routes limit defaults to 100 and goes to 5,000; each route's card states its own bounds. Keyset paging costs the same on every page and keeps rows from shifting between pages when new ones arrive.
import os, requests
H = {"X-API-KEY": os.environ["VIKE_API_KEY"]}
url = "https://data.vike.io/v1/hyperliquid/positions"
cursor = None
while True:
params = {"symbol": "BTC", "limit": 5000} | ({"cursor": cursor} if cursor else {})
page = requests.get(url, params=params, headers=H, timeout=60).json()
rows = page["positions"] # the list key differs per route
if not rows:
break
handle(rows)
cursor = page.get("nextCursor") # null on the last page
if cursor is None:
break
Try the pager on a real captured answer: Hyperliquid AI & API → Paging.
There is no single convention across the namespaces, and a client written against one silently stops short against another:
| Style | Example route | Move to the next page | Stop when |
|---|---|---|---|
| Cursor | /v1/hyperliquid/liquidations | pass nextCursor back as cursor, every other parameter unchanged | nextCursor is null, or a page comes back empty |
| Cursor in an envelope | /v1/polymarket/settlements/bulk | pass next_cursor back as cursor, every other parameter unchanged | has_more is false |
| Offset | /v1/polymarket/archive/datasets | add the rows received to offset | offset + len(datasets) >= total |
| Time window | /v1/hyperliquid/ohlcv | restart start one bar after the last candle you hold | a page shorter than limit |
How each namespace pages
| Namespace | How it pages | Pass back |
|---|---|---|
/v1/hyperliquid and /v1/hl-cohorts list routes | Keyset. The response carries nextCursor (a string, or null), opaque and bound to the query that issued it. | cursor=<nextCursor>, verbatim |
/v1/hyperliquid/ohlcv | No cursor: one page per request; move start / end. | — |
/v1/polymarket/events | {"events": [...], "has_more": …, "next_cursor": …}, at most 24 hours per call. | cursor=<next_cursor> while has_more is true |
/v1/polymarket/settlements/bulk | {"settlements": [...], "has_more": …, "next_cursor": …}, oldest resolution first, up to 5,000 a page. | cursor=<next_cursor> while has_more is true |
Other /v1/polymarket routes | A bare JSON array, except /archive/datasets ({"datasets": [...], "limit", "offset", "total"}, limit 200 by default, at most 1,000) and /leaderboard (offset up to 1,000); /wallets/{address} and /archive/url return one object. | offset |
/v1/onchain | Does not page: limit only, one page (below). | — |
Details that decide whether the walk is complete
- A full last page still carries a cursor.
nextCursoris set whenever a page holdslimitrows, so a result whose size is an exact multiple ends on an empty page. Stop on either. - A cursor is bound to the query that issued it. Reuse it after changing
start/endor on another route and you get a400telling you to restart without one (nextCursor belongs to a different query mode, ornextCursor was issued for different query parameterson the feeds and orders); a malformed cursor is a400too, never a silent reset to page 1. Send the same filters with every page. - The envelope says when to stop.
next_cursoris a string only whilehas_moreis true; the cursor is a position and the walk resumes strictly after it. totalis the filtered count before paging on the archive index, and the only field that says when to stop there.- Not every list route pages.
/v1/hyperliquid/tradersstops at 100 rows and/v1/polymarket/marketsat 100: narrow the filter instead. - A board rebuilt hourly (
/v1/polymarket/leaderboard) can repeat or skip a wallet across a rebuild: fetch the pages of one walk close together.
Following a feed
The Hyperliquid event feeds, /twaps, /wallets/{address}/twaps, /ledger and /leverage-changes, page by cursor and add a mode for reading forward as events arrive.
order=descis the default, newest first, with the cursor rules above (/ledgerdrops duplicate rows from a page, so stop there on an empty page).order=ascruns oldest first and is the polling mode: keep the lastnextCursorand send it back, every other parameter unchanged. Once you hold a cursor it is nevernullin this mode: stop a bounded walk at the first empty page; a poller keeps the cursor and asks again later. An empty page returns the cursor you sent.- Without a
wallet(or, on/twaps, atwap_id), a call with nostartcovers the 24 hours beforeend. A window of more than 31 days is a400, an old poller's cursor included: restart it withstart. - A forward poll can miss a late row inserted with an older key (the ledger's newest two hours can still receive one). If you must not miss one, re-read an overlap with
startand drop what you hold by the full key:ts,hash,walleton the ledger;ts,twap_id,statuson the TWAP feeds;ts,wallet,symbolon the leverage feed. - A feed trails the venue by a minute or more (Freshness): an empty poll is "nothing new yet".
On-chain: one page and a silent cap
- No
/v1/onchainroute takes a cursor or an offset. Each returns one page, and nothing in the answer says it was cut. limitis checked against one ceiling and applied under another:/wallets/{address}/transactionsaccepts up to 1000 and returns at most 500,/wallets/{address}/counterpartiesaccepts up to 1000 and returns at most 50. Each parameter list names both.- A full page at the cap is not the whole answer. Narrow the question instead: a shorter
windowor fewerdays, a highermin_usd, adirectionor onetoken_address, in several smaller calls.
Every dataset has its own request builder, fields and loading code on its product's pages: On-chain, Hyperliquid, HL Cohorts and Polymarket.
Conventions
Times, windows, identifiers and the two Polymarket differences.
| Where | Field | Format |
|---|---|---|
| Hyperliquid rows: positions, fills, cohort metrics, liquidations | ts, snapshot_ts, liq_ts | ISO-8601 string with offset, 2026-10-12T09:00:00+00:00 |
Hyperliquid /ohlcv | ts (bar open) | epoch milliseconds |
Hyperliquid /symbols/hourly | ts | epoch seconds |
| Polymarket: API and archive | ts, local_ts, as_of_ts, first_ms, last_ms | epoch milliseconds. ts is the venue's event time, local_ts the recorder's receive time |
| Polymarket trader routes | row times | YYYY-MM-DD HH:MM:SS UTC strings |
Every time is UTC. There is no API-wide window rule: each route's parameter list is the authority.
/v1/hyperliquidand/v1/hl-cohortstakestartandendas ISO-8601 UTC where a route takes a range, most keeping a row stamped exactly atend./ohlcv,/ohlcv/exportand/symbols/hourlyare half-open (endexclusive, so consecutive windows tile) and also accept epoch time. Other routes take a namedwindow(traders, leaderboards, builders) or a number ofhours(cohort series)./v1/onchaintakes no timestamps: a named window (window,period) or a number ofdaysback from now./v1/polymarketspells it per route:/ohlcvtakesfrom/toin epoch milliseconds with an inclusiveto;/eventsand/settlements/bulktakefrom_ms/to_ms, half-open; the archive index takesfrom/toas dates, both inclusive.
| Identifier | Means |
|---|---|
symbol | A Hyperliquid instrument: BTC, a HIP-3 market xyz:TSLA, a spot pair @107. Until 2026-11-19 responses also carry its old name (asset or coin) |
wallet · address | Hyperliquid and EVM wallets, 0x and 40 hex characters |
cohort | A Hyperliquid cohort rung on the chosen axis; paths take its slug |
token_id | Polymarket: one outcome token (a decimal string; keep it a string) |
condition_id | Polymarket: the market, 0x -prefixed. Joins the API to the archive |
There is no API-wide identifier: hyperliquid and hl-cohorts take a symbol, onchain an address, polymarket a token_id on its book and tape routes and a condition_id on its market and settlement routes. A response echoes the identity it resolved.
Two /v1/polymarket differences. Its 400 and 503 bodies are {"error": "<message>"} where the other namespaces answer {"detail": …}, and it reads its query parameters as text: an unknown parameter is a 400 naming the one you probably meant, never the framework's 422.
Every gated response carries Cache-Control: private, no-store, and every /v1 response a Link header to the spec (rel="service-desc").
Data-quality notes
- Two identical requests return identical bytes. Aggregates are snapped to a fixed precision before they leave the server, so a re-run of a query cannot drift in its last digits, and a response can be cached, diffed or content-addressed.
- Cohort labels declare their basis. Graded Hyperliquid responses say whether a label is
point_in_time(as it was that hour) orcurrent(today's):label_basison the envelope orpnl_cohort_basison the row. - Polymarket recording gaps have markers, and `seq` is not one. A lost socket shows as
statusrows:gap_start, thenlive_resumewhen the socket answers again, which does not prove rows have resumed: the token's next row does.stalemeans no fresh data while the socket stayed up. A recorder restart writes nostatusrow:seqstarts again at 1 on the token's nextbook. Otherwiseseqcounts the frames the recorder applied, so it runs on across a hole, and a jump means only rows dropped after they arrived.manifest.jsonlists every partition with its byte size and, where recorded, its row count. - Feed delay is measurable per event. Polymarket rows carry both the venue's
tsand the recorder'slocal_ts. - A failure is never an empty 200, and an unknown Polymarket id is an empty array, not a 404. Both under Errors.
Freshness and completeness
How current each answer is, and how an answer marks what it leaves out.
Data reaches the API a little behind the clock, and some answers carry an old value in the shape of a current one. Each item names the field or the rule that tells the two apart.
On-chain
- Balance amounts are live:
/wallets/{address}/balancesreads each amount from the chain at request time; which tokens it checks comes from our copy of the wallet's transfers, so a token first received after the last sync is not listed yet. - Transfers trail the chain by minutes: a sync extends our copy of each chain every few minutes, and a transfer's USD value arrives when pricing runs, so
min_usdabove 0 can skip the newest, still unpriced hours. - Leaderboard windows are rebuilt once a day: the
24h,7d,30dand90dfigures of/wallets/discovercan be up to about a day old. Onbaseandhyperevma window other thanallwhose data is over 2 days old is a400, not an old answer.
Hyperliquid
- Position matching has a 15-minute gate:
/positions/matchmatches only positions seen within 15 minutes of the newest snapshot batch, so it returns fewer rows than the position history holds. - The daily files appear in
/archive/datasetsonce the day is complete, early the next UTC morning; thelatestpositions file is replaced about every 5 minutes, andlatest.positionscarries itssnapshot_tsandgenerated_at. - Event feeds trail the venue: TWAP statuses within a minute or two, ledger events about a minute (the newest two hours can still gain a late event; each hour is re-read about two hours after it ends), leverage changes a few minutes to about twenty after the change. A row's
tsis when the event happened.
Orders: the snapshot is the clock
/orders, /wallets/{address}/orders and /symbols/{symbol}/order-clusters answer from a snapshot, not from the live book. snapshotTs (snapshot_ts on the clusters route) names the instant an answer describes, and source names the feed that answered (on the wallet route, the feed behind the newest snapshot, not the window): l4_5m, the 5-minute snapshots from 2026-10-23 21:45 UTC, or state_12m, the checkpoints taken about every 12 minutes from 2026-07-29. Read snapshotTs for the age of an answer rather than assume it. On l4_5m the instant is a snapshot on the 5-minute grid and the answer holds every order seen in it, except for an at inside an outage of that feed (below).
For a past at answered from checkpoints, snapshotTs is your at, not a checkpoint. For a past at answered from the checkpoints (before 2026-10-23 21:45 UTC, or after a stalled 5-minute feed's last snapshot), snapshotTs is the at floored to 5 minutes, not a checkpoint instant. Checkpoints are about 12 minutes apart and off the 5-minute grid, so an order is listed only if a checkpoint at or before that instant saw it and another at or after it saw it again. An order placed after the earlier checkpoint, or gone before the later one, is missing although it was resting at snapshotTs. An at inside an outage of either feed is answered the same way, from the sightings on both sides of the gap, with a normal-looking source. Without at, a state_12m answer is the newest checkpoint and snapshotTs is its instant; /wallets/{address}/orders has no at and names the newest snapshot, or the snapshot of page 1 on a cursor page.
If the 5-minute feed stops, a call without at is answered from the 12-minute checkpoints. When the newest 5-minute snapshot is more than 15 minutes behind the newest checkpoint, a call without at, and an at after that feed's last snapshot, are answered from the checkpoints and source says state_12m, instead of serving a stale 5-minute snapshot in the shape of a current one.
HL Cohorts
- The newest row of
/symbols/{symbol}/metricsis provisional:provisional: trueon at most one row, the hour in progress; itstsmoves on every poll, so drop it before storing a series. /symbols/{symbol}/cohort-metricscarriesfreshnesson a window that reaches the present:newest_ts,behind_hours,settled_lag_ceiling_hoursandstale, the pre-computed verdict. Read the age there, never from prose.grading=realized-pitis rebuilt once a day, just after 00:00 UTC, through 23:00 of the last complete UTC day, so its newest hour is 1 to 25 hours old;freshness.stalesays so. For live use takegrading=realizedwithlabelBasis=point_in_time, one hour behind the clock. A window ending in the past carries nofreshnessblock.
Polymarket
- A book snapshot is as old as
as_of_tson/orderbook, not thetsyou asked for. - The recorded book starts on 2026-07-25:
/ohlcv,/eventsand/orderbookanswer empty before it, with anX-Vike-Noteheader saying why. - The leaderboard is rebuilt hourly; its
24h,7dand30dwindows can be revised by a later rebuild. - Bulk settlements are rebuilt every 5 minutes: a market that has just resolved can take up to about 5 minutes to appear in
/settlements/bulk;/settlements/{condition_id}is always current. - The archive: read
generated_atinmanifest.jsonbefore trusting a coverage figure.
What an answer can leave out
- On-chain routes return one page, capped (On-chain: one page).
- On-chain history starts on a different date per chain;
/wallets/{address}/first-fundersays where incoverage. /labels/{address}/provenancereturns fewer rows while a label source is being refreshed.- Each Hyperliquid
positionsday in the archive index says how many 5-minute snapshots it holds, andreconstructed_hourslists the hours rebuilt from fills. - The live channels run on demand, and a window nobody subscribed to is a
gapframe, never filled in (WebSocket).
OpenAPI: the spec, Swagger UI and ReDoc
The spec describes every route's parameters; response fields are on each route's card.
The spec is public at vike.io/data/openapi.json: no key, not metered, the same document the API serves plus a top-level x-vike-limits block. Every /v1 response names it in a Link header (rel="service-desc"), so a client holding only a response can find it.
| Surface | URL | Key |
|---|---|---|
| openapi.json, the keyless copy | https://vike.io/data/openapi.json | no |
| openapi.json on the API host | https://data.vike.io/v1/openapi.json | yes |
| Swagger UI | https://data.vike.io/v1/docs | yes |
| ReDoc | https://data.vike.io/v1/redoc | yes |
The three on the API host sit under /v1/ and behind the same header-only gate as the operations they describe (the bare /openapi.json, /docs and /redoc answer 404), and each request there is an ordinary metered call.
openapi.json
What the spec declares, counted from the published document:
| Field | Value |
|---|---|
| OpenAPI version | 3.1.0 |
info.title / info.version | vike.io Data API / 1.0.0 |
servers | https://data.vike.io |
| Operations | 121: 113 GET, 8 POST |
| Parameters | 520, 520 with a description |
components.schemas | HTTPValidationError, ValidationError |
| Response codes declared | 200 on every operation, 422 on 113 |
securitySchemes | OAuth2Bearer, ApiKeyHeader |
x-vike-limits | hourly_limit 20,000, minute_limit 200, credits_per_call 1 and the credit_overrides |
Parameters are the part it carries in full: name, location, type, whether required and a description, the same text each route card on these pages shows. The auth block is real: both ApiKeyHeader (X-API-KEY) and OAuth2Bearer, as a list of single-key requirements, which in OpenAPI means either, not both.
You cannot generate a full client from it. It declares no response schemas (every 200 is an undeclared object; the two components.schemas are validation errors), only 200 and 422 among the status codes the API really returns (Errors), and nothing of the archive or the WebSockets, which OpenAPI has no operation for. Response fields are on each route's card, read from real responses.
curl -s https://vike.io/data/openapi.json -o openapi.json
# what it declares, in one jq
jq '{paths: (.paths | length), params: [.paths[][].parameters // [] | .[]] | length, schemas: (.components.schemas | keys)}' openapi.json
Swagger UI
Swagger UI at https://data.vike.io/v1/docs: the interactive, one-operation-at-a-time view of the same document. It is an HTML page behind the header-only gate, so a browser cannot open it by itself: no cookie, session or ?token= authenticates data.vike.io. Reach it with a header-injecting extension or proxy, or work from the keyless file. The page then fetches /v1/openapi.json itself, a second request behind the same gate, so the injected header must cover that fetch too; its Authorize dialog attaches your key to Try it out calls.
ReDoc
ReDoc at https://data.vike.io/v1/redoc: the same document as one scrollable reference page, behind the same gate as Swagger UI.
Search across every product: GET /v1/search
The vike.io search box as one route: a name, symbol, address, ENS name or pasted link in, every product's matches out.
Search · REST · CLI · MCP
Search by name, symbol or address
Route
GET /v1/search
CLI
vike-data search
MCP
search · 1 credit
Cost
1 credit, one request of the hourly limit
Free API Archive Stream MCP
Tokens, Hyperliquid markets, traders, vaults and builders, labelled wallets and entities on four chains, Polymarket wallets and markets, Predict.fun markets and traders, and did_you_mean, for one query: the vike.io search box, across every product.
{"tokens", "hl_markets", "wallets", "traders", "vaults", "builders", "polymarket", "markets", "predictfun", "did_you_mean"}, each a list of at most limit rows. A token's price_usd and chg_24h_pct are Hyperliquid's, or null for a token not traded there; a symbol listed only on a centralised exchange is not offered (unlike the website's box). A token's chain (ethereum, bsc, base, hyperevm, or another chain it is issued on) and token_address (its contract on that chain, an EVM one as lowercase hex without 0x; null for a coin with no contract) say which token it is. A wallets row carries ens, the address's ENS name, when it has one, and an entity row confidence, "verified" or "predicted", the best label confidence among its wallets; each key is absent when unknown. A wallets row of a labelled entity (entity) has wallet_count and url, the vike.io page listing its wallets; it has an address only when the entity is one wallet (a group has no single address). Several Polymarket markets of one event come back as one markets row with event_slug, title and markets_n. A predictfun row is a Predict.fun market (market_id, title, question, status: open, closed or resolved; open ones, the busiest (24 h volume) first, then the newest, and the others only when nothing else matched) or, for a pasted address that has traded there, that trader (address). A section whose read fails is an empty list (the website's behaviour), so an empty section is not proof of "no match". url fields are vike.io paths. Unlike the website, no Polymarket trader is found by X handle and no handle is returned (Polymarket's terms). One credit.
hl_markets holds Hyperliquid builder markets such as xyz:GOLD; polymarket holds Polymarket wallets by name, pseudonym or address, with the owner of an account or the accounts a personal wallet owns; markets holds open Polymarket markets with every word of q, and closed ones only when nothing else matched; predictfun holds open Predict.fun markets with every word of q (closed and resolved ones only when nothing else matched) and the Predict.fun trader a pasted address names; did_you_mean holds coins one typo away when no token matched. An ENS name is searched as the address it points to, a polymarket.com link as its market, and a type word such as cex, bridge or hacker lists that type's biggest entities.
Parameters
| Parameter | In | Description |
|---|---|---|
q required | query | What to look for, 1 to 256 characters: a name, symbol, label, address, ENS name or polymarket.com link. An address inside a pasted URL is found. Filter words may go with it or alone: chain:base (eth, bsc, base, hyperevm), type:cex (an entity type), pnl>1m or pnl<-100k (traders' all-time PnL; k, m, b); the answer's filters lists the ones understood |
limit | query | Rows per section, 1 to 20. Default 5 |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/search?q=wintermute&limit=2"
A real answer
A real answer: GET /v1/search?q=wintermute&limit=2, captured 2026-11-04 11:03 UTC.
{
"tokens": [],
"hl_markets": [],
"wallets": [
{
"chain": "ethereum",
"label": "Wintermute",
"sub_label": "2 chains",
"is_flagged": false,
"type": "",
"entity": "wintermute",
"wallet_count": 77,
"url": "/onchain/entity/wintermute"
},
{
"chain": "ethereum",
"label": "Wintermute Deposit",
"sub_label": "market_maker · 43 wallets · 4 chains",
"is_flagged": false,
"type": "market_maker",
"entity": "wintermute deposit",
"wallet_count": 43,
"url": "/onchain/entity/wintermute%20deposit",
"confidence": "verified"
}
],
"traders": [],
"vaults": [],
"builders": [],
"polymarket": [
{
"address": "0x1050854d71bde435fb32b0cd6704e7ea7e0c43af",
"name": "Wintermute-Sport",
"pseudonym": "Sharp-Preservation",
"volume_usd": 454942.476049,
"badges": [
"🐬 Dolphin"
],
"pnl_usd": -17260.08753
},
{
"address": "0xf239d3d01e1e9a1d17f455f27feffbdaa8c13627",
"name": "wintermute",
"pseudonym": "",
"volume_usd": 7423.453015000001,
"badges": [],
"pnl_usd": -1374.500349
}
],
"markets": [],
"did_you_mean": []
}
10 routes under /v1/alerts
Your own alert rules, channels, webhook secrets and the events your rules produced: the REST face of the alert tools.
The 10 routes under /v1/alerts are the REST face of the alert tools: each runs the same code as its MCP tool, takes the same input and costs the same credits. They read and write the account your key belongs to, not one of the data products: another user's rule or channel is a 404, the same answer as one that does not exist. The same rules can be made under Account → Alerts.
- A
POSTbody is the tool's input, field for field (the rule id of an edit is in the path). A field the tool does not take, or a value of the wrong JSON type, is a400naming the field; every other route takes no body. 400the rule or body was refused, in the tool's words (No token BTC on eth: an alert on it could never fire. Did you mean WBTC?);401no valid credential;404not yours or does not exist;429the hourly budget or the per-minute cap;503nothing was stored, ask again. A call answered with an error costs nothing.- A generated webhook secret is returned once, in the answer, with
Cache-Control: no-store, and shown again under Account → Alerts → channels (Show secret). The body of a call is not recorded in your usage history, only its query string. - At most 200 active alerts per account, of any kinds.
GET /v1/alerts/rules Your alert rules · alerts_list
POST /v1/alerts/rules Create an alert rule · alerts_create
POST /v1/alerts/rules/{alert_id}/edit Change an alert rule · alerts_edit
POST /v1/alerts/rules/{alert_id}/delete Delete an alert rule · alerts_delete
GET /v1/alerts/channels Your notification channels · alerts_list_channels
POST /v1/alerts/channels/webhooks Register a webhook channel · alerts_register_webhook
POST /v1/alerts/channels/{channel_id}/rotate-secret Rotate a webhook's signing secret · alerts_rotate_webhook_secret
POST /v1/alerts/channels/{channel_id}/test Send a webhook the test event again · alerts_test_webhook
GET /v1/alerts/events Your alert events, oldest first · alerts_events
POST /v1/alerts/deliveries/{delivery_id}/resend Send one of your deliveries again · alerts_resend_delivery
Alerts · REST · CLI · MCP
Your alert rules
Route
GET /v1/alerts/rules
CLI
vike-data alerts list
MCP
alerts_list · 0 credits
Cost
0 credits, one request of the hourly limit
Free API Archive Stream MCP
Your alert rules, newest first: {"alerts": [...]}, each with id, label, alert_type, chain, token_symbol, min_value_usd, max_value_usd, min_amount, address_conditions, cooldown_minutes, notify_channel_ids, is_active, created_at, policy (how often it notifies: per_event, cooldown or hysteresis) and, for a Hyperliquid rule, hl (its parameters). Free (0 credits), like the alerts_list MCP tool.
Parameters
| Parameter | In | Description |
|---|---|---|
include_inactive | query | Also list paused rules. Default false. |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/alerts/rules?include_inactive=true"
The answer
Its shape, with illustrative values: these routes read and write your own account, so nothing here is a capture.
{
"alerts": [
{
"id": 77,
"label": "Big USDC moves",
"alert_type": "transfer",
"chain": "eth",
"token_symbol": "USDC",
"min_value_usd": 1000000.0,
"max_value_usd": null,
"min_amount": null,
"address_conditions": [],
"cooldown_minutes": 60,
"max_triggers_day": 10,
"notify_channel_ids": [
12
],
"is_active": true,
"created_at": "2026-11-04T09:00:00+00:00",
"policy": "cooldown",
"hl": null,
"pm": null
}
]
}
Alerts · REST · CLI · MCP
Create an alert rule
Route
POST /v1/alerts/rules
CLI
vike-data alerts create
MCP
alerts_create · 3 credits
Cost
3 credits, one request of the hourly limit
Free API Archive Stream MCP
Create an alert rule. The JSON body is the input of the alerts_create MCP tool (same fields, same rules): an on-chain transfer alert (chain, token_symbol, min_value_usd, max_value_usd, min_amount in whole tokens, address, address_role) or a Hyperliquid alert chosen by alert_type; label is required. A token the chain does not have is refused with the near matches it does have (BTC on eth: did you mean WBTC?). Returns {"alert": {...}}, the new rule as GET /v1/alerts/rules lists it. 400 for a field the rule type does not take or a value it refuses; 503 when the token could not be checked (nothing is stored: retry). 3 credits, none on an error.
Parameters
| Parameter | In | Description |
|---|---|---|
label required | body | Name for this alert (e.g. 'Big USDC moves') |
alert_type | body | transfer (default), a Hyperliquid type (hl_wallet, hl_cohort_fill, hl_liquidation, hl_cohort_bias, hl_liq_proximity, hl_wallet_orders) or a Polymarket type (pm_wallet_trade, pm_market_trade, pm_price_move, pm_resolution). Fixed once created. |
token_symbol | body | Transfer: token ticker in UPPERCASE (e.g. USDC, ETH, PEPE). Hyperliquid: the perp coin as Hyperliquid names it (BTC, kPEPE, xyz:CL); required for hl_cohort_bias, optional for the rest (omit = every coin). |
chain | body | Transfer alerts only: the chain to watch (single value). Default: eth. Ask the user to create two alerts if they want both chains. Leave out for Hyperliquid alerts. |
min_value_usd | body | Transfer: alert when transfer USD value >= this. Hyperliquid: a floor on the fill or liquidation size in USD (hl_wallet, hl_liquidation); required and >= 10000 for hl_cohort_fill; the order's size x price for hl_wallet_orders (>= 1000, default 10000); not used by hl_cohort_bias or hl_liq_proximity. Polymarket: a floor on the trade's USD (size x price) for pm_wallet_trade; required and >= 1000 for pm_market_trade. |
max_value_usd | body | Transfer alerts only: an upper bound on the transfer's USD value. A transfer worth more than this does not notify (one worth exactly this does). Like min_value_usd it needs a transfer that already carries a USD value, so an unpriced transfer never matches a rule with a bound. Keep it at or above min_value_usd: a rule whose minimum is above its maximum never fires. |
min_amount | body | Transfer alerts only, with token_symbol: alert when the transfer moves at least this many WHOLE tokens (1000000 USDT, 0.5 ETH), whether or not it carries a USD value. With min_value_usd or max_value_usd as well, both must hold. A token whose decimals are unknown never matches it. |
address | body | Transfer alerts only: wallet to watch, one 0x + 40-hex EVM address. Matches transfers (native or token) the address sent or received on chain, per address_role. |
address_role | body | Transfer alerts only: any = sent or received (default), from = only transfers the address sent, to = only transfers it received. Needs address. |
wallet | body | One 0x + 40-hex address. Hyperliquid: the account to watch, required for hl_wallet, hl_wallet_orders and hl_liq_proximity, optional for hl_liquidation. Polymarket: the proxy wallet (the address of its vike.io/polymarket/wallet page and of polymarket_tape rows), required for pm_wallet_trade. |
condition_id | body | Polymarket only: the market, its 0x + 64-hex condition id. Required for pm_market_trade, pm_price_move and pm_resolution; optional for pm_wallet_trade (one market of that wallet). |
actions | body | hl_wallet only: which fills notify. Default: open, close, flip, increase ('add' is accepted for increase); reduce is opt-in. |
side | body | hl_wallet_orders only: only buy or only sell orders. Leave out for both. |
events | body | hl_wallet_orders only: placed = a new order appeared in the book, gone = an order left it (its outcome filled, partially_filled or canceled comes from the order's own fills). Default both. Each check's orders arrive together, as one event. |
pnl_cohorts | body | hl_cohort_fill and hl_liquidation: only wallets in these realised-PnL cohorts (the ladder runs from '3x smart' = top 1% to '3x rekt' = bottom 5%). hl_cohort_fill REQUIRES at least one PnL or size cohort; for hl_liquidation empty = any cohort. |
size_cohorts | body | hl_cohort_fill and hl_liquidation: only wallets in these account-size cohorts (the ladder runs from '4x whale' = $10M+ to 'shrimp' = under $1k). hl_cohort_fill REQUIRES at least one PnL or size cohort; for hl_liquidation empty = any size. |
index | body | 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 | body | 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). |
band | body | 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. |
direction | body | hl_cohort_bias: which crossings notify. pm_price_move: which moves of the market's first outcome (Yes on a yes/no market) notify. Default both. |
channels | body | Which kinds of the user's enabled channels to notify. email: any alert (a Hyperliquid or Polymarket alert mails the account address). webhook: any alert (register one with alerts_register_webhook; asking for a Hyperliquid or Polymarket alert's webhook the user has not registered is an error). Discord is temporarily off and is refused, as is any other type. A kind the user has no enabled channel of is dropped from a transfer alert, and a transfer alert left with no channel e-mails the account address: call alerts_list_channels to see what is set up. Whatever the channels, every match is recorded as an event in the feed read with alerts_events (kept 15 days), so leave channels empty for a Hyperliquid or Polymarket alert you will only read there. |
cooldown_minutes | body | Token-wide transfer alerts (no address): minimum gap between notifications. Default 60. Address alerts and Hyperliquid and Polymarket alerts notify on every event and ignore it. |
Call it
curl -X POST -H "X-API-KEY: $VIKE_API_KEY" -H "Content-Type: application/json" \
"https://data.vike.io/v1/alerts/rules" \
-d '{"label": "Big USDC moves", "chain": "eth", "token_symbol": "USDC", "min_value_usd": 1000000, "channels": ["email"]}'
The answer
Its shape, with illustrative values: these routes read and write your own account, so nothing here is a capture.
{
"alert": {
"id": 77,
"label": "Big USDC moves",
"alert_type": "transfer",
"chain": "eth",
"token_symbol": "USDC",
"min_value_usd": 1000000.0,
"max_value_usd": null,
"min_amount": null,
"address_conditions": [],
"cooldown_minutes": 60,
"max_triggers_day": 10,
"notify_channel_ids": [
12
],
"is_active": true,
"created_at": "2026-11-04T09:00:00+00:00",
"policy": "cooldown",
"hl": null,
"pm": null
}
}
Alerts · REST · CLI · MCP
Change an alert rule
Route
POST /v1/alerts/rules/{alert_id}/edit
CLI
vike-data alerts edit
MCP
alerts_edit · 2 credits
Cost
2 credits, one request of the hourly limit
Free API Archive Stream MCP
Change one of your alert rules: only the fields in the JSON body change (the input of the alerts_edit MCP tool, less alert_id, which is the path). is_active: false pauses it; address: "" removes its wallet condition; a Hyperliquid rule is checked again whole. Returns {"alert": {...}}. 404 for a rule that is not yours or does not exist, 400 for a refused change, 503 when a changed token could not be checked (nothing is stored: retry). 2 credits, none on an error.
Parameters
| Parameter | In | Description |
|---|---|---|
alert_id required | path | The alert rule's id (from GET /v1/alerts/rules). |
label | body | |
token_symbol | body | |
chain | body | |
min_value_usd | body | |
max_value_usd | body | |
min_amount | body | Whole tokens of token_symbol; '' or 0 removes the amount floor. |
address | body | 0x + 40-hex EVM address; '' removes the wallet condition. |
address_role | body | |
wallet | body | One 0x + 40-hex address. Hyperliquid: the account to watch, required for hl_wallet, hl_wallet_orders and hl_liq_proximity, optional for hl_liquidation. Polymarket: the proxy wallet (the address of its vike.io/polymarket/wallet page and of polymarket_tape rows), required for pm_wallet_trade. |
condition_id | body | Polymarket only: the market, its 0x + 64-hex condition id. Required for pm_market_trade, pm_price_move and pm_resolution; optional for pm_wallet_trade (one market of that wallet). |
actions | body | hl_wallet only: which fills notify. Default: open, close, flip, increase ('add' is accepted for increase); reduce is opt-in. |
side | body | hl_wallet_orders only: only buy or only sell orders. Leave out for both. |
events | body | hl_wallet_orders only: placed = a new order appeared in the book, gone = an order left it (its outcome filled, partially_filled or canceled comes from the order's own fills). Default both. Each check's orders arrive together, as one event. |
pnl_cohorts | body | hl_cohort_fill and hl_liquidation: only wallets in these realised-PnL cohorts (the ladder runs from '3x smart' = top 1% to '3x rekt' = bottom 5%). hl_cohort_fill REQUIRES at least one PnL or size cohort; for hl_liquidation empty = any cohort. |
size_cohorts | body | hl_cohort_fill and hl_liquidation: only wallets in these account-size cohorts (the ladder runs from '4x whale' = $10M+ to 'shrimp' = under $1k). hl_cohort_fill REQUIRES at least one PnL or size cohort; for hl_liquidation empty = any size. |
index | body | 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 | body | 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). |
band | body | 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. |
direction | body | hl_cohort_bias: which crossings notify. pm_price_move: which moves of the market's first outcome (Yes on a yes/no market) notify. Default both. |
channels | body | |
cooldown_minutes | body | |
is_active | body |
Call it
curl -X POST -H "X-API-KEY: $VIKE_API_KEY" -H "Content-Type: application/json" \
"https://data.vike.io/v1/alerts/rules/77/edit" \
-d '{"min_value_usd": 5000000, "channels": ["webhook"]}'
The answer
Its shape, with illustrative values: these routes read and write your own account, so nothing here is a capture.
{
"alert": {
"id": 77,
"label": "Big USDC moves",
"alert_type": "transfer",
"chain": "eth",
"token_symbol": "USDC",
"min_value_usd": 5000000.0,
"max_value_usd": null,
"min_amount": null,
"address_conditions": [],
"cooldown_minutes": 60,
"max_triggers_day": 10,
"notify_channel_ids": [
12
],
"is_active": true,
"created_at": "2026-11-04T09:00:00+00:00",
"policy": "cooldown",
"hl": null,
"pm": null
}
}
Alerts · REST · CLI · MCP
Delete an alert rule
Route
POST /v1/alerts/rules/{alert_id}/delete
CLI
vike-data alerts delete
MCP
alerts_delete · 2 credits
Cost
2 credits, one request of the hourly limit
Free API Archive Stream MCP
Delete one of your alert rules: paused and kept by default, removed for good with hard=true. {"ok": true, "alert_id", "hard"}. 404 for a rule that is not yours or does not exist. 2 credits, none on an error.
Parameters
| Parameter | In | Description |
|---|---|---|
alert_id required | path | The alert rule's id (from GET /v1/alerts/rules). |
hard | query | Remove the rule with its history, events and queued deliveries. Default false: the rule is paused and kept. |
Call it
curl -X POST -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/alerts/rules/77/delete?hard=true"
The answer
Its shape, with illustrative values: these routes read and write your own account, so nothing here is a capture.
{
"ok": true,
"alert_id": 77,
"hard": true
}
Alerts · REST · CLI · MCP
Your notification channels
Route
GET /v1/alerts/channels
CLI
vike-data alerts channels
MCP
alerts_list_channels · 0 credits
Cost
0 credits, one request of the hourly limit
Free API Archive Stream MCP
Your enabled e-mail and webhook channels: {"channels": [{id, channel_type, is_verified, label}]}; a webhook's label is the first 40 characters of its URL. A channel id is what a rule's notify_channel_ids and the routes below take. Free (0 credits), like the alerts_list_channels MCP tool.
Parameters
None.
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/alerts/channels"
The answer
Its shape, with illustrative values: these routes read and write your own account, so nothing here is a capture.
{
"channels": [
{
"id": 11,
"channel_type": "email",
"is_verified": true,
"label": "you@example.com"
},
{
"id": 12,
"channel_type": "webhook",
"is_verified": true,
"label": "https://example.com/vike-alerts"
}
]
}
Register an https:// webhook as a channel and send it one signed test event now. The JSON body is the input of the alerts_register_webhook MCP tool: url, and optionally your own secret (16+ characters). Without one a secret is generated and returned ONCE here as secret (also shown on Account -> Alerts -> channels); every delivery is signed with it (HMAC-SHA256, X-Vike-Signature). test is {ok, status} or {ok: false, error}; a failing test does not refuse the channel. Cache-Control: no-store. 2 credits, none on an error.
Parameters
| Parameter | In | Description |
|---|---|---|
url required | body | Webhook URL, https:// only. Must be reachable from our server. |
secret | body | Optional HMAC secret (>=16 chars). If omitted, we generate one and return it as secret in the answer. |
Call it
curl -X POST -H "X-API-KEY: $VIKE_API_KEY" -H "Content-Type: application/json" \
"https://data.vike.io/v1/alerts/channels/webhooks" \
-d '{"url": "https://example.com/vike-alerts"}'
The answer
Its shape, with illustrative values: these routes read and write your own account, so nothing here is a capture.
{
"ok": true,
"channel_id": 12,
"channel_type": "webhook",
"url_preview": "https://example.com/vike-alerts",
"signed": true,
"secret": "<shown once here, and again on Account → Alerts → channels>",
"secret_note": "…",
"test": {
"ok": true,
"status": 200
}
}
Alerts · REST · CLI · MCP
Rotate a webhook's signing secret
Route
POST /v1/alerts/channels/{channel_id}/rotate-secret
CLI
vike-data alerts rotate-secret
MCP
alerts_rotate_webhook_secret · 2 credits
Cost
2 credits, one request of the hourly limit
Free API Archive Stream MCP
Give one of your webhook channels a new generated signing secret, returned as secret. The old secret keeps signing for 24 hours (previous_secret_valid_until): until then each delivery carries two sig= values, and a receiver must accept it when ANY of them matches. 404 for a channel that is not yours, not a webhook or does not exist. Cache-Control: no-store. 2 credits, none on an error.
Parameters
| Parameter | In | Description |
|---|---|---|
channel_id required | path | The webhook channel's id (from GET /v1/alerts/channels). |
Call it
curl -X POST -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/alerts/channels/12/rotate-secret"
The answer
Its shape, with illustrative values: these routes read and write your own account, so nothing here is a capture.
{
"ok": true,
"channel_id": 12,
"secret": "<the new secret>",
"secret_note": "…",
"previous_secret_valid_until": "2026-11-05T09:00:00+00:00"
}
Alerts · REST · CLI · MCP
Send a webhook the test event again
Route
POST /v1/alerts/channels/{channel_id}/test
CLI
vike-data alerts test-webhook
MCP
alerts_test_webhook · 1 credit
Cost
1 credit, one request of the hourly limit
Free API Archive Stream MCP
Send the signed test event to one of your webhook channels again, for example after the receiver installed a new secret. {"ok": true, "channel_id", "test"}, test being {ok, status} when the endpoint answered or {ok: false, error} (timeout, network, ssrf_refused,...). Nothing is queued or retried. 404 for a channel that is not yours, not a webhook or does not exist. 1 credit, none on an error.
Parameters
| Parameter | In | Description |
|---|---|---|
channel_id required | path | The webhook channel's id (from GET /v1/alerts/channels). |
Call it
curl -X POST -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/alerts/channels/12/test"
The answer
Its shape, with illustrative values: these routes read and write your own account, so nothing here is a capture.
{
"ok": true,
"channel_id": 12,
"test": {
"ok": true,
"status": 200
}
}
Alerts · REST · CLI · MCP
Your alert events, oldest first
Route
GET /v1/alerts/events
CLI
vike-data alerts events
MCP
alerts_events · 1 credit
Cost
1 credit, one request of the hourly limit
Free API Archive Stream MCP
The events your alert rules matched, oldest first, one row per (rule, event).
{"events": [{id, rule_id, alert_label, alert_type, created_at, event, delivery}], "next_cursor", "has_more"}. event is the event as the webhook sends it; delivery lists each channel's delivery (an empty list for a rule with no channel: the feed is the channel): id, channel, channel_key, status, attempts, last_attempt_at, next_attempt_at (while it is waiting), last_error as a code only (http 500, timeout, network,...). Poll with next_cursor: an empty page returns the cursor it was given. A page also ends once its events pass about 2 MB (a wallet-orders event can hold 200 orders): it may then hold fewer than limit events with has_more true; keep paging with next_cursor, nothing is skipped. Events and deliveries are kept 15 days. One credit.
Parameters
| Parameter | In | Description |
|---|---|---|
cursor | query | Pass back the previous next_cursor verbatim. Empty reads from the oldest event still kept (15 days). |
limit | query | Default 100, clamped to 1-500 |
rule_id | query | Only the events of this alert rule |
Call it
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/alerts/events?limit=50&rule_id=77"
Alerts · REST · CLI · MCP
Send one of your deliveries again
Route
POST /v1/alerts/deliveries/{delivery_id}/resend
CLI
vike-data alerts resend
MCP
alerts_resend_delivery · 1 credit
Cost
1 credit, one request of the hourly limit
Free API Archive Stream MCP
Send one of your own deliveries again: the id of an item of an event's delivery list (from /v1/alerts/events).
Whatever its status (sent, retrying, expired, skipped), up to 15 days after its event. It goes back to the sender for an immediate attempt with the same event id, a fresh signature and the same X-Vike-Delivery-Id, and the sender then retries it as it would a new one (72 h). {"id", "status", "result"}: result is queued, already_queued (it was already waiting: a second call changes nothing) or sending (an attempt is in flight right now). queued is a request, not a promise: the sender checks the rule and the channel again, so a delivery whose rule is off or whose channel is disabled or deleted is skipped again, and a paused channel delays the attempt. 404 for a delivery that is not yours or does not exist, 410 past 15 days (404 once the hourly prune removed it), 409 for an unsupported delivery (its channel cannot carry that kind of event, an event with no e-mail template: it would fail the same way). One credit; none on an error.
Parameters
| Parameter | In | Description |
|---|---|---|
delivery_id required | path | The delivery's id, from an event's delivery list (GET /v1/alerts/events). |
Call it
curl -X POST -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/alerts/deliveries/5531/resend"
The answer
Its shape, with illustrative values: these routes read and write your own account, so nothing here is a capture.
{
"id": 5531,
"status": "retry",
"result": "queued"
}
The alert feed and signed webhooks
What your rules produced, the webhook body, how to verify a delivery, and every event shape.
Every rule you own produces events: one per transfer, fill, liquidation, index crossing, liquidation-risk crossing or Polymarket trade, price move or resolution it matched. Each is sent to the rule's channels and kept for 15 days in a feed you read with GET /v1/alerts/events or the alerts_events tool; a rule with no channel fills the feed only.
The answer's shape (illustrative values, not a capture)
{
"events": [
{
"id": 1042,
"rule_id": 77,
"alert_label": "Whale BTC fills",
"alert_type": "hl_wallet",
"created_at": "2026-10-28T09:14:41.200Z",
"event": {
"schema": "vike.event.v1",
"event_id": "hl.fills:0x1111111111111111111111111111111111111111:1793178870482:123456789",
"source": "hl.fills",
"kind": "fill",
"ts": "2026-10-28T09:14:30.482Z",
"cursor": "1793178870482-00000000000123456789",
"chain": "hyperliquid",
"subject": {
"wallet": "0x1111111111111111111111111111111111111111",
"symbol": "BTC"
},
"data": {
"action": "open",
"side": "buy",
"size_usd": 1250000.0,
"size_token": 12.5,
"price": 100000.0,
"closed_pnl": 0.0,
"start_position": 0.0,
"position_after": 12.5,
"crossed": true,
"trade_id": 123456789,
"dex": "HL",
"fills": 1
},
"labels": {
"pnl_cohort": "2x smart",
"pnl_cohort_basis": "live",
"pnl_cum_usd": 4210000.0,
"size_cohort": "3x whale"
}
},
"delivery": [
{
"id": 5531,
"channel": "webhook",
"channel_key": "12",
"status": "sent",
"attempts": 1,
"last_attempt_at": "2026-10-28T09:14:42.010Z",
"next_attempt_at": null,
"last_error": null
}
]
}
],
"next_cursor": "eyJpZCI6MTA0Mn0",
"has_more": false
}
| Field | What it is |
|---|---|
events | The page, oldest first; empty when nothing is newer than cursor |
next_cursor, has_more | Pass next_cursor back as cursor; an empty page returns the cursor you sent, so keep it and ask again |
id | The feed's row for this (rule, event) pair; the event's own id is event.event_id |
rule_id, alert_label, alert_type | The rule that matched |
created_at | When the event was recorded, UTC, milliseconds |
event | Exactly the event the webhook carries (Event shapes) |
delivery | One entry per channel: id (what send again takes), channel, channel_key, status (pending, retry, sent, expired after 72 hours, skipped, unsupported), attempts, last_attempt_at, next_attempt_at and last_error as a code only: http 500, timeout, network, tls, blocked, expired or error |
- Events appear in the order they are numbered, as soon as they are recorded (before the e-mail or webhook goes out), so a poller that always passes back the last
next_cursormisses none and sees none twice. - A cursor older than 15 days does not fail: the feed continues from the oldest event still kept.
- Deleting a rule for good (
hard=true) removes its events; pausing keeps them. 400for a cursor that is not one,422for alimitorrule_idthat is not an integer,503when the feed could not be read.
The webhook body
A webhook channel receives the same event as a signed POST. Every rule type sends one envelope:
| Field | What it is |
|---|---|
schema | vike.alert.v1 |
alert_id, alert_label, alert_type | The rule: its id, its name (alert #<id> when it has none) and its type |
triggered_at | The event's time, ISO 8601 with a +00:00 offset |
event | The event, as in the feed |
A transfer body also carries the flat keys of the first webhook format, so an old receiver keeps working: chain, token, value_usd, and from_address, to_address and tx_hash as bare lowercase hex without 0x.
Delivery is at least once: dedupe on alert_id and event.event_id from the body, the pair the signature covers. X-Vike-Delivery-Id is the same on every try of one delivery and X-Vike-Attempt counts them. Answer any 2xx within 20 seconds; a failed delivery is retried with back-off for 72 hours, and a 429 with Retry-After pauses that webhook. Every delivery is listed under Account → Alerts → Webhook log.
Verifying the signature
Each POST carries these headers. Compute HMAC-SHA256(secret, "<t>.<raw body>") with the t= of the header, hex-encode it and compare it in constant time with every sig= value: accept when any matches (two for 24 hours after a secret rotation). Check the raw bytes before any JSON parser touches them, and reject a timestamp more than about five minutes old.
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>
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
Event shapes
Every event, in the feed, a webhook or the WebSocket, has the same ten fields: schema (vike.event.v1), event_id (stable, unique, starts with its source), source, kind, ts (when it happened, UTC, milliseconds), cursor (a position in its source, not the feed's cursor), chain, subject, data and labels. kind says which subject and data to expect:
kind | Rule types | subject | data |
|---|---|---|---|
transfer | transfer | from, to, token_address, token_symbol, tx_hash | value_usd (null when unpriced), priced, block_number, log_index, tx_hash, transfer_type |
fill | hl_wallet, hl_cohort_fill | wallet, symbol | action, side, size_usd, size_token, price, closed_pnl, start_position, position_after, crossed, trade_id, dex; fills on hl_wallet (one event merges a check's fills per coin, action and side) |
liquidation | hl_liquidation | wallet, symbol | direction, size_usd, size_token, mark_price, inferred |
index | hl_cohort_bias | symbol | index, value, threshold, band, from_side, to_side, snapshot_ts |
liq_risk | hl_liq_proximity | wallet, symbol | asset, direction, liq_ratio, threshold, band, mark_price, entry_price, liquidation_price, size_usd, leverage, snapshot_ts |
wallet_orders | hl_wallet_orders | wallet | orders: every order of the check, each with action (placed or gone), symbol, side, order_type (limit or trigger), trigger_type, limit_px, trigger_px, size, notional_usd, placed_at, slot_at, dex, oid, and for a gone one last_seen, outcome, filled_size, filled_usd (at most 200 orders an event); beside it part and parts and the check's totals n_placed, n_gone, placed_usd, gone_usd, n_filled, n_partially_filled, n_canceled, n_unknown and slots |
pm_trade | pm_wallet_trade, pm_market_trade | wallet, condition_id, token_id, slug, title, outcome, outcome_index | side, size, price, usd, role, tx_hash, log_index, block_number |
pm_price_move | pm_price_move | token_id, condition_id, slug, outcome, outcome_index | mid_from, mid_to, move, direction, window_s, bid, ask |
pm_resolution | pm_resolution | condition_id, slug, question, outcomes | winning_index, winning_outcome, payouts, invalid, resolved_block, traded |
labelsholds the cohorts ofsubject.walletwhen they were looked up (pnl_cohort,pnl_cohort_basis,pnl_cum_usd,size_cohort); a missing key means unknown, never a cohort. A Polymarket trade carries the wallet'sname,pseudonymand tiers.- A fill's
trade_idcan be larger than 2^53, which JavaScript reads inexactly: take it from the end ofevent_id.
Ask: “What have my alerts fired since yesterday?”
WebSocket: live events on stream.vike.io
The rules every channel shares: connect, subscribe, filter, resume, close codes. Each product's channels are on its own page.
- Connect
wss://stream.vike.io/v1/stream/hl(or another address) withX-API-KEYon the handshake. - Subscribe
Send
{"action": "subscribe", "id", "channel", "filter"}. - Resume
Keep each frame's
cursor; after a drop, subscribe with it and get what you missed, up to 3 days back.
import json, os
from websockets.sync.client import connect # websockets >= 14
with connect("wss://stream.vike.io/v1/stream/hl",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]}) as ws:
ws.send(json.dumps({"action": "subscribe", "id": "s1", "channel": "hl.liquidations",
"filter": {"coin": ["BTC", "ETH"]}}))
for message in ws:
frame = json.loads(message)
if frame["type"] == "ping":
ws.send(json.dumps({"action": "pong"}))
elif frame["type"] == "event":
e = frame["event"]
print(json.dumps(e)) # keep frame["cursor"] to resume after a drop
22 channels on 4 addresses, all on stream.vike.io. Connect to an address, then subscribe to one or more of its channels, each with an optional filter. One key works on every address; a channel belongs to one, and asking for it on another is unknown_channel. Each product's channels, their fields and a real event per channel are on its page.
| Address | Product | Channels | REST catch-up |
|---|---|---|---|
wss://stream.vike.io/v1/stream/onchain | On-chain | chain.transfers | GET /v1/onchain/stream-events |
wss://stream.vike.io/v1/stream/hl | Hyperliquid | hl.candles, hl.closed_trades, hl.fills, hl.funding, hl.ledger, hl.leverage, hl.liq_risk, hl.liquidations, hl.order_walls, hl.positions, hl.top_positions, hl.twaps | GET /v1/hyperliquid/stream-events |
wss://stream.vike.io/v1/stream/hl-cohorts | HL Cohorts | hl.cohort_bias, hl.cohort_flow, hl.cohort_leverage, hl.indices | GET /v1/hl-cohorts/stream-events |
wss://stream.vike.io/v1/stream/polymarket | Polymarket | pm.new_markets, pm.positions, pm.price_moves, pm.resolutions, pm.trades | GET /v1/polymarket/stream-events |
- Limits: 1 connection per key per address, 3 per account per address, 100 subscriptions on one connection. A subscription's list filter takes many values (
walletup to 1,000), so the cap counts subscriptions, not wallets. - Ping: the server sends
{"type": "ping"}every 15 s; answer with the text frame{"action": "pong"}(a protocol-level pong does not count). 3 missed pongs close the connection with4412. - Delivery is at least once: dedupe on event.event_id.
- Not counted: connecting, subscribing and reconnecting spend no hourly budget and no credits, never answer
429and never close for a rate limit. - Compression: a client that offers
permessage-deflategets compressed frames (each message inflates on its own); one that offers nothing gets plain frames.
Frames
| You send | Fields | What it does |
|---|---|---|
subscribe | id, channel, filter, cursor | Starts a subscription. id is yours: 1-32 characters of A-Z a-z 0-9 _ -, unique per connection. filter is optional (Filters); cursor resumes. |
unsubscribe | id | Ends one subscription. |
pong | none | Answers a ping. |
JSON text frames only, at most 64 KiB each. The server sends subscribed (with the channel's current cursor and a replay summary), event (one event, ids listing every subscription it matched, and the frame's cursor), ping, gap and error. Keep the frame's cursor, never the cursor inside event (that one is the source's position). The event itself has the envelope under Event shapes; drop a repeat by event.event_id.
Channels run on demand
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. So the first events of a channel nobody was watching are new ones, not history: they come with the source's next pass (seconds for fills, about 12 minutes for position captures). While one client is subscribed, the channel runs for everyone.
Channel by channel
Each event's event_id is built from the parts below, so it is the same every time the event arrives; what a channel holds beyond its filters; and when its first events come after an idle start. Fields and a real event of each channel are on its product's page.
| Channel | event_id | What is in it | First events |
|---|---|---|---|
hl.fills | hl.fills:<wallet>:<fill ms>:<trade id> | Every perp fill; without min_usd, fills of $1,000 or more. crossed true is the taker. | within about 10 s of the fill reaching our store; our node can run a minute or more behind the venue |
hl.positions | hl.positions:<wallet>:<fill ms>:<trade id> | The same fill again, as its own event, when it is worth $1,000 or more (a floor you cannot lower), could be classified, and the wallet is shark or bigger or the position before or after it is worth $250,000 or more. Adds leverage, liquidation_px and position_ts from the last capture that held the position (up to about 25 minutes old, null on an open) and labels.tags, the wallet's trader tags (the tag filter). | with hl.fills |
hl.liquidations | hl.liquidations:<wallet>:<coin>:<ms> | Every liquidation; direction is the side that was closed. inferred is false for one read from the liquidated account's own fill (every one since 2026-11-01), true for an older one inferred from position snapshots. | checked every minute |
hl.liq_risk | hl.liq_risk:<wallet>:<coin>:<long|short>:<capture s> | A position of $100,000 or more whose liq_ratio (0 at entry, 1 at the liquidation price) reaches 0.80, announced once; it re-arms below 0.70 or when the position closes. prev is "out" when it has just entered, null on the channel's first capture, which announces every position already in the zone. Not the alert rule's liq_risk event. | with the next position capture, about every 12 minutes |
hl.order_walls | hl.order_walls:<order id>:appeared or :disappeared | Perp orders of $1,000,000 or more: resting limit orders (limit price × original size, kind resting) and stop orders not yet triggered (trigger price × original size, kind trigger, with trigger_type and trigger_px); spot is not included. The book is read in 5-minute slots (source l4_5m; the ~12-minute snapshots, state_12m, while those stop): ts is the slot, ts_placed the placement, a wall that lives between two slots is in neither. disappeared says what happened: outcome filled, partially_filled, canceled, or unknown when its fills were still not in 10 minutes later, and filled_usd; it can come a few minutes after the slot while the fills arrive. | with the next 5-minute slot (checked every minute) |
hl.twaps | hl.twaps:<twap id>:<line µs>:<status> | One event per TWAP status line; error is a rejected placement. size_usd is the order's whole size × the coin's mid at the newest 5-minute snapshot, null with no mid in 15 minutes. | the node's TWAP lines are read every minute: a minute or two after the line |
hl.ledger | hl.ledger:<wallet>:<ms>:<hash>:<12 hex of event type, token and nonce> (- without a hash) | Deposits, withdrawals, transfers, vault and lending lines; a transfer is two events, one per wallet. A line stored more than 5 minutes late is not streamed. | about a minute behind the node |
hl.leverage | hl.leverage:<wallet>:<coin>:<capture s> | A leverage or margin-mode change seen between two position captures; one made and undone between them is never seen. | with the position captures, about every 12 minutes |
hl.funding | hl.funding:<coin>:<snapshot s> | Every perp's funding rate, premium, mid and open interest at each 5-minute snapshot, HIP-3 coins included, spot not. | every 5 minutes |
hl.candles | hl.candles:<interval>:<asset>:<bar open s> | Closed bars only, never a forming or a revised one. A spot pair's symbol is its token name (HYPE) and asset keeps Hyperliquid's id (@107), so coin: ["HYPE"] matches the perp and the spot pair: add market for one. | 30 to 60 seconds after a bar ends |
hl.top_positions | hl.top_positions:<coin>:<wallet>:<entered|left|moved>:<capture s> | The 10 largest positions of each of the 50 coins whose top 10 is worth most: one event when a position enters, leaves or changes rank. The first capture, and a coin joining the 50, send its whole top 10 as entered with first: true. | with the next position capture, about every 12 minutes |
hl.closed_trades | hl.closed_trades:<wallet>:<coin>:<long|short>:<open trade id> | A round trip that closed, sent once; open_tid can exceed 2^53, so read it from the event_id. | in batches, every 30 minutes |
hl.cohort_flow | hl.cohort_flow:<coin>:<axis>:<cohort, spaces as _>:<minute s>, plus :r1 on a revision | Per closed minute, coin, axis and cohort: every fill, each side counted, unknown included. Each minute is read again about 150 seconds after it ends; a coin whose count changed is sent again with rev 1 and full totals: keep the highest rev. After a catch-up, order by window_start. | about a minute after each minute closes |
hl.indices | hl.indices:<coin>:<capture s> | The five indices of the 50 coins with the most open position value; crossed lists those that moved from at or below −0.10 to at or above +0.10, or back (a coin's first sighting crosses nothing). An index filter keeps only the events where that index crossed. | every position capture, about every 12 minutes |
hl.cohort_bias | hl.cohort_bias:<coin>:<axis>:<cohort, spaces as _>:<capture s> | Long and short value per coin, axis and cohort for the same 50 coins; unknown included, so an axis adds up to the coin's book. | every position capture |
hl.cohort_leverage | hl.cohort_leverage:<axis>:<cohort, spaces as _>:<hour s> | Each cohort's hourly leverage against its wallets' account value; an hour rebuilt from fills is not sent. | the newest hour at once, then each hour about an hour after it starts |
chain.transfers | chain.transfers:<chain>:<tx hash>:<log index>, :native for the native coin | chain is eth, not ethereum. Sent once pricing has run over the block: an unpriced transfer has value_usd null and priced false, and no min_usd filter passes it. A repaired block is not sent again. Fields and ready-made filters: Whale alerts. | 1 to 6 minutes after the block |
pm.trades | pm.trades:<tx hash>:<log index> | One event per maker order's fill: a match is the makers' events plus one taker event of their summed size, so add usd over one role only. | read every 15 seconds, about 4 minutes behind the chain (150 blocks of confirmations) |
pm.positions | pm.positions:<tx hash>:<log index> | What each trade did to the wallet's position in that outcome; under 0.01 shares is flat. Shares count trades only: a split, merge or transfer is not on the tape, so a position can go below zero. | with pm.trades |
pm.resolutions | pm.resolutions:<condition id> | Every on-chain resolution, traded or not (data.traded); winning_index is -1 when the top payout is shared. | checked every minute |
pm.price_moves | pm.price_moves:<token id>:<5-minute bucket>:<up|down> | A mid move of 0.05 or more within five minutes, per outcome token; a move across a bucket boundary can appear twice. | checked every 30 seconds |
pm.new_markets | pm.new_markets:<condition id> | A market the order-book recorder sees for the first time, sent once; class is updown_5m, updown_15m or other. One first seen while the channel was idle is never sent later. | within about a minute of the recorder seeing it |
One connection per key
A new connection is always accepted; an older one of the same key (or past the account's 3 on a path) is closed 4410 with a resume cursor, never refused. Two programs on one key therefore replace each other forever: after a 4410, do not reconnect automatically. Use one key per program, at most three programs per account, on each address.
Resuming
- A subscribe without a cursor gets new events from now; with a cursor it gets what it missed, up to 3 days back, oldest first, then the live events, with nothing skipped or doubled at the hand-over.
- Keep one cursor per subscription, not one per connection: across subscriptions, the replay of one can arrive after live events of another. Cursors are fixed-width strings, so a later one compares greater as text.
- Whenever a connection ends, open a new one at once and subscribe with each subscription's last cursor. There is no waiting list and no penalty. The exceptions: a refused or revoked credential, and
4410. - The
subscribedframe'sreplaysays how the missed events come:fromnone(no cursor, nothing replayed),buffer(recent ones;eventsis how many follow) orstore(older ones, in pages;eventsnull). - Resuming from the cursor of a
4408,4409or4410frame can repeat a few events that were being sent: drop them byevent_id. Such a frame names one channel; resume the connection's other subscriptions from your own cursors. - Without a socket, the same stored events are the REST catch-up of each path (
GET /v1/<product>/stream-events) and thestream_eventstool.
Keep one cursor per subscription, and resume with it
import json
cursors = {} # subscription id -> cursor of the last event handled on it
channels = {} # subscription id -> its channel
handled = set() # event ids already handled (keep the last few thousand)
def on_message(text, send):
frame = json.loads(text)
if frame["type"] == "event":
if frame["event"]["event_id"] not in handled:
handled.add(frame["event"]["event_id"])
handle(frame["event"]) # your code
for sub_id in frame["ids"]:
cursors[sub_id] = max(cursors.get(sub_id, ""), frame["cursor"])
elif frame["type"] == "subscribed":
cursors.setdefault(frame["id"], frame["cursor"])
channels[frame["id"]] = frame["channel"]
elif frame["type"] == "gap": # about the channel: every subscription on it moves on
for sub_id, channel in channels.items():
if channel == frame["channel"]:
cursors[sub_id] = max(cursors.get(sub_id, ""), frame["cursor"])
elif frame["type"] == "ping":
send(json.dumps({"action": "pong"}))
def subscribe_all(send, subscriptions):
for sub in subscriptions: # {"id": ..., "channel": ..., "filter": ...}
message = {"action": "subscribe", **sub}
if sub["id"] in cursors:
message["cursor"] = cursors[sub["id"]]
send(json.dumps(message))
Gap frames
A window in which a channel was not running is never filled in afterwards. A client that resumes across one gets one gap frame for it (channel, cursor, from, to, reason: "channel_idle"), in order and with no ids: apply its cursor to every subscription on that channel. It says the channel did not run, not that nothing happened: read the window from the product's REST routes if you need it.
Authenticating the handshake
Send the X-API-KEY header (or Authorization: Bearer <OAuth token>) on the handshake.?token=<key> exists only for a browser, which cannot set WebSocket headers: a key in a URL can end up in proxy and browser logs, so never use it where a header is possible. The handshake tries the Authorization bearer, then X-API-KEY, then ?token=, and stops at the first that produces an identity. The stream checks every open connection's credential about once a minute and closes it with 4401 when it stopped being valid.
Filters
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. min_amount needs token. A key a channel does not take is refused with bad_subscription, as is a bad value (with the key and the reason). A list key takes a JSON list ({"coin": ["BTC", "ETH"]}), a one-value key one string ({"side": "buy"}), a number key one number, contract a JSON boolean. Check a filter without connecting with stream_check_filter or vike-data stream check.
| Key | Takes | Values or format | Channels |
|---|---|---|---|
action | list of up to 50 | close, flip, increase, open, reduce | hl.fills, hl.positions, pm.positions |
axis | one value | pnl, size | hl.cohort_bias, hl.cohort_flow, hl.cohort_leverage |
chain | list of up to 50 | base, bsc, eth, hyperevm | chain.transfers |
class | list of up to 50 | other, updown_15m, updown_5m | pm.new_markets |
cohort | list of up to 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 | hl.cohort_bias, hl.cohort_flow, hl.cohort_leverage |
coin | list of up to 200 | not a coin name (A-Z, 0-9 and: @. _ / -, at most 32 characters) | hl.candles, hl.closed_trades, hl.cohort_bias, hl.cohort_flow, hl.fills, hl.funding, hl.indices, hl.leverage, hl.liq_risk, hl.liquidations, hl.order_walls, hl.positions, hl.top_positions, hl.twaps |
contract | boolean | chain.transfers | |
dex | list of up to 50 | not a dex name (A-Z and 0-9, at most 16 characters; HL is the core venue) | hl.candles |
direction | one value | long, short | hl.closed_trades, hl.leverage, hl.liq_risk, hl.liquidations, hl.top_positions |
entity | list of up to 50 | must be a printable name of 1 to 64 characters | chain.transfers |
event | one value | appeared, disappeared | hl.order_walls |
event | one value | entered, left, moved | hl.top_positions |
event_type | list of up to 50 | accountClassTransfer, borrowLend, deposit, internalTransfer, rewardsClaim, send, spotTransfer, subAccountTransfer, vaultCreate, vaultDeposit, vaultDistribution, vaultLeaderCommission, vaultWithdraw, withdraw | hl.ledger |
index | list of up to 50 | smart_bias, smi, smi_wsi_gap, whale_bias, wsi | hl.indices |
interval | list of up to 50 | 15m, 1d, 1h, 1m, 30m, 4h, 5m | hl.candles |
kind | one value | resting, trigger | hl.order_walls |
market | list of up to 200 | markets are condition ids: 0x followed by 64 hex characters | pm.positions, pm.price_moves, pm.resolutions, pm.trades |
market | list of up to 50 | perp, spot | hl.candles |
min_amount | number | must be a finite number >= 0 | chain.transfers |
min_leverage | number | must be a finite number >= 0 | hl.positions |
min_move | number | must be a finite number >= 0; must be a price move above 0 and at most 1 (0.05 = five points) | pm.price_moves |
min_usd | number | must be a finite number >= 0 | chain.transfers, hl.closed_trades, hl.fills, hl.ledger, hl.leverage, hl.liq_risk, hl.liquidations, hl.order_walls, hl.positions, hl.top_positions, hl.twaps, pm.positions, pm.trades |
move | one value | down, up | pm.price_moves |
pnl_cohort | list of up to 50 | 2x loser, 2x rekt, 2x smart, 2x winner, 3x loser, 3x rekt, 3x smart, 3x winner, loser, rekt, smart, unknown, winner | hl.closed_trades, hl.fills, hl.ledger, hl.leverage, hl.liq_risk, hl.liquidations, hl.order_walls, hl.positions, hl.top_positions, hl.twaps |
pnl_tier | list of up to 50 | belowmedian, bot1, bot10, bot5, median, top1, top10, top5, unknown, unranked | pm.trades |
role | one value | maker, taker | pm.trades |
side | one value | buy, sell | hl.fills, hl.order_walls, hl.positions, hl.twaps, pm.trades |
size_cohort | list of up to 50 | 2x dolphin, 2x fish, 2x shark, 2x shrimp, 2x whale, 3x whale, 4x whale, dolphin, fish, shark, shrimp, unknown, whale | hl.closed_trades, hl.fills, hl.ledger, hl.leverage, hl.liq_risk, hl.liquidations, hl.order_walls, hl.positions, hl.top_positions, hl.twaps |
status | list of up to 50 | activated, error, finished, stopped, terminated, waitingForTrigger | hl.twaps |
tag | list of up to 50 | must be a printable name of 1 to 64 characters | hl.positions |
token | list of up to 200 | tokens are 0x followed by 40 hex characters (the token contract address) | chain.transfers |
token_id | list of up to 200 | token ids are 1 to 80 decimal digits | pm.price_moves |
volume_tier | list of up to 50 | dolphin, fish, shrimp, unknown, whale | pm.trades |
wallet | list of up to 1,000 | wallets are 0x followed by 40 hex characters | chain.transfers, hl.closed_trades, hl.fills, hl.ledger, hl.leverage, hl.liq_risk, hl.liquidations, hl.order_walls, hl.positions, hl.top_positions, hl.twaps, pm.positions, pm.trades |
wallet_type | list of up to 50 | must be a printable name of 1 to 64 characters | chain.transfers |
Defaults: hl.fills min_usd 1,000, unless the filter sets it (0 removes it).
Close codes, refusals and error frames
Five closes come after an error frame with a cursor to resume from on that channel (4408 to 4412); 4401 sends one without. The close's reason is the same word as the frame's code; every other close has no frame, so keep your own cursors as you go.
| Code | Reason | What it means and what to do |
|---|---|---|
4401 | key_revoked | the key or OAuth token no longer authenticates (revoked, or its account deactivated): no resume cursor; reconnect only with a valid credential |
4408 | slow_consumer | the client stopped reading; resume from the last error frame's cursor |
4409 | server_catching_up | the client reads, but the server got further ahead than its queue holds; resume |
4410 | replaced | a newer connection of the same key, or the account's 4th on this path, took over: do not auto-reconnect, or the two replace each other forever |
4411 | memory_backstop | the worker's queues passed its memory budget and this connection held the largest backlog; resume |
4412 | ping_timeout | no pong to the last 3 pings; resume |
1011 | none | a server fault: reconnect with your cursor |
1012 | none | a server restart: reconnect with your cursor |
1009 | none | a message you sent was over 64 KiB: send smaller ones, reconnect, resume |
1001, 1006, none | none | the server went away or the connection dropped with no close frame: reconnect at once with your own cursors |
4408 comes after 30 seconds in which frames waited and the client took none; a client that reads, however slowly, is closed 4409 instead. One that kept asking for replays it did not read gets a 4408 frame with no cursor and a message starting replay backlog full: resume from your own cursors.
Refused at the handshake
A connection refused before it opens is an HTTP answer to the upgrade request, not a close. The body is JSON (except Cloudflare's 521 / 522); its close_code names the number the refusal would carry as a close.
| Status | code | close_code | Meaning | What the client does |
|---|---|---|---|---|
| 401 | unauthorized | 4401 | No valid credential. | Fix the credential; a retry without one fails the same way. |
| 503 | capacity | 4503 | The server process that took the connection is at its connection limit. | Send the same upgrade request again. |
| 503 | unavailable | 1013 | The credential could not be checked just now. | Send the same upgrade request again. |
| 521 or 522 | none: a Cloudflare HTML page | none | The Hyperliquid or HL Cohorts stream is stopped or restarting. | Send the same upgrade request again, then resume. |
| 503 | STREAM_UNAVAILABLE | none | The on-chain or Polymarket stream is stopped or restarting. | Send the same upgrade request again, then resume. |
| 403 | STREAM_ORIGIN | none | The on-chain or Polymarket stream asked on data.vike.io. | Use wss://stream.vike.io/v1/stream/onchain or /polymarket. |
| 410 | STREAM_MOVED | none | An old address on data.vike.io (the Hyperliquid stream or the closed candle stream). | Use wss://stream.vike.io/v1/stream/hl; candles are its hl.candles channel. |
The edge's own bodies, as it answers them:
410 STREAM_MOVED: the closed candle stream, wss://data.vike.io/v1/stream (and wss://vike.io/v1/stream)
{"error":{"code":"STREAM_MOVED","message":"the candle stream moved to the hl.candles channel of wss://stream.vike.io/v1/stream/hl"}}
410 STREAM_MOVED: the old Hyperliquid address on data.vike.io
{"error":{"code":"STREAM_MOVED","message":"the stream moved to wss://stream.vike.io/v1/stream/hl"}}
403 STREAM_ORIGIN: the on-chain path on data.vike.io (Polymarket's names its own)
{"error":{"code":"STREAM_ORIGIN","message":"use wss://stream.vike.io/v1/stream/onchain"}}
503 STREAM_UNAVAILABLE: the on-chain or Polymarket stream is stopped or restarting
{"error":{"code":"STREAM_UNAVAILABLE","message":"the stream server is unavailable"}}
Error frames
An error frame that no close follows leaves the connection open: the rejected message is discarded and the rest continue. Every one has type: "error", code, message, and id when it is about one of your subscribes.
code | When | What the client does |
|---|---|---|
bad_subscription | The message or its filter is not valid; the message names the fault: invalid JSON, unknown action 'x', duplicate subscription id 'x', no such subscription (an unsubscribe), filter key 'x' is not supported on hl.fills, filter.min_usd: must be a number, filter.wallet: takes at most 1000 values, cursor is not a stream cursor (take one from a subscribed or event frame). | Fix the message. Nothing was subscribed. |
unknown_channel | Not a channel of this address: a typo, or a channel of another path. The message lists what this address serves: unknown channel 'x'; available: [...]. | Use one from the list, or connect to the channel's own path. |
rate_limited | The connection already holds the maximum number of subscriptions (at most 100 subscriptions per connection). | Unsubscribe one, or put several wallets or coins into one subscription's filter. |
cursor_expired | The cursor is older than the stream keeps. | Subscribe without a cursor and read the older part over REST. |
cursor_ahead | The cursor is newer than the stream. | Send a cursor you took from a frame. |
replay_too_large | The replay is too big for the socket; the frame carries cursor and rest. | Subscribe again from the frame's cursor, or narrow the filter. |
catchup_unavailable | The replay could not be read (catch-up is unavailable right now; subscribe again); the frame carries cursor and rest. | Subscribe again at once with the frame's cursor. |
For replay_too_large and catchup_unavailable, cursor is the last position the server had sent you on that subscription (or the cursor you sent, when it had sent none), and the events up to it are valid: start from it. rest is the REST catch-up path, and may be absent.
The command line: vike-data
Install, sign in, and every command group. Open source, on npm.
vike-data is the Data API's command line: the npm package @vike-io/cli (0.17.1), MIT-licensed, Node.js 22.12 or newer. Every data command is one call to the MCP server, so it reaches the same tools with the same key and spends the same hourly budget; vike-data stream holds a WebSocket instead, outside that budget.
- The key is read from
VIKE_API_KEYfirst and from~/.vike/config.jsonsecond. - Output is a table on a terminal and JSON with
--jsonor whenever stdout is piped, sovike-data search btc | jq .needs no flag. A tool that refuses its input exits 1. callreaches every tool by name, including those with no command of their own, and prints the raw result, so an integer above 2^53 stays exact (--args,--args-fileor-for stdin,--timeout60 s by default).streamanswers the pings, printseventandgapframes on stdout and its status on stderr, and after any drop resumes from each subscription's last cursor;--cursorstarts a run from a frame's cursor, at most 3 days old. It exits 1 without reconnecting on4410, a rejected key or a refused subscription; a closed pipe ends it with 0.- It is not the Trader's command line:
vike-clidrives the trading platform (Trader docs). - Each product's commands, with real arguments: On-chain, Hyperliquid, HL Cohorts, Polymarket.
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
| Command | What it does |
|---|---|
vike-data init | Interactive setup: save your API key and verify connectivity (--api-key skips the prompt) |
vike-data login --api-key vk_… | Save an API key to ~/.vike/config.json |
vike-data logout | Remove the saved API key |
vike-data whoami | Show which API key is in use (truncated) and where it came from |
vike-data doctor | Diagnose the setup, endpoint reachability and tool availability |
vike-data schema --pretty | Print every MCP tool with its description and input schema |
vike-data token … | Token search, transfers, screener and holders · its commands |
vike-data wallet … | Wallet metrics, discovery and ranking · its commands |
vike-data labels <address> | The label sources of one wallet · its commands |
vike-data hl … | Hyperliquid: cohorts, wallets, leaderboards, liquidations, markets, builders, vaults · its commands |
vike-data perp … | Cross-venue perp funding and spreads; Hyperliquid top traders and share-card lookup · its commands |
vike-data candles … | Hyperliquid and Polymarket candles · its commands |
vike-data polymarket … (pm) | Markets, traders, smart money, tape, order book, settlements, archive · its commands |
vike-data alerts … | Alert rules, channels, webhooks, the event feed (at most 200 active per account) · its commands |
vike-data search <q> | Search everything by name, symbol, address, ENS name or polymarket.com link · its commands |
vike-data stream <path> | Live events as NDJSON from wss://stream.vike.io/v1/stream/<path>; reconnects and resumes · its commands |
vike-data stream info | check | events | connections | The stream registry, a filter check, stored events, your connections · its commands |
vike-data call <tool> --args '{…}' | Call any MCP tool by name and print its raw result · its commands |
vike-data install --claude | --cursor | --copilot | --agents | --all | Copy the vike skill where each agent looks for it |
| Variable | Effect |
|---|---|
VIKE_API_KEY | the key; wins over the saved config |
VIKE_MCP_URL | the MCP endpoint, https://data.vike.io/mcp by default |
VIKE_NO_TELEMETRY=1 or DO_NOT_TRACK=1 | turns off anonymous usage telemetry (also off whenever CI is set) |
VIKE_NO_UPDATE_CHECK=1 or NO_UPDATE_NOTIFIER=1 | turns off the background update check |
MCP: the server for AI agents
One URL for Claude, ChatGPT, Cursor and every other MCP client.
https://data.vike.io/mcp is the Vike Data API's Model Context Protocol server: one URL, 83 tools, JSON-RPC 2.0 over HTTP POST. Point an MCP client at it and an agent reads wallets, tokens, Hyperliquid, HL Cohorts, Polymarket and Predict.fun, and manages your alerts, in plain words. It is a remote server: nothing to install, nothing running on your machine. Same data, same key, same hourly budget as REST.
| Property | Value |
|---|---|
| Endpoint | https://data.vike.io/mcp |
| Transport | Streamable HTTP, POST only |
| Protocol | JSON-RPC 2.0 |
| MCP version | 2025-03-26 |
| Server identity | serverInfo = vike-data, version 1.0.0 |
| Methods | initialize, notifications/initialized, tools/list, tools/call; any other is -32601 |
| Credential | OAuth 2.1 (Claude Desktop, ChatGPT) or X-API-KEY; Authentication |
tools/list returns the whole catalogue in one response, with no paging. The tools, by family:
| Family | Tools | Credits a call | Names |
|---|---|---|---|
| On-chain wallets, tokens and transactions | 20 | 0–5 | token_search, wallet_summary, wallet_discover, token_transfers, wallet_label_provenance, token_holders, wallet_transactions, wallet_compare, wallet_trace, wallet_batch_labels, token_screener, wallet_counterparties, wallet_pnl_history, wallet_token_balances, wallet_ens, wallet_first_funder, wallet_exchange_usage, wallet_check, wallet_positions, transaction_lookup |
| Hyperliquid | 18 | 1–2 | hl_perp_top_traders, hl_position_match, hl_wallet, hl_wallet_activity, hl_leaderboard, hl_liquidations, hl_market, hl_builders, hl_vaults, hl_vault, hl_orders, hl_twaps, hl_ledger, hl_leverage_changes, hl_wallet_trades, hl_stats, hl_hype_holders, hl_archive |
| HL Cohorts | 2 | 1 | hl_cohort_metrics, hl_segments |
| Polymarket | 13 | 1–2 | polymarket_markets, polymarket_market_detail, polymarket_wallet, polymarket_market_trades, polymarket_smart_money, polymarket_tape, polymarket_orderbook, polymarket_market_stats, polymarket_settlement, polymarket_settlements_bulk, polymarket_archive, polymarket_leaderboard, polymarket_chainlink_prices |
| Predict.fun | 9 | 1 | predictfun_overview, predictfun_markets, predictfun_market_detail, predictfun_market_book, predictfun_market_trades, predictfun_wallet_trades, predictfun_wallet_positions, predictfun_leaderboard, predictfun_wallet_flows |
| Alerts (the only tools that write: your own rules and channels) | 10 | 0–3 | alerts_create, alerts_delete, alerts_edit, alerts_events, alerts_list, alerts_list_channels, alerts_register_webhook, alerts_resend_delivery, alerts_rotate_webhook_secret, alerts_test_webhook |
| Search and streams | 5 | 0–1 | search, stream_info, stream_check_filter, stream_events, stream_connections |
| Candles | 2 | 1 | hl_ohlcv, polymarket_ohlcv |
| Perps (venues compared, MCP only) | 4 | 1 | perp_funding, perp_spreads, perp_open_interest, perp_long_short |
alerts_* is the only family that writes anything, and what it writes is your own alert rules and channels. The server holds no keys and signs no transactions: nothing here touches a market, an order or a wallet you control. 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.
Vike's other MCP server, the trading platform's vike-cli mcp, is a local process set up apart (Trader docs): in a client's configuration it is a second entry under another name, with command and args instead of url, and the two share no credential, budget or transport.
Connect your client
Pick your client; Claude Desktop and ChatGPT need no key. The concrete steps, and what fails, below.
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 · client
Claude Desktop
Credential
OAuth 2.1, no key
Free API Archive Stream MCP
Claude Desktop registers itself with Vike's authorization server and you approve in a browser; nothing is copied or pasted.
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
Leaving the client id and secret empty is the point: Claude Desktop calls POST /oauth/register (RFC 7591) and gets its own. If the hammer icon shows no Vike tools, authorization did not complete: remove the connector and add it again, watching for the browser tab. Revoke under Account → API Keys → Connected Apps; OAuth 2.1 has what the server enforces.
Ask: “Search for PEPE token using Vike”
That calls token_search, which costs 0 credits, so a failed attempt costs nothing. If the tools are listed but never used, name one in the prompt: "use the vike token_search tool".
claude mcp add --transport http vike https://data.vike.io/mcp \
--header "X-API-KEY: $VIKE_API_KEY"
--transport httpis required: the endpoint is Streamable HTTP, not a local process. Without it Claude Code tries to run the URL as a command.--scopepicks local, project or user; checkclaude mcp listafterwards. A project scope writes a file that may be committed: keep the key in the environment, as above.- Restart any session that was already open: sessions read the server list at start.
$VIKE_API_KEYthat expands to nothing registers an empty header: echo it first.- Remove it with
claude mcp remove vike; deleting the key under Account → API Keys is what retires it.
Ask: “Search for PEPE token using Vike”
That calls token_search, which costs 0 credits, so a failed attempt costs nothing. If the tools are listed but never used, name one in the prompt: "use the vike token_search tool".
MCP · client
ChatGPT
Credential
OAuth sign-in, or X-API-KEY
Free API Archive Stream MCP
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_…
- OAuth, where the ChatGPT surface offers it. ChatGPT finds the authorization server from the
401discovery header, registers itself and you approve in a browser. The server accepts the two redirect URIs OpenAI documents for connectors,https://chatgpt.com/connector_platform_oauth_redirectandhttps://chatgpt.com/connector/oauth/{callback_id}; any other isinvalid_redirect_uri. - An API key, where ChatGPT lets you send a custom header:
X-API-KEY: vk_…. - Which plans and menus offer a custom connector is OpenAI's to say; a surface with neither an OAuth option nor a header field cannot carry either credential.
- Prove the key outside ChatGPT first with the raw HTTP
tools/listcall.
Ask: “Search for PEPE token using Vike”
That calls token_search, which costs 0 credits, so a failed attempt costs nothing. If the tools are listed but never used, name one in the prompt: "use the vike token_search tool".
MCP · client
Cursor
Credential
X-API-KEY in mcp.json
Free API Archive Stream MCP
Settings → MCP → Add new server, or edit ~/.cursor/mcp.json (every project) or .cursor/mcp.json in a repository (that project only):
{
"mcpServers": {
"vike": {
"url": "https://data.vike.io/mcp",
"headers": { "X-API-KEY": "YOUR_KEY" }
}
}
}
url+headersis the remote shape and the only one that works: acommandkey would not reach an HTTP service.- A per-project file sits in the repository: add it to
.gitignore, or keep the server in the global file. If a key does get committed, delete it under Account → API Keys and make a new one: the old key keeps working until it is deleted, and for up to 45 seconds after. - Reload Cursor or toggle the server; a healthy
vikeentry lists the tools. A red entry is almost always the JSON: a trailing comma, orurlandheadersoutside thevikeobject. - Keep the server in one file: two
vikeentries with different keys spend two budgets.
Ask: “Search for PEPE token using Vike”
That calls token_search, which costs 0 credits, so a failed attempt costs nothing. If the tools are listed but never used, name one in the prompt: "use the vike token_search tool".
gemini mcp add --transport http \
--header "X-API-KEY: $VIKE_API_KEY" \
vike https://data.vike.io/mcp
- Order: options, then the name (
vike), then the URL.--transport httpis required: the default is stdio, which expects a local command. - A header with spaces must be one quoted argument, as above;
$VIKE_API_KEYthat expands to nothing registers an empty header. - Check it with
gemini mcp list.
Ask: “Search for PEPE token using Vike”
That calls token_search, which costs 0 credits, so a failed attempt costs nothing. If the tools are listed but never used, name one in the prompt: "use the vike token_search tool".
MCP · client
VS Code (GitHub Copilot)
Credential
X-API-KEY in .vscode/mcp.json
Free API Archive Stream MCP
Create .vscode/mcp.json in the workspace and reload the window:
{
"servers": {
"vike": {
"type": "http",
"url": "https://data.vike.io/mcp",
"headers": { "X-API-KEY": "YOUR_KEY" }
}
}
}
- The top-level key is
servers(notmcpServers), and each entry carries"type": "http". - The file sits in the workspace: add it to
.gitignore, or use VS Code's input variables so the key is prompted for. If a key does get committed, delete it under Account → API Keys and make a new one: the old key keeps working until it is deleted, and for up to 45 seconds after. - A trailing comma silently disables the whole file; MCP configuration is read at startup.
- In Copilot Chat's agent mode the tools picker lists the server; its tools may need enabling for the conversation.
Ask: “Search for PEPE token using Vike”
That calls token_search, which costs 0 credits, so a failed attempt costs nothing. If the tools are listed but never used, name one in the prompt: "use the vike token_search tool".
MCP · client
Raw HTTP
Credential
X-API-KEY on every POST
Free API Archive Stream MCP
Everything is POST https://data.vike.io/mcp with Content-Type: application/json and one credential header. Each method is dispatched on its own (initialize is not a gate), so a bare tools/list proves the connection:
curl -s https://data.vike.io/mcp \
-H "X-API-KEY: $VIKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -c 400
A tool call
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "token_search",
"arguments": {
"query": "PEPE"
}
}
}
The result is {"content": [{"type": "text", "text": "<the tool's JSON>"}], "isError": false}: decode the JSON-RPC response, then result.content[0].text. An empty arguments is a valid call but not "everything": each tool's defaults apply (alerts_list lists active rules only).
| Failure | HTTP | Body |
|---|---|---|
| Bad or missing credential | 401 + WWW-Authenticate: Bearer resource_metadata=… | {"error": {"code": -32000, "message": "Invalid or missing API key"}} |
| Hourly limit | 429 + Retry-After: 60 | -32000, Hourly rate limit exceeded, data: {"code": "RATE_LIMITED", "retry_after": 60} |
| Malformed JSON | 200 | -32700 with id: null |
| Unknown method or tool | 200 | -32601, Unknown tool: … |
| A tool that failed | 200 | a result with isError: true and {"error": "…"} as its text |
Check the body, not just the status. Only a credential failure and the rate limit change the HTTP status. A script that checks response.ok or only the JSON-RPC error reads a failed tool call as a success: check result.isError too.
The alert tools: 10 tools that write to your account
Make, change and delete alert rules, register a signed webhook, read the feed. Each is also a /v1/alerts route.
Tools for alert rules: rules that fire when a transfer happens on chain. Unlike every other tool on this page, these write to your account — they are the only ones that change state.
Transfer alerts (alert_type transfer, the default) watch eth, bsc, base or hyperevm: a token crossing a USD threshold, or a given wallet sending or receiving. See Watching a wallet. The same tools also make Hyperliquid and Polymarket alerts; their types, cohorts and limits are on the Alerts reference.
What constrains a rule, all of it stated by the tools themselves:
- One chain per transfer alert. Passing two silently watches only the first, with no error to tell you the second was dropped. Watching both means creating two alerts.
- Two kinds of threshold on transfers. A USD value (
min_value_usdat least,max_value_usdat most) and, for an alert on onetoken_symbol, a size in whole tokens (min_amountat least). Set together, both must hold. - One wallet per alert, as a 20-byte EVM address (
0xand 40 hex characters). An ENS name, an entity name ("Binance 14") or a non-EVM address is refused, not resolved. - The type is fixed once created.
alerts_editchanges a rule's parameters, never its type. - Delivery is email, a signed webhook, and the feed that every rule fills. Discord alerts are temporarily off. See Where an alert goes.
Alerts belong to the authenticated user, and every tool enforces ownership: you can only list, edit or delete your own. Both credentials resolve to a user, so an API key and an OAuth token reach the same alerts. The same rules can be made under Account → Alerts, with the same checks, and the two always agree on what a rule means. Every tool here is also a REST route under /v1/alerts, on the same code and at the same price: Alerts API.
Alert types
alert_type picks what a rule watches; each type's parameters, a question to ask, the /account link and the CLI line are on its product's page:
alert_type | Product page |
|---|---|
transfer | On-chain |
hl_wallet | Hyperliquid |
hl_cohort_bias | HL Cohorts |
hl_cohort_fill | Hyperliquid |
hl_liquidation | Hyperliquid |
hl_liq_proximity | Hyperliquid |
hl_wallet_orders | Hyperliquid |
pm_wallet_trade | Polymarket |
pm_market_trade | Polymarket |
pm_price_move | Polymarket |
pm_resolution | Polymarket |
Hyperliquid alerts
They watch perpetual markets, builder markets such as xyz:CL included. Give a coin as Hyperliquid names it (case is forgiven: kpepe is stored as kPEPE); a spot market (PURR/USDC, @107) is refused, and so is a coin Hyperliquid does not list. Delivery is e-mail, a webhook, or the feed alone (channels empty).
| Type | Fires | Needs | Policy |
|---|---|---|---|
hl_wallet | on a fill of one wallet, narrowed by actions | wallet | per_event |
hl_cohort_fill | on a fill by any wallet in the cohorts you name | a cohort, and min_value_usd of 10,000 or more | per_event |
hl_liquidation | on a liquidation | nothing: every filter is optional | per_event |
hl_cohort_bias | when a cohort index of a coin flips | token_symbol, index | hysteresis |
hl_liq_proximity | when a wallet's position nears its liquidation price | wallet | hysteresis |
hl_wallet_orders | once a check, listing the orders one wallet placed and the ones that left the book | wallet; min_value_usd of 1,000 or more (default 10,000) | per_event |
hl_wallet:actionspicks fromopen(flat to long or short),close(back to flat),flip(long to short or the reverse in one fill),increase(addis accepted) andreduce; the default isopen,close,flip,increase, soreduceis opt-in. The fills one check reads are merged into one event per coin, action and side, withfillstheir count;min_value_usdis a floor on that merged total, and a fill whose action cannot be worked out is never reported.hl_cohort_fill:pnl_cohortsandsize_cohortstogether; a wallet must match both lists when you give both. Name at least one cohort, in either list.hl_liquidation:wallet,token_symbol,min_value_usd(the size that was closed) and the two cohort lists, all optional and all required to match together; with none set it reports every liquidation.hl_cohort_bias:indexis one ofsmi,wsi,smart_bias,whale_bias,smi_wsi_gap(each -1 to 1,smi_wsi_gap-2 to 2).threshold(default 0) is the line,band(default 0.1, above 0) the margin on each side,direction(up,downorboth, the default) the crossings that notify; |threshold| +bandcannot exceed the index's range. The index is high once it reachesthreshold+band, low atthreshold−band, and nothing changes in between. The first reading only sets the side, and a coin with no position in a cohort leaves the state alone.hl_liq_proximity: fires once for a position when itsliq_ratio(0 at entry, 1 at liquidation) reachesthreshold(default 0.8, strictly between 0 and 1), and re-arms when the ratio falls belowthreshold−band(default 0.1, smaller thanthreshold) or the position closes. A long and a short on one coin are watched apart; a position already past the threshold when the rule starts alerts only after it has re-armed.hl_wallet_orders: the wallet's orders from the order-book snapshots taken every 5 minutes:placedwhen an order is new in a snapshot,gonewhen it is missing from one, withoutcomefilled,partially_filledorcanceledfrom the order's own fills (orunknownwhen the fills arrive more than 10 minutes late). Each check sends one event whoseorderslists every order it found, with the check's totals beside it; nothing is left out. A check of more than 200 orders (a market maker's: over a thousand) comes as several events of 200,part1 toparts, each carrying the same totals. The e-mail shows the totals and the 20 largest orders; the webhook and the feed carry them all.min_value_usdis the order's size × limit price (a stop at its trigger price);events(placed,gone, default both),side(buyorsell, default both) andtoken_symbolnarrow it. An order placed and gone between two snapshots is never seen. These events are private: only the alert's owner receives them, never the public WebSocket.- Cohorts:
pnl_cohortsandsize_cohortstake the labels of the two ladders (Cohorts), read when the event is checked. A wallet with no label on a ladder you named never matches: one with fewer than 10 closed trades has no PnL cohort, and one with no open position has no size cohort, the usual case right after a liquidation, so ahl_liquidationrule withsize_cohortscan miss those. - 200 watched wallets: an account can watch up to 200 distinct wallets across its active
hl_wallet,hl_wallet_ordersandhl_liq_proximityrules. A second rule on a wallet you already watch costs nothing, a paused rule does not count, and the optionalwalletofhl_liquidationdoes not count. - An edit checks the whole rule again: the stored fields with the ones you pass laid over them.
""or[]clears an optional field; a field the type does not read is an error. A rule that no longer passes (a coin Hyperliquid delisted) cannot be paused withalerts_edit: delete it.
Polymarket alerts
They watch Polymarket's on-chain trade tape, order books and resolutions. A wallet is the proxy wallet, the address of its wallet page; a market is its condition_id, 0x and 64 hex characters. Delivery is e-mail, a webhook or the feed.
| Type | Fires | Needs | Policy |
|---|---|---|---|
pm_wallet_trade | on every trade of one wallet, optionally in one market and above a USD value | wallet | per_event |
pm_market_trade | on every trade in one market at or above a USD value | condition_id, min_value_usd of 1,000 or more | per_event |
pm_price_move | when the market's price moves 0.05 or more within 5 minutes | condition_id | per_event |
pm_resolution | when the market resolves | condition_id | per_event |
- A trade is one fill of a maker order, as the tape records it: a large order filled against several makers is several trades, each with its own USD value (size × price).
- A price move follows the market's first outcome (Yes, Up, or the first team): the other outcome mirrors it, so each move is reported once.
directionpicksup,downorboth(the default). A market opened in the last few minutes is not followed yet, and a move spanning two 5-minute windows can be reported twice. - A resolution is reported once, with the winning outcome, or none when the payout is split.
How soon a Hyperliquid or Polymarket alert arrives
| Alert | Checked | Arrives |
|---|---|---|
hl_wallet, hl_cohort_fill | every 10 seconds, sooner when new fills are announced | usually within a minute or two of the fill |
hl_liquidation | every minute | usually within a minute or two of the liquidation fill |
hl_cohort_bias, hl_liq_proximity | each new capture of all positions, about every 12 minutes | up to about 20 minutes after the move; a new or edited rule is armed one capture later |
hl_wallet_orders | every minute, against the order-book snapshot taken every 5 minutes | up to about 7 minutes after the order is placed or gone (later while the node lags), all of a check's orders in one event; a gone order waits for its fills, usually a minute or two, and comes in the check that decides it |
pm_wallet_trade, pm_market_trade, pm_resolution | every 15 seconds (resolutions every minute) | about 5 minutes after the block: the tape waits for confirmations |
pm_price_move | every 30 seconds | within a minute of the move |
When a Hyperliquid or Polymarket rule is refused
| You asked for | The tool says |
|---|---|
| a parameter the type does not read | index does not apply to hl_wallet; it applies to hl_cohort_bias |
hl_wallet without a wallet | wallet is required for hl_wallet: pass the 0x + 40 hex address to watch |
hl_cohort_fill below the floor | hl_cohort_fill needs min_value_usd >= 10,000: without a size floor it would match every fill on Hyperliquid |
hl_cohort_fill with no cohort | Pick at least one PnL or size cohort: a rule without one would alert on every fill above $10,000. |
| a coin Hyperliquid does not list | unknown Hyperliquid perp 'BTCC': use the coin as Hyperliquid names it …; did you mean BTC? |
| a spot market | symbol 'PURR/USDC' is a spot market; alerts cover perpetuals only (BTC, ETH, kPEPE, xyz:CL) |
| an unknown cohort or action | unknown pnl cohort 'x'; valid: 3x smart, … |
| a bias line the index cannot reach | threshold 0.9 and band 0.2 are unreachable for smi (range -1..1), then what to keep within the range |
| a band of 0 or less | band must be greater than 0 …: a zero band re-fires on every tick around the threshold |
| a band that cannot re-arm | band 0.9 must be below threshold 0.8: the rule could never re-arm; pass a smaller band |
a market that is not 0x and 64 hex characters | condition_id '…' is not a Polymarket market id: 0x + 64 hex characters, … |
pm_market_trade below the floor | pm_market_trade needs min_value_usd >= 1,000: a busy market trades thousands of times a day |
| a webhook you have not registered | no enabled webhook channel: register one first …, or leave channels empty and read GET /v1/alerts/events |
| a 201st watched wallet | You can watch up to 200 wallets. Remove or pause a rule on another wallet first. |
chain, address … on a Hyperliquid or Polymarket alert | … apply only to transfer alerts; a Hyperliquid or Polymarket alert has no chain, address, max_value_usd or min_amount (use wallet, symbol, condition_id and min_value_usd) |
The website's alert form makes every one of these types (What to watch?). Its edit view changes only a Hyperliquid or Polymarket alert's name, description, tags, e-mail and webhooks (and a followed wallet's coin, floor and actions); change its parameters with alerts_edit, or make a new alert.
Alerts · MCP tool
alerts_list
Cost
0 credits a call
Free API Archive Stream MCP
Lists your alert rules. Active only by default.
| Parameter | Type | Default | Notes |
|---|---|---|---|
include_inactive | boolean | false | Include paused and soft-deleted alerts |
Returns {"alerts": [...]}, one row each with id, label, alert_type, chain, token_symbol, min_value_usd, max_value_usd, min_amount, address_conditions, cooldown_minutes, max_triggers_day, notify_channel_ids, is_active, created_at, policy, hl and pm.
address_conditionsis a list of{"role": "from" | "to" | "any", "address": "0x…"}, empty when the alert watches no wallet; an alert made on the website can carry both afromand atoaddress, which is why it is a list.policysays how often the rule notifies:per_event,cooldownorhysteresis(see How often a rule notifies). It isnullfor a rule that cannot run, for example one whose coin Hyperliquid has delisted.hlandpmarenullfor a transfer alert; for a Hyperliquid or Polymarket alert they hold its parameters (Alerts reference).
This is also how you get an alert_id — alerts_edit and alerts_delete both need one, and neither accepts a label.
Ask: “List my alerts, including the paused ones.”
Alerts · MCP tool
alerts_list_channels
Cost
0 credits a call
Free API Archive Stream MCP
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?”
Alerts · MCP tool
alerts_create
Cost
3 credits a call
Free API Archive Stream MCP
Creates an alert rule. Below, a transfer alert; the same tool creates the Hyperliquid and Polymarket types, whose parameters are on the Alerts reference.
| Parameter | Type | Default | Notes |
|---|---|---|---|
label | string | required | The alert's user-visible name |
alert_type | enum | transfer | transfer for an on-chain alert, or a Hyperliquid or Polymarket type. Fixed once created |
token_symbol | string | — | Transfer alerts: the token's ticker in UPPERCASE (USDC, ETH, PEPE) |
chain | enum | eth | Transfer alerts only: eth, bsc, base or hyperevm, single value. Leave it out for a Hyperliquid alert |
min_value_usd | number | — | Transfer alerts: fire when the transfer's USD value is at least this |
max_value_usd | number | — | Transfer alerts only: fire only when the transfer's USD value is at most this |
min_amount | number | — | Transfer alerts with a token_symbol only: fire when the transfer moves at least this many whole tokens (1000000 USDT, 0.5 ETH), priced or not. With a USD bound as well, both must hold. A token whose decimals we do not know never matches it |
address | string | — | Transfer alerts only: one wallet to watch, 0x and 40 hex characters |
address_role | enum | any | Transfer alerts only: any (sent or received), from (it sent) or to (it received); needs address |
channels | array | — | ["email"], ["webhook"] or both (see Where an alert goes). Discord is refused |
cooldown_minutes | integer | 60 | Token-wide transfer alerts only (no address): the minimum gap between notifications, floor of 1. Every other kind ignores it |
The Hyperliquid and Polymarket parameters (wallet, condition_id, actions, pnl_cohorts, size_cohorts, index, threshold, band, direction, side, events) are on the Alerts reference.
Returns {"alert": {...}} — the new row in the shape alerts_list uses, including its id and its policy. A request the checks refuse comes back as {"error": "..."} and nothing is created; see When a rule is refused.
A parameter that the type does not read is an error, never ignored: a rule that quietly lacked the filter you typed would alert on the wrong thing. alert_type is transfer unless you say otherwise, so chain defaults to eth.
Ask: “Alert me on email when more than $5M of USDC moves on Ethereum, at most once an hour.”
Ask: “Tell me every time 0x28C6…1d60 sends more than $1M on Ethereum.”
Alerts · MCP tool
alerts_edit
Cost
2 credits a call
Free API Archive Stream MCP
A partial update: only the fields you pass change.
| Parameter | Type | Default | Notes |
|---|---|---|---|
alert_id | integer | required | Required. From alerts_list. |
label | string | — | Same meaning as in alerts_create |
token_symbol | string | — | Same meaning as in alerts_create |
chain | string | — | Same meaning as in alerts_create |
min_value_usd | number | — | Same meaning as in alerts_create |
max_value_usd | number | — | Same meaning as in alerts_create |
min_amount | number | — | Same meaning as in alerts_create; min_amount: "" or 0 removes the size floor |
channels | array | — | Same meaning as in alerts_create |
cooldown_minutes | integer | — | Same meaning as in alerts_create |
address | string | — | Same rules as in alerts_create. address replaces the alert's wallet condition; address: "" removes it; address_role alone moves the existing address to that side |
address_role | enum | — | Same rules as in alerts_create. address replaces the alert's wallet condition; address: "" removes it; address_role alone moves the existing address to that side |
is_active | boolean | — | false pauses the alert without deleting it |
An edit that leaves address out keeps the wallet condition as it was, and address without address_role keeps the current side (any when the alert had no wallet). A rejected address changes nothing, including the other fields in the same call.
An edit that changes what a rule matches (token, chain, thresholds, wallet) starts the rule afresh: it reports events from that moment on and never replays earlier ones. The same holds when you turn a paused rule back on. Changing the label, the channels or cooldown_minutes does not restart anything. Editing a Hyperliquid or Polymarket alert: Alerts reference.
Ask: “Pause my USDC alert.”
Ask: “Raise the threshold on alert 41 to $10M.”
Alerts · MCP tool
alerts_delete
Cost
2 credits a call
Free API Archive Stream MCP
Removes an alert.
| Parameter | Type | Default | Notes |
|---|---|---|---|
alert_id | integer | required | The alert to delete |
hard | boolean | false | true removes permanently; the default pauses and keeps history |
Returns {"ok": true, "alert_id": ..., "hard": ...} on success. A soft delete is the same state as is_active: false, so an alert deleted this way still appears under alerts_list with include_inactive: true and can be re-enabled with alerts_edit. A hard delete also removes the rule's events from the feed.
Ask: “Delete alert 41 — actually, just pause it.”
Alerts · MCP tool
alerts_register_webhook
Cost
2 credits a call
Free API Archive Stream MCP
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.
Ask: “Send my alerts to https://example.com/hooks/vike as well, with a signing secret.”
Alerts · MCP tool
alerts_rotate_webhook_secret
Cost
2 credits a call
Free API Archive Stream MCP
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.
Ask: “Rotate the signing secret of my webhook.”
Alerts · MCP tool
alerts_test_webhook
Cost
1 credit a call
Free API Archive Stream MCP
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.
Ask: “Send a test event to my webhook.”
Alerts · MCP tool
alerts_events
Cost
1 credit a call
Free API Archive Stream MCP
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.
Ask: “Show me what my alerts have fired since yesterday.”
Alerts · MCP tool
alerts_resend_delivery
Cost
1 credit a call
Free API Archive Stream MCP
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.
Ask: “The webhook of my last USDC alert never arrived: send it again.”
Alerts
How often a rule notifies
Free API Archive Stream MCP
policy in alerts_list is fixed by the rule's type. A transfer alert is one of two:
policy | Rules | What it does |
|---|---|---|
per_event | a transfer alert with an address | One notification for every matching event. No cooldown and no daily cap; cooldown_minutes is ignored. A busy wallet or a low floor is a flood, so set a floor |
cooldown | a transfer alert without an address (token-wide) | At most one notification per cooldown_minutes (default 60) and 10 a day, naming the newest matching transfer |
Every event, whatever the policy, is kept in the feed for 15 days. An account can have up to 200 active alerts in all, of any kinds; the 201st is refused with "You can have up to 200 active alerts. Pause or delete one first.", when you create it and when you turn a paused one back on.
Alerts
Watching a wallet
Free API Archive Stream MCP
address and address_role ask for a transfer that involves one wallet: from matches a transfer the wallet sent, to one it received, any either. Stored on the alert exactly as the website's alert form stores it (from_address, to_address or any_address in the alert's notes), so an alert made here opens and edits correctly under Account → Alerts, and the reverse.
What the match is, and is not:
- A transfer row, any token. The alert compares the address with the sender and receiver of each recorded transfer on the alert's chain: native coin and token transfers alike. With no
token_symbolevery token counts, spam and dust tokens included. - Every match notifies. An alert with an
addressisper_event: one notification for each matching transfer, with no cooldown and no daily cap, so a busy wallet with no threshold is a flood that nothing throttles. min_value_usdis optional, and it changes what matches. With it, only transfers that already carry a USD value at or above it match, so an unpriced token never does. Without it, every transfer touching the address matches, priced or not.max_value_usdis an upper bound on the same value and likewise needs a priced transfer.min_amount(with atoken_symbol) is a floor in whole tokens that needs no price.- Not supported. An ENS name or an entity label as the address; a non-EVM address; two wallets on one alert; "any wallet in this group". Create one alert per wallet and per chain.
- Not the same as a wallet's balance or PnL. The alert looks at individual transfers; it does not know a wallet's net position.
A token-wide transfer alert (no address) is the cooldown policy above: one notification per cooldown, for the newest matching transfer, at most 10 a day.
Alerts
When a rule is refused
Free API Archive Stream MCP
A request the checks refuse returns {"error": "..."} in the tool result (the website form shows the same text in a red notice), nothing is created or changed, and the call is not billed. Each message says what to change.
| You asked for | The tool says |
|---|---|
| an address that is not a 20-byte EVM address | address '…' is not a 20-byte EVM address: pass '0x' + 40 hex characters. ENS names, entity names and non-EVM addresses (Solana, Bitcoin) are not supported. |
a token the transfer alert's chain does not have (its native coin, ETH on eth, is accepted) | No token BTC on eth: an alert on it could never fire. Did you mean WBTC? |
| Discord on any alert | Discord alerts are temporarily off … |
| a 201st active alert | You can have up to 200 active alerts. Pause or delete one first. |
wallet, index … on a transfer alert | … apply only to Hyperliquid and Polymarket alerts …; this is a transfer alert |
The website's alert form creates these alerts too (What to watch? at the top of the form). The Hyperliquid and Polymarket types, their checks and their messages are on the Alerts reference.
The search tool is the vike.io search box, the same answer as GET /v1/search, at 1 credit. The other four help an agent use the WebSocket, which it cannot hold open itself: describe the stream, check a subscription before a program sends it, read what a channel carried, and see your own connections. Each also has a CLI command: vike-data stream info, check, events, connections.
Streams · MCP tool
stream_info
Cost
0 credits a call
Free API Archive Stream MCP
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 value, a number (min_usd and the other min_ keys) or a boolean (contract: true or false, not a string), with the allowed values or the server's own message for a bad one, and its defaults (hl.fills: min_usd 1000 unless given); the REST catch-up route of each path; the limits (ONE connection per API key per path and THREE per account per path, newest wins with close 4410; 100 subscriptions per connection; a ping every 15 s); the close codes; cursor retention (3 days) and the subscribe message's shape. Check a filter with stream_check_filter and read missed events with stream_events.
No parameters.
Streams · MCP tool
stream_check_filter
Cost
0 credits a call
Free API Archive Stream MCP
Validate a stream subscribe message (or a channel + filter) with the SAME validator the stream server runs, without connecting. Returns {valid: true, url, subscribe} with the filter exactly as the server will apply it (values normalised, lists sorted, defaults added: hl.fills gets min_usd 1000 unless given), or {valid: false, code, message} with the server's own error (bad_subscription naming the key, unknown_channel listing the path's channels). path checks the channel against the endpoint the connection would use (a channel of another path is unknown_channel there). Costs nothing.
| Parameter | Type | Default | Notes |
|---|---|---|---|
message | object or string | — | A whole subscribe message, e.g. {"action": "subscribe", "id": "w1", "channel": "hl.fills", "filter": {"coin": ["BTC"]}}. Or give channel and filter instead. |
channel | string | — | A stream channel, e.g. hl.fills, hl.liquidations, chain.transfers, pm.trades; stream_info lists them all. Used when message is not given. |
filter | object or string | — | The filter object, e.g. {"wallet": ["0x..."], "min_usd": 50000}. Used when message is not given. |
path | string: hl, hl-cohorts, onchain, polymarket | — | The endpoint the connection would use. Default: the channel's own path. |
Streams · MCP tool
stream_events
Cost
1 credit a call
Free API Archive Stream MCP
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. Returns {events: [{cursor, event}], next_cursor, has_more}; event is byte for byte what the WebSocket frame carried. An idle window of the on-demand channel is an item {cursor, gap} in its place. Pass next_cursor back as cursor for the next page; a page at the end returns the channel head, so an empty page on a quiet channel returns the same cursor. Events are kept 3 days: an older cursor is refused with a resume cursor in the message. since_ms starts at an ARRIVAL time instead of a cursor. Delivery is at least once: dedupe on event.event_id. hl.fills without min_usd is min_usd 1000, as on the socket (0 = every fill). Every channel stream_info lists is accepted, including one the stream does not run yet (a subscribe there answers unknown_channel): its pages are empty, like a quiet channel's. Same data as GET /v1/{hyperliquid,onchain,polymarket}/stream-events.
| Parameter | Type | Default | Notes |
|---|---|---|---|
channel | string | required | A stream channel, e.g. hl.fills, hl.liquidations, chain.transfers, pm.trades; stream_info lists them all. Required. |
cursor | string | — | next_cursor of the previous page or the cursor of a stream frame. Omit (and omit since_ms) for the oldest retained event. |
since_ms | integer or string | — | Epoch MILLISECONDS: start after this arrival time instead of a cursor. Not together with cursor. |
filter | object or string | — | The channel's filter keys as on a subscribe, e.g. {"coin": ["BTC"], "min_usd": 100000}. An unknown or channel-foreign key is refused. |
limit | integer or string | 200 | Events per page, 1 to 1000 (default 200). |
Streams · MCP tool
stream_connections
Cost
0 credits a call
Free API Archive Stream MCP
Your own recent stream connections (this account only, every API key and OAuth app of it), newest first: path, url, api_key_id (or oauth_client_id), connected_at, state open or closed, the subscribe count and, once closed, closed_at, duration_s, the channels, close_code (4410 = replaced by a newer connection) and events_sent. RECONSTRUCTED from the connect, subscribe and close records the stream writes, not read from the stream's live state, and it may lag a few seconds: open means no close has been recorded yet (a server crash records none), and an open connection's channels are named only when it closes. Use it to see why a socket was closed or whether two programs share a key (each closes the other with 4410). truncated is true when only the newest records of the window could be read.
| Parameter | Type | Default | Notes |
|---|---|---|---|
hours | integer or string | 168 | How far back to look, 1 to 720 hours (default 168, a week). A connection opened before it is not listed. |
limit | integer or string | 50 | Connections to return, newest first, 1 to 1000 (default 50). |
Candles
Hyperliquid markets and Polymarket outcome tokens.
Two candle tools, one per venue whose data is on chain. Neither takes a contract address: there are no candles for Ethereum, BNB Chain, Base or HyperEVM tokens, and none for centralised-exchange tickers. Asked to chart a token, an agent says so rather than substituting another series.
Candles · MCP tool
hl_ohlcv
Cost
1 credit a call
Free API Archive Stream MCP
Historical OHLCV candles for a Hyperliquid market, aggregated from HL's own executed fills, by view. candles (default): one symbol's bars. Pass symbol as the HL asset (BTC, ETH, HYPE); market picks perps or HL spot and dex picks a builder-deployed HIP-3 perp DEX (default HL, the core venue). Buckets are left-aligned UTC and the window is half-open [start, end). Returns candles as {ts (epoch ms, UTC), open, high, low, close, volume, volume_usd, n_trades} ascending by ts, plus source naming the rollup that answered. export: the same, with limit defaulting to the maximum, 5000 (the REST export streams it as NDJSON). latest: the newest 1-minute bar of EVERY symbol that traded in the last hour in one call (takes only market, dex and interval; dex omitted = every DEX, each row naming its own; a bar may still be forming). Same data as GET /v1/hyperliquid/ohlcv, /ohlcv/export and /ohlcv/latest.
| Parameter | Type | Default | Notes |
|---|---|---|---|
view | string: candles, latest, export | "candles" | candles (default): one symbol's bars; export: the same with limit defaulting to 5000; latest: the newest 1-minute bar of every symbol. |
symbol | string | — | HL asset: perp ticker (BTC), spot token (HYPE) or raw spot index (@107). Required by candles and export. |
interval | string: 1m, 5m, 15m, 30m, 1h, 4h, 1d | — | Candle width. Default 1h; latest takes 1m only, its default. |
market | string: perp, spot | "perp" | Perpetual or HL spot. Default perp. |
dex | string | — | Perp DEX. Default HL, the core venue; the others are builder-deployed HIP-3 DEXs. On latest, omitted = every DEX. |
start | string | — | ISO-8601 UTC or epoch ms (inclusive). Default: end - limit*interval. |
end | string | — | ISO-8601 UTC or epoch ms (exclusive). Default: now. |
limit | integer or string | — | Max candles (default 500 on candles, 5000 on export; hard max 5000). |
Built from Hyperliquid's own executed fills. The route, a builder and the bars: Hyperliquid AI & API, /v1/hyperliquid/ohlcv and historical candles; closed bars live on the hl.candles channel.
Candles · MCP tool
polymarket_ohlcv
Cost
1 credit a call
Free API Archive Stream MCP
Historical OHLCV candles for ONE Polymarket outcome token, from the recorded L2 tape. token_id is the ERC-1155 outcome token id — a market's condition_id will NOT work here; read the token ids off clob_token_ids from polymarket_market_detail. Prices are probabilities in [0, 1]. Returns bars OLDEST first as {ts (epoch ms, UTC), open, high, low, close, volume, trades}. There is no cursor, so a window wider than limit is truncated at the OLD end — raise from to reach the recent end. With to and no from, limit keeps the bars nearest to (to =X, limit =1 is the last bar at or before X).
| Parameter | Type | Default | Notes |
|---|---|---|---|
token_id | string | required | ERC-1155 outcome token id. Required. |
interval | string: 1m, 5m, 1h, 1d | required | Candle width. Required. |
from | integer or string | — | Epoch MILLISECONDS, inclusive lower bound. Omit for no lower bound. |
to | integer or string | — | Epoch MILLISECONDS, INCLUSIVE upper bound. Omit for no upper bound. |
limit | integer or string | 500 | Max candles (default 500, hard max 1000). |
One outcome token, priced in probability (0 to 1). The route: /v1/polymarket/ohlcv.
Perps: venues compared
Funding, spreads, open interest and long/short across perpetual-futures venues.
MCP and chat only. These four compare perpetual-futures venues: a funding rate, a price, how much is open or how a venue's traders lean, per exchange, Hyperliquid among them. They have no REST route: centralised-exchange market data is not part of the REST API. They answer where and how much, never who; who is on Hyperliquid, which settles on chain, is on Hyperliquid and HL Cohorts.
perp_funding and perp_spreads take symbol and nothing else; the venue comes back in the answer. perp_open_interest and perp_long_short take an optional exchange. From a terminal: vike-data perp funding, vike-data perp spreads, and vike-data call for the other two.
Perps · MCP tool
perp_funding
Cost
1 credit a call
Free API Archive Stream MCP
Perpetual-futures funding rates. Pass symbol (e.g. 'BTC') to get current per-venue rates for one asset. Omit symbol to get top cross-venue funding-arbitrage opportunities (pre-ranked, normalised to daily %, spreads >= 0.3%). Each arb row includes long_exchange+long_daily_pct, short_exchange+short_daily_pct, spread_daily_pct. A halted or no-longer-polled venue (Coinbase International, halted 2026-10-01) is left out, and so is a venue silent for over an hour in the per-symbol list.
| Parameter | Type | Default | Notes |
|---|---|---|---|
symbol | string | — | Specific symbol (e.g. 'BTC'). Omit for all symbols. |
Perps · MCP tool
perp_spreads
Cost
1 credit a call
Free API Archive Stream MCP
Get top cross-venue spread-arbitrage opportunities for perpetual futures. Returns symbols where the price gap between the cheapest and most-expensive exchange is >= 0.5%. Each row includes buy_exchange+buy_price, sell_exchange+sell_price, and spread_pct. Pre-ranked — just cite the rows directly. Outliers (>400 bps from cross-venue median, typically unit-mismatch listings like 1000SHIB) are already filtered out.
| Parameter | Type | Default | Notes |
|---|---|---|---|
symbol | string | — | Symbol to compare. Omit for full matrix. |
Perps · MCP tool
perp_open_interest
Cost
1 credit a call
Free API Archive Stream MCP
Open interest of one perpetual-futures symbol on each venue Vike polls: aster, binance, bitget, bybit, hyperliquid, kucoin, okx. oi_usd is USD NOTIONAL (open contracts x mark price when polled), not contracts or coins. Without hours: the newest print per venue and its age in seconds; venues printed within the last hour, stale_venues went silent (last print within 30 days) and must NOT be summed in. With hours (1-168): an hourly series per venue, the last print in each UTC hour. Venues are polled roughly every 5-8 minutes. symbol is the canonical base asset ('BTC', not 'BTCUSDT'). A symbol no venue lists returns empty lists and a note, never zeros.
| Parameter | Type | Default | Notes |
|---|---|---|---|
symbol | string | required | Required. Base asset, e.g. 'BTC', 'ETH', 'SOL' (not 'BTCUSDT'). |
exchange | string | — | Only this venue (lower case), e.g. 'binance'. Omit for every venue. |
hours | integer or string | — | Omit for the latest print per venue. 1 to 168 for an hourly series over that many hours. |
Perps · MCP tool
perp_long_short
Cost
1 credit a call
Free API Archive Stream MCP
Long/short positioning that perp venues publish for one symbol: binance, bybit, gate, okx. The venues do NOT measure the same thing, so every row carries basis: binance = top traders by POSITIONS; okx and gate = top traders by ACCOUNTS; bybit = ALL accounts. Compare a venue with itself over time, never as a like-for-like average across venues. long_share / short_share are fractions of 1 (0.63 = 63 %); long_short_ratio = long / short (1.72 = 1.72 longs per short; above 1 is long-heavy). Without hours: the newest 5-minute print per venue and its age in seconds (venues printed within the last hour, stale_venues went silent). With hours (1-168): an hourly long_short_ratio series per venue, the last print in each UTC hour. Covers about 40 major symbols only; any other symbol returns empty lists and a note, never zeros. symbol is the canonical base asset ('BTC').
| Parameter | Type | Default | Notes |
|---|---|---|---|
symbol | string | required | Required. Base asset, e.g. 'BTC', 'ETH', 'SOL' (not 'BTCUSDT'). |
exchange | string | — | Only this venue (lower case), e.g. 'okx'. Omit for every venue. |
hours | integer or string | — | Omit for the latest print per venue. 1 to 168 for an hourly series over that many hours. |
Glossary
The words these pages use in a specific sense.
applied cap
On /v1/onchain routes, the most rows a route returns, which can sit below the limit it accepts: a limit above the cap but within the accepted maximum is silently cut to the cap. Each parameter list names it. The namespace does not page, so a page as long as the cap may not be the whole answer; see Page through large reads.
axis
The dimension a cohort matrix groups by. On /symbols/{symbol}/cohort-metrics it is one of size, pnl, pnl_abs, tier or pnl_unrealized; each axis has its own ladder of cohort labels.
builder
A Hyperliquid builder code: the address an order was routed through, which earns a fee on the fill (builder_fee). The /builders routes rank builders by fee revenue, routed volume, unique traders and fills.
cohort
A labelled bucket on an axis: wallets grouped by account value or PnL rung, or, on the tier axis, positions grouped by their own notional. /cohorts publishes the exact label strings ?cohort= accepts. Not the same as the Polymarket smart money cohort.
condition_id
A 0x -prefixed hex string identifying one Polymarket market. It is what /markets/{condition_id} and /settlements/{condition_id} take in the path. Not interchangeable with a token_id: a token_id passed where a condition_id belongs answers 200 with an empty array.
credit
The accounting figure recorded on every usage row (credits_used). A REST call logs one flat default; an MCP tool has its own price. Credits are never compared against a balance, and a rate-limited or rejected call logs zero. The hourly limit counts requests, not credits. The table is on Limits and credits.
current
The default label basis on axis=pnl: the cohort is resolved at query time, so a historical row carries today's grading. Right for "who is smart right now", and lookahead for a backtest.
freshness
An object on cohort-metrics responses whose window reaches the present: {newest_ts, behind_hours, settled_lag_ceiling_hours, stale}. stale is the precomputed verdict, so a client need not re-derive the rollup's cadence. A window ending in the past carries none.
grading
How wallets are ranked into PnL cohorts on axis=pnl / axis=pnl_unrealized: realized (cumulative realized PnL), realized-pit (the same ladder graded point in time across the whole window) or unrealized (unrealized PnL of open positions - who is onside now).
HIP-3 dex
A builder-deployed perpetuals exchange on Hyperliquid, selected with dex (for example XYZ) and distinct from Hyperliquid core, which is HL. New ones are added over time, so do not hard-code a list.
HL Cohorts
The product, and the /v1/hl-cohorts namespace, for Hyperliquid positioning by cohort: the cohort ladders and segments, the coin by cohort heatmap and per-coin cohort series. It reads the same Hyperliquid data as /v1/hyperliquid, and the same paths under /v1/hyperliquid still answer. Reference: HL Cohorts.
hourly limit
One rolling-hour request budget per credential, shared by /mcp and /v1 and not spent by archive downloads. It counts requests, not credits. Over it you get 429 with Retry-After: 60. The number is on Rate limits.
keyset cursor
The pagination token a Hyperliquid or HL Cohorts list route returns as nextCursor. Pass it back verbatim as cursor; do not construct one. It resumes after the last row's sort key instead of skipping rows, so a deep page costs no more than the first and a concurrent insert cannot shift rows across a page boundary. On cohort-metrics it pages over hours, not rows. Polymarket's /events and /settlements/bulk spell theirs next_cursor, inside an envelope with has_more; see Page through large reads.
L1
Level 1: the derived top of the order book - best bid, best ask and both sizes, one row per change. Published as the l1_quotes archive stream, derived from L2 and far smaller. Archive only.
L2
Level 2: the full order-book event tape - book snapshots, price_change deltas, trades, status markers and, from November 2026, tick-size markers (earlier days have none). Served by /events and /orderbook and published as the book_events archive stream.
label basis
Which grading a cohort label comes from, set with labelBasis: current or point_in_time. Responses declare it in label_basis, and label_computed says when the label was computed.
lookahead
Using information that did not exist yet. Here: a historical cohort row labelled with a grading computed later, so a backtest is told which wallets eventually became rankable. label_computed.lookahead states which you got; use point_in_time for backtests.
manifest
manifest.json: the open index of every Polymarket archive partition - its streams, schema, row counts and bytes. It needs no API key, so read it before downloading anything. The Hyperliquid files are indexed by /v1/hyperliquid/archive/datasets instead.
MCP tool
A function an MCP client calls on data.vike.io/mcp, listed by tools/list and priced in credits. REST routes and tools share one hourly limit. Reference: AI data tools.
partition
One UTC day of one archive stream, venue=polymarket/date=YYYY-MM-DD/{stream}.parquet, with a second layout grouped by asset and tenor. A partition is downloaded as a whole file; see the bulk archive guide.
point_in_time
The label basis that returns each row's cohort label as it was that hour, so a backtest sees no lookahead. On pnl, every field is present while each hour of the window carries its stored label (every hour from 2026-08-06 09:00 UTC, and most earlier ones); a window reaching an hour without one (all of April 2026, about 365 hours from May to 2026-08-06 08:00) comes back with five fields null, named in unavailable_fields. size, tier and pnl_unrealized are always point in time. openedWithin is refused (400) with pnl on this basis.
provenance
Which label source claimed what about a wallet, beside the consensus label built from them. /labels/{address}/provenance returns it for Ethereum addresses, and answers with fewer sources while one of them is being refreshed. See Wallet labels.
provisional
A flag on /symbols/{symbol}/metrics rows, true on at most one of them: the newest, still-unsettled hour. Drop it before persisting a series. /cohort-metrics carries none, because a partial hour inside a matrix fakes a move at the edge.
proxy wallet
The 0x address a Polymarket trader's activity is recorded under on the on-chain trade tape. It is what /wallets/{address} takes and what the response echoes as proxy_wallet.
recorded market
A Polymarket market in the set whose order book we record, the set /markets lists. /markets/{condition_id} also answers for any other market from Polymarket's full catalog, and every row says which with source: recorded or catalog. A catalog row has tick_size and neg_risk null, and no recorded order book.
settlement
The on-chain resolution of a Polymarket market: which outcome won, the payouts vector (in outcome order) and the block and timestamp it resolved at. The authority on how a market ended; the directory's closed flag is derived from it. See /settlements/{condition_id}.
smart money
On Polymarket, the cohort of wallets with at least $1,000 of realized P&L, at least five resolved markets and at least $5,000 of volume. /smart_money reports what that cohort is doing.
symbol
The one name for an instrument on every /v1/hyperliquid route: the symbol= parameter, the /symbols/{symbol}/... paths and the symbol field of every row. It is Hyperliquid's own identifier — BTC, a HIP-3 dex market such as xyz:TSLA, or a spot pair's index such as @107. Until 2026-11-19 responses also carry the old field names (asset, coin, by_coin and the rest).
token_id
A long decimal string identifying one outcome token of a Polymarket market; a binary market has two, returned as token_ids. The per-token routes (/market_stats, /ohlcv, /orderbook, /events) take it. Not a condition_id.
What the API does not cover
Listed plainly, so a person or an AI assistant does not have to guess.
The API answers from the data it holds. These questions need data it does not hold, so no route, tool or file answers them today.
| Area | Not available |
|---|---|
| Prices and markets | A token's price by contract, its candles, biggest movers, all-time high or moving averages on Ethereum, BNB Chain, Base or HyperEVM (the USD value of what a wallet holds is in its balances); DEX pools, swaps and new pool launches; centralised-exchange candles, trades and order books over REST; prediction markets other than Polymarket and Predict.fun |
| Wallets and chains | Token approvals, cost basis and tax lots; DeFi and lending positions (balances list tokens only); funds followed across a bridge (a trace follows one chain); Solana and Bitcoin |
| Hyperliquid | Rejected actions and linked accounts; margin tiers (the leverage a coin allows at a given notional); HIP-4 outcome markets |
| Polymarket | The USDC funding of a proxy wallet on Polygon; dispute filings, per-contract volume and Polygon-wide totals; combos and parlays; platform totals (new wallets, deposits against withdrawals, builder volume); trades seen early from the mempool |
What the API does hold, product by product: On-chain, Hyperliquid, HL Cohorts, Polymarket and Predict.fun.
License
Data from the API, MCP, the CLI and the Parquet files falls under the Terms of Service: personal use, including trading your own capital; commercial use needs a written agreement. Platform code is FSL-1.1, converting to Apache-2.0, a separate question from the data entirely. The licensing page draws the line precisely.