跳到主要内容

Agent 身份

给你的 AI Agent 一个符合 ERC-8004 标准、可被独立验证的链上身份。

一句话概括: 先创建 Agent 钱包,再调一次身份注册接口;几秒钟后链上会有一个任何人都能自行验证的 Agent ID。钱包与身份的链上绑定需在注册完成后另行请求。

本章面向要把自己的系统接入 BOT Chain Agent 身份服务的开发者。读完你应该能申请凭据、注册身份、请求钱包绑定、知道它什么时候就绪、维护它的描述信息,以及让第三方在不信任你的前提下验证它。

本章所有路径都相对于下面两个地址之一:

环境地址
BOT 主网https://identity-api.botchain.ai
Bohr 测试网https://identity-api.bohr.life

1. 概览​

问题回答
我能得到什么?一个链上 agentId,以及一份公开发布的 JSON 描述文件
怎么鉴权?请求头 X-API-Key
注册是同步的吗?不是。创建立刻返回 202,链上操作在后台进行
怎么知道完成了?轮询身份状态,或接收状态变化的 webhook
要多久?通常几秒,取决于出块时间
gas 谁出?我们出。你的系统不持有私钥,也不需要给账户充值
第三方能验证什么?owner、Agent 钱包、描述文件地址,全部直接从链上读
支持哪些链?BOT 主网(chain ID 677)和 Bohr 测试网(chain ID 968)

需要记住的一点:

你通过接口描述这个 Agent,链上记录它归谁所有、通过哪个钱包行动。 后面这部分,别人不需要问你就能自己查。

2. 获取凭据​

每个接入方是一个项目,有一个项目 ID 和一个或多个 API Key。Key 由我们签发, 目前没有自助开通流程。把你想要的项目名称告诉我们,我们会返回:

project_id  launchpad
key_id k_01J8XV…
api_key ak_live_k_01J8XV….<secret>

project_id 会在开通时一并告知,便于你存档;业务请求里不要、也不必再传 project_id。服务端用 API Key 反查租户,调用方无法通过改请求体把资源建到别人的项目下。

API Key 由一个点分隔成两部分:

ak_<environment>_k_<key_id>.<secret>

其中 <environment> 是签发环境标签(如 live 或 test),不是 project_id。项目归属写在服务端的 api_keys 表里,与 key_id 绑定。

把整串(含中间那个点)放在 X-API-Key 请求头里。secret 只在创建时返回一次, 之后无法再取回,请立即存进密钥管理系统。丢失只能重新签发并吊销旧的,没有找回途径。

Key 区分环境,测试网的 Key 在主网上不可用。

3. 注册身份​

请先创建钱包

身份是为某个 Agent 钱包注册的,所以 wallet_id 和 wallet_address 都是必填, 且必须来自一个已经存在的钱包。请先创建钱包再调用本接口,见 Agent 钱包。

一个 wallet_id 全局只能绑定一个身份;重复用于另一个身份会返回 409。

POST /v1/agents/identities
Content-Type: application/json
X-API-Key: ak_…
Idempotency-Key: <你的幂等键>
{
"external_agent_id": "你自己的-agent-id",
"wallet_id": "wal_de43139cef869dc9c4…",
"chain_id": 677,
"owner_address": "0x5f5802fA78d5eE2A3B4fD67F49CD502430C3E88C",
"wallet_address": "0xd9DC4F46d9da603A2A077A7190BAa85f4e2f05fe",
"metadata": {
"name": "示例 Agent",
"description": "这个 Agent 做什么"
},
"callback_url": "https://your-service.example/webhooks/identity"
}
字段必填含义
external_agent_id是你自己系统里的 Agent 标识,在项目和链的范围内唯一
wallet_id是这个身份所属的 Agent 钱包(全局唯一)
chain_id是主网 677,测试网 968,其他值不接受
owner_address是控制这个身份的地址,必须是合法 EVM 地址
wallet_address是Agent 用于交易的钱包地址,必须是合法 EVM 地址
metadata是自由格式的描述,其中 name 必须存在且非空
callback_url否绝对 http/https 地址,用于推送状态变化;不填则改用轮询
idempotency_key是可放在请求体里,也可以放在 Idempotency-Key 请求头里

注意请求体里没有项目 ID,也没有任何密钥材料。项目是从 API Key 解析出来的, 所以调用方无法通过传一个别的值把身份创建到别人的项目下。

external_agent_id 在 (项目, 链) 范围内唯一。同一 (项目, 链, external_agent_id) 再次创建时,服务会返回已有身份并带 "replayed": true(即使请求内容有差异,也不会新建第二条)。真正的永久性冲突(例如同一个 wallet_id 已被别的身份占用)才返回 409。

返回 202 Accepted,身份记录已创建、链上操作还没发生:

{
"identity": {
"identity_id": "idn_c39e39dd-1030-4c33-b888-5beeb876247d",
"project_id": "launchpad",
"external_agent_id": "你自己的-agent-id",
"wallet_id": "wal_de43139cef869dc9c4…",
"chain_id": 677,
"owner_address": "0x5f5802fa78d5ee2a3b4fd67f49cd502430c3e88c",
"wallet_address": "0xd9dc4f46d9da603a2a077a7190baa85f4e2f05fe",
"registry_address": "0xb43edfb9c7609cf645e932b2ff20f26f0d4488de",
"status": "PENDING",
"wallet_binding_status": "PENDING",
"created_at": "2026-09-08T06:12:44Z",
"updated_at": "2026-09-08T06:12:44Z"
},
"replayed": false
}

记下 identity_id,后续每个接口都用它。 创建时只会排队 owner Gas Funding 与后续的 register() / 正式 metadata 发布;不会自动排队钱包绑定。

读写接口的返回结构不一样

写接口把身份对象包在 identity 字段里,读接口直接返回对象本身。所以创建之后是 response.identity.status,而 GET 之后是 response.status。用同一种方式读取两者, 是接入过程中最常见的错误。

幂等​

每一次逻辑上的创建都用一个唯一的 Idempotency-Key。用同一个键重放会返回 200 和 "replayed": true,而不是 202,所以超时重试不会创建出第二个身份。这个键应该由你自己的 Agent ID 推导出来,而不是每次 HTTP 请求随机生成。随机值会让幂等失去意义。

4. 等待注册完成​

注册在后台会经过若干阶段,你不需要逐个处理,只需要关心 status 什么时候变成 REGISTERED。

状态含义
PENDING已受理,排队中
GAS_FUNDING正在给 owner 账户注入 gas
REGISTERINGregister() 已提交,等待上链
METADATA_PENDING正在发布或更新正式的注册文件(首次注册后、以及后续更新 metadata 时都会进入)
REGISTERED注册完成。 erc8004_agent_id 和 metadata_uri 已填充
WALLET_BINDING钱包绑定交易已提交,等待上链确认;确认后回到 REGISTERED

wallet_binding_status 是独立推进的:PENDING → 链上绑定完成后变为 CONFIRMED。

身份可以已经是 REGISTERED 而 wallet_binding_status 仍为 PENDING(绑定尚未请求,或已请求但交易还没提交)。绑定交易在途时,status 会短暂变为 WALLET_BINDING。

4.1 轮询​

GET /v1/agents/identities/{identity_id}
X-API-Key: ak_…

读接口直接返回身份对象,没有外层包装:

{
"identity_id": "idn_c39e39dd-1030-4c33-b888-5beeb876247d",
"project_id": "launchpad",
"wallet_id": "wal_de43139cef869dc9c4…",
"status": "REGISTERED",
"wallet_binding_status": "CONFIRMED",
"erc8004_agent_id": "45",
"registry_address": "0xb43edfb9c7609cf645e932b2ff20f26f0d4488de",
"metadata_uri": "https://…/v2-….json",
"owner_address": "0x5f5802fa78d5ee2a3b4fd67f49cd502430c3e88c",
"wallet_address": "0xd9dc4f46d9da603a2a077a7190baa85f4e2f05fe",
"updated_at": "2026-09-08T06:12:51Z"
}

也可以用你自己的标识反查,这样不必存我们的 ID:

GET /v1/agents/by-external-id/{chain_id}/{external_agent_id}
GET /v1/agents/by-erc8004/{chain_id}/{registry_address}/{erc8004_agent_id}

三个接口对不属于你项目的身份都返回 404。「不是你的」和「不存在」在返回上没有区别, 这是刻意的。

4.2 回调(Webhook)​

如果你填了 callback_url,每次状态变化我们都会 POST 过去:

POST <你的 callback_url>
Content-Type: application/json
X-Callback-Token: <开通时约定的 token>

X-Callback-Token 在项目开通时与 API Key 一并约定,由身份服务 worker 配置;不是每次创建身份时下发的新 token。接收端应使用开通时拿到的同一个值做校验。

{
"identity_id": "idn_c39e39dd-1030-4c33-b888-5beeb876247d",
"external_agent_id": "你自己的-agent-id",
"chain_id": 677,
"status": "REGISTERED",
"wallet_binding_status": "CONFIRMED",
"erc8004_agent_id": "45",
"registry_address": "0xb43edfb9c7609cf645e932b2ff20f26f0d4488de",
"metadata_uri": "https://…/v2-….json",
"updated_at": "2026-09-08T06:12:51Z"
}

接收端有四条规则:

  1. 先校验 X-Callback-Token 再处理请求体,用常数时间比较。跳过这一步的回调端点, 等于接受任何知道这个 URL 的人伪造的状态更新。
  2. 尽快返回 2xx。 其他响应都算失败并会被重试,真正的处理放到异步去做。
  3. 要能处理重复投递。 重试意味着同一个状态可能到达多次,用 identity_id 加 updated_at 做幂等键就够了。
  4. 重试会导致乱序。 updated_at 比你已存记录旧的 payload 直接忽略。

重试退避依次是 15 秒、1 分钟、5 分钟,之后每 15 分钟一次。投递不会被放弃: 一个长期失败的端点会不断累积重试,而不是丢掉状态变化。回调是对轮询的优化而不是替代: 你的端点挂掉一小时,轮询依然能立刻拿到当前状态。

5. 更新描述信息​

PUT /v1/agents/identities/{identity_id}/metadata
X-API-Key: ak_…
{
"metadata": { "name": "示例 Agent", "description": "更新后的描述" },
"idempotency_key": "update-2026-09-08-1"
}

这会发布一个新版本的注册文件并更新链上的 URI。返回 202 和 {"identity": …, "replayed": false};重发同一个幂等键则返回 200 和 "replayed": true。

受理后身份的 status 会回到 METADATA_PENDING,等 setAgentURI 确认后再变为 REGISTERED。观察 metadata_uri 变化(或等 status 回到 REGISTERED)即可确认生效。

agentId、owner、walletAddress 和 registrations 不接受在这里传入。 它们是链上决定的字段:服务会把当前链上的值合并进每一个新版本,所以调用方无法发布一份 声称自己拥有某个 owner 或某个 agentId 的文件。

请求钱包绑定​

POST /v1/agents/identities/{identity_id}/wallet-binding
Content-Type: application/json
X-API-Key: ak_…

{ "idempotency_key": "bind-2026-09-08-1" }

请求体只有一个幂等键。你不需要提供地址,也不需要提供签名:钱包在创建身份时就已经 确定,链上的授权签名由我们这边持有该钱包密钥的一方生成。

创建身份不会自动排队绑定。 请在身份 status 变为 REGISTERED 之后调用本接口(若过早调用会收到 425)。它同时用于首次绑定,以及绑定仍停留在 PENDING 时的重新入队。返回 202,或 200 加 "replayed": true。

观察 wallet_binding_status 变成 CONFIRMED 即完成。

暂停​

POST /v1/agents/identities/{identity_id}/suspend
X-API-Key: ak_…

返回 200 和 {"identity": …}。被暂停的身份不再接受新任务(新的注册步骤、metadata 更新、钱包绑定),suspended_at 会被置上。已经广播出去的交易仍会继续确认。

链上身份不受影响。暂停是平台侧的状态而不是链上操作,所以第三方读注册表看到的仍然是一个有效身份。

6. 错误处理​

错误返回体里带一个 error 字符串。是否应该重试看状态码:

状态码含义可重试?
400请求格式错误或缺少必填字段否。改请求
401API Key 缺失或无效否
404身份不存在,或不属于你的项目否
409永久性冲突,例如 wallet_id 已被其他身份占用否
425身份还没走到所需的状态是,稍后重试
5xx我们这边出错是,退避后重试

425 Too Early 需要特别注意。它表示该操作本身合法,只是身份尚未推进到可执行该操作的 阶段,例如在注册尚未完成时请求钱包绑定。它的形式与 409 相似,含义却相反: 409 表示应当终止,425 表示应当稍后重试。 若将 425 视为永久失败,会导致 本可成功的身份被提前放弃。

7. 在链上验证身份​

前面所有内容都是我们的接口在描述我们自己的记录。ERC-8004 的价值在于,第三方不需要 信任我们就能核实其中的关键部分。当有人要求你「证明这个 Agent 是你的」时,本节就是 你交给对方的东西。

下面全部是只读调用:不需要私钥、不需要 gas、不需要账户。

7.1 三个核心读取​

你想知道调用返回
谁控制这个 AgentownerOf(uint256 agentId)address
它通过哪个钱包行动getAgentWallet(uint256 agentId)address
它在哪里描述自己tokenURI(uint256 agentId)string
import { ethers } from 'ethers'

const provider = new ethers.JsonRpcProvider('https://rpc.botchain.ai')
const registry = new ethers.Contract(
'0xB43Edfb9C7609cF645e932B2fF20f26F0d4488dE',
[
'function ownerOf(uint256) view returns (address)',
'function getAgentWallet(uint256) view returns (address)',
'function tokenURI(uint256) view returns (string)',
],
provider,
)

const agentId = 45n
console.log(await registry.ownerOf(agentId)) // 0x5f5802fA78d5eE2A3B4fD67F49CD502430C3E88C
console.log(await registry.getAgentWallet(agentId)) // 0xd9DC4F46d9da603A2A077A7190BAa85f4e2f05fe
console.log(await registry.tokenURI(agentId)) // https://…/v2-….json

不想引入依赖的话,用原生 JSON-RPC 也一样:

# ownerOf(45),选择器 0x6352211e,参数补齐到 32 字节
curl -s https://rpc.botchain.ai \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{
"to":"0xB43Edfb9C7609cF645e932B2fF20f26F0d4488dE",
"data":"0x6352211e000000000000000000000000000000000000000000000000000000000000002d"
},"latest"]}'

示例连接的是主网。测试网请改用 https://rpc.bohr.life,并换成第 8 节中的 IdentityRegistry 地址,其余完全相同。

有两个行为需要知道:

  • 对未注册的 ID,ownerOf 会 revert,而不是返回零地址。把 revert 当作 「没有这个 Agent」,不要和网络错误混为一谈。
  • getAgentWallet 不校验 ID 是否存在。未注册的 ID 和已注册但未绑定钱包的 ID, 它都返回零地址。判断身份是否存在只能用 ownerOf,不能用 getAgentWallet。

7.2 注册文件​

tokenURI 指向一份符合 ERC-8004 注册文件格式的 JSON:

{
"type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
"name": "示例 Agent",
"description": "这个 Agent 做什么",
"owner": "0x5f5802fa78d5ee2a3b4fd67f49cd502430c3e88c",
"walletAddress": "0xd9dc4f46d9da603a2a077a7190baa85f4e2f05fe",
"registrations": [
{
"agentId": "45",
"agentRegistry": "eip155:677:0xb43edfb9c7609cf645e932b2ff20f26f0d4488de"
}
]
}

要验证这份文件,而不是信任它。 一份不是通过 tokenURI 拿到的地址提供的文件, 说明不了任何问题。正确的做法是:

  1. 从注册表读 tokenURI(agentId)。
  2. 请求这个地址,不要换成别的。
  3. 核对 registrations[0].agentId 等于你要查的 ID,且 registrations[0].agentRegistry 指向你调用的那个注册表。
  4. 核对 owner 与 ownerOf 一致、walletAddress 与 getAgentWallet 一致。

文件和链上不一致时,以链上为准。注册表是所有权和钱包绑定的权威来源,文件只是一份 应当与之一致的描述。

7.3 会随时间变化的部分​

变化链上方法需要重新读取
描述信息更新setAgentURItokenURI
绑定或更换钱包setAgentWalletgetAgentWallet
解绑钱包unsetAgentWalletgetAgentWallet
所有权转移transferFromownerOf

因为 URI 会变,不要无限期缓存注册文件。缓存要以 URI 为键,需要最新数据时重新读一次 tokenURI。

钱包绑定支持两种签名验证方式,所以被绑定的钱包可以是任意一种账户:

钱包类型验证方式
EOA,含 EIP-7702 委托的 EOAECDSA
合约钱包(ERC-4337 账户、Safe 等)ERC-1271

这一点对 Agent 很重要:Agent 钱包通常是智能账户而不是 EOA。

8. 环境与合约地址​

请始终调用代理(proxy)地址;实现合约地址会在升级时变化。

身份服务当前只编排 IdentityRegistry(注册、URI、钱包绑定)。下表中的 Reputation / Validation 地址属于 ERC-8004 配套合约,链上已部署,但本服务目前不调用它们;第三方若自行对接声誉/验证流程可参考,接入身份注册本身只需 IdentityRegistry。

BOT 主网(chain ID 677)​

项目地址
RPChttps://rpc.botchain.ai
区块浏览器https://scan.botchain.ai
IdentityRegistry0xB43Edfb9C7609cF645e932B2fF20f26F0d4488dE
ReputationRegistry0xB3f7061354193a424856Cc63028cE3556262dAc9
ValidationRegistry0x85Bf081D1492393C3Fb2742b379b6ADbC7dbC79b

Bohr 测试网(chain ID 968)​

项目地址
RPChttps://rpc.bohr.life
区块浏览器https://scan.bohr.life
IdentityRegistry0xec8fFbC3c9A34AdDCbB3A14F91db2bd26A8b99c0
ReputationRegistry0xD4084c9397adca7fD0c24617681a2eA4C286402A
ValidationRegistry0x0ff64E68C0e5fB2c57E6B3e1D7F3c65FBD0eEA33

agentId 是按注册表编号的。主网的 Agent 45 和测试网的 Agent 45 是两个毫无关系的 身份。传递 agentId 时必须同时带上 chain ID,注册文件里的 agentRegistry 字段正是 为此存在。

注册表是一个完整的 ERC-721 实现,名称 AgentIdentity(AGENT)。版本请调用合约上的 getVersion()(不是常见的 version());两条链上当前均为 2.0.0。因此支持展示 NFT 的钱包和浏览器不需要任何适配就能显示 Agent 身份。