Get a key. Send one header.
Base URL https://data.vike.io/v1. Free — no plans, no tiers, nothing billed. 87 REST routes: 17 on-chain, 45 Hyperliquid, 10 HL Cohorts and 15 Polymarket, plus a WebSocket, an MCP server and the Parquet archives.
- 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 startsvk_. 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_…
Authentication
curl -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/ohlcv?symbol=BTC&interval=1h&limit=3"
- 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 is never read on the REST API, the archive or MCP.
- An OAuth access token (
vk_oat_…, issued to connected apps such as Claude Desktop) goes inAuthorization: Bearer. If it does not validate, the request falls through toX-API-KEY. - Every key has the same reach: there are no scopes and nothing to narrow.
- Revocation takes a few seconds, not zero: a credential check is cached briefly, so a deleted or deactivated key can keep working for a moment afterwards.
| Surface | Credential |
|---|---|
https://data.vike.io/v1/… | X-API-KEY or an OAuth bearer |
https://data.vike.io/archive/… | X-API-KEY — except manifest.json, samples/ and hl-samples/, which are open. Hyperliquid files are not in this tree: https://data.vike.io/v1/hyperliquid/archive/url answers with a signed link, and the link is the credential |
https://data.vike.io/mcp | OAuth for desktop clients, X-API-KEY for everything else |
wss://data.vike.io/v1/stream | Either header, or ?token=vk_… for browsers, which cannot set WebSocket headers. A key in a URL can be written to request logs, so use a header wherever you can |
Limits
| 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. Polymarket files need a key; a Hyperliquid file needs one only to ask for its signed link (one ordinary request), and the download itself carries none |
| Hyperliquid 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 |
| 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 — so keys are how you keep a bulk job from starving an interactive client. The one lower limit is the per-minute cap, 200 a minute per key: it 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 archive files are authenticated and recorded on every request, and charged against the hour on none. Hyperliquid files have no URL of their own: the API answers with a signed link per file (one ordinary request), valid 7 days and served from Cloudflare R2.
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, at a transfer rate capped per connection.
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 — so the boundary is soft in both directions. Design to stay under it, not to spend it to the last request. When you cross it, REST and MCP answer 429 with Retry-After: 60. Cross the per-minute cap and /v1 answers 429 with Retry-After: 1 — wait a second and carry on, or pace a bulk job to under 200 requests a minute.
Pagination
Hyperliquid pages 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 reference states its own bounds.
A cursor is bound to the query that issued it — reuse it after changing start / end or on another route and you get a 400 telling you to restart without one. A malformed cursor is also a 400 (pass back the value verbatim), never a silent reset to page 1.
Polymarket /events returns {"events": […], "has_more": …, "next_cursor": …}: feed next_cursor back as cursor while has_more is true. One call covers at most 24 hours. /settlements/bulk pages the same way, in a settlements envelope. Most other Polymarket routes return a bare array; the exceptions are /wallets/{address} (one profile object) and /archive/datasets and /archive/url (one object each). /v1/onchain does not page at all.
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 = {"coin": "BTC", "limit": 5000} | ({"cursor": cursor} if cursor else {})
page = requests.get(url, params=params, headers=H, timeout=60).json()
rows = page["positions"] # the list key differs per route
if not rows:
break
handle(rows)
cursor = page.get("nextCursor") # null on the last page
if cursor is None:
break
Try it — Position snapshots
Send replays a response the API really returned for this exact URL — its nextCursor follows the rows.
/v1/hyperliquid/positions?symbol={symbol}&limit={limit}Request builder — Polymarket
Markets & settlements: pick the parameters, copy the call in cURL, Python or polars, and Send to see the real captured response.
/v1/polymarket/markets?active={active}&limit={limit}Every dataset has its own builder on the explorer and its fields and loading code on Schemas.
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 and the archive alike. | Turned away before the API runs, so the body can be an HTML page. Do not assume JSON. |
| 404 | No such route or archive file. An unknown condition_id on a Polymarket detail route is 200 [], not a 404. | {"detail": "Not Found"} |
| 422 | A parameter failed validation: wrong type, or outside the bounds the route declares. | {"detail": [ … ]} |
| 429 | Hourly limit exceeded, or on /v1 the per-minute cap (200 a minute per key). | Retry-After: 60 (hourly) or Retry-After: 1 (per-minute) header, JSON error body |
| 503 | The data store failed. Not your request — retry with back-off. | Hyperliquid / on-chain {"detail": "upstream unavailable"} · Polymarket {"error": "upstream unavailable"} |
- A failed query is never a 200 with an empty body. An empty list means there were no rows.
- The
/v1/onchainroutes and Hyperliquid/tradersand/positions/matchrun through a shared tool layer, which reports its own failures in-band — so an internal error there can arrive as a 400 carrying the error text. - MCP answers a bad credential with HTTP 401, a
WWW-Authenticate: Bearer resource_metadata=...header (where an OAuth client starts discovery) and a JSON-RPC error (-32000, Invalid or missing API key). A rate-limit trip there is a real 429. Check the body, not just the status.
WebSocket — closed Hyperliquid bars
wss://data.vike.io/v1/stream pushes a bar frame each time a Hyperliquid candle closes, for every subscription you hold. Connecting, reconnecting and subscribing are not counted against the hourly limit, and neither are the frames you receive.
- Subscribe with
{"action": "subscribe", "symbol": "BTC", "interval": "1m"}; optionalmarket(perp|spot) anddex.unsubscribetakes the same fields. - Intervals:
1m5m15m30m1h4h1d. Symbols are Hyperliquid assets (BTC,HYPE,@107). - Frames:
{"type": "bar", …}withts(epoch ms), OHLCV,closed: trueand aseqthat rises by one per frame for each subscription — a jump means frames were dropped, butseqmeans nothing after a reconnect. Find a gap byts: bars are one interval apart, so a bigger step is a range to check against REST (a bucket with no trades has no bar); a{"type": "ping"}heartbeat (answer{"action": "pong"}); and{"type": "error", …}with codebad_subscription, orrate_limitedwhen the connection already holds 20 subscriptions, the hourly limit is reached or the stream is at capacity. - A refused credential closes the socket with code 4401 before the handshake completes — most clients report it as an HTTP 403 on the upgrade.
- Backfill a gap from REST:
https://data.vike.io/v1/hyperliquid/ohlcvserves the same bars.
# wscat (npm i -g wscat)
wscat -c wss://data.vike.io/v1/stream -H "X-API-KEY: $VIKE_API_KEY"
> {"action":"subscribe","symbol":"BTC","interval":"1m"}
import asyncio, json, os
import websockets # websockets >= 14
async def main():
async with websockets.connect(
"wss://data.vike.io/v1/stream",
additional_headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
) as ws:
await ws.send(json.dumps({"action": "subscribe", "symbol": "BTC", "interval": "1m"}))
async for msg in ws:
frame = json.loads(msg)
if frame.get("type") == "ping":
await ws.send(json.dumps({"action": "pong"}))
elif frame.get("type") == "bar":
print(frame["ts"], frame["close"], frame["seq"])
asyncio.run(main())
MCP for AI agents
Server: https://data.vike.io/mcp. Sign-in (OAuth) opens vike.io; an X-API-KEY header works too. Same data, same key, same hourly budget as REST.
- Claude Desktop and other OAuth clients need no key: add the URL, sign in once in the browser tab that opens, and approve. Connected apps are listed — and revocable — on your API keys page.
- Everything else sends
X-API-KEY, exactly as on REST.
{
"mcpServers": {
"vike": {
"url": "https://data.vike.io/mcp",
"headers": { "X-API-KEY": "vk_…" }
}
}
}
OpenAPI
The spec is public at vike.io/data/openapi.json — no key needed, the same document the API publishes. The copy on the API host, https://data.vike.io/v1/openapi.json, and Swagger UI at https://data.vike.io/v1/docs do need a key: the credential check covers the whole /v1/ prefix on data.vike.io.
It documents every route's parameters. Response shapes are not modelled in it — they are on the hand-written reference at vike.io/docs/data, read from real responses.
curl -o openapi.json https://vike.io/data/openapi.json
License
Archive data files are CC BY 4.0 — use them for anything, credit Vike. API and MCP responses carry the same attribution ask. 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.