应用与 AI 智能体的身份验证
一把密钥即可打开三个通道。它在每个通道中都放在请求头里(X-API-KEY,用于 REST、MCP 以及 WebSocket 握手);网页无法保存机密,因此改用由你的服务器签发的短期令牌打开数据流。密钥的值处处相同,且在每个通道上适用的都是你方案的权限。
#REST API
密钥放在 X-API-KEY 请求头中;只能发送 Bearer 令牌的客户端也可以放在 Authorization: Bearer 中(两者同时发送时以 X-API-KEY 为准)。切勿放在 URL 中:放在查询字符串中的密钥会出现在沿途每个中间节点的访问日志里。仅支持 HTTPS:通过明文 http:// 发出的请求会被拒绝并返回 403 https_required,而不会被重定向,因为重定向会让密钥在每次调用时以明文传输。如果你看到这个错误,请轮换它所携带的密钥。
curl -H "X-API-KEY: $BYTNODE_API_KEY" \
"https://api.bytnode.com/v1/basis?symbol=ETHUSDT"比较以恒定时间执行:错误的密钥与几乎正确的密钥被拒绝所需的时间完全相同。
#无需密钥的接口
只有一个接口无需认证即可访问:/v1/health,用于告知服务是否正常。其他所有接口都需要密钥。有两个接口从不计入月度配额(仅受速率限制约束):/v1/health 和 /v1/limits(你的密钥的限制)。/v1/status(数据新鲜度)和 /v1/symbols(资产目录)属于价格与成交量市场数据族,在所有方案中开放,与其他调用一样计数。
此接口的全部 1 个字段(取自接口契约)
database | `connected` when the service reaches its database. A failing check answers 503 `service_unavailable` with `database` in `error.details`. |
此接口的全部 15 个字段(取自接口契约)
computed_at | ISO 8601 UTC, milliseconds (2026-09-28T14:00:00.000Z) | When the status was measured: the OLDEST of its measurements (each data family is measured on its own, cached up to 5 minutes for market feeds and 30 seconds for the others). Every `age_s` is measured at or after this instant, not at `server_time`. |
state | `ok`, or `degraded` when at least one feed is stale or failed its check. The envelope's `status` stays `ok` (the request succeeded). | |
feed_count | count | Public feeds checked. |
stale_feed_count | count | Feeds older than their threshold. |
error_feed_count | count | Feeds that could not be checked. |
max_age_s | seconds | Age of the oldest feed. |
feeds | One row per public feed. | |
feeds.feed | Public feed name. The closed list of this contract version; `routes` gives the routes each one supplies. | |
feeds.family | Data family the feed belongs to, as in `/v1/limits` and the `x-family` of each route: `price_volume`, `derivatives`, `microstructure`, `trades`, `options`, `onchain`, `macro`, `etf`, `tokenomics` or `sentiment`. | |
feeds.routes | The public routes (contract paths) this feed supplies: watch this feed to know whether they are fresh. A per-timeframe feed (`cvd_1h`) supplies them at that timeframe. | |
feeds.age_s | seconds | Age of the latest row of the feed. For a feed made of several series (one per asset, per macro series, per ETF), the age of its OLDEST series. For the market-hours intraday series (`macro_intraday_<tf>`, not `_continuous`), counted on weekdays only, like their threshold. `null` if the feed is empty or could not be checked. |
feeds.expected_interval_s | seconds | Maximum legitimate age of the feed. |
feeds.threshold_s | seconds | Age above which the feed is stale (1.5 × expected_interval_s). |
feeds.is_stale | Whether the feed is stale: older than `threshold_s`, or (series feed) at least one of its series is. `null` when the feed could not be checked (counted in `error_feed_count`). | |
feeds.stale_venue_count | count | Venues lagging on this feed; `null` when the feed is not split by venue (every non-market feed). |
此接口的全部 5 个字段(取自接口契约)
symbol | Trading pair, e.g. `BTCUSDT`. | |
base_asset | Base asset of the pair. | |
name | Human-readable name. | |
quote_asset | Quote currency of the pair (`USDT` for BTCUSDT, `USDC` for USDTUSDC): the currency of its prices. | |
markets | Markets served for the pair: `spot` and `futures` (USDT-margined perpetual), or `spot` only for the two stablecoin pairs (their `/futures` routes answer `unavailable: not_applicable`). |
# The only route that answers without a key.
curl https://api.bytnode.com/v1/health
# /v1/limits requires the key but is never counted against the quota.
curl -H "X-API-KEY: $BYTNODE_API_KEY" https://api.bytnode.com/v1/limits
# /v1/status and /v1/symbols require the key and count as one request
# of the price & volume family, like any other call.
curl -H "X-API-KEY: $BYTNODE_API_KEY" https://api.bytnode.com/v1/status
curl -H "X-API-KEY: $BYTNODE_API_KEY" https://api.bytnode.com/v1/symbols#WebSocket 数据流
在服务器端,密钥放在握手的 X-API-KEY 请求头中,与 REST 完全相同。网页不应持有你的密钥,而浏览器也无法为 WebSocket 设置请求头:网页改用 ?token= 打开数据流,该短期令牌由你的服务器通过 POST /v1/ws/token 获取。两种方式下,凭据都在握手时校验,早于连接建立。
import asyncio
import json
import os
import websockets # pip install websockets
async def main() -> None:
# From a server, the key goes in the X-API-KEY header of the handshake:
# the connection opens already authenticated. A missing or invalid key gets
# 401 before it opens (a plan without the stream: 403).
headers = {"X-API-KEY": os.environ["BYTNODE_API_KEY"]}
async with websockets.connect("wss://api.bytnode.com/ws", additional_headers=headers) as ws:
await ws.send(json.dumps({
"op": "subscribe",
"channels": ["trades.futures.BTCUSDT"],
}))
print(await ws.recv()) # {"op": "subscribed", "channels": [...]}
asyncio.run(main())POST /v1/ws/token 由你的服务器携带密钥调用,返回 token 和一个可直接打开的 url。令牌有效期为 60 秒,且只能打开一个连接:一旦使用或过期即被拒绝,因此每次连接和每次重连都需申请新令牌。它只能打开 WebSocket,不能访问任何 REST 接口;适用的权限是握手那一刻密钥的权限(其间被吊销的密钥将无法连接)。不含数据流的方案会收到 403 plan_websocket;签发令牌不计入每月配额。
# On YOUR server (the key stays there): issue a token for the browser.
curl -X POST -H "X-API-KEY: $BYTNODE_API_KEY" https://api.bytnode.com/v1/ws/token
# {"status":"ok", …, "data_type":"ws_token","data":{"token":"wst_…",
# "expires_at":"…","expires_in_s":60,"url":"wss://api.bytnode.com/ws?token=wst_…"}}会话在打开时即已认证:第一条消息就可以是订阅。缺少凭据时,握手返回 401 missing_api_key;密钥未知或已吊销,或令牌无效、过期或已被使用时返回 401 invalid_api_key;方案不包含数据流时返回 403 plan_websocket,连接不会建立。握手时的 503 与密钥无关:数据流服务已达容量上限,请稍后重试。方案的同时连接上限按整个账户计算:超出的连接以代码 4003 关闭。旧的 {"op": "auth", "api_key": "…"} 帧仍被接受并返回 authenticated,但它是可选的。
?token= 和 ?api_key= 参数只在 WebSocket 握手时被接受,REST 接口从不接受,且网关不会记录 /ws 的请求行。?api_key= 对现有客户端仍然有效,但放在 URL 中的密钥会进入浏览器历史和代理日志:在服务器端请使用请求头,在网页中请使用令牌。协议细节见 WebSocket 数据流页面。
#MCP 服务器
MCP 服务器需要同一把密钥,放在同一个 X-API-KEY 请求头中,设置在 HTTP 连接上。没有密钥时不会暴露任何工具:服务器默认拒绝,而不是默认开放。
import asyncio
import os
from fastmcp import Client # pip install fastmcp
from fastmcp.client.transports import StreamableHttpTransport
async def main() -> None:
transport = StreamableHttpTransport(
"https://api.bytnode.com/mcp", headers={"X-API-KEY": os.environ["BYTNODE_API_KEY"]}
)
async with Client(transport) as client:
info = await client.call_tool("get_system_info", {})
print(info)
asyncio.run(main())配置文件的具体格式取决于AI 智能体;原则不变:一个 URL 和一个请求头。见 MCP 服务器。
#拒绝响应说明了什么
| 通道 | 响应 | 原因 |
|---|---|---|
| REST | 401, WWW-Authenticate | 缺少请求头:error.code: missing_api_key。密钥未知或已吊销:error.code: invalid_api_key。消息会说明该怎么做,例如 « API key missing: send it in the X-API-KEY header. Create a free key at https://bytnode.com/sign-up. » |
| REST | 403, X-Deny-Reason: plan_family | 密钥有效,但该端点所属的市场数据族不在你的方案中。 |
| MCP | 401, WWW-Authenticate | 连接上缺少请求头,或密钥被拒绝。校验发生在公布任何工具之前;之后,每次工具调用都会按密钥的权限判断。 |
| WebSocket | 握手时返回 401(密钥)或 403(方案) | 密钥或令牌缺失、无效、过期或已被使用(missing_api_key、invalid_api_key),或方案不含数据流(plan_websocket)。连接不会打开。 |
| WebSocket | 关闭 4003 / 4005 | 4003:已达到方案的同时连接上限(按整个账户计算),关闭原因中给出上限。4005:客户端消费数据流过慢,原因为 client too slow。 |
| 明文 HTTP | 403, https_required | 使用 http:// 而不是 https:// 发起调用:被拒绝,绝不重定向。如果该请求携带了你的密钥,它已以明文传输:请轮换密钥。 |
| WebSocket | 握手时返回 503 | 数据流服务已达到容量上限(所有账户合计)。问题不在密钥,也不在方案:请稍后以递增的间隔重试。 |
#保护与轮换密钥
存放在哪里
- 放在环境变量或密钥管理器中,绝不放入代码仓库。
- 每个环境一把密钥:开发、预发布、生产。这样一旦泄露,可以只吊销它而不影响其他环境。
- 绝不要放在网页中:前端代码里的密钥任何访问者都能读到。请从你的后端调用 REST 接口;对于数据流,请把
POST /v1/ws/token签发的令牌交给网页。
轮换
- 在控制台中创建新密钥。重叠期内两把密钥均有效。
- 将新密钥部署到所有地方,然后确认旧密钥上的流量已降为零。
- 吊销旧密钥。吊销立即生效。