On-chain REST
Endpoints for EVM wallets, tokens, transfers, labels and PnL — the one namespace where every REST route runs the same code as its MCP tool.
/v1/onchain/ answers questions about EVM addresses and ERC-20 tokens: what a wallet holds, who it
trades with, where its money went, who is accumulating a token, and which labelled institution sits
behind an address. 15 endpoints, all on https://data.vike.io, all behind the
API key and the shared hourly budget.
The chain is a filter, not a namespace — ?chain=ethereum|bsc|base on most routes, because the
data behind this namespace is multi-chain by design.
One route accepts chain and then ignores it
POST /labels/batch is Ethereum-only whatever you send.
Every call carries the credential in a header — X-API-Key: vk_... or
Authorization: Bearer vk_oat_..., either alone is enough
(Authentication) — and spends one unit of the shared hourly counter
(Rate limits):
# the 20 counterparties this wallet moved the most USD with over the past 7 days
curl -H "X-API-Key: $VIKE_API_KEY" \
"https://data.vike.io/v1/onchain/wallets/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045/counterparties?window=7d&limit=20"Each endpoint has its own page, carrying its full request line in a fence at the top; the page titles are written as the path, without the method.
Start here
| Task | Endpoint |
|---|---|
| Size up an address before spending calls on it: volume, inflow, outflow and activity across five windows | /wallets/{address} |
| See who a wallet deals with most over the past week or month | /wallets/{address}/counterparties |
| Follow an exchange wallet's money outward, hop by hop | /wallets/{address}/trace |
| Check whether two wallets are related by the counterparties and tokens they share | /wallets/compare |
| Read what a wallet holds right now, priced where a price exists | /wallets/{address}/balances |
| Rank the most active wallets, or the biggest net buyers, of the week | /wallets/discover |
| Find the tokens with the biggest net inflow over 24h, or only among smart money | /tokens/screener |
| See who is selling or buying a token in the last hour or day | /tokens/{token_address}/transfers |
| Label a CSV of up to 100 addresses in one call | /labels/batch |
| Find out which source says an address is an exchange | /labels/{address}/provenance |
What it looks like in use
# find: the wallets with the biggest net inflow over 7 days
curl -s "https://data.vike.io/v1/onchain/wallets/discover?window=7d&sort=net_flow&order=desc&size=5" \
-H "X-API-Key: $VIKE_API_KEY"
# size one up: WALLET is an address from that board; five windows of activity in one call
curl -s "https://data.vike.io/v1/onchain/wallets/$WALLET" \
-H "X-API-Key: $VIKE_API_KEY"
# who it deals with: its ten biggest counterparties over 30 days
curl -s "https://data.vike.io/v1/onchain/wallets/$WALLET/counterparties?window=30d&limit=10" \
-H "X-API-Key: $VIKE_API_KEY"
# where the money went from there: two hops, five counterparties per hop
curl -s "https://data.vike.io/v1/onchain/wallets/$WALLET/trace?depth=2&width=5&window=30d" \
-H "X-API-Key: $VIKE_API_KEY"
# who those counterparties are: CPS is their addresses joined with commas
curl -s -X POST "https://data.vike.io/v1/onchain/labels/batch?addresses=$CPS" \
-H "X-API-Key: $VIKE_API_KEY"Full MCP parity — the REST and MCP answers cannot drift
Every endpoint on this namespace maps one-to-one onto one of the tools the MCP server at vike.io/mcp
advertises, and each pair is answered by the same query — there is no second implementation to drift.
So a REST call and the matching MCP tool call return the same payload. What they do not share is the HTTP contract on top: parameter names, defaults, ceilings and billing differ, and the three sections below are about exactly those differences.
Conventions on this namespace
Omitting a parameter is not the same as passing zero
A numeric parameter sent as 0 — or as an empty string — is treated exactly like one you did not
send at all: the endpoint falls back to its own default, and that default is not always zero.
| Parameter | Passing 0, or omitting it, resolves to |
|---|---|
min_usd on /trace | 10,000 USD |
min_usd on /balances (omitting only; an explicit 0 is honoured) | 1.0 USD |
min_usd on /transactions | 0 (no floor) |
days on /pnl and /transactions | 30 |
depth / width on /trace | 2 / 5 |
You cannot ask /trace for a floor of exactly zero
On /trace, min_usd=0 selects the default floor instead of removing it. Pass a small
positive number (min_usd=0.01) if you want everything.
Two ceilings, and the lower one is silent
The limit a route accepts and the limit it applies are two different numbers. The request
validator enforces the higher one; the query underneath then quietly caps at the lower one. Nothing
rejects an over-cap value and nothing reports the cap — a request for 1,000 rows returns 50 and looks
exactly like a wallet with only 50 counterparties.
| Endpoint | Parameter | HTTP accepts | Actually applied |
|---|---|---|---|
/wallets/compare | limit | ≤ 1000 (route default 100) | ≤ 50 |
/wallets/{address}/transactions | limit | ≤ 1000 (route default 100) | ≤ 500 |
/wallets/{address}/counterparties | limit | ≤ 1000 (route default 100) | ≤ 50 |
/wallets/{address}/pnl | days | any integer | 1–365 |
/wallets/{address}/trace | depth / width | any integer | 1–5 / 1–10 |
/tokens/screener | limit | ≤ 500 (route default 50) | ≤ 100 |
/tokens/{token_address}/holders | limit | ≤ 500 (route default 50) | ≤ 50 |
REST and MCP produce different default row counts from the same query
A REST route always applies its own limit default, even when you send none, so
/wallets/{address}/counterparties defaults to 100 and caps at 50 rows, while the
wallet_counterparties MCP tool defaults to 10. Same data, different page size. Set limit
explicitly if you care.
Credits are counted differently from MCP
Every on-chain REST call logs one credit, whatever it cost to serve — no route on this namespace
charges more. Over MCP the same work is priced by tool instead, from 0 (token_search) to 4
(wallet_trace, the heaviest graph traversal). The hourly request budget is separate again and
charges one unit per request on every surface, this namespace included.
Errors
Status codes and the error envelope are namespace-wide and documented under Errors. The mapping: a rejected input becomes 400 carrying the reason, and any other failure becomes 503.
A rejected batch on POST /labels/batch — more than 100
addresses, or none — is a 400 like any other rejected input. A malformed address on /balances and /ens is a 400
too, with a message reading Bad EVM address: …; the check there is strict: a 0x prefix and
exactly 42 characters.
An unrecognised chain name is a 400 (chain 'x' not supported (use ethereum, bsc, or base)) on
/wallets/{address}/counterparties, /pnl, /transactions, /trace, /wallets/compare,
/tokens/screener, /tokens/{token_address}/holders and /labels/batch. The one exception is
/tokens/{token_address}/transfers, which maps an unknown chain to ethereum and answers 200.
A batch of malformed addresses is a 200 with an empty array
POST /labels/batch skips an address that is not valid hex, so a batch in which every address is
malformed answers 200 with []. Check the length of the array as well as the status.
Wallets
Nine endpoints. /wallets/compare and /wallets/discover are literal paths rather than addresses —
they take no {address} segment.
| Endpoint | What it answers |
|---|---|
GET /wallets/{address} | Aggregate metrics for one wallet across the 24h / 7d / 30d / 90d / all windows in a single response |
GET /wallets/{address}/transactions | The raw transfer list for a wallet: individual transfers, not aggregates |
GET /wallets/{address}/counterparties | The addresses this wallet transacted with most, by combined transfer volume over a window |
GET /wallets/{address}/pnl | A daily net-flow series for the wallet — net inflow USD, cumulative, gross volume, transaction count |
GET /wallets/{address}/balances | Current ERC-20 balances across the major EVM chains, with a USD value and price per holding |
GET /wallets/{address}/trace | Follow the money outward — top counterparties per hop, up to depth hops |
GET /wallets/{address}/ens | Reverse ENS lookup — turn an address into vitalik.eth |
GET /wallets/compare | Are these two wallets related? Shared counterparties and shared tokens within a window |
GET /wallets/discover | The leaderboard: most active wallets over a window, ranked by the metric you pick |
Tokens
| Endpoint | What it answers |
|---|---|
GET /tokens/search | Resolve a symbol or name to the tokens we know about |
GET /tokens/screener | Find tokens by activity within a window — volume, net inflow, holder count or smart-money inflow |
GET /tokens/{token_address}/transfers | Who bought and who sold a token over a period — the top buyers or the top sellers |
GET /tokens/{token_address}/holders | The holder leaderboard for a token, ranked by cumulative net inflow USD |
Labels
| Endpoint | What it answers |
|---|---|
POST /labels/batch | Enrich a list of addresses in one call — entity, wallet type, risk flag and 30-day activity |
GET /labels/{address}/provenance | The full label cascade for one address, with per-source attribution |
/ohlcv/export
Streams the same single page as NDJSON — one header line naming what answered, then one line per bar.
/wallets/{address}
Aggregate metrics for one wallet — volume, inflow, outflow, net flow, transaction count and tokens traded, across the 24h / 7d / 30d / 90d / all windows in a single response.