Limits and errors
The hourly request limit, the per-minute cap on /v1, what each call costs in credits, every status code and error body, and what to do about each.
Two meters run on every authenticated call: the hourly limit, which can refuse a call, and credits, which only record what a call cost. Everything a refusal or a failure looks like is under Errors.
| What | Limit | What to do |
|---|---|---|
| Requests per credential | 20,000 requests per rolling hour, shared by /v1 and /mcp; archive downloads do not spend it | on 429, wait the Retry-After seconds (below) |
Requests per key on /v1 | a per-minute cap of 200 requests per key | on 429 with Retry-After: 1, slow down |
| Credits | 1 credit per REST call; each MCP tool has its own price | nothing: credits are recorded, never compared against a balance (Credits) |
Rate limits
Every MCP tool call and every REST API call spends one unit of the same hourly budget. Archive
downloads are authenticated on every request and recorded on every request, and charged on none.
There are two numbers — an hourly budget and, on /v1 only, a per-minute cap — and one exclusion.
The numbers
20,000 requests per rolling hour. One value applies to every credential — there are no plan tiers, no per-endpoint ceilings and nothing to configure.
Older numbers you may have seen are wrong
This limit was raised on 2026-07-29. Documentation and product pages previously stated 100 calls/hour for MCP and 2,000/hour for the REST API; both are stale by an order of magnitude or more.
Separately, archive downloads stopped spending this budget on 2026-09-02. Any page telling you that a Parquet download costs you requests, or that an hour of archive downloads leaves nothing for your MCP client, predates that change.
The window slides. What counts is your usage over the previous hour, measured from now, so capacity returns continuously as old requests age out — there is no fixed reset at the top of the hour, and waiting for one will not help you. A request is counted before it is compared against the ceiling, so 20,000 is the number of requests admitted per window, inclusive.
200 requests per minute per key on https://data.vike.io/v1/..., with a burst of up to 100 at
once (since 2026-10-20). It is a sustained rate, not a fixed minute: one request's worth of capacity
returns every 0.3 seconds. Unlike the hourly budget it is counted once for your key across every
server, so it is exact. MCP and the archive are not subject to it. A refusal is a 429 with
Retry-After: 1; anything that pages a large window should run at about 3 requests per second, or
simply honour that header.
X-RateLimit-Limit and X-RateLimit-Remaining
Every /v1 response — the 429 included — carries X-RateLimit-Limit (the hourly budget, 20000)
and X-RateLimit-Remaining (what is left of it). Remaining is the serving process's view and is as
approximate as the counter behind it (see below). There is
no X-RateLimit-Reset: a sliding window has no reset instant to report.
One counter, three surfaces
Every authenticated request is recorded against your credential under a label you can see in your usage view. Two of the three surfaces also spend budget:
| You call | Recorded as | Spends the hourly budget |
|---|---|---|
https://vike.io/mcp | mcp | yes |
https://data.vike.io/v1/... | api_<namespace>_<resource> | yes |
https://data.vike.io/archive/... | archive_download | no |
a Hyperliquid file link, https://data.vike.io/dl/... | archive_download | no |
The two metered surfaces do not have budgets of their own — they share the single counter. An
hour of /v1 calls leaves nothing for your MCP client: between those two, the labels separate your
usage analytics, not your budget. The archive is the one surface separated in both.
Per key, not per account
The counter is keyed on the credential, not on its owner, so each of your API keys gets its own 20,000. Issuing a second key is a legitimate way to isolate a bulk job from an interactive one, and one key tripping the limit does not affect another.
OAuth is the exception. Every OAuth token you own shares one pooled counter, held per user rather than per token. Two connected Claude Desktop clients draw from the same budget; two API keys do not.
The archive is authenticated but not metered against your hour
Hyperliquid files: the link request is metered, the download is not
A Hyperliquid file is reached through a temporary link, not a path under /archive/. Asking for the link,
GET /v1/hyperliquid/archive/url, is an ordinary /v1 call: it spends one
request of the hourly budget and counts against the per-minute cap. Downloading with the link is recorded as an
archive_download and spends neither. The link needs no key, so there is nothing to authenticate on the download; the
row is filed under the key that asked.
Every request for a data file under https://data.vike.io/archive/... still validates its credential, and still
appears in the usage view on your account page. It simply never reaches the rate check. No archive
request can consume your hourly budget, and no amount of downloading can rate-limit you. The open parts,
manifest.json and the free samples of both archives, need no key at all.
Why a request is the wrong unit for a bulk file
The gate in front of the archive counts HTTP requests. It does not see files, or bytes, or downloads — and a Range request is a separate HTTP request.
A remote Parquet read fetches the footer, then each column chunk of each row group it needs, as its
own Range request. The archive actively invites this: manifest.json advertises
"range_requests": true (verified in the live manifest), and the server returns Accept-Ranges,
so pyarrow, DuckDB, fsspec and every other reader will use ranges by default.
The measurement that removed the charge
Measured on the live archive: one 1.26 GB partition produced 20,237 separate requests — more than the entire hourly ceiling, for a single file. Almost every rate-limit rejection recorded in the platform's whole history was an archive download, and every one of them was a customer using the format exactly as intended.
The meter was counting the reader's access pattern rather than the download, so the archive stopped spending the budget. The real cost of a bulk transfer is bandwidth, which is bounded separately at 50 MB/s per connection.
Download whole files anyway
Remote-reading no longer burns your hour — it still costs you time
Pointing pandas, DuckDB or fsspec straight at an archive URL re-fetches the file's footer and column chunks across the network on every query, at a rate capped per connection. Pull the partition once instead.
One file is one request. Fetch it, then point your reader at the local copy:
curl -O -H "X-API-Key: $VIKE_API_KEY" \
"https://data.vike.io/archive/venue=polymarket/date=2026-09-01/trades.parquet"An interrupted transfer resumes with -C - rather than starting over:
curl -C - -O -H "X-API-Key: $VIKE_API_KEY" \
"https://data.vike.io/archive/venue=polymarket/date=2026-09-01/book_events.parquet"Row-group pushdown and column pruning still work against the local file; they just work against your disk instead of across a 50 MB/s link.
Download less, not more cleverly
Full-day book_events partitions run 4–7 GiB. If you need one market family rather than the whole
Polymarket universe, the archive publishes a second, additive layout that groups the crypto up/down
families into much smaller files:
/archive/venue=polymarket/asset=btc/tenor=5m/date=2026-09-01/book_events.parquetSame schema as the date= layout. manifest.json lists every (asset, tenor, date) triple with
its row count and byte size, so you can pick exactly the files you need before transferring a byte
— and the manifest itself needs no key at all, like the free samples under /archive/samples/ (the Hyperliquid free
samples under /archive/hl-samples/ need none either).
The counter is a backstop, not a meter
The limit is enforced approximately, and deliberately favours availability over exactness:
- It is enforced per server process. Requests from one client can be handled by different processes, each keeping its own view of your count, so a client whose requests spread across them can see an effective ceiling meaningfully above the nominal one before every process agrees it is over.
- It refreshes on a short interval. Between refreshes the count is tracked locally with no round trip, so a burst can briefly overshoot before the next refresh catches up.
- A backend failure does not open the gate. If a refresh fails or times out, the last known count is reused rather than the check being skipped.
Treat 20,000 as the number you must design under, not a number you can calibrate against. Do not build a client that runs deliberately at the edge of it.
When you are limited
You get HTTP 429. Over the hourly budget it carries Retry-After: 60 on every metered surface;
every layer that can return that one quotes the same 60, deliberately, so two hops cannot disagree —
and it is advisory: a sliding hourly window has no single correct answer to "when will capacity
exist", so 60 seconds is a sane back-off hint rather than a promise. Over the /v1 per-minute cap it
carries Retry-After: 1 and the message says per minute.
The archive cannot return a rate-limit 429 at all, because an /archive/ request is never metered.
A missing or invalid credential there is still a 401.
On a /v1 route usually called in a loop — one settlement, one market or one token's tape at a
time, a wallet's fills or positions page by page — the 429 also carries a hint naming the bulk call
that replaces the loop: /v1/polymarket/settlements/bulk, or the daily Parquet files, which spend
neither limit. Hitting a limit by looping over single items is the cue to switch.
Honour the header, back off, and retry. A rate-limited request is itself recorded and itself counts, so hammering a closed door keeps the door closed. The exact body shapes, and the one surface where a rate-limit trip and an auth failure carry different HTTP statuses, are on Errors.
Watching your usage
Recent calls, per key, are visible under Account → API Keys at
vike.io/account?tab=api-keys. Archive downloads appear here
like everything else — they are recorded, just not charged. The MCP Calls tab
(vike.io/account?tab=mcp-calls) shows each key's requests
in the last hour against the limit, and the last seven days by endpoint with ok / error /
rate_limited counts, credits and p95 latency. Each call also carries a credit value
(0–4 for an MCP tool) recorded for accounting, but the hourly limit counts requests, not
credits — a 0-credit call consumes a unit of the hourly budget exactly like any other. What each endpoint and tool costs is on Limits and credits.
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; it shows in your usage view. A call that fails (a 400, 429 or 503) logs 0 credits, though it
still spends one unit of the hourly limit.
REST
Every /v1 endpoint logs 1 credit per successful call, except GET /v1/hyperliquid/ohlcv/export (2 credits, written before the stream starts). The endpoint list below is every operation in the published spec.
On-chain
Hyperliquid
HL Cohorts
Polymarket
MCP
Each MCP tool has its own price, set in the tool registry: 0 credits for 3 tools; 1 credit for 23 tools; 2 credits for 17 tools; 3 credits for 5 tools; 4 credits for wallet_trace.
Errors
Every Vike data surface answers failures with an HTTP status code, and usually a JSON body. The status code is the part you should branch on; the body shape varies by which hop rejected you.
Status codes
| Code | Meaning |
|---|---|
| 200 | Success — and, on /mcp, a JSON-RPC protocol error or a failed tool call, both reported in the body. See MCP auth failures are HTTP 401. |
| 400 | Your input was rejected: an unknown market, a malformed identifier, both identity forms at once. |
| 401 | Invalid or missing credential — on /v1, on the archive and on /mcp alike; on /v1 and the archive the body is the UNAUTHORIZED envelope shown below. |
| 403 | A Hyperliquid download link that has expired or was altered. It is never a refused credential. |
| 404 | No such route or file: a missing archive object, a path that matches no endpoint, or a direct /archive/venue=hyperliquid/ path (Hyperliquid files are reached only through a temporary link). |
| 414 | The request line is longer than 16 KB. Split a long id list across several requests (/v1/polymarket/events takes at most 100 token_ids). The body is the envelope below with code URI_TOO_LONG. |
| 422 | A parameter failed type or range checking on /v1 (limit above its maximum, a word where a number goes). See 422. |
| 429 | Rate limit exceeded — the hourly budget, or the /v1 per-minute cap. Carries Retry-After. A request for a Hyperliquid archive link past its 20 GB allowance is also a 429, but of another shape: a plain {"detail": "..."} saying how much was issued, with no Retry-After; the allowance rolls over 24 hours, so a retry within minutes is refused again. |
| 503 | An upstream data-store failure. Not your fault; retry. |
A 404 on a path you expected
The paths people guess most often, and the call that answers the same question. There is no
wallets/{address}/positions, /candles or top-level /cohort-metrics:
| You tried | Use instead |
|---|---|
/v1/hyperliquid/wallets/{address}/positions (a wallet's positions, now or at a past time) | /v1/hyperliquid/positions?wallet={address} — add start and end (ISO-8601 UTC) for the history, symbol to narrow it to one market. The default history resolution is 1h when wallet is given. |
/v1/hyperliquid/candles, /v1/hyperliquid/coins/BTC/candles | /v1/hyperliquid/ohlcv?symbol=BTC&interval=1h |
/v1/hl-cohorts/cohort-metrics, /v1/hl-cohorts/markets/cohort-metrics (or the same under /v1/hyperliquid) | /v1/hl-cohorts/symbols/BTC/cohort-metrics — the symbol is in the path, and the parameter is symbol, not coin. |
anything else under /v1/hl-cohorts/ (/positions, /leaderboard, /wallets/{address}, …) | Only the 10 cohort routes live there — HL Cohorts. Positions, leaderboards, wallets, liquidations, builders and the archive are under /v1/hyperliquid. |
/v1/polymarket/settlements | /v1/polymarket/settlements/bulk for many markets, or /v1/polymarket/settlements/{condition_id} for one. |
/v1/marketdata/* (perp funding, tickers, open interest) | Not part of the API: centralised-exchange data is not offered. The namespaces are /v1/onchain, /v1/hyperliquid, /v1/hl-cohorts and /v1/polymarket. |
A 404 on one of these paths also carries the answer in a hint field. The complete list of paths is in the endpoint index, and GET /v1/openapi.json carries every route and parameter.
Two properties worth relying on:
- A failed query is never a 200 with an empty body. An upstream failure comes back as a 503 rather than an empty list, deliberately — a silent empty result is indistinguishable from "no data", which is how bad backfills get built on top of an outage.
- A 403 is a dead download link, not a refused credential. There are no scopes to be refused: every valid
credential has the same reach across the whole API. The one 403 is a Hyperliquid file link past its 300 seconds or
altered. It is answered by the download host, not by
/v1, with{"error":"download link expired or invalid","hint":"request a new one: GET /v1/hyperliquid/archive/url?dataset=fills&date=YYYY-MM-DD"}. Ask for a new link.
The JSON envelope
The API emits one envelope shape:
{
"error": {
"code": "RATE_LIMITED",
"message": "Hourly rate limit exceeded",
"retry_after": 60
}
}retry_after appears only on 429. A /v1 429 on a route you are probably calling in a loop also
carries hint, naming the bulk call that replaces the loop:
{
"error": {
"code": "RATE_LIMITED",
"message": "Hourly rate limit exceeded",
"retry_after": 60,
"hint": "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)."
}
}| Route | hint names |
|---|---|
/v1/polymarket/settlements/{condition_id} | /v1/polymarket/settlements/bulk |
/v1/polymarket/markets/{condition_id} | /v1/polymarket/markets, up to 100 per request |
/v1/polymarket/events, /market_stats, /ohlcv, /orderbook | the daily Parquet files: /v1/polymarket/archive/datasets |
/v1/hyperliquid/positions, /v1/hyperliquid/wallets/{address}/fills | the daily Parquet files: /v1/hyperliquid/archive/url |
/v1/hyperliquid/ohlcv | /v1/hyperliquid/ohlcv/latest, the newest 1-minute candle of every symbol in one request |
Archive downloads spend neither the hourly budget nor the per-minute cap. MCP's 429 carries no hint.
A 401 (a missing or invalid key, on /v1 and on the archive) carries a docs field instead:
{
"error": {
"code": "UNAUTHORIZED",
"message": "invalid or missing credential — see https://vike.io/docs/data/ for how to authenticate",
"docs": "https://vike.io/docs/data/"
}
}Branch on the status code, not the body
Every refusal our servers produce carries a JSON body. That includes the two answered before the API is reached: the 401 for a missing or invalid key and the 414 for a request line over 16 KB. Until 2026-10-24 both of those were stock HTML error pages. A hop in front of us can still answer with its own HTML page (a Cloudflare 5xx, for one).
The status code and the Retry-After header are correct on every path and always have been. A
client that parses the body to decide what happened will break on a hop it did not expect; a client
that reads the status code will not.
400 on /v1/polymarket: did you mean
/v1/polymarket refuses a query parameter it does not know (the other namespaces ignore one), and
names the parameter you probably meant after the list of accepted ones:
{"error": "unknown query parameter 'q' (this endpoint accepts: query, active, closed, limit) — did you mean 'query' for 'q'?"}A value outside a fixed set gets the nearest valid one:
{"error": "invalid interval '15m' (must be one of 1m, 5m, 1h, 1d). Nearest: 5m or 1h"}The suggestion is always appended after the original message, so a client matching the start of the message keeps working. The same text reaches an MCP tool's error.
422: a parameter out of range
detail is the standard validation list, unchanged; error adds one line per failing parameter:
{
"detail": [{"type": "less_than_equal", "loc": ["query", "limit"], "msg": "Input should be less than or equal to 5000", "input": "10000", "ctx": {"le": 5000}}],
"error": "limit must be at most 5000 (got 10000)"
}Advisory headers
Some answers carry advice in a header. The body is never changed by it: two identical requests still return identical bytes.
| Header | When |
|---|---|
Deprecation: true and Link: <…>; rel="successor-version" | A /v1/hyperliquid request used a retired spelling: the coin= parameter (now symbol=), or a /coins/{symbol}/…, /positions/coins or /assets/hourly path (now /symbols/{symbol}/…, /positions/symbols, /symbols/hourly; for /coins/{symbol}/metrics, /cohort-metrics and /size-breakdown, the published /v1/hl-cohorts/symbols/{symbol}/…). 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 itself 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 within 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 above does. |
429 and Retry-After
An hourly rate-limit trip returns 429 with Retry-After: 60 on every surface. Every layer
that can return one quotes the same 60, deliberately, so two hops cannot disagree. The /v1
per-minute cap (200 per key) returns the same 429 envelope with Retry-After: 1.
The value is an advisory back-off hint, not a promise. The hourly window slides continuously and the counter refreshes on its own schedule, so "when will capacity exist" has no per-request answer anyone stores. Sixty seconds is a sane hint chosen over a false precision.
What a client should do:
r = requests.get(url, headers={"X-API-Key": key})
if r.status_code == 429:
wait = r.headers.get("Retry-After")
if wait is None:
# No Retry-After: not a rate-limit window. The one case is the Hyperliquid archive-link
# allowance (20 GB per rolling 24 hours); waiting a minute does not free it.
raise RuntimeError(r.json()["detail"])
time.sleep(int(wait))
# ...then retry. Do not retry immediately, and do not retry in a tight loop:
# a rate-limited request is itself metered.A rejected request is recorded and counted like any other, so retrying without backing off keeps you
rejected. The archive-link allowance is the exception: its 429 has no Retry-After and is not a window you
can wait out in a minute (see /archive/url).
Almost every 429 in practice has one cause — see Rate limits, particularly the
section on Range requests against the archive.
MCP auth failures are HTTP 401
https://vike.io/mcp speaks JSON-RPC 2.0, and both failure modes carry a JSON-RPC error object in
the body as well as an HTTP status:
| Failure | HTTP status | Body |
|---|---|---|
| Invalid or missing credential | 401 + WWW-Authenticate | JSON-RPC error object, code -32000 |
| Hourly rate limit exceeded | 429 + Retry-After | JSON-RPC error object, code -32000 |
An unauthenticated call returns this, with HTTP 401 and WWW-Authenticate: Bearer resource_metadata="https://vike.io/.well-known/oauth-protected-resource" (the MCP authorization spec — an OAuth client starts discovery from that URL):
{"jsonrpc": "2.0", "id": null, "error": {"code": -32000, "message": "Invalid or missing API key"}}A rate-limited call returns HTTP 429, and puts the envelope's fields in the JSON-RPC error's data
member so both kinds of client can read it:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32000,
"message": "Hourly rate limit exceeded",
"data": { "code": "RATE_LIMITED", "retry_after": 60 }
}
}Other JSON-RPC errors (-32700 parse error, -32601 unknown method or tool) still arrive at HTTP
200, so if you are writing a raw HTTP client against /mcp, checking response.ok is not
enough — you must also look for an error member in the body.
Getting a 401 you did not expect
Three causes, in rough order of likelihood:
- The header name. The archive and
/v1wantX-API-Key. An OAuth token goes inAuthorization: Bearer. A key in a query string is never read. - The key is inactive or was deleted. Deletion is a soft delete; an inactive key authenticates no better than a deleted one.
- You just changed something. Credential decisions are cached for up to 45 seconds (positive) and 8 seconds (negative), so a newly fixed key can take a few seconds to start working and a revoked one a few seconds to stop. See Authentication.
API reference
Every endpoint of the Vike Data API, grouped by namespace, with the base URL, the one endpoint that is not on it, and the conventions every namespace page assumes.
REST conventions
The rules that hold across every /v1 namespace on data.vike.io — one credential, one hourly counter, one error envelope, one request helper — and where the per-endpoint reference lives.