---
title: "Onchain Data API"
description: "通过 REST 获取 DEX 代币价格、池子、K 线、成交与持币者，使用独立的 key 与额度"
url: "https://blockreq.com/docs/zh-cn/build/onchain-data-api/"
---

# Onchain Data API

**Onchain Data API** 通过 REST 提供已索引的 DEX 数据：代币价格（当前与历史）、批量价格、流动性池、OHLCV K 线、成交、持币者排名与轻量资产组合。它是与 RPC 分开的服务：有独立的端点类型、独立的 key 和独立的额度。

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

支持的链：**Ethereum**（`ethereum`）、**Base**（`base`）、**Arbitrum**（`arbitrum`）。BSC 和 Polygon 在历史数据补齐后加入。

## 获取 key

1. 在 Dashboard 打开 **Endpoints**，点击 **+ Add Endpoint**。
2. 选择 **Onchain Data API** 卡片（标有 Ethereum / Base / Arbitrum）。
3. 创建后得到 URL `https://api.blockreq.com/v1/dex` 和一个只属于该端点的**专用 key**，它绝不是 RPC 端点使用的共享 key。

端点列表里的 Onchain Data API 端点会显示账户本周期的用量与额度，例如 `120 / 20k`。

在 `X-API-Key` 请求头里发送 key：

```bash
export BLOCKREQ_DATA_API_KEY="你的 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>`。key 放在其他位置（查询参数、Cookie、其他请求头或路径）会被拒绝，返回 `401 invalid_key`；无效的 key 不会退回匿名访问。

### key 的作用范围

- **Onchain Data API key** 只能调用 `/v1/dex`，不能调用任何 RPC 端点。
- **RPC key** 用于 RPC。过渡期到 **2026-12-03 00:00:00 UTC** 结束，在此之前 RPC key 仍可调用 `/v1/dex`，这些调用计入同一账户的 Onchain Data API 额度。过渡期结束后 `/v1/dex` 只接受 Onchain Data API key，请在此之前创建 Onchain Data API 端点并替换 `X-API-Key`。

### 不带 key

`/v1/dex` 也接受匿名请求，按 IP 地址限速（每秒 1 次）。匿名访问用于试用；持续运行的程序请使用 key。

## 套餐与额度

Onchain Data API 的额度与 RPC Requests 互不共享：数据 API 调用不消耗 RPC Requests、Request Pack 或钱包余额，RPC 流量也不消耗数据 API 额度。

| 套餐     | 每 30 天价格 | 每 30 天调用次数 | 持续 / 突发（次/秒） | 开通                             |
| -------- | ------------ | ---------------- | -------------------- | -------------------------------- |
| Free     | $0           | 100,000          | 1 / 1                | 创建 Onchain Data API 端点后获得 |
| Starter  | $19          | 1,000,000        | 25 / 50              | 即将开放                         |
| Pro      | $79          | 10,000,000       | 50 / 100             | 即将开放                         |
| Business | $249         | 不限             | 100 / 200            | 即将开放                         |

- **创建第一个 Onchain Data API 端点后才获得 Free。** 没有数据 API 端点的账户没有数据 API 套餐；在 RPC key 过渡期内（到 2026-12-03 00:00:00 UTC），这类账户的 RPC key 仍按 Free 的限制提供服务。
- 付费套餐暂未开售。任何套餐的额度用完后，调用返回 `429 quota_exceeded`，直到周期结束；不做超额计费。
- 30 天周期从该周期的第一次调用开始；周期结束后的下一次调用开启新周期，用量从零计。
- **已有账户：** Free 周期按开始时的额度计算，直到该周期结束：开始于 **2026-11-04 00:00:00 UTC** 之前的周期为 100,000 次，此后开始的为 10,000 次，开始于 **2026-11-06 00:00:00 UTC** 及之后的为 20,000 次，开始于 **2026-11-08 00:00:00 UTC** 及之后的为 100,000 次。
- Business 不设周期额度，只限制频率。
- 因 RPC Requests 用完而暂停 RPC 的账户仍可使用数据 API；被停用（suspended）的账户不能。

### 计次规则

| 请求                        | 计入次数                                                            |
| --------------------------- | ------------------------------------------------------------------- |
| 任意 `GET /v1/dex/...` 请求 | 1                                                                   |
| `POST /v1/dex/prices`       | 请求体里每 10 个代币计 1 次，向上取整（10 个 = 1 次，50 个 = 5 次） |

带有效 key 的请求，每个响应都会带上本次计入的次数；不成功的响应（4xx、5xx、429）不计次，值为 `0`。匿名请求和无效 key 的响应不带这个头。

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

### 限制与 429

超过频率或本周期额度用完时，接口返回 **429 Too Many Requests**，用响应体里的 `error.code` 区分：

| `error.code`     | 含义                                                                       | 响应头                                                                                 |
| ---------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `rate_limited`   | 超过频率：带 key 时为套餐频率（Free 每秒 1 次），不带 key 时为按 IP 的窗口 | 带 key 时 `Retry-After: 1`                                                             |
| `quota_exceeded` | 当前 30 天周期的额度已用完                                                 | `Retry-After`：距周期结束的秒数；`X-BlockReq-Data-Api-Quota-Reset`：周期结束的 Unix 秒 |

```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."
  }
}
```

频率按请求数算，额度按计入次数算：一个含 50 个代币的 `POST /v1/dex/prices` 占 1 次频率、5 次额度。额度用完后会稍有延迟才被标记，因此可能还有少量调用成功。请等待 `Retry-After` 后再发送；`quota_exceeded` 在重置时间之前重试只会继续得到 429。

### 错误

错误为 JSON `{"error": {"code": "...", "message": "..."}}`，message 为英文。请按 `code` 分支处理，不要依赖 `message`。

| 状态码 | `error.code`                            | 何时出现                                                                                                |
| ------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_request`                       | 参数或请求体不合法，message 会说明是哪一个                                                              |
| 401    | `invalid_key`                           | key 不存在或已吊销，或者没有放在 `X-API-Key` / `Authorization: Bearer` 里                               |
| 403    | `forbidden`                             | 账户被停用；key 的 Onchain Data API 端点已过期或不允许你的 IP；或过渡期（2026-12-03）结束后使用 RPC key |
| 404    | `not_found`                             | 链、代币或池子未被索引，或没有这个路由                                                                  |
| 429    | `rate_limited`、`quota_exceeded`        | 见上文                                                                                                  |
| 502    | `upstream_failed`、`response_too_large` | 数据服务出错或响应过大；请退避重试                                                                      |
| 503    | `unavailable`                           | 暂时不可用；稍后重试                                                                                    |
| 504    | `timeout`                               | 超时；缩小查询范围或重试                                                                                |

兼容 CoinGecko 的路径（`/v1/dex/onchain/...`）的数据错误使用 CoinGecko 的格式（`{"errors": [{"status": "404", "title": "..."}]}`）；上面的 key、频率和额度错误在这些路径上也仍是 `{"error": ...}` 格式。

## OpenAPI 参考

所有路由、参数、响应字段和错误都写在一份 OpenAPI 3.1 文档里：

- [交互式参考（Swagger UI）](https://blockreq.com/docs/build/onchain-data-api/reference/)：浏览全部路由，点 **Authorize** 填入 key 后可 **Try it out**。key 只留在页面里，只发往 `api.blockreq.com`。
- [OpenAPI JSON](https://blockreq.com/docs/build/onchain-data-api/openapi.json)：可导入 Postman、Insomnia 或客户端代码生成器。

## 路由

`{chain}` 为 `ethereum`、`base` 或 `arbitrum`。地址为 `0x` 加 40 位十六进制；池子 id 为 `0x` 加 40 位（池子合约）或 64 位（pool key）十六进制。响应是带 `data` 字段的 JSON；错误为 `{"error": {"code": "...", "message": "..."}}`。

### 代币

| 方法与路径                                         | 查询参数                                                                                                    |
| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `GET /v1/dex/{chain}/tokens/{address}`             | 无：代币元数据、当前美元价格、24 小时涨跌与成交额                                                           |
| `GET /v1/dex/{chain}/tokens/{address}/price`       | `at`：历史价格的 Unix 秒（默认当前）                                                                        |
| `GET /v1/dex/{chain}/tokens/{address}/pools`       | `limit`：1–100（默认 20）                                                                                   |
| `GET /v1/dex/{chain}/tokens/{address}/ohlcv`       | `interval`：`1m`、`5m`、`15m`、`1h`（默认）、`4h`、`1d`；`from`、`to`：Unix 秒；`limit`：1–1000（默认 300） |
| `GET /v1/dex/{chain}/tokens/{address}/holders`     | `limit`：1–5000（默认 5000）；仅限已跟踪的代币                                                              |
| `GET /v1/dex/{chain}/accounts/{address}/portfolio` | `tokens`：最多 50 个逗号分隔的 ERC-20 地址，读取其余额                                                      |

### 池子与成交

| 方法与路径                              | 查询参数                                                                                    |
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
| `GET /v1/dex/{chain}/pools/top`         | `sort`：`volume`（默认）或 `liquidity`；`limit`：1–100（默认 50）                           |
| `GET /v1/dex/{chain}/pools/new`         | `limit`：1–100（默认 50）；`min_liquidity_usd`（默认 0）                                    |
| `GET /v1/dex/{chain}/pools/{id}`        | 无                                                                                          |
| `GET /v1/dex/{chain}/pools/{id}/ohlcv`  | 同代币 OHLCV，另有 `currency`：`token`（默认）或 `usd`；`side`：`token0`（默认）或 `token1` |
| `GET /v1/dex/{chain}/pools/{id}/trades` | `limit`：1–100（默认 50）；`cursor`：上一页返回的 `next_cursor`                             |

### 批量价格

`POST /v1/dex/prices` 一次接受 1 到 50 个代币，可混合各支持链，JSON 请求体不超过 64 KiB：

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

### 兼容 CoinGecko 的路径

`/v1/dex/onchain/...` 提供兼容 CoinGecko onchain（GeckoTerminal）的路径与响应格式，例如 `/v1/dex/onchain/networks/eth/tokens/{address}` 和 `/v1/dex/onchain/simple/networks/base/token_price/{addresses}`。网络 id 为 `eth`、`base`、`arbitrum`。每个请求计 1 次。

## 示例

Ethereum 上 WETH 的当前价格：

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

Base 上 WETH 的代币详情：

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

Arbitrum 上 WETH 的小时 K 线（最近 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"
```

Ethereum 上按流动性排名的池子：

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

Ethereum 上 Uniswap v3 WETH/USDC 池子的最新成交：

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

一次请求查三条链上的 WETH（3 个代币，计 1 次）；`-i` 会显示 `X-BlockReq-Data-Api-Units` 响应头：

```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"}
  ]}'
```

价格响应示例：

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

## WebSocket 订阅

不用轮询：开一条 WebSocket，订阅实时的**价格**、**成交**、**新池子**和 **K 线**。每条推送的 `data` 就是对应 REST 路由返回的同一个对象，一套解析代码两边通用。完整协议是一份 AsyncAPI 3.1 文档：[AsyncAPI YAML](https://blockreq.com/docs/build/onchain-data-api/asyncapi.yaml)（其中的 payload 引用 [OpenAPI JSON](https://blockreq.com/docs/build/onchain-data-api/openapi.json) 的 schema）。

### 连接

使用和 REST 相同的 Onchain Data API key。在升级请求的 header 里带上；无法设置 header 的环境（浏览器）把 key 放进路径：

```
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"
```

key 放在 query 参数里会被拒绝（`401`），和 REST 一致。超过套餐的连接数上限会被拒绝（`429`）。WebSocket 不支持匿名访问。

一条连接可以承载多个订阅。发送 `subscribe`，服务端用你给的 `id` 回 `ack`（或 `error`）：

```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}
```

这个订阅的每条推送都带 `"subscription":"s1"`。`prices` 的第一条推送紧跟在 `ack` 之后，包含全部所列代币。`prices` 订阅不续传（见下文），不要把它的 `seq` 当作续传起点。取消订阅：

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

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

被拒绝的请求收到 `error`，按机器可读的 `code` 分支处理（不要按 `message`）：

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

| `code`                 | 何时                                                         |
| ---------------------- | ------------------------------------------------------------ |
| `invalid_request`      | 消息或参数格式错误                                           |
| `not_found`            | 代币或池子未收录                                             |
| `pool_not_listed`      | `trades`：池子存在但未上架                                   |
| `unsupported_chain`    | 不支持该链                                                   |
| `subscription_limit`   | 会超过套餐的每连接订阅数                                     |
| `channel_not_in_plan`  | 套餐不含该频道（Free 只有 `prices`）                         |
| `interval_not_in_plan` | `interval` 低于套餐的最小值                                  |
| `quota_exhausted`      | 本周期额度已用完；随后服务端关闭连接（关闭码 1008）          |
| `rate_limited`         | `subscribe` / `unsubscribe` / `ping` 消息过多（Free 每秒超过 1 条） |
| `internal`             | 服务端错误；稍等后重新订阅                                   |

Free 每秒最多发 1 条 `subscribe` / `unsubscribe` / `ping`，更快会收到 `rate_limited`。服务端每 30 秒发一次 WebSocket ping。看不到 ping 的客户端可以发 `{"op":"ping","id":"9"}`，收到 `{"op":"pong","id":"9","time":...}`。没有任何订阅的连接 60 秒后会被关闭。

### 频道

| 频道      | `params`                                                                      | 推送（`op`） | `data` 与下列对象相同                                                                       |
| --------- | ----------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------- |
| `prices`  | `chain`、`addresses`（1–50 个代币）、`interval`、`min_change_bps`、`heartbeat` | `prices`     | `POST /v1/dex/prices` 的 `data[]`（数组：达到推送条件的代币）                               |
| `trades`  | `chain`、`pool`                                                               | `trade`      | `GET /v1/dex/{chain}/pools/{id}/trades` 的一项                                              |
| `pools`   | `chain`                                                                       | `pool`       | `GET /v1/dex/{chain}/pools/{id}` 的返回（上架与否都推，见 `indexed`）                       |
| `candles` | `chain`、`interval`（`1m` … `1d`），以及 `token` 或 `pool` 二选一               | `candle`     | `GET /v1/dex/{chain}/tokens/{address}/ohlcv` 或 `GET /v1/dex/{chain}/pools/{id}/ohlcv` 的一项 |

每条推送在 `data` 外层带 `subscription`、`chain` 和 `seq`。

**`prices`** 从不按成交逐笔推送。服务端每 `interval` 秒（1–3600；默认值和最小值由套餐决定）检查一次所列代币，把满足以下任一条件的代币合成**一条**推送：价格相对**上次推给你的值**变动 ≥ `min_change_bps`（0–1000，默认 10 = 0.1%）；新价格的 `stale` 与上次推送的不同；或设置了 `heartbeat`（0 = 关闭，或 60–86400 秒）且超过该时长没推过。没有代币达标就不推。`subscribe` 之后的第一条推送包含全部所列代币。因为比较的是上次推送的值，缓慢涨跌累计到阈值时也会推送。代币只因时间流逝变成 stale（7 天无成交、没有新价格）时不会因此推送，心跳帧也原样重发上次的价格（`stale` 不重算）：请自己判断 `price_time` 是否已超过 7 天，或查 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`** 推送一个池子的每笔成交，信封里另带 `pool`。数量是代币的原始单位，和 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`** 推送一条链上每个新登记的池子（`{"channel":"pools","params":{"chain":"base"}}`），上架与否都推（看 `indexed`；被屏蔽的不推），`data` 与 `GET /v1/dex/{chain}/pools/{id}` 的返回相同。`GET /pools/new` 只列已上架的池子，而新池子刚创建时几乎都还没上架（筛选每隔几分钟跑一次），这是两者的区别。

**`candles`** 在当前 K 线变化时推送（频率不超过套餐的最小间隔），收线时一定推送最终那一根；用 `closed` 区分：

```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}}
```

不提供按代币订阅成交（WETH 这类代币几乎每个块都有成交）；代币用 `prices` 或 `candles`，池子用 `trades`。CoinGecko 兼容路径没有 WebSocket 版本。

### 续传、缺口与重组

对 `trades`、`pools`、`candles`，`seq` **按链**编号，沿一个订阅严格递增（不连续），并且在所有连接上相同；请为每条链记住最后处理的 `seq`。断线后带 `since` 重新订阅：

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

- **5 分钟以内**：`trades`、`pools` 补发漏掉的推送（带 `"replay": true`），然后继续实时推送。`candles` 从不补发：除非 `since` 之后链上没有任何变化，否则先收到一条当前未收盘 K 线的 `snapshot`（该桶还没有成交时 `data` 为 `null`）。
- **`prices`** 不续传，也不接受 `since`：每个新的 `prices` 订阅都先推一帧全部所列代币（照常计费），之后推变化。它的 `seq` 按订阅各自计（心跳帧可能带较旧的值），不能作为续传起点。
- **超过 5 分钟**：`pools` 先收到最新 20 个池子的 `snapshot`，`candles` 先收到当前未收盘 K 线的 `snapshot`，其 `seq` 之后的推送随后到来；`trades` 改为先收到一条 `gap`，然后是实时推送：

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

  用 `GET /v1/dex/{chain}/pools/{id}/trades` 从 `to_block` 往前翻页，直到你已存的最后一笔成交（`from_block` 不为 `null` 时是漏掉的第一个块）。
- `gap` 也可能带 `reason: "slow_consumer"`（客户端读得太慢，成交推送被丢弃；持续 30 秒会断开连接）、`"indexer_lag"`（索引器在追块）、`"stream_lag"`（推送服务落后）或 `"read_failed"`（服务端没能读出漏掉的变更），补法相同。`candles` 上的 `slow_consumer` gap（两个 block 字段都是 `null`）表示已收盘的 K 线被丢弃，用 REST `.../ohlcv` 重读。
- 推送在最终确认之前发出。发生重组时会收到 `{"op":"rollback","chain":"base","seq":…,"from_block":37901230}`：丢弃该链 `from_block` 及以上的所有成交；替代的成交随后推送，变化了的价格和 K 线会重新推送。
- `warning` 是提示：`slow_consumer`、`quota_low`（额度已用 90%）、`closing`、`indexer_lag`。

JavaScript（Node 22+ 和浏览器用全局 `WebSocket`；浏览器必须把 key 放进路径）：

```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())
```

### 计费与限额

推送和 REST 调用共用同一份 Onchain Data API 额度：

| 消息                                                             | 计入次数                                                 |
| ---------------------------------------------------------------- | -------------------------------------------------------- |
| `trade`、`pool`、`candle` 推送                                   | 1                                                        |
| `prices` 推送                                                    | 每 10 个代币计 1 次，向上取整（与 `POST /v1/dex/prices` 相同） |
| `ack`、`error`、`pong`、`rollback`、`gap`、`snapshot`、`warning` | 0                                                        |
| 因慢消费者被丢弃、或没有写到连接上的推送                         | 0                                                        |

`subscribe` 之后第一条 `prices` 推送包含全部所列代币，和其他推送一样计费。

**订阅不会比轮询同样的数据更贵。** 每 `interval` 秒轮询一次 `POST /v1/dex/prices` 查 n 个代币，每次计 ceil(n/10)；`prices` 订阅每个间隔最多一条推送，计 ceil(达标代币数/10) ≤ ceil(n/10)，没有代币超过阈值时不推也不计。例如每秒查 50 个代币，轮询每天 432,000 次，订阅最多也是这么多；在默认 0.1% 阈值下，WETH 这类代币通常每天只有几百到一千次左右越过阈值。想省额度，就调大 `interval` 或 `min_change_bps`。热门池子的 `trades` 是最贵的频道（Base 上最热的池子每秒好几笔）；不需要逐笔成交时用 `candles` 或 `prices`。

| 套餐     | 并发连接 | 每连接订阅数 | 频道     | `prices` / `candles` 最小间隔 |
| -------- | -------- | ------------ | -------- | ----------------------------- |
| Free     | 1        | 5            | `prices` | 5 s                           |
| Starter  | 2        | 25           | 全部     | 1 s                           |
| Pro      | 5        | 100          | 全部     | 1 s                           |
| Business | 20       | 100          | 全部     | 1 s                           |

- 连接数按账户、跨我们所有服务器合计。`prices` 订阅里每个代币算 1 个订阅，其他订阅各算 1 个。
- `subscribe`、`unsubscribe`、`ping` 共用套餐的 REST 请求速率（不计入额度）。
- 额度用到 90% 时收到 `warning` `quota_low`；用完时收到 `error` `quota_exhausted`，随后连接关闭。

## 用量、统计与日志

在 Dashboard 的 **Statistics** 和 **Logs** 里，Onchain Data API 流量与 RPC 流量显示在一起，可按服务筛选（全部 / RPC / Onchain Data API）。数据 API 调用按路由归类，例如 `GET /v1/dex/{chain}/tokens/{address}/price` 或 `POST /v1/dex/prices`；每条日志都会显示本次计入的次数。

## 另见

- [Onchain Data API 参考（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)
- [方案与定价](https://blockreq.com/docs/zh-cn/pricing/plans/index.md)
- [创建端点](https://blockreq.com/docs/zh-cn/getting-started/create-endpoint/index.md)
