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 |
REGISTERING | register() 已提交,等待上链 |
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"
}
接收端有四条规则:
- 先校验
X-Callback-Token再处理请求体,用常数时间比较。跳过这一步的回调端点, 等于接受任何知道这个 URL 的人伪造的状态更新。 - 尽快返回
2xx。 其他响应都算失败并会被重试,真正的处理放到异步去做。 - 要能处理重复投递。 重试意味着同一个状态可能到达多次,用
identity_id加updated_at做幂等键就够了。 - 重试会导致乱序。
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 | 请求格式错误或缺少必填字段 | 否。改请求 |
401 | API Key 缺失或无效 | 否 |
404 | 身份不存在,或不属于你的项目 | 否 |
409 | 永久性冲突,例如 wallet_id 已被其他身份占用 | 否 |
425 | 身份还没走到所需的状态 | 是,稍后重试 |
5xx | 我们这边出错 | 是,退避后重试 |
425 Too Early 需要特别注意。它表示该操作本身合法,只是身份尚未推进到可执行该操作的
阶段,例如在注册尚未完成时请求钱包绑定。它的形式与 409 相似,含义却相反:
409 表示应当终止,425 表示应当稍后重试。 若将 425 视为永久失败,会导致
本可成功的身份被提前放弃。
7. 在链上验证身份
前面所有内容都是我们的接口在描述我们自己的记录。ERC-8004 的价值在于,第三方不需要 信任我们就能核实其中的关键部分。当有人要求你「证明这个 Agent 是你的」时,本节就是 你交给对方的东西。
下面全部是只读调用:不需要私钥、不需要 gas、不需要账户。
7.1 三个核心读取
| 你想知道 | 调用 | 返回 |
|---|---|---|
| 谁控制这个 Agent | ownerOf(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 拿到的地址提供的文件,
说明不了任何问题。正确的做法是:
- 从注册表读
tokenURI(agentId)。 - 请求这个地址,不要换成别的。
- 核对
registrations[0].agentId等于你要查的 ID,且registrations[0].agentRegistry指向你调用的那个注册表。 - 核对
owner与ownerOf一致、walletAddress与getAgentWallet一致。
文件和链上不一致时,以链上为准。注册表是所有权和钱包绑定的权威来源,文件只是一份 应当与之一致的描述。
7.3 会随时间变化的部分
| 变化 | 链上方法 | 需要重新读取 |
|---|---|---|
| 描述信息更新 | setAgentURI | tokenURI |
| 绑定或更换钱包 | setAgentWallet | getAgentWallet |
| 解绑钱包 | unsetAgentWallet | getAgentWallet |
| 所有权转移 | transferFrom | ownerOf |
因为 URI 会变,不要无限期缓存注册文件。缓存要以 URI 为键,需要最新数据时重新读一次
tokenURI。
钱包绑定支持两种签名验证方式,所以被绑定的钱包可以是任意一种账户:
| 钱包类型 | 验证方式 |
|---|---|
| EOA,含 EIP-7702 委托的 EOA | ECDSA |
| 合约钱包(ERC-4337 账户、Safe 等) | ERC-1271 |
这一点对 Agent 很重要:Agent 钱包通常是智能账户而不是 EOA。
8. 环境与合约地址
请始终调用代理(proxy)地址;实现合约地址会在升级时变化。
身份服务当前只编排 IdentityRegistry(注册、URI、钱包绑定)。下表中的 Reputation / Validation 地址属于 ERC-8004 配套合约,链上已部署,但本服务目前不调用它们;第三方若自行对接声誉/验证流程可参考,接入身份注册本身只需 IdentityRegistry。
BOT 主网(chain ID 677)
| 项目 | 地址 |
|---|---|
| RPC | https://rpc.botchain.ai |
| 区块浏览器 | https://scan.botchain.ai |
IdentityRegistry | 0xB43Edfb9C7609cF645e932B2fF20f26F0d4488dE |
ReputationRegistry | 0xB3f7061354193a424856Cc63028cE3556262dAc9 |
ValidationRegistry | 0x85Bf081D1492393C3Fb2742b379b6ADbC7dbC79b |
Bohr 测试网(chain ID 968)
| 项目 | 地址 |
|---|---|
| RPC | https://rpc.bohr.life |
| 区块浏览器 | https://scan.bohr.life |
IdentityRegistry | 0xec8fFbC3c9A34AdDCbB3A14F91db2bd26A8b99c0 |
ReputationRegistry | 0xD4084c9397adca7fD0c24617681a2eA4C286402A |
ValidationRegistry | 0x0ff64E68C0e5fB2c57E6B3e1D7F3c65FBD0eEA33 |
agentId 是按注册表编号的。主网的 Agent 45 和测试网的 Agent 45 是两个毫无关系的
身份。传递 agentId 时必须同时带上 chain ID,注册文件里的 agentRegistry 字段正是
为此存在。
注册表是一个完整的 ERC-721 实现,名称 AgentIdentity(AGENT)。版本请调用合约上的 getVersion()(不是常见的 version());两条链上当前均为 2.0.0。因此支持展示 NFT 的钱包和浏览器不需要任何适配就能显示 Agent 身份。