---
title: "Onchain Data API"
description: "DEX token prices, pools, candles, trades and holders over REST, with its own key and quota"
url: "https://blockreq.com/docs/build/onchain-data-api/"
---

# Onchain Data API

The **Onchain Data API** serves indexed DEX data over REST: token prices (current and historical), batch prices, liquidity pools, OHLCV candles, trades, holder rankings and a lightweight portfolio. It is a separate service from RPC: it has its own endpoint type, its own key and its own quota.

```
https://api.blockreq.com/v1/dex
```

Supported chains: **Ethereum** (`ethereum`), **Base** (`base`) and **Arbitrum** (`arbitrum`). BSC and Polygon will be added when their historical data is complete.

## Get a key

1. In the Dashboard, open **Endpoints** and click **+ Add Endpoint**.
2. Choose the **Onchain Data API** card (labelled Ethereum / Base / Arbitrum).
3. Create it. You get the URL `https://api.blockreq.com/v1/dex` and a **dedicated key** that belongs to this endpoint only. It is never the shared key of your RPC endpoints.

The endpoint list shows the Onchain Data API endpoint with your account's usage in the current period, for example `120 / 20k`.

Send the key in the `X-API-Key` header:

```bash
export BLOCKREQ_DATA_API_KEY="your Onchain Data API key"
curl -s "https://api.blockreq.com/v1/dex/ethereum/tokens/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/price" \
  -H "X-API-Key: $BLOCKREQ_DATA_API_KEY"
```

`Authorization: Bearer <key>` works too. A key anywhere else (a query parameter, a cookie, another header or a path segment) is refused with `401 invalid_key`, and an invalid key never falls back to anonymous access.

### Key scope

- An **Onchain Data API key** can call only `/v1/dex`. It cannot call any RPC endpoint.
- An **RPC key** is for RPC. During a transition period that ends at **2026-12-03 00:00:00 UTC**, an RPC key can still call `/v1/dex`; those calls count against the same account's Onchain Data API quota. After that time, `/v1/dex` accepts only Onchain Data API keys, so create an Onchain Data API endpoint and switch your `X-API-Key` before then.

### Without a key

`/v1/dex` also answers anonymous requests, limited per IP address (1 request per second). Anonymous use is for trying the API; use a key for anything that runs continuously.

## Plans and quota

The Onchain Data API quota is separate from RPC Requests: data API calls never use your RPC Requests, Request Packs or wallet balance, and RPC traffic never uses the data API quota.

| Plan     | Price per 30 days | Calls per 30 days | Sustained / burst (requests per second) | Availability                              |
| -------- | ----------------- | ----------------- | --------------------------------------- | ----------------------------------------- |
| Free     | $0                | 100,000           | 1 / 1                                   | With your first Onchain Data API endpoint |
| Starter  | $19               | 1,000,000         | 25 / 50                                 | Coming soon                               |
| Pro      | $79               | 10,000,000        | 50 / 100                                | Coming soon                               |
| Business | $249              | Unlimited         | 100 / 200                               | Coming soon                               |

- **Free comes with your first Onchain Data API endpoint.** An account without one has no data API plan. During the RPC-key transition (until 2026-12-03 00:00:00 UTC), an RPC key of such an account is still served at the Free limits.
- Paid plans are not on sale yet. When the quota of any plan is used up, calls answer `429 quota_exceeded` until the period ends; there is no overage billing.
- A 30-day period starts with the first call of the period. The first call after it ends starts a new period from zero.
- **Existing accounts:** a Free period keeps the quota it started with until it ends. A period that started before **2026-11-04 00:00:00 UTC** has 100,000 calls, one that started from then 10,000, one that started from **2026-11-06 00:00:00 UTC** 20,000, and one that starts from **2026-11-08 00:00:00 UTC** 100,000.
- Business has no period quota, only its rate limit.
- An account whose RPC usage is paused for running out of Requests can still use the data API. A suspended account cannot.

### How calls are counted

| Request                       | Counted calls                                                   |
| ----------------------------- | --------------------------------------------------------------- |
| Any `GET /v1/dex/...` request | 1                                                               |
| `POST /v1/dex/prices`         | 1 per 10 tokens in the body, rounded up (10 tokens = 1, 50 = 5) |

Every response to a request with a valid key carries the number of calls it counted. It is `0` on any answer that is not a success (4xx, 5xx, 429), which counts nothing. Anonymous requests and invalid keys get no such header.

```
X-BlockReq-Data-Api-Units: 5
```

### Limits and 429

When you exceed the rate, or the period's quota is used up, the API answers **429 Too Many Requests**. The `error.code` in the body tells the two apart:

| `error.code`     | Meaning                                                                                                 | Headers                                                                                                |
| ---------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| `rate_limited`   | Over the rate: the plan's rate with a key (Free: 1 request per second), the per-IP window without a key | `Retry-After: 1` with a key                                                                            |
| `quota_exceeded` | The quota of the current 30-day period is used up                                                       | `Retry-After`: seconds left in the period; `X-BlockReq-Data-Api-Quota-Reset`: period end, Unix seconds |

```json
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit reached for this API key. See blockreq.com for higher limits."
  }
}
```

```json
{
  "error": {
    "code": "quota_exceeded",
    "message": "Data API quota reached for this period; it resets at 2026-12-01T08:00:00Z. See blockreq.com for higher limits."
  }
}
```

The rate counts requests and the quota counts calls: a `POST /v1/dex/prices` with 50 tokens takes one request from the rate but 5 calls from the quota. The quota is marked used up shortly after it runs out, so a few calls past it may still succeed. Wait for `Retry-After` before sending again; for `quota_exceeded`, retrying before the reset time only gets 429 again.

### Errors

Errors are JSON `{"error": {"code": "...", "message": "..."}}` with English messages. Branch on `code`, not on `message`.

| Status | `error.code`                            | When                                                                                                                                                     |
| ------ | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_request`                       | A parameter or the body is invalid; the message says which                                                                                               |
| 401    | `invalid_key`                           | The key is unknown or revoked, or was sent somewhere other than `X-API-Key` / `Authorization: Bearer`                                                    |
| 403    | `forbidden`                             | The account is suspended; the key's Onchain Data API endpoints are expired or do not allow your IP; or an RPC key after the transition ends (2026-12-03) |
| 404    | `not_found`                             | The chain, token or pool is not indexed, or no such route                                                                                                |
| 429    | `rate_limited`, `quota_exceeded`        | See above                                                                                                                                                |
| 502    | `upstream_failed`, `response_too_large` | The data service failed or answered too much; send again after a growing pause                                                                           |
| 503    | `unavailable`                           | Briefly unavailable; try again shortly                                                                                                                   |
| 504    | `timeout`                               | Ran out of time; narrow the range or try again                                                                                                           |

The CoinGecko-compatible paths (`/v1/dex/onchain/...`) answer their own data errors in CoinGecko's format (`{"errors": [{"status": "404", "title": "..."}]}`); the key, rate and quota errors above keep the `{"error": ...}` format there too.

## OpenAPI reference

Every route, parameter, response field and error is described in an OpenAPI 3.1 document:

- [Interactive reference (Swagger UI)](https://blockreq.com/docs/build/onchain-data-api/reference/): browse every route, then **Authorize** with your key and **Try it out**. The key stays in the page and is sent only to `api.blockreq.com`.
- [OpenAPI JSON](https://blockreq.com/docs/build/onchain-data-api/openapi.json): import it into Postman, Insomnia or a client generator.

## Routes

`{chain}` is `ethereum`, `base` or `arbitrum`. Addresses are `0x` followed by 40 hex digits; a pool id is `0x` followed by 40 (pool contract) or 64 (pool key) hex digits. Responses are JSON with a `data` field; errors are `{"error": {"code": "...", "message": "..."}}`.

### Tokens

| Method and path                                    | Query parameters                                                                                                     |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/dex/{chain}/tokens/{address}`             | none: token metadata, current USD price, 24h change and volume                                                       |
| `GET /v1/dex/{chain}/tokens/{address}/price`       | `at`: Unix seconds for a historical price (default now)                                                              |
| `GET /v1/dex/{chain}/tokens/{address}/pools`       | `limit`: 1–100 (default 20)                                                                                          |
| `GET /v1/dex/{chain}/tokens/{address}/ohlcv`       | `interval`: `1m`, `5m`, `15m`, `1h` (default), `4h`, `1d`; `from`, `to`: Unix seconds; `limit`: 1–1000 (default 300) |
| `GET /v1/dex/{chain}/tokens/{address}/holders`     | `limit`: 1–5000 (default 5000); available for tracked tokens                                                         |
| `GET /v1/dex/{chain}/accounts/{address}/portfolio` | `tokens`: up to 50 comma-separated ERC-20 addresses to read balances for                                             |

### Pools and trades

| Method and path                         | Query parameters                                                                                    |
| --------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `GET /v1/dex/{chain}/pools/top`         | `sort`: `volume` (default) or `liquidity`; `limit`: 1–100 (default 50)                              |
| `GET /v1/dex/{chain}/pools/new`         | `limit`: 1–100 (default 50); `min_liquidity_usd` (default 0)                                        |
| `GET /v1/dex/{chain}/pools/{id}`        | none                                                                                                |
| `GET /v1/dex/{chain}/pools/{id}/ohlcv`  | as token OHLCV, plus `currency`: `token` (default) or `usd`; `side`: `token0` (default) or `token1` |
| `GET /v1/dex/{chain}/pools/{id}/trades` | `limit`: 1–100 (default 50); `cursor`: the `next_cursor` of the previous page                       |

### Batch prices

`POST /v1/dex/prices` takes 1 to 50 tokens, on any supported chains, in a JSON body of at most 64 KiB:

```json
{ "tokens": [{ "chain": "ethereum", "address": "0x..." }] }
```

### CoinGecko-compatible paths

`/v1/dex/onchain/...` serves CoinGecko onchain (GeckoTerminal) compatible paths and response formats, for example `/v1/dex/onchain/networks/eth/tokens/{address}` and `/v1/dex/onchain/simple/networks/base/token_price/{addresses}`. Network ids are `eth`, `base` and `arbitrum`. Each request counts 1 call.

## Examples

Current price of WETH on Ethereum:

```bash
curl -s "https://api.blockreq.com/v1/dex/ethereum/tokens/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/price" \
  -H "X-API-Key: $BLOCKREQ_DATA_API_KEY"
```

Token details of WETH on Base:

```bash
curl -s "https://api.blockreq.com/v1/dex/base/tokens/0x4200000000000000000000000000000000000006" \
  -H "X-API-Key: $BLOCKREQ_DATA_API_KEY"
```

Hourly candles of WETH on Arbitrum (last 24):

```bash
curl -s "https://api.blockreq.com/v1/dex/arbitrum/tokens/0x82aF49447D8a07e3bd95BD0d56f35241523fBab1/ohlcv?interval=1h&limit=24" \
  -H "X-API-Key: $BLOCKREQ_DATA_API_KEY"
```

Top Ethereum pools by liquidity:

```bash
curl -s "https://api.blockreq.com/v1/dex/ethereum/pools/top?sort=liquidity&limit=10" \
  -H "X-API-Key: $BLOCKREQ_DATA_API_KEY"
```

Latest trades of the Uniswap v3 WETH/USDC pool on Ethereum:

```bash
curl -s "https://api.blockreq.com/v1/dex/ethereum/pools/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640/trades?limit=20" \
  -H "X-API-Key: $BLOCKREQ_DATA_API_KEY"
```

WETH on all three chains in one request (3 tokens, counted as 1 call); `-i` shows the `X-BlockReq-Data-Api-Units` header:

```bash
curl -si -X POST "https://api.blockreq.com/v1/dex/prices" \
  -H "X-API-Key: $BLOCKREQ_DATA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"tokens":[
    {"chain":"ethereum","address":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"},
    {"chain":"base","address":"0x4200000000000000000000000000000000000006"},
    {"chain":"arbitrum","address":"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"}
  ]}'
```

A price response looks like:

```json
{
  "data": {
    "chain": "base",
    "address": "0x4200000000000000000000000000000000000006",
    "at": 1793735964,
    "price_usd": "2289.33461727787809",
    "price_time": 1793735961,
    "source": "current",
    "low_liquidity": false,
    "stale": false
  }
}
```

## WebSocket subscriptions

Instead of polling, open one WebSocket and subscribe to live **prices**, **trades**, **new pools** and **candles**. Every push carries the same object the matching REST route returns, so one parser serves both. The full protocol is an AsyncAPI 3.1 document: [AsyncAPI YAML](https://blockreq.com/docs/build/onchain-data-api/asyncapi.yaml) (its payloads reference the schemas of the [OpenAPI JSON](https://blockreq.com/docs/build/onchain-data-api/openapi.json)).

### Connect

Use the same Onchain Data API key as for REST. Send it in a header on the upgrade request, or, where headers cannot be set (browsers), in the path:

```
wss://api.blockreq.com/v1/dex/ws              # key in X-API-Key or Authorization: Bearer
wss://api.blockreq.com/v1/dex/ws/{key}        # key in the path
```

```bash
websocat wss://api.blockreq.com/v1/dex/ws -H "X-API-Key: $BLOCKREQ_DATA_API_KEY"
websocat "wss://api.blockreq.com/v1/dex/ws/$BLOCKREQ_DATA_API_KEY"
```

A key in a query parameter is refused (`401`), as on REST. Connecting beyond your plan's connection limit is refused with `429`. Anonymous WebSocket access is not available.

One socket carries many subscriptions. Send `subscribe`; the server answers `ack` (or `error`) with the `id` you chose:

```json
{"op":"subscribe","id":"1","channel":"prices","params":{"chain":"base","addresses":["0x4200000000000000000000000000000000000006","0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"],"interval":5,"min_change_bps":10}}
```

```json
{"op":"ack","id":"1","subscription":"s1","seq":28311551,"interval":5,"min_change_bps":10,"heartbeat":0}
```

Every push of that subscription carries `"subscription":"s1"`. For `prices`, the first push follows the `ack` at once and carries every listed token. A `prices` subscription is never resumed (see below), so do not use its `seq` as a resume point. To stop it:

```json
{"op":"unsubscribe","id":"2","subscription":"s1"}
```

```json
{"op":"ack","id":"2","subscription":"s1"}
```

A rejected request gets an `error` with a machine-readable `code` (branch on it, not on `message`):

```json
{"op":"error","id":"3","code":"channel_not_in_plan","message":"channel trades is not in this plan"}
```

| `code`                 | When                                                                         |
| ---------------------- | ---------------------------------------------------------------------------- |
| `invalid_request`      | Malformed message or parameter                                               |
| `not_found`            | The token or pool is not indexed                                             |
| `pool_not_listed`      | `trades`: the pool is known but not listed                                   |
| `unsupported_chain`    | The chain is not served                                                      |
| `subscription_limit`   | The connection would exceed the plan's subscriptions per connection          |
| `channel_not_in_plan`  | The channel is not in your plan (Free has only `prices`)                     |
| `interval_not_in_plan` | `interval` is below your plan's minimum                                      |
| `quota_exhausted`      | The period's quota is used up; the server then closes the socket (code 1008) |
| `rate_limited`         | Too many `subscribe` / `unsubscribe` / `ping` messages (on Free, more than one a second) |
| `internal`             | Server error; subscribe again after a pause                                  |

On Free, send at most one `subscribe` / `unsubscribe` / `ping` a second; a faster one gets `rate_limited`. The server sends WebSocket pings every 30 seconds. Clients that cannot see them may send `{"op":"ping","id":"9"}` and get `{"op":"pong","id":"9","time":...}`. A connection with no subscription is closed after 60 seconds.

### Channels

| Channel   | `params`                                                                             | Push (`op`) | `data` is the same object as                                                                 |
| --------- | ------------------------------------------------------------------------------------ | ----------- | -------------------------------------------------------------------------------------------- |
| `prices`  | `chain`, `addresses` (1–50 tokens), `interval`, `min_change_bps`, `heartbeat`        | `prices`    | `data[]` of `POST /v1/dex/prices` (an array: the tokens that qualified)                      |
| `trades`  | `chain`, `pool`                                                                      | `trade`     | an item of `GET /v1/dex/{chain}/pools/{id}/trades`                                           |
| `pools`   | `chain`                                                                              | `pool`      | `GET /v1/dex/{chain}/pools/{id}` (listed or not; see `indexed`)                              |
| `candles` | `chain`, `interval` (`1m` … `1d`), and either `token` or `pool`                      | `candle`    | an item of `GET /v1/dex/{chain}/tokens/{address}/ohlcv` or `GET /v1/dex/{chain}/pools/{id}/ohlcv` |

Every push has `subscription`, `chain` and `seq` around `data`.

**`prices`** is never pushed per trade. Every `interval` seconds (1–3600; default and minimum set by your plan) the server checks each listed token and sends **one** push with the tokens whose price moved by at least `min_change_bps` (0–1000, default 10 = 0.1%) from the value last pushed to you, whose new price has a `stale` flag other than the one last pushed, or, if `heartbeat` (0 = off, or 60–86400 seconds) is set, that were not pushed for that long. When no token qualifies, nothing is sent. The first push after `subscribe` carries every listed token. Comparing with the last pushed value means a slow drift is pushed once it adds up to the threshold. A token that turns stale only because time passed (no trade for 7 days, so no new price) is not pushed for that, and a heartbeat push resends the last price unchanged, `stale` included: check whether `price_time` is more than 7 days old yourself, or ask REST.

```json
{"op":"prices","subscription":"s1","chain":"base","seq":28311551,"data":[{"chain":"base","address":"0x4200000000000000000000000000000000000006","price_usd":"2289.33461727787809","price_block":37901234,"price_time":"2026-11-06T09:16:16.582Z","reference_liquidity_usd":"1000000","low_liquidity":false,"stale":false}]}
```

**`trades`** pushes every trade of one pool; the envelope also names the `pool`. Amounts are raw token units, as in REST:

```json
{"op":"subscribe","id":"4","channel":"trades","params":{"chain":"ethereum","pool":"0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"}}
```

```json
{"op":"trade","subscription":"s2","chain":"ethereum","seq":9120334,"pool":"0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640","data":{"block_number":23712345,"log_index":112,"time":"2026-11-06T09:18:16.581Z","tx_hash":"0x…","trader":"0x…","amount0":"-1520000000","amount1":"663412000000000000","price":"2291.18","amount_usd":"1520.00","price0_usd":"1","price1_usd":"2291.18"}}
```

**`pools`** pushes each newly registered pool of a chain (`{"channel":"pools","params":{"chain":"base"}}`), listed or not (`indexed` tells which; blocked pools are left out), with `data` as `GET /v1/dex/{chain}/pools/{id}` returns it. Unlike `GET /pools/new`, which lists indexed pools only, a new pool is almost never indexed yet when it is created (screening runs every few minutes).

**`candles`** pushes the open candle when it changes (at most once per your plan's minimum interval) and always the final one when it closes; `closed` tells which:

```json
{"op":"subscribe","id":"5","channel":"candles","params":{"chain":"arbitrum","token":"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1","interval":"1m"}}
```

```json
{"op":"candle","subscription":"s3","chain":"arbitrum","seq":31188201,"interval":"1m","closed":false,"data":{"time":1793735940,"open":"2290.10","high":"2291.40","low":"2289.95","close":"2291.02","volume_usd":"48211.77","trades":37}}
```

Trades by token are not offered (a token such as WETH trades almost every block); use `prices` or `candles` for a token and `trades` for pools. The CoinGecko-compatible paths have no WebSocket version.

### Resume, gaps and reorganisations

For `trades`, `pools` and `candles`, `seq` numbers changes **per chain**: it is strictly increasing along a subscription (not contiguous) and the same on every connection, so keep the last `seq` you processed for each chain. After a disconnect, subscribe again with `since`:

```json
{"op":"subscribe","id":"6","channel":"trades","params":{"chain":"ethereum","pool":"0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"},"since":9120334}
```

- **Within 5 minutes**, `trades` and `pools` replay what you missed (pushes carry `"replay": true`), then continue live. `candles` never replays: unless nothing changed on the chain after `since`, it starts with a `snapshot` of the open candle (`data` is `null` when its bucket has no trade yet).
- **`prices`** does not resume and takes no `since`: every new `prices` subscription starts with a push of all listed tokens (counted as usual), then pushes changes. Its `seq` is per subscription (a heartbeat push can carry an older one), not a resume point.
- **Beyond 5 minutes**, `pools` starts with a `snapshot` of the newest 20 pools and `candles` with a `snapshot` of the open candle; pushes after its `seq` follow. `trades` gets a `gap` instead, then live pushes:

  ```json
  {"op":"gap","subscription":"s1","chain":"base","from_seq":2,"to_seq":66060287,"from_block":null,"to_block":30000062,"reason":"replay_window"}
  ```

  Page back through `GET /v1/dex/{chain}/pools/{id}/trades` from `to_block` to the last trade you stored (`from_block`, when not `null`, is the first block missed).
- A `gap` can also come with `reason: "slow_consumer"` (your client read too slowly and trade pushes were dropped; the socket is closed if this lasts 30 seconds), `"indexer_lag"` (our indexer is catching up), `"stream_lag"` (our stream service fell behind) or `"read_failed"` (the server could not read the missed changes). Fill it the same way. On `candles`, a `slow_consumer` gap (both block fields `null`) means closed candles were dropped: re-read them with REST `.../ohlcv`.
- Pushes are sent before finality. On a reorganisation you get `{"op":"rollback","chain":"base","seq":…,"from_block":37901230}`: drop every trade of that chain at or above `from_block`; the replacing trades follow, and changed prices and candles are pushed again.
- `warning` messages are advisory: `slow_consumer`, `quota_low` (90% of the quota used), `closing`, `indexer_lag`.

JavaScript (Node 22+, browsers: the global `WebSocket`; browsers must put the key in the path):

```js
const key = process.env.BLOCKREQ_DATA_API_KEY;
const lastSeq = {}; // chain -> last processed seq
const pool = "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640";
let trades = []; // your store

function connect() {
  const ws = new WebSocket(`wss://api.blockreq.com/v1/dex/ws/${key}`);
  ws.onopen = () => {
    const since = lastSeq.ethereum;
    ws.send(JSON.stringify({ op: "subscribe", id: "t1", channel: "trades",
      params: { chain: "ethereum", pool }, ...(since !== undefined && { since }) }));
  };
  ws.onmessage = ({ data }) => {
    const m = JSON.parse(data);
    switch (m.op) {
      case "trade":
        trades.push(m.data);
        lastSeq[m.chain] = m.seq;
        break;
      case "rollback":
        trades = trades.filter((t) => t.block_number < m.from_block);
        lastSeq[m.chain] = m.seq;
        break;
      case "gap":
        console.warn(`gap ${m.reason}: read trades up to block ${m.to_block} with REST`);
        lastSeq[m.chain] = m.to_seq;
        break;
      case "error":
        console.error(m.code, m.message);
        break;
    }
  };
  ws.onclose = () => setTimeout(connect, 1000 + Math.random() * 2000);
}
connect();
```

Python (`pip install websockets`):

```python
import asyncio, json, os, random
import websockets

KEY = os.environ["BLOCKREQ_DATA_API_KEY"]
TOKENS = ["0x4200000000000000000000000000000000000006"]
prices = {}  # address -> price_usd

async def run():
    while True:
        try:
            async with websockets.connect(
                "wss://api.blockreq.com/v1/dex/ws",
                additional_headers={"X-API-Key": KEY},
            ) as ws:
                sub = {"op": "subscribe", "id": "p1", "channel": "prices",
                       "params": {"chain": "base", "addresses": TOKENS, "interval": 5}}
                # prices never resume: each new subscription first pushes every token
                await ws.send(json.dumps(sub))
                async for raw in ws:
                    m = json.loads(raw)
                    if m["op"] == "prices":
                        for p in m["data"]:
                            prices[p["address"]] = p["price_usd"]
                    elif m["op"] == "error":
                        print("error", m["code"], m["message"])
        except (websockets.ConnectionClosed, OSError):
            await asyncio.sleep(1 + random.random() * 2)

asyncio.run(run())
```

### Billing and limits

Pushes use the same Onchain Data API quota as REST calls:

| Message                                                                           | Counted calls                                       |
| --------------------------------------------------------------------------------- | --------------------------------------------------- |
| `trade`, `pool`, `candle` push                                                    | 1                                                   |
| `prices` push                                                                     | 1 per 10 tokens it carries, rounded up (as `POST /v1/dex/prices`) |
| `ack`, `error`, `pong`, `rollback`, `gap`, `snapshot`, `warning`                  | 0                                                   |
| A push dropped for a slow consumer, or never written to the socket               | 0                                                   |

The first `prices` push after `subscribe` carries every listed token and is counted like any other.

**A subscription never costs more than polling the same thing.** Polling `POST /v1/dex/prices` for n tokens every `interval` seconds counts ceil(n/10) per poll; the `prices` subscription sends at most one push per interval, counted ceil(tokens that qualified/10) ≤ ceil(n/10), and nothing when no token moved past the threshold. For example, 50 tokens every second is 432,000 calls a day by polling and at most that many by subscription; with the default 0.1% threshold, a token such as WETH typically moves past it a few hundred to about a thousand times a day. To spend less, raise `interval` or `min_change_bps`. `trades` of a busy pool is the expensive channel (a top Base pool trades several times a second); use `candles` or `prices` if you do not need every trade.

| Plan     | Connections | Subscriptions per connection | Channels | Minimum `prices` / `candles` interval |
| -------- | ----------- | ---------------------------- | -------- | ------------------------------------- |
| Free     | 1           | 5                            | `prices` | 5 s                                   |
| Starter  | 2           | 25                           | all      | 1 s                                   |
| Pro      | 5           | 100                          | all      | 1 s                                   |
| Business | 20          | 100                          | all      | 1 s                                   |

- Connections are counted per account across all of our servers. Each token of a `prices` subscription counts as one subscription; any other subscription counts one.
- `subscribe`, `unsubscribe` and `ping` share the plan's REST request rate (they are not counted against the quota).
- At 90% of the quota you get `warning` `quota_low`; when it is used up, `error` `quota_exhausted` and the socket closes.

## Usage, statistics and logs

In the Dashboard, **Statistics** and **Logs** show Onchain Data API traffic next to RPC traffic; filter by service (All / RPC / Onchain Data API). Data API calls are grouped by route, for example `GET /v1/dex/{chain}/tokens/{address}/price` or `POST /v1/dex/prices`, and each log row shows the calls it counted.

## See also

- [Onchain Data API reference (Swagger UI)](https://blockreq.com/docs/build/onchain-data-api/reference/)
- [Onchain Data API WebSocket (AsyncAPI YAML)](https://blockreq.com/docs/build/onchain-data-api/asyncapi.yaml)
- [Plans & Pricing](https://blockreq.com/docs/pricing/plans/index.md)
- [Create Your First Endpoint](https://blockreq.com/docs/getting-started/create-endpoint/index.md)
