Schemas Get a free key
LIVE --:--:-- UTC Hyperliquid positions snapshot on a 5-min UTC grid — last boundary --:-- next in -:--
Build · API

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.

  1. Sign in at vike.io

    Any account. No card, no plan to pick.

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

  3. Send the header

    X-API-KEY: vk_… on every request. OAuth clients: Authorization: Bearer vk_oat_…

Authentication

bash
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-KEY and X-API-Key are 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 in Authorization: Bearer. If it does not validate, the request falls through to X-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.
Which credential each surface accepts
SurfaceCredential
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/mcpOAuth for desktop clients, X-API-KEY for everything else
wss://data.vike.io/v1/streamEither 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

Requests20,000 per hour, per key — a rolling hour, so capacity returns continuously
Per minute200 per minute, per key, on /v1 only — a sustained rate, with room for a short burst above it
KeysUp to 3 active per account. Each key is its own budget.
OAuth appsAll of an account's OAuth tokens share one pooled budget
Price$0 — nothing is billed
Archive downloadsNot 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 linksValid 7 days, for any number of requests (range reads included). No download quota
Over the limit429 with Retry-After: 60 over the hourly limit, Retry-After: 1 over the per-minute cap
Failed requestsCount 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.

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

symbol
limit
GET/v1/hyperliquid/positions?symbol={symbol}&limit={limit}

    
Press Send ▶ — the captured response appears here

    

Request builder — Polymarket

Markets & settlements: pick the parameters, copy the call in cURL, Python or polars, and Send to see the real captured response.

active
limit
GET/v1/polymarket/markets?active={active}&limit={limit}

    
Press Send ▶ — the captured response appears here

    

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.

StatusMeaningBody
400The 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": "…"}
401Missing, 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.
404No such route or archive file. An unknown condition_id on a Polymarket detail route is 200 [], not a 404.{"detail": "Not Found"}
422A parameter failed validation: wrong type, or outside the bounds the route declares.{"detail": [ … ]}
429Hourly 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
503The 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/onchain routes and Hyperliquid /traders and /positions/match run 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"}; optional market (perp | spot) and dex. unsubscribe takes the same fields.
  • Intervals: 1m 5m 15m 30m 1h 4h 1d. Symbols are Hyperliquid assets (BTC, HYPE, @107).
  • Frames: {"type": "bar", …} with ts (epoch ms), OHLCV, closed: true and a seq that rises by one per frame for each subscription — a jump means frames were dropped, but seq means nothing after a reconnect. Find a gap by ts: 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 code bad_subscription, or rate_limited when 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/ohlcv serves the same bars.
bash
# 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"}
python
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.
json
{
  "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.

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