@actora/sdk

中文文档
0.25.0发布于 2026/11/08Apache-2.0TypeScriptESM + CJS近 30 天下载 1778

Actora — Decentralized AI Agent SDK with ReAct AgentLoop + Multi-Tenant LLM Providers

原始使用文档
文档目录54 节
  1. 1. 支持的多链市场
  2. 各链完整合约地址(4 个核心合约)
  3. 白名单稳定币(USDT / USDC)
  4. 链选择指引
  5. RPC 注意事项
  6. 2. 安装
  7. Peer Dependencies(均为 optional)
  8. 3. 快速开始 —— B 端典型接入流程
  9. a. 开户:注册 partner 租户,拿到 API Key
  10. b. 连接钱包(viem)
  11. c. 铸造 Agent(AgentRegistry)
  12. d. 创建订阅计划(SubscriptionManager)
  13. e. 订阅(SubscriptionManager.subscribe 或 SubscriptionPayments.…
  14. f. 与 Agent 对话(ConversationClient)
  15. 4. 多链指南
  16. 如何切链
  17. 价格单位
  18. 合约地址获取(不要硬编码)
  19. 托管对话:链由平台解析,调用方 不需要 声明(0.17.0 起)
  20. 5. 模块参考
  21. 5.1 AgentRegistry —— 链上铸造 / 查询
  22. 5.2 SubscriptionManager —— 链上计划 / 订阅(即时分账,无托管无退款)
  23. 5.3 SubscriptionPayments —— 统一支付(chain / x402)
  24. 5.4 ConversationClient —— 托管对话(REST/SSE)
  25. 5.5 CentralAgentClient —— 中心化市场(C-6)
  26. 5.6 TenantPlanPayments / A2AClient / PeriodClient
  27. 5.7 A2AProtocol —— 链上 A2A(Agent Card / Task)
  28. 5.8 IPFSUploader —— IPFS 上传
  29. 5.9 MCPConnector —— MCP 远程工具
  30. 5.10 AgentRunner + AgentLoop —— 本地运行 Agent
  31. 5.11 加密核心(crypto)
  32. 5.12 浏览器钱包(EIP-1193)—— 发布与运行
  33. 6. 中心化(Central)vs 链上(On-chain)市场对比
  34. 7. B 端常见问题(FAQ)
  35. 鉴权方式对照表
  36. B 端开户后为什么调不通平台 LLM?
  37. 一个租户如何服务多个终端用户?
  38. archive RPC 的坑
  39. nonce 缓存 / 交易重试的坑
  40. Agent 发在 BSC / Ethereum / Base,托管对话会 404 吗?需要我传链吗?
  41. 托管对话里 Agent 的工具(skills)是空的?
  42. 托管对话的 system prompt(人格)从哪儿取?需要把 tokenURI 改成公开 CID 吗?
  43. 其他
  44. 8. 网关 REST API 参考(B 端接入面)
  45. 8.1 鉴权头
  46. 8.2 认证与租户
  47. 8.3 市场与订阅
  48. 8.4 支付与计费
  49. 8.5 托管对话与任务
  50. 8.6 计费与配额语义
  51. 8.7 常见错误码
  52. 8.8 B 端上线检查清单
  53. 9. 版本记录
  54. 附录:可运行示例

@actora/sdk v0.25.0

去中心化 AI Agent 平台 SDK —— 链上铸造 / 订阅 / 支付(多链)+ 托管对话(REST/SSE)+ 多链中心化市场,面向 B 端(合作伙伴)接入。

Agent = Prompt + Skills + MCP

一个 Agent 就是一枚链上 NFT:Prompt(系统提示词)+ Skills(可执行工具)+ MCP(远程工具连接),加密存放在 IPFS,通过 AgentRegistry 铸造、SubscriptionManager 订阅、ConversationClient 托管对话。B 端只需一个 actora_... API Key 即可接入,无需自建钱包基础设施。


1. 支持的多链市场

链chainIdIdentityRegistrySubscriptionManager原生代币RPC
BSC560x8004A169FB4a3325136EB29fA0ceB6D2e539a4320x5a36150C5865B4dA16218724a1c5668561E4d981BNBhttps://bsc-rpc.publicnode.com
Ethereum10x8004A169FB4a3325136EB29fA0ceB6D2e539a4320xF3D14ac4D51BC4B7354838af3137248476D540BDETHhttps://ethereum-rpc.publicnode.com
Base84530x8004A169FB4a3325136EB29fA0ceB6D2e539a4320x41A42cDe1bd7AA39a9beB3745Ae08f50e8656B36ETHhttps://base-rpc.publicnode.com
Sepolia 测试网111551110xe94ad380d3F8d08a7590eda0C84f354a93F96e5F0xC15fE80b9d800abb72121F353a6ae6d6E9077E63ETH自备(Alchemy/Infura 等)

⚠️ 2026-10-24 身份层迁移:BSC / Ethereum / Base 的 IdentityRegistry 已统一指向官方 ERC-8004 canonical 单例(三链同址 0x8004A169…),旧的自建注册表仅作只读存档。 因此 agentId 编号随之变化(BSC 存量 agent 由 1..22 变为 382297..382416,Base 的 1 变为 1123065);SubscriptionManager / A2AProtocolRegistry 已通过 setIdentityRegistry 指向同一单例。

⚠️ 地址来源:上表全部取自 KNOWN_CHAINS[chainId](src/config/config.ts)。生产环境请优先从 KNOWN_CHAINS[chainId].contracts.identityRegistry / subscriptionManager 读取,避免硬编码——合约地址可能随部署修正而更新。

各链完整合约地址(4 个核心合约)

3 条对外市场链(BSC / Ethereum / Base)均已部署 4 个核心合约,可直接用于链上铸造 / 订阅 / A2A。下表与 KNOWN_CHAINS[chainId].contracts 一一对应:

合约BSC (56)Ethereum (1)Base (8453)
IdentityRegistry0x8004A169FB4a3325136EB29fA0ceB6D2e539a4320x8004A169FB4a3325136EB29fA0ceB6D2e539a4320x8004A169FB4a3325136EB29fA0ceB6D2e539a432
SubscriptionManager0x5a36150C5865B4dA16218724a1c5668561E4d9810xF3D14ac4D51BC4B7354838af3137248476D540BD0x41A42cDe1bd7AA39a9beB3745Ae08f50e8656B36
A2AProtocolRegistry0x97cad48D250C00A394449A5E613773e82b20089D0x5a36150C5865B4dA16218724a1c5668561E4d9810x88cce24854505e2d3DC8625a0Bf27Cca10f901C1
ReputationRegistry0x8004BAa17C55a88189AE136b182e5fdA19dE9b630x8004BAa17C55a88189AE136b182e5fdA19dE9b630x8004BAa17C55a88189AE136b182e5fdA19dE9b63

ReputationRegistry 自 2026-10-24 起三链统一指向 官方 canonical 部署(0x8004BAa1…9b63)。 SDK 的 ReputationRegistry 会按 getReputationSummary 是否存在自动识别 legacy / standard 实现,业务侧无需改代码。

ConfigurationRegistry / MultiEndpointRegistry 默认不再部署(DEPLOY_CONFIG / DEPLOY_ENDPOINTS 默认 false),KNOWN_CHAINS[chainId].contracts 中对应字段恒为 ZERO_ADDRESS。 拿到零地址应视为「该能力未启用」,不要构造 ConfigurationRegistry 或发起 RPC。

部署起始块(网关索引 *_SYNC_FROM_BLOCK 用):BSC SM 129441669、Ethereum SM 26259798、Base SM 53011518。 程序化访问请直接读 KNOWN_CHAINS[chainId].contracts(字段名与上表一致),避免复制粘贴出错。

白名单稳定币(USDT / USDC)

BSC / Ethereum / Base 三链的 SubscriptionManager 已登记 USDT / USDC 白名单,计划可用稳定币定价(createPlan 的 payToken 传下表地址):

链代币地址精度
BSCUSDT0x55d398326f99059fF775485246999027B319795518
BSCUSDC0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d18
EthereumUSDT0xdAC17F958D2ee523a2206206994597C13D831ec76
EthereumUSDC0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB486
BaseUSDT0xfde4C96c8593536E31F229EA8f37b2ADa2699bb26
BaseUSDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA029136

ERC20 计划要点:

  • price 用代币最小单位(注意 BSC 上的 USDT/USDC 是 18 位,ETH/Base 上是 6 位,不要统一按 1e6 换算);
  • 订阅方必须先 approve(SubscriptionManager, price),再由 subscribe(planId) 完成订阅;SDK 的 SubscriptionManager.subscribe 会自动处理 approve(见 3.e);
  • ERC20 计划的订阅交易 不要带原生币(合约要求 msg.value == 0),原生币只用于付 gas;
  • 非白名单 ERC20 会被合约 revert,新增币种需由合约 owner 调 setTokenWhitelist(token, true)。
import { parseUnits } from 'viem'

// Base 上用 USDC 定价:9.9 USDC / 月
const { planId } = await sub.createPlan({
  agentId,
  price: parseUnits('9.9', 6),                        // Base USDC 精度 6
  period: 'month',
  payToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
})

链选择指引

  • B 端优先 Base / Ethereum:Gas 便宜或生态成熟,原生币即 ETH;
  • 测试联调用 Sepolia:无真实资金风险;
  • BSC 为对外默认链:平台默认支付/验证链(SubscriptionPayments 的 chain 参数默认 'bsc');
  • 每链的 SubscriptionManager 独立承载各自链上的订阅计划与付款,订阅与铸造必须在同一链上完成(agentId 是链内 ID)。

RPC 注意事项

  • eth_call / 事件监听:publicnode 即可(上面 BSC/ETH/Base 的 RPC);
  • getTransactionReceipt(archive 查询)在 publicnode 会 403,需换带 archive 的 RPC:
    • BSC:https://1rpc.io/bnb
    • Base:https://mainnet.base.org

2. 安装

npm install @actora/sdk
# 或锁定版本
npm install @actora/[email protected]

包内含 examples/(B 端可运行样例)与 UPGRADE.md(逐版本迁移指南),安装后可从 node_modules/@actora/sdk/ 查看。

Peer Dependencies(均为 optional)

包版本用途
react^18 或 ^19React hooks(@actora/sdk/react)
wagmi^2.0React hooks 内部
@tanstack/react-query^5.0React hooks 内部

viem(^2.0)不是 peer dependency:仅链上模块(AgentRegistry / SubscriptionManager / A2AProtocol 等)需要 viem 的 PublicClient / WalletClient,使用时请自行安装;纯 REST 客户端(ConversationClient / CentralAgentClient 等)完全不需要。仓库内可运行示例见 examples/(viem 已在 devDependencies 中)。


3. 快速开始 —— B 端典型接入流程

完整可运行脚本见 examples/mint-and-subscribe.mjs。下面按步骤说明。

a. 开户:注册 partner 租户,拿到 API Key

B 端接入的第一步是用钱包签名建一个 partner 租户,网关会签发一把长期有效的租户 API Key(actora_<hex>):

# 1) 取挑战
curl -X POST https://actora.codes/api/v1/auth/challenge \
  -H 'Content-Type: application/json' -d '{"address":"0xYourWallet"}'

# 2) 用钱包对 challenge 做 EIP-191 签名,再调 verify(intent=partner)
curl -X POST https://actora.codes/api/v1/auth/verify \
  -H 'Content-Type: application/json' \
  -d '{"address":"0xYourWallet","signature":"0x...","intent":"partner"}'
# → { "token": "<jwt>", "tenant": {...}, "api_key": "actora_..." }   ← api_key 仅返回一次
  • partner 租户初始无套餐:quota_daily=0、rate_limit_rpm=5、max_concurrent=1,平台 LLM 不可用;可先 BYOK,或购买平台档位(见 §8)。
  • 一个钱包只对应一个租户;已有租户再传 intent='partner' 不会改变其 kind。
  • api_key 丢失只能轮换(POST /api/v1/tenant/rotate-key),不能找回。
  • 拿到 Key 后即可跳过后面的链上部分(b–d),直接使用 e/f 的订阅支付与托管对话能力。

b. 连接钱包(viem)

import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { KNOWN_CHAINS } from '@actora/sdk'

const chainId = 8453 // Base —— 用 KNOWN_CHAINS 选链
const cfg = KNOWN_CHAINS[chainId]
const chain = {
  id: cfg.chainId,
  name: 'Base',
  nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
}

const account = privateKeyToAccount(`0x${process.env.PRIVATE_KEY}`) // 服务端私钥;浏览器用 window.ethereum 注入
const publicClient = createPublicClient({ chain, transport: http(cfg.rpcUrl) })
const walletClient = createWalletClient({ account, chain, transport: http(cfg.rpcUrl) })

c. 铸造 Agent(AgentRegistry)

import { AgentRegistry } from '@actora/sdk'

const registry = new AgentRegistry({
  contractAddress: cfg.contracts.identityRegistry,
  publicClient,
  walletClient,
})

// 简单铸造:仅 tokenURI
const { agentId, txHash } = await registry.registerSimple('ipfs://Qm...')

// 完整铸造:tokenURI + 链上元数据(加密载荷 CID / ECIES 公钥等)
const { agentId } = await registry.register('ipfs://Qm...', [
  { key: 'encryptedPayloadCid', value: encryptedCid },
  { key: 'eciesEncryptedKey', value: eciesEncryptedKeyHex },
])

d. 创建订阅计划(SubscriptionManager)

import { SubscriptionManager } from '@actora/sdk'
import { parseEther, parseUnits } from 'viem' // 价格单位工具来自 viem

const sub = new SubscriptionManager({
  contractAddress: cfg.contracts.subscriptionManager,
  publicClient,
  walletClient,
})

// 原生币计划(BNB / ETH)
const { planId, txHash } = await sub.createPlan({
  agentId,
  price: parseEther('0.01'),          // 原生币 wei
  period: 'month',                     // 'day' | 'week' | 'month' | 'quarter' | 'year'
})

// 稳定币计划(USDT / USDC)—— payToken 传白名单地址,price 用代币最小单位
const usdcPlan = await sub.createPlan({
  agentId,
  price: parseUnits('9.9', 6),                              // Base USDC 精度 6
  period: 'month',
  payToken: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
})

e. 订阅(SubscriptionManager.subscribe 或 SubscriptionPayments.pay)

链上原生币订阅(自动按 plan.price 支付;ERC20 计划自动 approve):

const result = await sub.subscribe(planId)
// → { subscriptionId, txHash, subscriber, agentId, expiresAt }

统一支付入口(三条支付轨道,B 端常用):

import { SubscriptionPayments } from '@actora/sdk'

// 轨道 1:chain —— 走链上 SubscriptionManager
const payments = new SubscriptionPayments({
  subscriptionManager: sub,
  chain: 'base',                       // 验证链
})
const res = await payments.pay({ method: 'chain', planId, agentId })
// → { method: 'chain', subscriptionId, txHash }

// 轨道 2:x402 —— 原生币按期支付(自动代付需 walletClient + subscriptionManager 读价格;
// 也可自己先发送原生币到平台 payTo 后传 txHash)
// ⚠️ 法币不在 SDK 内:Agent 订阅的法币由发布者自备收款页收取
// (`agent_listings.payment_link_url`),付款后由发布者的支付通道回调网关开通;
// 平台不代收、不碰钱。`TenantPlanPayments`(平台自卖套餐)仍保留 fiat 轨。
const payments3 = new SubscriptionPayments({
  gatewayUrl: 'https://actora.codes',
  subscriptionManager: sub,          // 读取计划价格(priceWei)
  walletClient,                      // 自动把原生币转到平台 payTo 钱包
  chain: 'base',
})
const res3 = await payments3.pay({ method: 'x402', planId, agentId, subscriber: '0x...', period: 'month' })
// → { method: 'x402', subscriptionId, txHash, creditedWei }

f. 与 Agent 对话(ConversationClient)

B 端只需 tenant API Key(actora_...),无需钱包:

import { ConversationClient } from '@actora/sdk'

const conv = new ConversationClient({
  gatewayUrl: 'https://actora.codes',
  apiKey: 'actora_your_tenant_key',    // X-Api-Key;与 accessToken(JWT) 二选一
  endUserId: 'user-123',               // X-End-User-Id:租户内最终用户的记忆隔离维度
  // llmApiKey: 'sk-...',              // BYOK:用自己的 LLM Key,不计入平台配额
  // llmEndpoint: 'https://api.deepseek.com/v1',
  // llmModel: 'deepseek-chat',
})

// 单轮对话(内部就是 stream 聚合)
const res = await conv.chat({ agentId, message: '帮我分析这份合约' })
console.log(res.text)

// 流式(SSE,逐事件)
for await (const ev of conv.stream({ agentId, message: '实时输出' })) {
  if (ev.type === 'text') process.stdout.write(ev.content ?? '')
  if (ev.type === 'done') console.log('\n[done]', ev.usage)
}

鉴权 / 隔离语义:

  • X-Api-Key: actora_... —— 租户级鉴权(注册后签发);
  • Authorization: Bearer <jwt> —— 钱包签名登录的 JWT,与 apiKey 二选一;
  • X-End-User-Id —— 租户内按最终用户隔离记忆;若传 0x<wallet>,网关会额外做订阅代理鉴权(该钱包的订阅/所有权决定访问权);
  • BYOK:llmApiKey / llmEndpoint / llmModel 三个头,运行时用自己的 Key(llmSource: 'byok' 不计费)。

4. 多链指南

如何切链

所有链上模块(AgentRegistry / SubscriptionManager / A2AProtocol)都接收 { contractAddress, publicClient, walletClient }——换链 = 换一组配置:

function makeChainClients(chainId: number) {
  const cfg = KNOWN_CHAINS[chainId]
  if (!cfg) throw new Error(`unsupported chain ${chainId}`)
  const publicClient = createPublicClient({ chain: chainById(chainId), transport: http(cfg.rpcUrl) })
  const walletClient = createWalletClient({ account, chain: chainById(chainId), transport: http(cfg.rpcUrl) })
  return { cfg, publicClient, walletClient }
}

价格单位

  • 原生币计划:price 为该链原生代币的 wei(BNB / ETH);
  • ERC20 计划:price 为代币最小单位 —— ETH/Base 的 USDT/USDC 是 6 位(1e6),BSC 的 USDT/USDC 是 18 位(1e18);
  • 跨链比较价格时统一用 getPlan(planId).price + plan.payToken 判断计价币种;
  • 中心化 Agent 的价格以 priceWei 字符串返回(见 CentralPlan),避免 JS 精度丢失。

合约地址获取(不要硬编码)

import { KNOWN_CHAINS } from '@actora/sdk'
const base = KNOWN_CHAINS[8453]
const registryAddr = base.contracts.identityRegistry
const subManagerAddr = base.contracts.subscriptionManager

托管对话:链由平台解析,调用方不需要声明(0.17.0 起)

链上模块(AgentRegistry / SubscriptionManager)是「哪条链的合约就读哪条链」,但托管对话(ConversationClient, agentId 模式)相反:你只要给出 agentId,平台会自己判断该 Agent 在哪条链、以及你在哪条链上有权。

  • 平台从 agents 表列出该 agentId 存在的全部候选链(按平台内部固定的链优先序:内部测试链 → 中心化 → BSC → Ethereum → Base 排序),逐链判定你(X-End-User-Id 或租户钱包)是否拥有 / 已订阅,取第一条放行的链;
  • 这条链会一路贯穿「访问判定 → 取 Agent 上下文 → 组装工具」,所以不会出现「按 A 链判权、按 B 链取到别人的同名 Agent」;
  • 为什么必须这样:各链独立编号,同一个数字 ID 可同时在多条链存在(实测 BSC 与 Ethereum 都有 id=2, 且是两个不同的 Agent)。只看「优先序第一条」会取错,故按「你在哪条链有权」来锚定;
  • 该判定发生在平台侧,托管对话请求里不接受调用方指定链(chat / stream / createTask 均无需、 也无法传 chain);只有只读的 GET /api/v1/agents/:id?chain= 可以显式指定链;
  • 历史行为(0.16.0 及以前):该路径只认内部测试链 / 中心化市场,Agent 发在 BSC / Ethereum / Base 时 详情接口直接 404、或判权失败 403 —— 已在 0.17.0 随平台侧修复。

实践建议:集成时不要猜链、也不要声明链,直接给出 agentId 调 POST /api/v1/agent/runs 或 POST /api/v1/sessions/:id/tasks 即可;平台会按「你在哪条链有权」自动锚定。


5. 模块参考

5.1 AgentRegistry —— 链上铸造 / 查询

  • 用途:封装 IdentityRegistry 合约,Agent NFT 的铸造与只读查询。
  • 构造:new AgentRegistry({ contractAddress, publicClient, walletClient })
  • 关键方法:
方法说明
register(tokenURI, metadata[], valueWei?)铸造 + 链上元数据(key/value 对),返回 { agentId, txHash }
registerSimple(tokenURI?, valueWei?)仅 tokenURI 铸造,返回 { agentId, txHash }
getAgentsByOwner(owner)某地址拥有的全部 agentId
getCurrentAgentId() / totalAgents()当前计数 / 总铸造数
agentExists(agentId)是否存在
getAttributes(agentId) / tokenURI(agentId)读元数据键值对 / tokenURI
overwriteAttributes(agentId, attrs[])整组覆盖链上属性(updateMetadata),返回 txHash
setAttributes(agentId, patch)增量更新链上属性:读回现有 → 合并 → 整组写回,返回 txHash
getAgentMetadata(agentId)结构化元数据(name/capabilities/encryptedPayloadCid 等)
getAllAgents(opts?)批量扫描(fromId/toId/activeOnly/capabilities/batchSize),返回 AgentSummary[]
const { agentId } = await registry.registerSimple('ipfs://QmX...')
const owners = await registry.getAgentsByOwner(account.address)
const meta = await registry.getAgentMetadata(agentId) // { name, description, capabilities, encryptedPayloadCid, ... }

// 存量 Agent 补写公开 skills(只改 skills,其余属性自动保留)
await registry.setAttributes(agentId, {
  skills: [{ name: 'consult', description: '向该专家发起咨询', inputSchema: { type: 'object' } }],
})
// 整组覆盖(只在需要「删掉某个属性键」时用;先读回原有属性,否则会被抹掉)
const current = await registry.getAttributes(agentId) // { name: '…', description: '…', … }
await registry.overwriteAttributes(agentId, [
  ...Object.entries(current).map(([key, value]) => ({ key, value })),
  { key: 'skills', value: '[]' },
])

⚠️ 生产合约的 updateMetadata 是整组覆盖语义(先清空全部属性键再写入传入数组),所以优先用 setAttributes()(自动读回合并);overwriteAttributes() 必须把原有 name / description / category / pricing_type / … 全部一起传,否则会被抹掉。辅助函数 toAttributes(patch) 可把 { key: value }(字符串 / 数字 / 布尔 / 数组 / 对象)规范化为链上属性数组。

5.2 SubscriptionManager —— 链上计划 / 订阅(即时分账,无托管无退款)

  • 用途:封装 SubscriptionManager v3(多币种、subscribe 即时分账、平台费;无 escrow / 无试用 / 无退款)。
  • 构造:new SubscriptionManager({ contractAddress, publicClient, walletClient })
  • 关键方法:getPlatformFeeBps()(内部读 v3 defaultPlatformFee())、isTokenWhitelisted(token)(内部读 priceOracle.isTokenSupported())、getPlan(planId)、createPlan(CreatePlanParams)、createPlanAndSubscribe(params)、subscribe(planId, opts?)、cancel(subscriptionId)、hasActiveSubscription(subscriber, agentId)、getSubscription(subscriber, agentId)、getSubscriptionDetail(subscriptionId)、getUserSubscriptions(user)
  • CreatePlanParams:{ agentId, name?, description?, planType?, price?, usagePrice?, period: 'day'|'week'|'month'|'quarter'|'year', payToken?, maxUsage?, totalUsageLimit? }
  • 常量:ZERO_ADDRESS(= 原生币)、SUBSCRIPTION_PERIODS
const { planId } = await sub.createPlan({ agentId, price: parseEther('0.005'), period: 'week' })
const ok = await sub.hasActiveSubscription(buyer, agentId)  // 网关/服务端鉴权常用
const detail = await sub.getSubscriptionDetail(subscriptionId)
// status: 0=Active 1=Cancelled 2=Expired 3=PaymentFailed 4=Suspended;取消即时生效、无退款

5.3 SubscriptionPayments —— 统一支付(chain / x402)

  • 用途:两条支付轨道统一入口。chain = 链上 SubscriptionManager;x402 = 原生币按期支付(走网关统一端点)。Agent 订阅的法币不在 SDK 内:由发布者自备收款页收取并提供回调(详见 payment-architecture.md §9.2);TenantPlanPayments 的平台套餐仍支持 fiat。
  • 构造:new SubscriptionPayments({ gatewayUrl?, accessToken?, subscriptionManager?, walletClient?, chain? })
    • chain 取值:'sepolia' | 'bsc' | 'ethereum' | 'base'(默认 'bsc')
  • 关键方法:pay(PaySubscriptionInput)、hasAccess(agentId, subscriber)(跨轨道访问检查)、fetchX402Info()(x402 发现:price/payTo/network)
  • PaySubscriptionInput 字段(以源码为准):planId, agentId, method, subscriber?, valueWei?, approveTokenFirst?, txHash?, priceWei?, period?
    • priceWei:中心化 Agent(无链上计划)时直接指定价格。
const payments = new SubscriptionPayments({ gatewayUrl: 'https://actora.codes', chain: 'base' })
const hasAccess = await payments.hasAccess(agentId, '0x...') // true / false
const info = await payments.fetchX402Info()                  // { enabled, priceWei, payTo, network, chain }

5.4 ConversationClient —— 托管对话(REST/SSE)

  • 用途:托管对话服务客户端(POST /api/v1/agent/runs,SSE 流式),支持会话、并行任务、A2A 委派。
  • 构造:new ConversationClient({ gatewayUrl, apiKey?, accessToken?, endUserId?, llmApiKey?, llmEndpoint?, llmModel?, timeoutMs? })
  • 关键方法:
    • 对话:chat(params) → { text, toolCalls, clarification?, usage? };stream(params, { signal? }) → SSE 事件(text | tool_call | tool_result | thinking | done | error | clarification | onchain_approval_required)
    • 会话/任务:createSession(params)、createTask(params)(立即返回 status: 'queued',后台执行)、getTask(taskId)、listTasks(sessionId)、cancelTask(taskId)、getCapabilities()(parallelTasks 开关,关闭时 createTask 返回 403 PARALLEL_TASKS_DISABLED,应降级为 chat())
    • A2A:getA2ATaskResult(taskId)
    • getSessionKeyInfo():已随 AA 下线(2026-11-08)并从 0.25.0 移除;按次付费统一走 x402 直付。
  • 参数中的 endUserId 传 0x<wallet> 会触发网关订阅代理鉴权。
  • 多链(0.17.0 起):chat() / stream() / createTask() 不需要传链,也无法传链 —— 平台按 「你在这条 agentId 的哪条链上有权」自动解析并全链路沿用(详见 §4「托管对话:链由平台解析」)。 因此 BSC / Ethereum / Base 上的 Agent 直接可用,不会再 404。
const conv = new ConversationClient({ gatewayUrl, apiKey: 'actora_...', endUserId: '0x...' })
const session = await conv.createSession({ agentId })
const task = await conv.createTask({ sessionId: session.id, agentId, message: '批量处理' })
while (true) {
  const t = await conv.getTask(task.id)
  if (t.status === 'done' || t.status === 'error') break
  await new Promise((r) => setTimeout(r, 1000))
}

5.5 CentralAgentClient —— 中心化市场(C-6)

  • 用途:中心化 Agent(source='central',负 ID,不上链)的公开查询 + 鉴权管理。订阅计划存在网关 DB(central_plans),付费复用 x402 通道(SubscriptionPayments.pay({ priceWei, period }))。
  • 构造:new CentralAgentClient({ baseUrl, accessToken?, apiKey? })
  • 关键方法:list(filters?)、get(agentId)、checkSubscription(subscriber, agentId)、mySubscriptions(subscriber)、create(input)、listMine()、update(agentId, input)、activate(agentId) / deactivate(agentId)、createPlan(agentId, input)、deactivatePlan(agentId, planId)
  • CreateCentralAgentInput:{ name, description?, tags?, capabilities?, skills?, category? }
  • CreateCentralPlanInput:{ priceWei: string, period?: 'day'|'week'|'month'|'year' }(priceWei 为十进制字符串)
const central = new CentralAgentClient({ baseUrl: 'https://actora.codes', apiKey: 'actora_...' })
const { agents, total } = await central.list({ activeOnly: true, page: 1, pageSize: 20 })
const { agent } = await central.create({ name: '行情分析助手', description: '...', capabilities: ['market-data'] })
const { plan } = await central.createPlan(agent.id, { priceWei: '10000000000000000', period: 'month' })

5.6 TenantPlanPayments / A2AClient / PeriodClient

⚠️ 0.25.0(2026-11-08)破坏性变更:SpendPolicyClient 已随 AA 下线从包内移除 (网关删除 /api/v1/spend-policy/* 全部端点)。按次付费统一走 x402 直付,订阅到期由用户主动续订。

类用途构造关键方法
TenantPlanPayments购买平台订阅档位(purpose='tenant-plan',绑定租户配额){ gatewayUrl, accessToken?, chain? }buy({ tenantPlanId, subscriber, method, txHash?, successUrl?, cancelUrl? })
A2AClienta2a-pay 支付意图两段式(create → pay → settle){ baseUrl, accessToken? }(ClientOptions)create({ payer, amountWei, payee?, chain?, metadata? })、settle({ paymentId, txHash, chain? })
PeriodClient周期授权(P4):按周期扣款/读状态{ baseUrl, accessToken? }(ClientOptions)charge(authorizationId)、authorization(authorizationId)

余额预充值模型与按次 session 授权均已下线:GET /api/v1/billing/balance、BillingClient 与 /api/v1/spend-policy/* 全部删除。订阅改为加密货币直付(收款方 = 开发者 payout_address) 或 Stripe;按次付费走 x402,平台不经手资金、不代扣。

5.7 A2AProtocol —— 链上 A2A(Agent Card / Task)

  • 用途:封装 A2AProtocolRegistry:Agent Card(能力声明)、Skill 注册、Task 生命周期。
  • 构造:new A2AProtocol({ contractAddress, publicClient, walletClient })
  • 关键方法:createAgentCard(agentId, card)、getAgentCard(agentId)、createTask(agentId, taskType, input)、completeTask(taskId, output, status?)、getTask(taskId)、getUserTasks(user)、getAgentTasks(agentId)
  • 相关:ConversationClient.getA2ATaskResult(网关侧 A2A 委派结果);A2AClient(a2a-pay 支付)。

5.8 IPFSUploader —— IPFS 上传

  • 用途:Pinata(JWT)/ 自定义端点上传 JSON / 文件 / 加密载荷。
  • 构造:new IPFSUploader({ pinataJwt?, customEndpoint?, customApiKey?, gatewayUrl?, timeoutMs?, pinataGroupId?, namePrefix? })
  • 关键方法:uploadJSON(data, metadata?)、uploadFile(content, fileName?, mimeType?)、uploadEncryptedPayload(payload, agentName?)、uploadString(content, name?)、getUrl(cid)、isConfigured()
  • 上传结果 { cid, url, raw }。
const uploader = new IPFSUploader({ pinataJwt: process.env.PINATA_JWT })
const { cid, url } = await uploader.uploadJSON({ name: 'my-agent', capabilities: ['web'] })

5.9 MCPConnector —— MCP 远程工具

  • 用途:最小 MCP 客户端(HTTP/SSE),用于 Closed Skill 的远程执行。
  • 构造:new MCPConnector({ url, transport?, authHeader?, timeoutMs?, subscriberAddress?, signature?, timestamp? })(或 MCPConnector.fromAgent(mcp, opts?))
  • 关键方法:listTools() → MCPTool[]、callTool(name, args?) → MCPCallResult、listResources()、readResource(uri)
const mcp = new MCPConnector({ url: 'https://mcp.example.com', authHeader: 'Bearer xxx' })
const tools = await mcp.listTools()
const out = await mcp.callTool('fetch_price', { symbol: 'ETH' })

5.10 AgentRunner + AgentLoop —— 本地运行 Agent

  • AgentRunner:加载 + 解密 Agent(订阅校验 → IPFS 拉取 → 解包 AES Key → AES-256-GCM 解包 prompt/skills)。构造 new AgentRunner({ reader, wallet, ipfsFetcher?, ipfsGateways?, unpack?, aesKeyHex? }),reader 实现 OnChainReader(getTokenURI/getAttributes/hasActiveSubscription),wallet 实现 WalletSigner(signMessage/getAddress/deriveEncryptionKey?/getPrivateKey?)。核心方法 useAgent(agentId) → { prompt, skills, mcp, subscriptionExpiry }。密钥来源优先级:config.unpack → config.aesKeyHex → wallet.deriveEncryptionKey() → wallet.getPrivateKey()(浏览器钱包见 §5.12)。
  • AgentLoop:ReAct 风格循环(Think → Tool Call → Execute → …)。构造 new AgentLoop({ ctx, llmProvider, maxIterations?, timeoutMs?, memory?, trace?, contextBudget?, onTextDelta?, onToolCall?, onToolResult?, onThinking?, onComplete?, onError? }),llmProvider 实现 LLMProvider.chatStream(request, signal?)。核心方法 run(userMessage, history?) → AgentLoopResult、abort()。
const runner = new AgentRunner({ reader, wallet })   // reader/wallet 由 viem/wagmi 实现
const ctx = await runner.useAgent(agentId)           // 需当前钱包已订阅该 agent
const loop = new AgentLoop({
  ctx,
  llmProvider,                                       // 任意 OpenAI 兼容 provider
  maxIterations: 5,
  onTextDelta: (d) => process.stdout.write(d),
})
const { finalText } = await loop.run('帮我做这件事')

B 端通常不需要自建 Runner/Loop——托管对话(ConversationClient)已覆盖;Runner/Loop 适合自托管、完全掌控加密与执行的场景。

5.11 加密核心(crypto)

  • aesEncrypt(plaintext, keyHex) / aesDecrypt(encryptedBase64, keyHex) —— AES-256-GCM(线格式 base64(IV[12]||ct||tag[16]))
  • eciesEncrypt(dataHex, publicKey) / eciesDecrypt(dataHex, privateKey) —— ECIES(secp256k1,与 eciesjs 兼容)
  • generateKeyPair() → { privateKey, publicKey };getPublicKey(privateKey)
  • derivePrivateKeyFromSignature(signature, address, params?) / derivePrivateKeyFromIkm(ikmHex, address, params?) / deriveKeyPairFromWallet({ mode?, signMessage, signTypedData?, getPrfOutput?, address, … }) —— 浏览器钱包密钥派生(v1 personal_sign / v2 EIP-712 / v3 WebAuthn PRF + HKDF-SHA256,见 §5.12);配套 buildPayloadKeyTypedData(address, overrides?) 与 DEFAULT_EIP712_DOMAIN / DEFAULT_EIP712_TYPES / DEFAULT_EIP712_PRIMARY_TYPE / DEFAULT_DERIVATION_INFO_PREFIX_V2 / DEFAULT_DERIVATION_INFO_PREFIX_V3 / DEFAULT_PRF_SALT(v3 的 IKM 不再是签名,直接用 derivePrivateKeyFromIkm 喂 32 字节 PRF 输出)
  • generateAesKey();高级封装:encryptPayload / decryptPayload / packAgentForPublish / publishAgent / unpackAgent
  • 发布绑定与自检:publishAgent({ ..., verifyWithPrivateKey })(铸造前 dry-run 解包);PAYLOAD_KEY_MODE_ATTRIBUTE / readPayloadKeyMode(attrs) / isDerivationMode(v) / AgentRegistry.setPayloadKeyMode(agentId, mode);v3 另有 PAYLOAD_KEY_CREDENTIAL_ATTRIBUTE / serializePayloadKeyCredential() / readPayloadKeyCredential(attrs) / AgentRegistry.setPayloadKeyCredential(agentId, credential)
import { generateKeyPair, aesEncrypt, aesDecrypt, eciesEncrypt, eciesDecrypt } from '@actora/sdk'
const { privateKey, publicKey } = generateKeyPair()
const enc = eciesEncrypt('my-aes-key-hex', publicKey)
const dec = eciesDecrypt(enc, privateKey) // 'my-aes-key-hex'
const c = aesEncrypt('hello', '00'.repeat(32))
aesDecrypt(c, '00'.repeat(32))            // 'hello'

5.12 浏览器钱包(EIP-1193)—— 发布与运行

结论先讲:MetaMask / OKX / Rabby / Trust / Coinbase 等注入式钱包无法导出账户私钥, 所以 AgentRunner.useAgent() 不能靠账户私钥做 ECIES 解密;而 eth_getEncryptionPublicKey (EIP-1024)已废弃且仅 MetaMask 实现。浏览器端的可行路径有三条:从确定性签名派生(v1 personal_sign / v2 EIP-712)、 从 WebAuthn PRF 派生(v3,非签名、来源绑定)、或改用托管密钥(服务端生成密钥对)。

派生规则(冻结契约)

三种模式只有「IKM 从哪来」不同,HKDF 构造逐字节一致:

v1  mode: 'personal-sign-hkdf'        IKM = personal_sign 签名(65 字节 r‖s‖v,要求 RFC 6979 确定性)
v2  mode: 'eip712-hkdf'               IKM = eth_signTypedData_v4 签名(65 字节)
v3  mode: 'webauthn-prf-hkdf'         IKM = WebAuthn PRF 输出(32 字节,非签名)

三者共用:
privateKey = HKDF-SHA256(IKM,
                         salt = "actora/expert-enc",
                         info = "<infoPrefix>/<地址小写>")[retry:i]
publicKey  = 未压缩公钥(0x04 + 128 hex)→ 传给 publishAgent

infoPrefix = "agent-payload-encryption/v1"    // v1
           | "agent-payload-encryption/v2"    // v2
           | "agent-payload-encryption/v3"    // v3
  • 账户私钥永不离开钱包;派生私钥只存在于内存,不落盘、不上传;解密时用同一来源重现同一 IKM 即可(v1/v2 再签一次,v3 再求一次 PRF);
  • 冻结面:改动其中任一值,已发布的 payload 永久解不开——上线前冻结,文案按字节冻结(不要本地化、不要改空格):
    • v1:message / salt / infoPrefix / 发布地址;
    • v2:domain / types / primaryType / infoPrefix / 发布地址(typed data 的 message 由地址派生,不含时间戳 / nonce, 否则签名不可复现、密钥不可复原);
    • v3:prfSalt / salt / infoPrefix / rpId / credentialId / 发布地址(prfSalt 即上表的 salt,经 PRF 扩展求值后作为 HKDF 的 salt 输入);
  • 越界(HKDF 输出非合法 secp256k1 标量)时按 info + "/retry:<i>" 重试,首次命中的方案即冻结方案;
  • v1 / v2 / v3 并存:0.18.0 起新档案建议用 v2;0.20.0 起需要「非签名、来源绑定」时用 v3。已用旧 mode 发布的载荷继续用旧 mode 解(不提供自动迁移, 因为 tokenURI 铸后不可改)。

选型:四条路径怎么选(先定 Runner 跑在哪,再选密钥来源)

第一问不是「哪个更安全」,而是「Runner 跑在哪」:

  • Runner 在你的服务端(服务化 Agent / 后端调用 / 自建执行)⇒ 用私钥托管,另外三条都不需要 —— 密钥不下发浏览器,落地见下方「托管密钥」一节。这是规避 v3 兼容性问题最彻底的一条路。
  • Runner 在终端用户浏览器(用户自持访问权、去中心化叙事)⇒ 服务端私钥下不到浏览器, 只能在 v1 / v2 / v3 之间选。
维度v1 personal-sign-hkdfv2 eip712-hkdfv3 webauthn-prf-hkdf私钥托管
IKM 来源personal_sign 签名eth_signTypedData_v4 签名WebAuthn PRF 输出(非签名)无(服务端 generateKeyPair())
对浏览器/设备的依赖注入式钱包注入式钱包 + EIP-712WebAuthn + PRF(移动端钱包内置浏览器普遍没有)无(解密在服务端)
可复用签名攻击面有有无无(根本没有签名)
来源绑定无无(domain 只是域分隔符)有(浏览器强制 RP ID)无 ⇒ 改为服务端鉴权
是否有盲签/签名提示文本盲签结构化签名无签名提示无
要求确定性签名(RFC 6979)是是否否
需额外落盘的信息——credentialId + rpId私钥的备份与恢复
主要不可逆风险钓鱼站可复现签名 ⇒ 派生私钥同 v1换域名 / 丢凭据 ⇒ 永久解不开私钥丢失 ⇒ 永久解不开,且不可轮换
v3 兼容性矩阵不涉及不涉及需自行承担(见 v3 一节)全部规避
默认值否否否(opt-in)否(opt-in)

一句话结论:若 B 端的 Runner 能跑在服务端,私钥托管可完全规避 WebAuthn/PRF 的兼容性问题 —— 移动端内置浏览器、跨设备、混合传输(QR)、域名迁移全都不涉及(密钥材料不进浏览器)。 但它的代价是信任边界转移到运维侧:私钥在 KMS/HSM、不可轮换、丢失即永久解不开, 且它提供的是服务端鉴权而非「客户端自持 + 域绑定」。若后者是硬需求,则只能走 v3。

v2:用 EIP-712 取代文本盲签(mode: 'eip712-hkdf')

v1 的 personal_sign 有两个先天问题:钱包一律按文本盲签渲染(MetaMask / Rabby / OKX 提示「无法验证来源」), 且文案公开固定、可无限复用(任何钓鱼站让用户签同一串文本即可派生出该用户的 payload 私钥)。v2 把「被签的东西」 换成 EIP-712 typed data,派生算法不变,钱包会展示结构化、带 domain 的签名请求。

⚠️ v2 只解决「呈现」,不解决「可复用」:typed data 依然是公开常量、无时效、可无限重放, 上面那条攻击路径照旧成立(见「前置条件与失败语义」)。换编码消不掉 IKM 是签名这个事实; 要消掉它有两条路:托管密钥(见「托管密钥」一节)或 WebAuthn PRF(v3,0.20.0 已交付,见下节)。

默认值(SDK 内置,刻意不含时间戳):

domain      = { name: 'Actora Agent Payload', version: '2' }
types       = { PayloadKey: [
  { name: 'address', type: 'address' },   // 与 HKDF info 的地址同口径(小写),双保险
  { name: 'purpose', type: 'string' },    // 'agent-payload-encryption'
  { name: 'version', type: 'uint256' },   // 2
] }
primaryType = 'PayloadKey'
message     = { address: <地址小写>, purpose: 'agent-payload-encryption', version: 2 }

发布侧(deriveKeyPairFromWallet 接受 typed data,不再是只有 signMessage):

import { deriveKeyPairFromWallet } from '@actora/sdk'

const { privateKey, publicKey } = await deriveKeyPairFromWallet({
  mode: 'eip712-hkdf',
  signTypedData: (typedData) => walletClient.signTypedData({ account, ...typedData }),
  address: account.address,
  // assertDeterministic 自 0.19.0 起默认开启;如需关掉:assertDeterministic: false
})
// 铸造前自检:把 privateKey 传给 publishAgent({ ..., verifyWithPrivateKey: privateKey })

运行侧(默认值即上表;domain / types / primaryType 可覆盖,覆盖后同样进入冻结面):

const { ctx, error } = useAgentRunner({
  agentId,
  encryptionKeyDerivation: { mode: 'eip712-hkdf' },   // v2
  // encryptionKeyDerivation: { mode: 'personal-sign-hkdf' },  // v1:解已有载荷时用
})

v3:WebAuthn PRF —— 把 IKM 从「签名」换成「浏览器绑定的密钥材料」(mode: 'webauthn-prf-hkdf',0.20.0+)

前两节的 IKM 都是签名,于是「公开 / 无时效 / 可复用」成为必然代价。v3 换掉 IKM 的来源:用 WebAuthn PRF 扩展 的确定性输出(32 字节)。PRF 由浏览器/认证器强制绑定到 RP ID(你的注册域),不是签名,因此:

维度v1 / v2(IKM = 签名)v3(IKM = PRF 输出)
是否可被重放✅ 载荷是公开常量,钓鱼站让用户签同一份即可派生❌ 浏览器拒绝在非注册域使用该凭据,拿不到同值
钱包提示文本盲签 / typed data 签名无签名提示(不经过 EIP-1193)
是否需要 assertDeterministic需要(非 RFC 6979 钱包不可复现)不需要:PRF 无计数器,构造上确定
与钱包地址的关系绑定发布地址(HKDF info)同样绑定地址;但密钥主体来自 PRF ⇒ 与钱包账户解耦
新增依赖面无rpId / credentialId(必须落盘,无法从地址反推)

发布侧(先注册 passkey,再派生):

import {
  getPrfCapability, createPrfCredential, evaluatePrf,
  deriveKeyPairFromWallet, publishAgent, DEFAULT_PRF_SALT,
} from '@actora/sdk'

// ① 能力探测(不弹窗)。prf: 'unknown' 是真实答案 —— 旧浏览器无法在不弹窗的前提下回答,
//    它**不等于**失败;v3 真不可用时会在 ③ 抛出可操作错误。
const cap = await getPrfCapability()

// ② 注册 passkey(residentKey + userVerification + PRF 一并开启)
const cred = await createPrfCredential()      // rpId 默认 location.hostname
// cred.credentialId 是 base64url,**务必持久化**(见 ④);丢了就再也派生不出同一把密钥

// ③ 派生:IKM = evaluatePrf 的 32 字节输出
const { privateKey, publicKey } = await deriveKeyPairFromWallet({
  mode: 'webauthn-prf-hkdf',
  address: account.address,
  getPrfOutput: async () => (await evaluatePrf({ credentialId: cred.credentialId })).prfOutputHex,
})
// v3 忽略 assertDeterministic(PRF 无计数器);铸造前自检照旧
const result = await publishAgent({ agent, publicKey, uploader, verifyWithPrivateKey: privateKey })

运行侧与链上绑定(把 rpId + credentialId 记到档案上,否则将来无从复现):

const { ctx, error } = useAgentRunner({
  agentId,
  encryptionKeyDerivation: {
    mode: 'webauthn-prf-hkdf',
    credentialId,                 // 从链上属性读回
    rpId: 'app.example.com',      // 必须就是注册该 passkey 时的域
    // prfSalt: DEFAULT_PRF_SALT, // 默认值,改动即进入冻结面
  },
})
// 发布后落盘绑定(跨实现复现所需的最小信息)
await registry.setPayloadKeyMode(agentId, 'webauthn-prf-hkdf')
await registry.setPayloadKeyCredential(agentId, {
  rpId: 'app.example.com',
  credentialId,                   // base64url
  prfSalt: DEFAULT_PRF_SALT,      // 显式写入,便于第三方 runner 复现
  address: account.address,
})
// 读取:readPayloadKeyCredential(await registry.getAttributes(agentId))
//      (属性名常量 PAYLOAD_KEY_CREDENTIAL_ATTRIBUTE = 'payload_key_credential',JSON 字符串)

零回退(有意设计):PRF 不可用时抛 PrfError,绝不静默退化为 signMessage —— 静默回退会让用户以为持有「passkey 绑定」的密钥,实际回到可复用签名,比直接报错危险得多。 错误码(error.code,error instanceof PrfError):

PrfErrorCode触发处置
PRF_UNSUPPORTED浏览器/认证器没有 PRF 扩展(移动端钱包内置浏览器普遍如此)改用 mode: 'eip712-hkdf'(v2)或托管密钥
PRF_CREDENTIAL_NOT_FOUND凭据不在本设备(InvalidStateError)在注册设备或同一 passkey 同步服务上重试
PRF_CANCELLED用户取消 / 超时 / 凭据不存在(NotAllowedError)重新发起;确认 RP ID 正确
PRF_FAILEDRP ID 与当前来源不匹配(SecurityError)、输出长度异常、其他检查 rpId 是否等于注册时的域

⚠️ v3 的已知局限(务必在选型时权衡): ① 移动端钱包内置浏览器普遍不支持 WebAuthn/PRF ⇒ 这类端侧请继续用 v2 或托管密钥; ② 密钥绑定 (rpId, credential) ⇒ 换域名、或丢失凭据,已发布载荷永久解不开(tokenURI 铸后不可改), 故 credentialId + rpId 必须随档案落盘(payload_key_credential 属性); ③ 跨设备 / 混合传输(QR)场景的 PRF 一致性存在公开缺陷史(如 WebKit bug 311099:Safari 对安全密钥返回 未解密的 hmac-secret 输出)⇒ v3 因此设计为 opt-in,不改变任何默认值。 ④ getPrfCapability() 在无 getClientCapabilities() 的浏览器返回 prf: 'unknown',只能靠真实 ceremony 判定。

发布(浏览器)

import { deriveKeyPairFromWallet, publishAgent } from '@actora/sdk'

// v1(沿用既有档案)
const { privateKey, publicKey } = await deriveKeyPairFromWallet({
  signMessage: (message) => walletClient.signMessage({ account, message }),
  address: account.address,
  // 0.19.0 起默认开启(多签一次即拦下非确定性钱包);确认钱包确定性时可关掉
  // assertDeterministic: true,
})
// v2(新档案推荐):见上一节 `mode: 'eip712-hkdf'`

// 铸造前自检:私钥解不开就不上传、不铸造(tokenURI 铸后不可改)
const result = await publishAgent({
  agent, publicKey, uploader,
  verifyWithPrivateKey: privateKey,
})
// result.encryptedCid → 链上 tokenURI;result.eciesEncryptedKeyHex → 元数据属性

// 铸造后记录派生 mode —— 跨实现的标准字段,第三方 runner 据此选解包方式
await registry.setPayloadKeyMode(agentId, 'personal-sign-hkdf')   // 或 'eip712-hkdf';v3 用 'webauthn-prf-hkdf'(另需 setPayloadKeyCredential)

发布链路上有三道铸造前护栏,都会在「已经不可回退」之前拦住你:

护栏何时触发默认
assertDeterministic钱包对同一载荷返回不同签名(非 RFC 6979)⇒ 将来必然解不开开(0.19.0+)
selfCheck派生出的密钥对无法 ECIES 往返(密钥本身有问题)开(0.19.0+)
verifyWithPrivateKey传入的私钥解不开本次包裹的 AES key(含 v1/v2 mode 用错)传了才查

读取模式用 readPayloadKeyMode(await registry.getAttributes(agentId))(属性名常量 PAYLOAD_KEY_MODE_ATTRIBUTE = payload_key_mode; 未知值返回 undefined,不要猜 —— 「用错 mode」与「载荷损坏」现象完全一样)。

运行 / 解密(浏览器)

const { ctx, error } = useAgentRunner({
  agentId,
  encryptionKeyDerivation: { mode: 'personal-sign-hkdf' },   // 显式 opt-in(v1 / v2 / v3 三选一)
})

非 React 场景:自行实现 WalletSigner.deriveEncryptionKey()(返回派生私钥);或由宿主完全接管密钥来源 (网关下发 / 会话密钥 / HSM 等),优先级最高的两种注入方式:

const runner = new AgentRunner({
  reader,
  wallet,                       // deriveEncryptionKey() / getPrivateKey() 二选一,见 WalletSigner
  // ① 完全自定义解包
  unpack: async (encryptedPayload, eciesEncryptedKey) => {
    const aesKeyHex = await myKeyService.resolve(agentId)
    return decryptPayload(encryptedPayload, aesKeyHex)
  },
})
// ② 或直接给已解出的 AES key(跳过 ECIES)
new AgentRunner({ reader, wallet, aesKeyHex: sessionAesKey })

密钥来源优先级:config.unpack → config.aesKeyHex → wallet.deriveEncryptionKey() → wallet.getPrivateKey()。

托管密钥:不经钱包派生(服务端生成密钥对)—— 受支持的一等用法

「密钥对由服务端生成、不经任何钱包签名」是受支持的发布形态,不是 hack:publishAgent({ publicKey }) 的公钥本来就是入参, generateKeyPair() 也是公开 API。服务化 Agent(咨询走自有后端)建议优先用这条路径 —— 它没有「可复用签名」问题 (根本没有签名),也不受钱包确定性、盲签提示、personal_sign 文案复用的影响。

它同时规避了 v3 的全部兼容性问题:密钥材料不进浏览器 ⇒ 移动端钱包内置浏览器无 WebAuthn、跨设备 / 混合传输(QR) 的 PRF 一致性、域名迁移全都不涉及。代价见下方「安全建议」表(尤以不可迁移 / 不可轮换为重)与 §5.12 开头的「选型」矩阵。

import { generateKeyPair, publishAgent, AgentRegistry } from '@actora/sdk'

// ① 服务端生成密钥对(私钥只留在你的 KMS / HSM,永不进浏览器)
const { privateKey, publicKey } = generateKeyPair()

// ② 用公钥发布;顺便做铸造前自检 —— 私钥能解开才算通过
const result = await publishAgent({
  agent, publicKey, uploader,
  verifyWithPrivateKey: privateKey,        // 0.19.0+:不通过就不上传、不铸造
})

// ③ 铸造并记录派生 mode(第三方 runner 据此选解包方式)
const { agentId } = await registry.register(result.encryptedCid, {
  eciesEncryptedKey: result.eciesEncryptedKeyHex,
})
await registry.setPayloadKeyMode(agentId, 'personal-sign-hkdf')   // 或 'eip712-hkdf';v3 用 'webauthn-prf-hkdf'(另需 setPayloadKeyCredential)

运行侧(只在这两类位置持有密钥,不要下发到浏览器):

// A. 后端解包后自行使用(推荐):完全自定义解包
new AgentRunner({
  reader, wallet,
  unpack: async (encryptedPayload, eciesEncryptedKey) => {
    const aesKeyHex = await kms.unwrapAgentKey(agentId)   // 你的 KMS / HSM
    return decryptPayload(encryptedPayload, aesKeyHex)
  },
})
// B. 让 SDK 用托管私钥做 ECIES 解包(私钥需从安全存储短期取出)
new AgentRunner({ reader, wallet, aesKeyHex: await kms.getAgentAesKey(agentId) })

安全建议

项建议
存放私钥进 KMS / HSM / 云 KMS(unpack 路径下私钥无需离开 KMS);绝不写进前端、CDN、仓库或日志;.env 只放引用 ID,不放密钥本体
轮换每个档案一把密钥(generateKeyPair() 每次调用都新生成),不要跨档案复用;单个档案的密钥不能轮换 —— 见下一条
审计记录「谁在何时用哪个 keyId 解包了哪个 agentId」;解包是敏感操作,建议限流 + 告警
⚠️ 不可迁移tokenURI / 链上属性铸造后不可改,而 payload 的 AES key 是用发布时的公钥包裹的 —— 所以这把私钥丢了就是载荷永久解不开,且不能换(不能像钱包派生那样「换个设备重新派生」)。请把「密钥备份与恢复」当成上线前的硬性检查项
与签名派生对比托管密钥:无签名可复用、可服务端审计,但中心化托管(你的运维即信任边界);签名派生:非托管、用户自持,但有上述固有边界。按业务选择,SDK 两条都支持

参考实现(Actora 平台自带):平台侧已按本节形态落地一条托管链路,可直接对照 —— 网关 POST /api/v1/agents/payload-keys 为每个 Agent 生成一把独立密钥对(keyId + 公钥返回, 私钥用环境主密钥 PAYLOAD_KEY_KEK 经 AES-256-GCM 封装后落库,明文不回传、不上链), 前端用公钥 eciesEncrypt(aesKey, publicKey) 包裹后把 eciesEncryptedKey 与 payload_key_id 写进链上属性(不需要第二笔交易 —— agentId → keyId → 公钥 的绑定随铸造元数据一并上链), 作者可随时 POST /api/v1/agents/payload-keys/:keyId/export 可重复导出私钥(带 export_count / last_exported_at 审计)。落库表见 gateway/db/migrations/039_agent_payload_keys.sql。 注意:该链路用 payload_key_mode = 'hosted' 标注,不属于 v1/v2/v3 三个 DerivationMode (readPayloadKeyMode() 对未知值返回 undefined,第三方 runner 见此值应改用「调用平台/导出私钥解包」而非派生)。

读取闭环(用托管私钥把 Agent 跑起来):发布方(owner)在 payload_key_mode === 'hosted' 时按下面的路径取回私钥并解包:

import { unpackAgent, eciesDecrypt } from '@actora/sdk/core'
import { AgentRunner } from '@actora/sdk'

// 1) 链上取密文引用(eciesEncryptedKey 是用托管公钥包裹的 AES key)
const attrs = await reader.getAttributes(agentId)
//   → { encryptedPayloadCid, eciesEncryptedKey, payload_key_mode, payload_key_id }
const encryptedPayload = await ipfsFetcher.fetchEncryptedPayload(attrs.encryptedPayloadCid)

// 2) owner 向平台按 agentId 取回私钥(可重复导出 + 审计;需登录该 Agent 的钱包)
const res = await fetch(
  `${GATEWAY}/api/v1/agents/payload-keys/by-agent/${agentId}/export?chain=bsc`,
  { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` } },
)
const { key } = await res.json()   // { privateKey, keyId, publicKey, exportCount, ... }

// 3a) 一步解包得到 { prompt, skills, mcp }
const privatePayload = unpackAgent(encryptedPayload, attrs.eciesEncryptedKey, key.privateKey)

// 3b) 或把 AES key 交给 Runner(私钥只在内存短暂存在,不落盘)
const runner = new AgentRunner({
  reader, wallet,
  aesKeyHex: eciesDecrypt(attrs.eciesEncryptedKey, key.privateKey),
})

网关不可用时,Studio 会本地兜底生成密钥对(payload_key_mode = 'local',平台不托管)并在发布成功后 立即下载私钥;此时改用 unpack / aesKeyHex 传入该本地私钥即可(by-agent 导出对它是 404)。

订阅门控解密(2026-10-23,P5.1):订阅者或 owner 也可以不导出私钥,直接由网关解密并返回明文 —— POST /api/v1/agents/payload-keys/by-agent/:agentId/decrypt(body 可选 { chain };B 端可带 X-End-User-Id 代理终端用户)。网关依次校验「owner 或有效订阅」→ 读链上 encryptedPayloadCid / eciesEncryptedKey / payload_key_id → 取回 IPFS 密文 → KEK 解封托管私钥 → 解出 { prompt, skills, mcp }。 失败语义:未订阅 403 subscription_required;未绑定托管密钥 / 缺少密文引用 409;密钥不匹配 422 decrypt_failed。响应只含明文 payload 与 keyId,私钥与 AES key 绝不返回 (实现见 gateway/src/services/agent-payload-access.ts)。信任边界仍是平台 KEK 托管。

前置条件与失败语义

项说明
确定性签名必须满足 RFC 6979(MetaMask / Ledger / Trezor 满足;部分 MPC / 托管钱包不满足),v1 的 personal_sign 与 v2 的 eth_signTypedData_v4 同样适用。非确定性钱包一旦发布,Agent 就再也解不开(静默数据损坏),故 deriveKeyPairFromWallet() 自 0.19.0 起默认开启 assertDeterministic(多一次签名提示);确认签名为确定性时可显式关掉。v3 不涉及签名,此约束不适用
域隔离派生载荷只在 Actora 使用:任何能让同一地址对同一载荷签名的场景都等价于拿到内容密钥;换地址即失效。v1 是自由文本(任何站点都能复现该提示词);v2 的 EIP-712 domain + types 让钱包渲染结构化、带域分隔符的请求,比盲签可辨;v3 由浏览器按 RP ID 强制隔离(换域即换密钥空间,无需依赖文案)
⚠️ 签名路径的固有边界(v1 与 v2 同样适用)只要 IKM 来自签名,「公开 / 无时效 / 可复用」就是必然代价:文案与 typed data 都是公开常量,钓鱼站让用户签同一份载荷即可派生出其 payload 私钥,配合公开的 encryptedPayloadCid 解开私有载荷。这不是 v1 的 bug,换编码消不掉 —— 加时间戳 / nonce 会直接让密钥不可复原(载荷永久解不开),所以「抗重放」与「可复原」在这条路径上互斥。在 typed data 里加「来源域名」字符串也不构成密码学绑定:字段值公开可复现、当前主流钱包对离线 typed data 不做 origin 校验(origin 仅用于 UI 展示)。真正做到来源绑定需要非签名密钥来源:托管密钥或 WebAuthn PRF(v3)
密钥来源(除签名外)① 托管密钥(服务端生成密钥对,见上一节)—— 无「可复用签名」问题,因为根本没有签名;② WebAuthn PRF(v3) —— 非签名、由浏览器按 RP ID 绑定,见 v3 一节;③ 签名派生(v1 / v2)
错误码钱包不提供任何密钥来源 → WALLET_NOT_CONNECTED(不再被包成 DECRYPTION_FAILED);密钥不对/密文损坏 → DECRYPTION_FAILED,原始错误见 error.cause;v3 的 PRF 失败 → PrfError(error.code 见 v3 一节,不回退到签名派生)
订阅者能否解密不能。payload 的 AES key 由发布时的公钥包裹,只有持有对应私钥的发布方解得开。订阅者侧要拿到明文 prompt/skills,需走托管对话,或由发布方经 MCP 下发

隐私边界:托管对话读的是什么

托管对话(ConversationClient,agentId 模式)由 Conversation Service 组装上下文,其 system prompt 取公开 metadata 的 description(回退 name),skills 取 metadata_json.skills —— 不会读取加密 payload (见 conversation-service agent-context-loader)。

metadata_json 的来源需要注意:它由网关索引器写入,基底是 tokenURI 解析出的 JSON(解析器 parseTokenURIJSON() 只认 data:application/json;base64,<base64>,ipfs:// 与普通 https URL 一律返回 null), 再叠加链上 skills 属性。

托管对话各字段的真实来源与优先级

托管对话要用的字段来源(前者优先)说明
system promptdescription → name索引侧:tokenURI JSON 的 description → 链上 description 属性兜底;都空则用 Agent <id>。只取这两者,不读链上 prompt 属性
工具列表(skills)metadata_json.skills → metadata_json.attributes.skillsfirst hit wins,两者不合并。顶层 skills 来自 tokenURI JSON;attributes.skills 来自链上 skills 属性(SkillDef[] 形态)
市场展示(skills 列)tokenURI JSON 的 skills → 链上 skills 属性(string[] 形态)只喂列表页,不参与工具构建

tokenURI 指向密文(ipfs://<encryptedCid>)既不会报错、也不会 404:parseTokenURIJSON() 只是解析不到 ⇒ metadata_json 基底为空,此时 name / description / skills 全部回落到链上属性。 所以「tokenURI 指密文 + 明文 metadata 走链上属性」是被正式支持的形态(Studio 发布流程正是如此)。

若想让 tokenURI 改指公开 CID:生产 IdentityRegistry 的对外方法只有 register / registerWithMetadata / updateMetadata / getCurrentAgentId / agentExists / getAgentOwner / getAgentsByOwner / getAgentMetadata / totalAgents —— 没有 updateTokenURI / setTokenURI ⇒ tokenURI 只在铸造时写入、之后不可改,要换只能重新铸造(换新 agentId)。 因此不推荐为了读 metadata 去重铸:把 name / description 当链上属性补写即可, 与 skills 同一笔 updateMetadata 一起写(省 gas,也不用动 tokenURI)。

可执行 skills 的公开约定如下:

发布方写什么落到哪里谁消费
链上属性 skills = JSON SkillDef[]([{ name, description, inputSchema, execution? }])metadata_json.attributes.skills托管对话 → 可执行工具(execution.type='mcp' / 'http' 会真实调用)
链上属性 skills = JSON string[](技能名列表)skills 列(tokenURI JSON 里已有 skills 时不覆盖)市场展示
tokenURI 为 base64 data URI JSON 且含 skillsmetadata_json.skills托管对话(优先于 attributes.skills,两者不合并)
  • 托管对话取值顺序:metadata_json.skills → metadata_json.attributes.skills;只有带非空 name 的对象才算技能 定义,纯字符串数组会被跳过(不会再因 inputSchema 缺失而在建工具时抛错);
  • ⚠️ 链上 skills 属性值必须是合法 JSON 数组,且整体必须是同一种形态:要么全是带非空 name 的对象 (SkillDef[]),要么全是非空字符串(string[])。混入一个字符串、或有一个对象缺 name,整组会被判为无效并 静默丢弃(不会报错,表现为「工具一个都不出现」);
  • 链上属性无长度/条数限制,但每条是一次 SSTORE ⇒ skills 序列化到数 KB 会显著抬高 gas;建议只公开托管对话真正 需要的那几条(例如单个 MCP/HTTP 入口),展示用的名字列表可另走 string[] 形态;
  • 存量 Agent 补写属性:链上属性只能整组覆盖写入 —— 所有者调 IdentityRegistry.updateMetadata(agentId, [{ key, value }…])(合约没有 setMetadata(agentId,key,bytes) 单键方法; 实现里会先 delete _agentMetadataKeys[agentId] 再写入传入数组),因此必须把原有 name / description / category / pricing_type / … 全部一起重传,只传 skills 会抹掉其余属性。value 为 bytes,写入时用 stringToHex(text) 编码; SDK 0.17.0+ 已封装:registry.setAttributes(agentId, { skills: [...] }) 自动读回现有属性、合并后整组写回 (未提及的键保持原值);需要删键时用 registry.overwriteAttributes(agentId, attrs[])(整组覆盖,须自行带上全部属性)。 写入后等网关 fallback 全量同步(AGENTS_SYNC_INTERVAL_SEC,默认 120s;或直接 POST /api/v1/agents-sync), 再等 Conversation Service 上下文缓存 TTL(CONTEXT_CACHE_TTL_SEC,默认 300s)过期。 注意:网关未监听 MetadataUpdated 事件(只监听 AgentRegistered / Transfer)⇒ 属性变更只能靠上述轮询或手动触发;
  • 多链(0.17.0 起):托管对话的 agentId 模式已支持 BSC / Ethereum / Base 全链。调用方不需要、 也无法声明链(详见 §4「托管对话:链由平台解析」):网关从 agents 表列出该 agentId 存在的候选链 (按平台内部固定的链优先序:内部测试链 → 中心化 → BSC → Ethereum → Base 排序),逐链判定调用方是否拥有 / 已订阅,取第一条放行的链, 并把它一路下传到 Conversation Service(SSE 单轮与后台任务两条路径都下传,后台任务在 chat_tasks.chain 落库)。 因此同 ID 跨链冲突(各链独立编号,实测 BSC 与 Ethereum 都存在 id=2)不会被取错:只要你在其中一条链上有权, 系统就会命中那条链。只读的 GET /api/v1/agents/:id 支持 ?chain= 精确指定;不带时按上述优先序跨链解析 (历史行为:只命中内部测试链 / 中心化,BSC / Base 会 404 —— 已修复)。

skills[].execution:可选,托管对话支持的类型与调用约定

execution 是可选字段。不写不影响工具被列出——工具列表只判断「是否为带非空 name 的对象」, execution 完全不参与过滤;但调用时固定返回一个 error 对象(不抛异常),用于「先让工具出现在列表里」:

{ "error": "Skill \"<name>\" has no remote execution configured. Add execution.type=\"mcp\" with an endpoint." }

⚠️ 三条路径支持的写法不完全相同:

  • 托管对话(agentId 模式):读链上公开属性,execution.type 支持 mcp / http / a2a(下表);
  • inline 注入(ConversationClient 传 prompt + skills):走 SDK 的 ConversationSkillDef, 同样已声明 mcp / http / a2a(有完整类型提示);
  • SDK 加密发布类型(Skill.execution,写入加密 payload、由本地 AgentRunner 解密执行): 只声明 mcp / a2a。注意 open 不是 execution.type 的取值,它是「未配置 execution 时本地执行器的默认模式」。
execution.type字段平台(托管对话)侧行为
mcpendpoint?(缺省 <gateway>/mcp)、toolName?(缺省技能名)真实发起 JSON-RPC 2.0:POST <endpoint>,body {"jsonrpc":"2.0","id":<ts>,"method":"tools/call","params":{"name":"<toolName>","arguments":<入参>}},取 result.content[0].text
httpendpoint(必填)真实发起 POST <endpoint>,Content-Type: application/json,body = 模型按 inputSchema 产出的入参 JSON 原样,响应按 JSON 解析后作为工具结果
a2atargetAgentId?、skillFilter?、promptOverride?不发起 HTTP:返回 { type: 'a2a_delegation', targetAgentId, skillFilter, promptOverride, callerInput } 作为工具结果,由上层决定是否走 A2A
其他 / 不写 execution—进工具列表;调用返回上面的 error 对象

http 类型的边界(当前实现实测):

  • 没有入参映射、没有包装:body 就是模型给出的工具参数。要让 body 对上你 endpoint 的形状,请把 inputSchema.properties 直接写成你 endpoint 期望的字段(例:{ question, history })——模型会按 schema 产出参数;
  • 不注入任何鉴权头(只有 Content-Type: application/json):无签名 / JWT / 共享密钥,平台不代理你的鉴权, 多轮长期接口请自备限流(平台侧无额外白名单机制,也无需登记密钥)。若要鉴权,只能把凭证放进 endpoint URL, 或改用 execution.type='mcp' 让你的 MCP server 自行校验调用者;
  • 方法固定 POST,无 method / headers / 重试 / 超时的配置项;
  • 超时 30 秒(硬编码 AbortSignal.timeout(30_000),mcp 相同);
  • 失败语义:HTTP 非 2xx ⇒ 抛 HTTP skill "<name>" failed: HTTP <status>;超时 / 网络错误 ⇒ 底层 fetch 抛错。 错误作为工具结果回给模型,不会中断整轮对话;
  • 响应约定:无强制结构,返回的 JSON 原样交给模型。建议返回对象而非超长文本。

因此:

  • 「密文存放于 IPFS、平台与 B 端都无法解密」成立,平台侧没有任何 Agent 内容解密实现;
  • 但不要把敏感内容写进 description:它以明文入库,并会作为托管对话的 system prompt 被使用;
  • 真 prompt/skills 要参与托管对话有三条路:① 公开链上 skills 属性(推荐,人格与细节仍留在私有载荷里); ② inline 模式(调用方自备 prompt/skills);③ 自托管 Runner(持派生密钥那一侧);
  • ConversationClient.getSessionKeyInfo() 指向的 A2A 代付会话密钥已随 AA 下线(2026-11-08) 删除,与内容解密无关;按次付费现走 x402 直付。

6. 中心化(Central)vs 链上(On-chain)市场对比

维度链上(On-chain)中心化(Central)
Agent 载体链上 NFT(AgentRegistry 铸造,正 ID)网关 DB 记录(source='central',负 ID)
订阅计划SubscriptionManager.createPlan(链上合约)CentralAgentClient.createPlan(central_plans 表)
付费subscribe(原生币/ERC20 直付开发者,无平台托管)SubscriptionPayments.pay({ method: 'x402', priceWei });法币走发布者收款页
鉴权判定链上 hasActiveSubscription网关 checkSubscription(fiat_subscriptions)
适用场景需要公开可验证、用户自持钱包、跨平台互操作平台自己建 Agent、快速上架、无需用户钱包的 B 端

流程差异:链上 Agent 的用户必须「铸造者/订阅者」都有钱包且在同一链;中心化 Agent 的订阅方可用 x402(无需钱包私钥以外的配置),或由发布者的收款页处理法币,付款后回调网关开通。


7. B 端常见问题(FAQ)

鉴权方式对照表

场景头适用客户端
租户 API Key(推荐 B 端)X-Api-Key: actora_...ConversationClient / CentralAgentClient(发布管理)
钱包签名 JWTAuthorization: Bearer <jwt>同上(accessToken 字段)
钱包/私钥(链上交易)无头,viem WalletClient 签名AgentRegistry / SubscriptionManager / A2AProtocol

ConversationClient 必须二选一提供 apiKey 或 accessToken,否则构造后请求即抛错。

B 端开户后为什么调不通平台 LLM?

partner 租户注册时不带套餐(quota_daily=0),平台模型列表为空。两条路:先配 BYOK(POST /api/v1/tenant/keys,不计额度),或购买平台档位(GET /api/v1/tenant/plans → POST /api/v1/payments,purpose='tenant-plan')。详见 §3.a 与 §8。

一个租户如何服务多个终端用户?

服务端调用时带 X-End-User-Id: 0x<用户钱包或业务 ID>,网关会以该地址作为计费 / 订阅判定的 subject;若传 0x 钱包地址,还会额外做订阅代理鉴权(该钱包的订阅决定访问权)。见 §8.1 / §8.6。

archive RPC 的坑

  • eth_call(读合约)与事件订阅用 publicnode 免费 RPC 即可;
  • getTransactionReceipt(历史交易收据 / archive 查询)在 publicnode 返回 403;
  • 换用:BSC → https://1rpc.io/bnb;Base → https://mainnet.base.org。

nonce 缓存 / 交易重试的坑

  • viem WalletClient 默认本地缓存 nonce,并发或失败重试时可能复用已用 nonce 导致 nonce too low;
  • 串行执行交易并等待 waitForTransactionReceipt;失败重试前调用 publicClient.reset() 或在 sendTransaction/writeContract 显式传 nonce: await publicClient.getTransactionCount({ address })。

Agent 发在 BSC / Ethereum / Base,托管对话会 404 吗?需要我传链吗?

不需要传链,也不会再 404(0.17.0 起)。平台侧会列出该 agentId 存在的候选链,逐链判定你在哪条链上有权, 取第一条放行的链并全链路沿用(详见 §4「托管对话:链由平台解析」)。

注意两条限制与易混点:

  • 托管对话请求里没有 chain 参数(chat / stream / createTask 都一样)——链是平台解析的,不是调用方声明的。 只有只读的 GET /api/v1/agents/:id?chain= 可以显式指定链;
  • 链上操作仍然必须明确链:AgentRegistry / SubscriptionManager / A2AProtocol 都是「哪条链的合约就读哪条链」, 而 agentId 是链内编号(各链独立,实测 BSC 与 Ethereum 都有 id=2 且是不同 Agent)⇒ 换链 = 换 ID,需重新铸造 / 订阅。

托管对话里 Agent 的工具(skills)是空的?

托管对话不读加密 payload,技能集取自公开 metadata:metadata_json.skills → metadata_json.attributes.skills (见 §5.12「隐私边界」)。链上没写公开 skills 属性时,工具列表必然为空 —— 这是发布方动作,不是调用方的 bug。

发布方用 registry.setAttributes(agentId, { skills: [...] }) 补写(SDK 0.17.0+,见 §5.1),随后等网关同步 (AGENTS_SYNC_INTERVAL_SEC 默认 120s,或手动 POST /api/v1/agents-sync)+ Conversation Service 上下文缓存 TTL(CONTEXT_CACHE_TTL_SEC 默认 300s)过期。

两个最常见的「工具不出现」原因:

  • 链上 skills 属性值不是合法 JSON 数组,或混了形态(既有对象又有字符串 / 有对象缺 name)⇒ 整组被静默丢弃;
  • 写成了 string[](技能名列表)——那只会进市场展示的 skills 列,不会变成可执行工具。

托管对话的 system prompt(人格)从哪儿取?需要把 tokenURI 改成公开 CID 吗?

不需要动 tokenURI。 system prompt 由索引器按 description → name 组装(见 §5.12「各字段的真实来源与优先级」):

  • tokenURI 解析出的 JSON 里的 description 优先,链上 description 属性兜底;
  • 都为空时退到 name(再空则 Agent <id>);不会读链上 prompt 属性(想公开人格就写 description)。

tokenURI 指向密文(ipfs://<encryptedCid>)是受支持的形态,不会报错。且生产 IdentityRegistry 没有 updateTokenURI / setTokenURI ⇒ tokenURI 铸造后不可改,换内容只能重新铸造(换新 agentId)。 因此推荐做法:不要重铸,直接把 name / description / skills 一起用 setAttributes 写成链上公开属性—— 与 skills 同一笔 updateMetadata,省 gas。

其他

  • agentId 是链内 ID:各链独立编号(同一个数字在多条链可能是不同的 Agent)⇒ 链上操作(铸造 / 订阅 / 退款 / A2A) 必须明确链,换链 = 换 ID,需重新铸造 / 订阅; 托管对话是例外:agentId 模式不用声明链,平台按「你在哪条链有权」自动解析(§4);
  • SubscriptionPayments.hasAccess 需要 gatewayUrl;method: 'chain' 需要 config 里的 subscriptionManager;
  • 中心化 Agent 的计划价格是 priceWei 字符串,传给 pay({ priceWei }) 时保持字符串避免精度丢失;
  • AgentRunner.useAgent 需要一条可用的解密密钥来源:config.unpack / config.aesKeyHex / wallet.deriveEncryptionKey() / wallet.getPrivateKey()。 EIP-1193 注入式钱包无法导出账户私钥,因此浏览器里必须用派生密钥路径(useAgentRunner 的 encryptionKeyDerivation opt-in,或自行实现 deriveEncryptionKey)——详见 §5.12;
  • 密钥来源全部缺失时抛 WALLET_NOT_CONNECTED;密钥不匹配时抛 DECRYPTION_FAILED,原始错误保留在 error.cause 中。

8. 网关 REST API 参考(B 端接入面)

SDK 是网关 REST API 的封装;需要自行实现 HTTP 客户端(Go / Java / Python 等)时,按本节对接即可。 网关地址:https://actora.codes(生产)。平台侧运维视角的完整端点清单见仓库根目录 DEPLOYMENT.md §13。

8.1 鉴权头

头取值适用
X-Api-Keyactora_<32hex>B 端主用(租户级)。服务端存 SHA-256 摘要,旧租户回退明文校验
AuthorizationBearer <jwt>钱包签名登录(POST /api/v1/auth/verify)签发的 JWT,与 X-Api-Key 二选一
X-End-User-Id0x... 或业务用户 ID代终端用户调用:计费 / 订阅判定以该地址为 subject;0x 地址还会触发订阅代理鉴权
X-Admin-Key<ADMIN_KEY>仅平台运维(/api/v1/admin/*),B 端不需要

X-Api-Key 缺失时请求会回落到 JWT 校验;两者都无 → 401。 MCP 端点(/mcp、/mcp/agent/:id)只接受注册用户 JWT,不接受 API Key,B 端需自行代理。

BYOK(自带模型 Key)另用三个头逐请求覆盖:X-LLM-Api-Key / X-LLM-Endpoint / X-LLM-Model(ConversationClient 的 llmApiKey / llmEndpoint / llmModel 即写这三个头),此类调用 llmSource='byok',不计平台额度。

8.2 认证与租户

方法路径说明
GET/api/v1/auth/challenge?address=0x...取登录挑战(nonce + 时间戳,5 分钟有效)
POST/api/v1/auth/verify提交签名换 JWT;带 intent:'partner' 时注册 B 端租户并一次性返回 api_key
GET/api/v1/auth/api-key取回明文 Key(仅旧租户可用;摘要化后的 Key 只在签发/轮换时返回)
GET/api/v1/tenant/me档位、quota_daily / quota_used、rate_limit_rpm / max_concurrent / parallel_tasks、usage_today
GET/api/v1/tenant/usage?days=30用量汇总(按 key_source=platform|byok)+ 日粒度 timeline(days 1–365)
GET/api/v1/tenant/plans可购买平台档位(含 price_wei,由 price_monthly USD 折算)
POST/api/v1/tenant/rotate-key轮换 Key,旧 Key 立即失效,新 Key 仅返回一次
GET/POST/DELETE/api/v1/tenant/keys[/:keyId]BYOK Key 增删查(api_key 密文入库)
POST/api/v1/tenant/keys/:keyId/validateBYOK Key 探活(请求 <endpoint>/models)
GET/api/v1/models可用模型:platform(按档位过滤,需平台 Key 存在)+ tenant_owned(BYOK)

8.3 市场与订阅

方法路径说明
GET/api/v1/agents市场列表,支持 ?chain=bsc|ethereum|base、分页 / 标签过滤
GET/api/v1/agents/:id详情。缺省跨链解析该 ID 所属链(平台内部链优先序:内部测试链 → 中心化 → BSC → Ethereum → Base);?chain= 可精确指定
GET/api/v1/agents/check-subscription校验订阅态(subscriber + agentId,可选 chain)
GET/api/v1/chain/health?chain=bsc链索引健康(status: ok | degraded | pending)
GET/api/v1/chain/plans/:planId · /chain/total · /chain/agents实时链上读(SDK 直读合约)
GET/api/v1/agents/central中心化 Agent 管理(需鉴权;CentralAgentClient 对应)

8.4 支付与计费

方法路径说明
POST/api/v1/payments统一下单。purpose = agent-subscription(默认)/ tenant-plan / a2a;method = chain / x402(fiat 仅 tenant-plan;Agent 订阅法币走发布者收款页)
POST/api/v1/payments/verify校验链上交易并记录回执({ txHash, chain };按 txHash 幂等,不再累加余额)
GET/api/v1/payments/access跨轨道访问检查(链上订阅 / x402 回执 / fiat 订阅)
GET/api/v1/payments/info · /quotex402 发现信息(priceWei / payTo / network)与报价
POST/api/v1/payments/a2a · /a2a/settleA2A 按次付费两段式(create → settle)
POST/api/v1/payments/period/authorize · /charge · GET /period/authorization周期授权扣款(P4)
POST/api/v1/payments/mpp/open · /voucher · /topup · /settle · /close · GET /mpp/sessionMPP 通道(按量结算)
GET/api/v1/x402/info · /paywallx402 支付发现(/balance 已随预充值账本下线)

/api/v1/spend-policy/session* 已于 2026-11-08(AA 下线) 删除:平台不再代持 session key、 不再代扣;按次付费统一走 x402 直付(见 POST /api/v1/x402/* 与 payments/*)。

purpose 与关键字段:

// 1) 购买平台档位(partner 开通平台 LLM)
POST /api/v1/payments
{ "method": "chain", "purpose": "tenant-plan", "tenantPlanId": "<plans.id>",
  "subscriber": "0xWallet", "chain": "bsc", "txHash": "0x..." }

// 2) 购买 token 加油包(超出日额度后自动消耗,先到期先用)
{ "method": "chain", "purpose": "token-pack", "packId": "<pack id>",
  "subscriber": "0xWallet", "chain": "bsc", "txHash": "0x..." }

// 3) 订阅链上 Agent(原生币 / ERC20)
{ "method": "chain", "purpose": "agent-subscription",
  "agentId": 1, "planId": 2, "subscriber": "0xWallet", "chain": "base", "txHash": "0x..." }

8.5 托管对话与任务

方法路径说明
POST/api/v1/agent/runs执行 Agent(SSE 流式返回;ConversationClient.chat/stream 对应)
POST/api/v1/chat/completions平台 / BYOK LLM 直连对话(计费入口,OpenAI 兼容请求体)
POST/api/v1/sessions · GET /api/v1/sessions/:id会话管理
POST/api/v1/tasks · GET /api/v1/tasks/:id · POST /api/v1/tasks/:id/cancel异步任务(parallel_tasks 关闭时返回 403 PARALLEL_TASKS_DISABLED)
POST/api/v1/a2a/tasks/:id/retry · GET /a2a/task-result/:taskIdA2A 委派任务重试 / 取结果(/resume 与 session-key/info 已于 2026-11-08 AA 下线时删除)

链不用你传:POST /api/v1/agent/runs 与 POST /api/v1/sessions/:id/tasks 都不接受调用方指定链, 平台自行按上文的多链解析判定(列出该 agentId 的候选链 → 取你在其中有权的那条),并把该链下传给执行侧 取 Agent 上下文;后台任务会把判权链落库(chat_tasks.chain)以保证后台执行时刻用同一条链。 只读的 GET /api/v1/agents/:id 支持 ?chain= 显式指定链(用于展示 / 调试)。

8.6 计费与配额语义

  • 两条 Key 来源:platform(平台 LLM,按 quota_daily 扣 token,超出后消耗 token 包)/ byok(自带 Key,不计费);用量日志按 key_source 分组。
  • 额度耗尽:返回 402(QUOTA_EXHAUSTED)。按次代付(awaiting_payment + session 授权)已于 2026-11-08(AA 下线) 移除:按次付费改走 x402 直付给开发者 payout_address,或先订阅该 Agent。
  • 限流:默认 partner 5 rpm / 并发 1;购买档位后按档位生效。超限返回 429。
  • X-End-User-Id:同一把租户 API Key 可服务多个终端用户,计费与订阅判定按该地址归属,是 B 端「一次接入多用户」的标准做法。

8.7 常见错误码

状态含义处理建议
401Key / JWT 缺失或失效检查 X-Api-Key;若被轮换需换新 Key
402额度耗尽 / 订阅过期 / 按次未付款购档位或加油包;按次调用按 402 载荷里的 payTo / token / amount 完成 x402 直付后重试
403租户被 suspend / 能力未开通;或对目标 Agent 无归属 / 无有效订阅(AGENT_ACCESS_DENIED)查 GET /tenant/me 的 status;PARALLEL_TASKS_DISABLED 时降级为 chat();AGENT_ACCESS_DENIED 则确认该钱包在 Agent 所在链上确实拥有 / 已订阅(多链下无需自己判断是哪条链,平台会逐链判定)
404路径或资源不存在(含 Key 摘要不可读回);GET /agents/:id 时表示该 ID 在你指定的链上不存在(?chain= 指定时),或不带 chain 时所有候选链都没有该 ID核对端点;Key 丢失只能轮换;Agent 详情可先不带 ?chain= 让平台跨链解析
429触发限流指数退避重试
503链未开通 / 索引不可用先调 GET /api/v1/chain/health?chain=,pending 表示该链未部署

8.8 B 端上线检查清单

  1. POST /api/v1/auth/verify(intent:'partner')建租户,立即落库 api_key;
  2. GET /api/v1/tenant/me 确认档位 / 限流;未购档位则先配 BYOK(POST /api/v1/tenant/keys)跑通链路;
  3. 需要平台 LLM → 购档位(GET /tenant/plans + POST /api/v1/payments,purpose='tenant-plan');
  4. 需要链上 Agent → 选链(GET /api/v1/chain/health 探活)→ 订阅(SubscriptionManager / SubscriptionPayments);
  5. 服务端调用一律带 X-Api-Key + X-End-User-Id,并实现 401/402/429/503 的重试与降级;
  6. 监控 GET /api/v1/tenant/usage,避免线上断供;GET /api/v1/billing/balance 已下线;
  7. 托管对话不要传链(也没有该参数):传 agentId 即可,BSC / Ethereum / Base 的 Agent 由平台自动解析 (§4);若收到 403 AGENT_ACCESS_DENIED,先确认该钱包在 Agent 所在链上确实拥有 / 已订阅;
  8. 若需 Agent 的可执行工具生效,确认发布方已用 registry.setAttributes(agentId, { skills: [...] }) 补写公开 skills 属性(§5.12 / FAQ),否则技能集恒为空。

9. 版本记录

版本要点
0.12.0+BSC 多链:KNOWN_CHAINS 增加 chainId 56(BSC 主网,Agent 市场开通),链上模块全链通用
0.13.0ETH / Base 预留:KNOWN_CHAINS 增加 1 / 8453 占位条目(合约未部署)
0.14.0ETH 开通:Ethereum 主网合约部署(SubscriptionManager block 25992521)
0.15.0Base 开通:Base 主网合约部署(SubscriptionManager block 51401326)
0.15.1Base 地址修正:按 broadcast/DeployBase.s.sol/8453/run-latest.json 的 contractName 映射修正 Base 合约地址(SubscriptionManager 修正为 0x0f129e...)
0.15.2B 端接入文档/样例:README 彻底重写(多链市场、接入全流程、模块参考、FAQ);examples/ 随包发布(mint-and-subscribe / conversation-bend / central-agent-bend)
0.15.3USDT/USDC 计价:BSC/ETH/Base 三链 SubscriptionManager 登记 USDT/USDC 白名单;README 补稳定币地址表、精度差异(BSC 18 / ETH·Base 6)与 ERC20 订阅要点;mint-and-subscribe 支持 PAY_TOKEN=usdt/usdc
0.16.0浏览器钱包(EIP-1193)发布/运行打通:derivePrivateKeyFromSignature / deriveKeyPairFromWallet(personal_sign + HKDF-SHA256)、useAgentRunner 的 encryptionKeyDerivation opt-in、AgentRunnerConfig.unpack?/aesKeyHex? 与 WalletSigner.deriveEncryptionKey?() 密钥注入;错误语义修正(WALLET_NOT_CONNECTED 不再降级为 DECRYPTION_FAILED,DECRYPTION_FAILED 携带 cause);README 新增 §5.12 与托管链路隐私边界说明
0.17.0链上属性写入助手:registry.setAttributes()(读回合并后整组写回,补写公开 skills 不必手写合约调用)/ overwriteAttributes() / toAttributes();配套托管对话多链修复(agentId 模式的 BSC / Ethereum / Base 404 与 403,见 §5.12)
0.17.1DX 修复:eciesDecrypt() / unpackAgent() / getPublicKey() 的 privateKey 接受可选 0x 前缀(此前抛底层库的模糊错误),非法格式时报 Invalid private key: expected 32-byte hex…;UPGRADE.md 加入 npm 包 files(此前包内缺失);修正 package-lock.json 残留旧包名 @agentxv2/sdk。无 API 破坏
0.18.0payload 密钥派生 v2(EIP-712):encryptionKeyDerivation: { mode: 'eip712-hkdf' } 与 deriveKeyPairFromWallet({ mode, signTypedData }),把「被签的东西」从自由文本换成 EIP-712 typed data(buildPayloadKeyTypedData + DEFAULT_EIP712_*),HKDF 构造与 IKM 不变、infoPrefix 版本化为 agent-payload-encryption/v2;v1(personal-sign-hkdf)保留且行为不变,旧档案继续可解
0.19.0密钥来源与发布护栏:① 托管密钥(服务端生成密钥对)写成受支持的一等用法(含安全建议 / 不可迁移性);② deriveKeyPairFromWallet() 默认开启 assertDeterministic(可关)并新增默认开启的 selfCheck;③ publishAgent({ verifyWithPrivateKey }) 铸造前 dry-run 解包校验(含 v1/v2 mode 用错);④ 新增跨实现的派生绑定字段 payload_key_mode(PAYLOAD_KEY_MODE_ATTRIBUTE / readPayloadKeyMode() / isDerivationMode() / AgentRegistry.setPayloadKeyMode());⑤ README 明确「签名路径固有边界」并收回 v2「来源可验证」的过度声明。若发布流程依赖单次签名提示,请显式传 assertDeterministic: false
0.20.0payload 密钥派生 v3(WebAuthn PRF,非签名):新增 mode: 'webauthn-prf-hkdf',IKM 改为 WebAuthn PRF 扩展输出(32 字节,由浏览器按 RP ID 绑定),HKDF 构造与 v1/v2 逐字节一致、infoPrefix 版本化为 agent-payload-encryption/v3;配套 getPrfCapability()(不弹窗探测)/ createPrfCredential() / evaluatePrf() / PrfError(PRF_UNSUPPORTED 等 4 码,不支持时报可操作错误、绝不静默回退到签名派生);派生入口新增 derivePrivateKeyFromIkm() 与 DEFAULT_PRF_SALT / DEFAULT_DERIVATION_INFO_PREFIX_V3;新增 credential 绑定字段 payload_key_credential(PAYLOAD_KEY_CREDENTIAL_ATTRIBUTE / serializePayloadKeyCredential() / readPayloadKeyCredential() / AgentRegistry.setPayloadKeyCredential())。v1/v2 保留且行为不变,默认值不变、v3 为 opt-in
0.20.1纯文档:§5.12 新增「选型:四条路径怎么选」矩阵(v1 / v2 / v3 / 私钥托管并列对照),并明确「Runner 跑在服务端 ⇒ 私钥托管可完全规避 WebAuthn/PRF 的兼容性问题」及其代价(信任边界转移、不可轮换、丢失即永久解不开、提供的是服务端鉴权而非客户端自持 + 域绑定)。无代码 / API / 行为变更
0.20.6包名与合约地址对齐(品牌退役):依赖 @0xinfrax/payments ^0.3.0 → @keelcodes/payments ^0.3.1(公开 API 逐字节一致,仅品牌 / 命名空间变更);PAYMENT_VERSION → 0.3.1;KNOWN_CHAINS 的 BSC / Ethereum ReputationRegistry 改为官方 canonical 0x8004BAa1…9b63(三链同址,legacy 仅剩 Sepolia);README 同步 SM / A2A / Reputation 地址表与部署起始块。无 API 破坏
0.23.0护照客户端 + 子路径补全:① 新增 @actora/sdk/passport(PassportClient / PassportApiError),覆盖 agent 护照、回执与验签、收款地址读写、Webhook 订阅、发布计划;需要鉴权的方法在无凭证时本地抛 UNAUTHORIZED 且不发请求。② 补齐 @actora/sdk/payment / /subscription / /a2a / /reputation / /proofs 五个子路径(此前只能用根入口,会把全部模块一起拉进来)。③ 声明 sideEffects: false,支持打包器 tree-shaking。所有子路径在根导出中同时可用。无 API 破坏

附录:可运行示例

见 examples/:

  • examples/mint-and-subscribe.mjs —— 链上完整流程(选链 → 铸造 → 建计划 → 订阅 → 验证)
  • examples/conversation-bend.mjs —— B 端托管对话(chat + stream,BYOK)
  • examples/central-agent-bend.mjs —— 中心化市场(list / create / createPlan)

版本与发布记录

@actora/sdk 共发布 28 个版本,首次发布于 2026/09/06;过去 12 个月发布了 28 个版本。

版本发布日期距今
0.25.0latest2026/11/08今天
0.24.12026/11/044 天前
0.24.02026/11/026 天前
0.23.22026/11/017 天前
0.23.12026/11/017 天前
0.23.02026/10/309 天前

关于 @actora/sdk 的常见问题

@actora/sdk 是什么?

Actora — Decentralized AI Agent SDK with ReAct AgentLoop + Multi-Tenant LLM Providers

如何安装 @actora/sdk?

在项目目录运行 npm install @actora/sdk,或使用 pnpm add / yarn add / bun add。

@actora/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@actora/sdk 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@actora/sdk 可以商用吗?

@actora/sdk 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@actora/sdk 的下载量有多少?

根据 npm 官方下载统计,@actora/sdk 在 2026/10/05 – 2026/11/03 这 30 天内被下载 1,778 次,日均约 59 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@actora/sdk 还在维护吗?

最新版本 0.25.0 发布于 2026/11/08(今天);至今共发布 28 个版本,其中过去 12 个月发布了 28 个;首次发布于 2026/09/06。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。