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:

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$0100,0001 / 1创建 Onchain Data API 端点后获得
Starter$191,000,00025 / 50即将开放
Pro$7910,000,00050 / 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 秒
{ "error": { "code": "rate_limited", "message": "Rate limit reached for this API key. See blockreq.com for higher limits." } }
{ "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何时出现
400invalid_request参数或请求体不合法,message 会说明是哪一个
401invalid_keykey 不存在或已吊销,或者没有放在 X-API-Key / Authorization: Bearer 里
403forbidden账户被停用;key 的 Onchain Data API 端点已过期或不允许你的 IP;或过渡期(2026-12-03)结束后使用 RPC key
404not_found链、代币或池子未被索引,或没有这个路由
429rate_limited、quota_exceeded见上文
502upstream_failed、response_too_large数据服务出错或响应过大;请退避重试
503unavailable暂时不可用;稍后重试
504timeout超时;缩小查询范围或重试

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

OpenAPI 参考

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

  • 交互式参考(Swagger UI):浏览全部路由,点 Authorize 填入 key 后可 Try it out。key 只留在页面里,只发往 api.blockreq.com。
  • 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}/priceat:历史价格的 Unix 秒(默认当前)
GET /v1/dex/{chain}/tokens/{address}/poolslimit:1–100(默认 20)
GET /v1/dex/{chain}/tokens/{address}/ohlcvinterval:1m、5m、15m、1h(默认)、4h、1d;from、to:Unix 秒;limit:1–1000(默认 300)
GET /v1/dex/{chain}/tokens/{address}/holderslimit:1–5000(默认 5000);仅限已跟踪的代币
GET /v1/dex/{chain}/accounts/{address}/portfoliotokens:最多 50 个逗号分隔的 ERC-20 地址,读取其余额

池子与成交

方法与路径查询参数
GET /v1/dex/{chain}/pools/topsort:volume(默认)或 liquidity;limit:1–100(默认 50)
GET /v1/dex/{chain}/pools/newlimit: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}/tradeslimit:1–100(默认 50);cursor:上一页返回的 next_cursor

批量价格

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

{ "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 的当前价格:

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

Base 上 WETH 的代币详情:

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

Arbitrum 上 WETH 的小时 K 线(最近 24 根):

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 上按流动性排名的池子:

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 池子的最新成交:

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 响应头:

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

价格响应示例:

{ "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(其中的 payload 引用 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
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):

{"op":"subscribe","id":"1","channel":"prices","params":{"chain":"base","addresses":["0x4200000000000000000000000000000000000006","0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"],"interval":5,"min_change_bps":10}}
{"op":"ack","id":"1","subscription":"s1","seq":28311551,"interval":5,"min_change_bps":10,"heartbeat":0}

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

{"op":"unsubscribe","id":"2","subscription":"s1"}
{"op":"ack","id":"2","subscription":"s1"}

被拒绝的请求收到 error,按机器可读的 code 分支处理(不要按 message):

{"op":"error","id":"3","code":"channel_not_in_plan","message":"channel trades is not in this plan"}
code何时
invalid_request消息或参数格式错误
not_found代币或池子未收录
pool_not_listedtrades:池子存在但未上架
unsupported_chain不支持该链
subscription_limit会超过套餐的每连接订阅数
channel_not_in_plan套餐不含该频道(Free 只有 prices)
interval_not_in_planinterval 低于套餐的最小值
quota_exhausted本周期额度已用完;随后服务端关闭连接(关闭码 1008)
rate_limitedsubscribe / 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 与下列对象相同
priceschain、addresses(1–50 个代币)、interval、min_change_bps、heartbeatpricesPOST /v1/dex/prices 的 data[](数组:达到推送条件的代币)
tradeschain、pooltradeGET /v1/dex/{chain}/pools/{id}/trades 的一项
poolschainpoolGET /v1/dex/{chain}/pools/{id} 的返回(上架与否都推,见 indexed)
candleschain、interval(1m … 1d),以及 token 或 pool 二选一candleGET /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。

{"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 一致:

{"op":"subscribe","id":"4","channel":"trades","params":{"chain":"ethereum","pool":"0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"}}
{"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 区分:

{"op":"subscribe","id":"5","channel":"candles","params":{"chain":"arbitrum","token":"0x82aF49447D8a07e3bd95BD0d56f35241523fBab1","interval":"1m"}}
{"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 重新订阅:

{"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,然后是实时推送:

    {"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 放进路径):

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):

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、warning0
因慢消费者被丢弃、或没有写到连接上的推送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 最小间隔
Free15prices5 s
Starter225全部1 s
Pro5100全部1 s
Business20100全部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;每条日志都会显示本次计入的次数。

另见