手册

调用服务

本指南介绍应用如何找到一个 TapeAPI 服务、调用它,并确认回答是真实的。它使用 @tapeapi/sdk,可运行于 Node 22+、浏览器、DeWEB 站点和 Cloudflare Workers。

1. 试用

一次性准备

这些包尚未发布到 npm,所以要在仓库的克隆里操作(Node.js 22 或更高版本):

git clone https://github.com/BruceLanLan/tapeapi.git && cd tapeapi && npm install

或者只把 SDK 装进你自己的项目,从 GitHub 版本发布页安装(不是 npm 仓库):

npm install https://github.com/BruceLanLan/tapeapi/releases/download/v3.0.0/tapeapi-sdk-3.0.0.tgz

把下面的脚本保存为 .mjs 文件,放在能解析到 @tapeapi/sdk 的地方,然后用 node <文件>.mjs 运行:用克隆时,放在 tapeapi 目录之内(包通过仓库的 workspace 解析);装了发布包时,放在那个项目里。放在其它位置的脚本会以 ERR_MODULE_NOT_FOUND 失败。

调用线上的公共服务

一个免费的公共服务运行在 https://api.tapeapi.fun,TapeOut 名称为 11.1013.tape。用 curl:

curl -s https://api.tapeapi.fun/tapeapi/v1/bnbUsd -H 'content-type: application/json' -d '{"id":"1","params":{}}'

curl 会显示签名信封(result、container、ts、block、sig),但不会验证它。SDK 从链上解析服务,并在返回之前验证回答:

import { createTapeAPI } from '@tapeapi/sdk'

const api = createTapeAPI({
  rpcUrls: ['https://bsc-dataseed.bnbchain.org', 'https://bsc-mainnet.public.blastapi.io', 'https://rpc-bsc.48.club'],
  quorum: 2,
})
const svc = await api.resolve('11.1013.tape')
const { result, verified } = await api.call(svc, 'bnbUsd', {})
console.log(result, verified)

调试台在浏览器里运行同一份 SDK,无需安装任何东西;公共 API 列出了公共服务的全部方法。

在本地运行服务

启动最小示例服务。它通过公共节点读取 BNB Chain,并用一把临时密钥签名:

node examples/reader-service/index.mjs          # :8787
import { createTapeAPI } from '@tapeapi/sdk'

const api = createTapeAPI({ dev: true })          // dev:接受没有链上清单的本地 http:// 服务
const svc = await api.resolve({ dev: 'http://127.0.0.1:8787' })
const { result, block, verified } = await api.call(svc, 'blockNumber', {})
console.log(result.blockNumber, block, verified)

api.call 只有在签名信封检查通过后才返回。如果回答在传输中被篡改、由另一把密钥签名,或已是五分钟以前的回答,你得到的是 TapeAPIError,永远不会是结果。

2. 在 BNB Chain 上解析真实服务

在主网上,SDK 所信任的一切都从链上读取,并经由多个必须达成一致的 RPC 节点:

const api = createTapeAPI({
  rpcUrls: ['https://bsc-dataseed.bnbchain.org', 'https://bsc-mainnet.public.blastapi.io', 'https://rpc-bsc.48.club'],
  quorum: 2,
})

const byName = await api.resolve('11.1013.tape')                   // <#ID>.<processor>.tape
const byContainer = await api.resolve('0x<container address>')
const byCircuit = await api.resolve({ circuits: '0x<processor contract>', tokenId: '11' })

TapeOut 名称 <#ID>.<processor>.tape 同样从链上读取:处理器编号给出处理器合约(factory.cpuAt),该合约加上 #ID 给出容器(DeWebHub.accountOf)。它不比 { circuits, tokenId } 形式多引入任何信任;上面三种形式经过同样的检查。

解析(TAPI-20 §3.6)依次执行:

  1. 从电路推导出容器(DeWebHub.accountOf),并检查处理器确实是 TapeOut 的处理器(factory.isCPU),使伪造的 NFT 无法冒充服务;
  2. 从容器的站点读取 .well-known/tapeapi.json,并对照链上数据检查其长度和 SHA-256;
  3. 校验清单;
  4. 对照当前持有者,检查电路持有者对服务签名密钥的 EIP-712 委托。

解析出的服务会被缓存;SDK 大约每小时重读一次,并在签名或价格不再匹配时立即重读。api.refresh(svc) 可强制重读。

对 AI 服务还有一条命令做同样的解析并把结果展示出来:tapeapi connect <名字>(实验性,2.1 新增,不受兼容承诺约束)打印服务卡片,并只为其清单声明的 API 格式给出客户端配置;它不索要任何密钥,也不付款。见 接入 AI 服务的“你的用户要做什么”。

3. 处理错误

每一种失败都是带有稳定 code 的 TapeAPIError:

import { TapeAPIError } from '@tapeapi/sdk'

try {
  await api.call(svc, 'quote', { symbol: 'BNB' })
} catch (e) {
  if (!(e instanceof TapeAPIError)) throw e
  console.error(e.code, e.message, e.signed ? '(signed by the service)' : '')
}
错误码含义应对
INVALID_ARGUMENT你自己的选项或参数有误(没配 rpcUrls、不支持的目标、选项不合法)。没有发出任何请求。改正调用;绝不重试。
BUDGET_EXCEEDED你自己的花费预算在发出之前拒绝了这次调用:WebMCP 的,或(3.0,实验性)本地策略文件的(回执对账)。等窗口过去,或改你的策略。
RPC_UNAVAILABLE作答的 RPC 节点少于法定人数。重试;增加节点。
RPC_DISAGREE节点返回了不同的字节。重试;若持续出现,说明某个节点落后或在说谎。
MANIFEST_INVALID没有清单、清单有误,或其字节与链上不符。该服务没有(正确地)发布。
DELEGATION_INVALID签名密钥未获当前持有者授权,或委托已过期。提供者必须续期。
BAD_SIGNATURE回答不是由被委托的密钥签名的。不要使用它。SDK 会重读一次清单,以防密钥已轮换。
METHOD_NOT_FOUND清单中没有该方法。检查 svc.manifest.methods。
PRICE_CHANGED价格涨到了你已接受的价格之上。先征询你的用户,再调用 api.acceptPrice(svc, method) 或传入 { maxPrice }。
QUORUM_FAILEDcallQuorum 中的提供者未达成一致。视为没有回答。

完整列表见升级到 1.0与 TAPI-21。服务发出的错误同样带签名(e.signed)。

4. 要求多个提供者之间达成一致

单个服务的签名回答只能证明是谁说的,不能证明它是真的。对于重要的数值,请向多个独立提供者询问同一个区块,只接受逐字节相同的结果:

const [a, b] = await Promise.all([api.resolve('0x<container A>'), api.resolve('0x<container B>')])
const first = await api.call(a, 'bnbUsd', {})
const block = first.result.blockPinned.blockNumber            // 把每个提供者都固定到同一个区块
const q = await api.callQuorum([a, b], 'bnbUsd', { block }, { quorum: 2 })
console.log(q.result, 'agreed by', q.agreed)                  // 否则抛出 TapeAPIError('QUORUM_FAILED')

只有描述以 [quorum] 开头的方法才能这样比较;[no-quorum] 方法(带随机 id 的报价、latest 读取)按设计就会各不相同。使用单一来源数值的协议仍应施加边界检查、新鲜度检查和熔断机制。

5. 为调用付费

实验性。 付费功能(TAPI-22 付费通道与托管合约)尚未部署、未经审计,不在兼容承诺之内:下面的名字可能在次版本里改变。见升级到 1.0。

免费方法无需任何东西。付费方法需要一张由消费者签名的凭证(TAPI-22):

const consumer = '0x<your address>'                             // 签署凭证的钱包账户
const payer = api.payer({
  consumer,
  signTypedData: (typed) => wallet.request({ method: 'eth_signTypedData_v4', params: [consumer, JSON.stringify(typed)] }),
})
const r = await api.call(svc, 'pairPrice', { pair: '0x…' }, { payer })

为避免每次调用都弹出钱包提示,可以一次性授权一把会话密钥(api.tx.authorizeSession(svc, sessionAddress, expires)),然后改为传入 { consumer, sessionKey, sessionExpiry }。为通道充值需要两笔交易,由 SDK 为你的钱包构建:先 await api.tx.approve({ amount, spender: svc }),再 await api.tx.fund(svc, amount)。两者都先在链上读取托管的代币,并且只为 SDK 已审计部署名单里的托管(目前为空:还没有任何托管经过审计)或你自己用 createTapeAPI({ allowEscrows: [...] }) 添加的托管构造,因为授权让那个合约可以拉走你的代币。amount 以代币自己的最小单位计:用 api.chain.escrow.paymentToken(svc) 读取代币(小数位从代币读取,绝不假设),用 formatPaymentAmount(amount, token) 显示金额。清单价格是 priceBEM,所以 SDK 拒绝为托管持有其它代币的服务充值或付费(UNSUPPORTED_PAYMENT_TOKEN,reason 为 not-bem):托管合约本身支持任意准入代币,但清单的多币种定价规范尚未规定,所以 fund 对持有 BEM 以外代币的托管一律不构造,无论你声明什么代币,即使你已把该托管加入 allowEscrows。

没有确认自己托管代币的提供者,以未签名的 HTTP 503 UNSUPPORTED_PAYMENT_TOKEN 拒绝付费调用。它没有签名,所以 SDK 报 PROVIDER_UNAVAILABLE(消息里写 unsigned claim: UNSUPPORTED_PAYMENT_TOKEN),释放这次调用的预留,不转交回答里的 data.reason。直接用 HTTP 的客户端能看到原因:escrow-token-mismatch 在该提供者实例内一直如此(不要重试),escrow-no-token 与 escrow-token-unreadable 在下一次付费调用时重读(TAPI-22 §3.2 的说明性段落)。

你只付提供者标明的价格:TapeAPI 不在消费者这一侧加任何费用。维护贡献(如果提供者保留它)从提供者的所得中划出(docs/FEES.md)。一次付费调用以它所用的托管实例的代币结算:下一版托管计划在 BNB Smart Chain 上首先支持 BEM,USDT 锚定币(Binance-Peg)与 WBNB(包装的 BNB)按需、用同一份字节码(见 docs/FEES.md)。清单里的价格在规范其它代币的写法之前仍以 BEM(priceBEM)书写,所以本 SDK 只为持有 BEM 的托管构造付费调用。

托管合约尚未部署,因此付费服务还没有在主网上线。以上内容都可以针对示例运行(本地使用 FREE_ALL=1 跳过付费)。

SDK 只在收到经过验证的回答之后才推进本地计量,并在提供者的计数不一致时自动重新同步。传入 store: { get, set } 可在重启之间保留计量。

6. 在浏览器或 DeWEB 站点中使用

SDK 是普通的 ES 模块。在 DeWEB 站点上,按相对路径导入它,并用 import map 映射 @noble/*;完整页面见 examples/demo-site/,更多代码片段见 examples/consumer-snippets.md。

7. 替不可信的用户解析和调用服务

核验站点、智能体,或任何按用户给出的名字去解析并调用服务的服务器,都会向陌生人写下的网址发请求:清单里的端点就是持有人发布的内容。SDK 会检查它们是 https 网址(开发模式下可为 http),但不会拒绝内网或保留地址。https://127.0.0.1/、 https://[::1]/ 与 https://169.254.169.254/ 都是合法的 endpoints.live,域名也可能解析到内网地址。在你自己的机器上这正是你想要的;在服务器上,这会让用户把你的服务器指向它自己的内网。

这类部署应当给 createTapeAPI({ fetch }) 传入一个拒绝内网目标的包装。RPC 请求也走同一个 fetch,所以要显式放行你自己的节点:

import { createTapeAPI } from '@tapeapi/sdk'

// 你自己的、可以是内网的主机(比如你的 RPC 节点);其余主机是内网地址时一律拒绝。
const ALLOWED = new Set(['rpc.internal.example:8545'])
const isPrivate = (host) => {
  const h = host.toLowerCase().replace(/^\[|\]$/g, '').replace(/\.$/, '')
  if (h === 'localhost' || /\.(localhost|local|internal|lan|home\.arpa)$/.test(h)) return true
  if (/^\d+\.\d+\.\d+\.\d+$/.test(h)) return /^(0|10|127|169\.254|172\.(1[6-9]|2\d|3[01])|192\.168|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7]))\./.test(h)
  return h.includes(':') && (h === '::' || h === '::1' || /^(f[cd]|fe[89ab])/.test(h) || h.startsWith('::ffff:'))
}
const guardedFetch = (input, init) => {
  const url = new URL(input instanceof Request ? input.url : String(input))
  if (!ALLOWED.has(url.host) && isPrivate(url.hostname)) return Promise.reject(new TypeError(`refused: ${url.origin} is a private address`))
  return fetch(input, { ...init, redirect: 'error' })   // 也不让公网主机重定向到内网
}
const api = createTapeAPI({ rpcUrls: [/* ... */], fetch: guardedFetch })

这只检查写出来的网址。解析到内网地址的公网域名能绕过它:在服务器上,还要在建立连接的地方执行同一规则(HTTP agent 的解析钩子,或拒绝内网网段的出口代理、防火墙)。浏览器对页面有自己的规则,所以这一点主要关系到服务器与智能体。

不使用 SDK 进行验证

任何语言都可以检查回答。对于发往容器 c 的方法 m 的请求 { id, params },服务使用 EIP-191 personal_sign 对以下内容签名:

digest = keccak256( "TAPI-1/resp/v2" ‖ c ‖ keccak256(id) ‖ keccak256(canonicalJSON({method: m, params}))
                    ‖ uint8(ok) ‖ keccak256(canonicalJSON(result or error)) ‖ uint64BE(ts) )

恢复出签名者(只接受低 s),将其与 manifest.signer 比较,检查持有者的委托指明的正是该签名者,并且 |now - ts| <= 300。规范 JSON 的定义见 TAPI-21;测试向量和一个独立的 Python 实现位于 spec/vectors/。

在 GitHub 上编辑此页