Agent 钱包
给你的 AI Agent 一个能发起交易的账户,全程不接触私钥。
一句话概括: 调一次接口拿到一个地址,这是一个 ERC-4337 智能账户,私钥由我们保管, 部署 gas 由我们代付,之后转账的 gas 由账户自付。
本章面向要把自己的系统接入 BOT Chain Agent 钱包服务的开发者。读完你应该能创建钱包、 理解为什么地址先于合约存在、从钱包转出代币,以及限制它能转给谁。
请先读本章,再读 Agent 身份。注册身份需要一个已经存在的钱包。
本章所有路径都相对于下面两个地址之一:
| 环境 | 地址 |
|---|---|
| BOT 主网 | https://wallet-api.botchain.ai |
| Bohr 测试网 | https://wallet-api.bohr.life |
API Key 与环境绑定。使用测试网的 Key 请求主网地址将被拒绝,不会被静默路由至其他环境。
1. 概览
| 问题 | 回答 |
|---|---|
| 我能得到什么? | 一个 ERC-4337 智能账户地址,Agent 从这里发起交易 |
| 我需要保管私钥吗? | 不需要。私钥在我们这边保管和使用,你的系统不做任何签名 |
| gas 谁出? | 部署 gas 由我们代付。创建时 Agent 不需要有余额,之后转账的 gas 由账户自付 |
| 地址能立刻用吗? | 收款可以。转出需要账户先完成部署 |
| 怎么转账? | POST /wallets/{id}/transfers,会提交一笔 UserOperation |
| 能限制转账目标吗? | 能,而且是强制的。白名单在执行前校验 |
| 支持哪些链? | BOT 主网(chain ID 677)和 Bohr 测试网(chain ID 968) |
需要记住的一点:
创建钱包的那一刻地址就存在了,但那个地址上的合约是后面才部署的。 两者都正常,一个没有代码的地址照样可以收款。
2. 获取凭据
钱包和身份共用一套凭据。每个接入方是一个项目,有一个项目 ID 和一个或多个 API Key, 由我们签发:
ak_<environment>_k_<key_id>.<secret>
把整串(含中间那个点)放在 X-API-Key 请求头里。secret 只在创建时返回一次,
之后无法再取回,拿到后立即存进密钥管理系统。
Key 区分环境,测试网的 Key 在主网上不可用。
3. 创建钱包
POST /v1/wallets
Content-Type: application/json
X-API-Key: ak_…
{
"external_agent_id": "你自己的-agent-id",
"chain_id": 677,
"idempotency_key": "create-agent-42",
"callback_url": "https://your-service.example/webhooks/wallet"
}
| 字段 | 必填 | 含义 |
|---|---|---|
external_agent_id | 是 | 你自己系统里的 Agent 标识 |
chain_id | 是 | 主网 677,测试网 968 |
idempotency_key | 是 | 也可以放在 Idempotency-Key 请求头里 |
identity_id | 否 | 关联已有身份。一般不填,身份是后一步 |
callback_url | 否 | 状态变化推送到这个地址 |
注意你不需要提供的东西:没有 owner 地址,也没有账户地址。两者都在我们这边生成后 返回给你。你的系统不需要生成任何东西,也没有任何密钥材料跨越边界。
返回 202:
{
"wallet": {
"wallet_id": "wal_de43139cef869dc9c42ae3",
"project_id": "launchpad",
"external_agent_id": "你自己的-agent-id",
"chain_id": 677,
"owner_address": "0x5f5802fA78d5eE2A3B4fD67F49CD502430C3E88C",
"account_address": "0xd9DC4F46d9da603A2A077A7190BAa85f4e2f05fe",
"deployment_status": "PREDICTED",
"status": "ACTIVE",
"created_at": "2026-09-07T09:37:40Z"
},
"replayed": false
}
返回里有两个地址,它们不能混用:
| 字段 | 是什么 |
|---|---|
account_address | Agent 的地址。 往这里打款,链上显示的发起方也是它 |
owner_address | 控制这个账户的地址。注册身份时要用,不要往这里打款 |
记下 wallet_id,后续每个接口都用它。
写接口把对象包在 wallet 字段里,读接口直接返回对象本身。所以创建之后是
response.wallet.account_address,而 GET 之后是 response.account_address。
用同一个幂等键重放会返回 200 和 "replayed": true,以及原来那个钱包,所以超时重试
不会创建出第二个账户。
也可以用你自己的标识反查,这样不必存我们的 ID:
GET /v1/wallets/{wallet_id}
GET /v1/wallets/by-external-id/{chain_id}/{external_agent_id}
不属于你项目的钱包一律返回 404。「不是你的」和「不存在」在返回上没有区别。
4. 地址先于合约存在
deployment_status 初始是 PREDICTED。这不是一个需要等待的中间态,而是 ERC-4337
账户本来的工作方式。
地址由 CREATE2 从 owner 和 salt 算出,所以能提前知道,而且不会变。合约稍后才部署到
这个地址上。在那之前:
| 操作 | PREDICTED 状态下可用? |
|---|---|
| 接收原生代币或 ERC-20 | 可以。 向一个没有代码的地址转账是正常的 |
| 把地址展示给用户 | 可以 |
| 转出代币 | 不可以 |
| 状态 | 含义 |
|---|---|
PREDICTED | 地址已算出,链上还没有合约 |
DEPLOYING | 部署已提交,等待上链 |
DEPLOYED | 合约已就绪,账户可以转出 |
deployment_status 不是 DEPLOYED 时转账会被拒绝,而且转账不会自动触发部署,
只会返回 409 WALLET_NOT_READY。请把部署放进你的初始化流程,不要指望首次付款时惰性触发。
POST /v1/wallets/{wallet_id}/deploy
{ "idempotency_key": "deploy-agent-42" }
返回 202 和一笔 UserOperation。轮询钱包直到 deployment_status 变成 DEPLOYED,
之后转账才会被接受。
部署不需要账户里有余额,gas 由我们代付。
5. 转出代币
POST /v1/wallets/{wallet_id}/transfers
Content-Type: application/json
X-API-Key: ak_…
{
"receiver": "0x1111111111111111111111111111111111111111",
"token_address": "0x2222222222222222222222222222222222222222",
"amount": "1000000",
"request_source": "API",
"idempotency_key": "payout-2026-09-08-001"
}
| 字段 | 必填 | 含义 |
|---|---|---|
receiver | 是 | 目标地址,必须在该钱包的白名单里 |
token_address | 是 | ERC-20 合约地址,原生代币填字面量 BOT_NATIVE |
amount | 是 | 最小单位,纯数字。 6 位精度的 USDT,"1000000" 表示 1 USDT |
request_source | 是 | API、CONSOLE 或 ADMIN,记录转账来源 |
idempotency_key | 是 | 防止重试导致重复转账 |
有四种情况会被直接拒绝:
token_address不能省略。 转原生代币要填字符串BOT_NATIVE,不是留空, 也不是零地址。amount只能是数字。 小数和科学计数法会被拒绝,而不是四舍五入,"1.5"和"1e6"均会失败。浮点数转换存在精度损失,可能导致实际转出数量与预期不符, 因此接口不接受此类格式。request_source必填,且必须是上面三个值之一。- 收款方不能是钱包自己的账户地址。 自己转给自己在链上是一次真实执行, 余额不变但 gas 照扣,所以在这里就拦掉。
amount 是字符串不是数字。代币数量会超出 JavaScript 数字能精确表示的范围。
返回 202 和一笔 UserOperation,转账已提交、尚未最终确认:
{
"user_op_hash": "0x…",
"wallet_id": "wal_de43139cef869dc9c42ae3",
"chain_id": 677,
"op_type": "TRANSFER",
"status": "BUILT",
"sender": "0xd9DC4F46d9da603A2A077A7190BAa85f4e2f05fe",
"attempt": 0,
"created_at": "2026-09-08T07:20:11Z"
}
跟踪一笔转账
GET /v1/user-operations/{user_op_hash}
| 状态 | 含义 | 终态? |
|---|---|---|
BUILT | 已构造,尚未签名 | 否 |
SIGNED | 已签名,尚未提交 | 否 |
SUBMITTED | 已提交给 bundler | 否 |
INCLUDED | 已进块,回执未确认 | 否 |
SUCCEEDED | 完成。 tx_hash 和 block_number 已填充 | 是 |
REVERTED | 上链执行失败,gas 已消耗 | 是 |
FAILED | 从未上链,原因见 last_error | 是 |
REVERTED 和 FAILED 含义不同,处理方式也应该不同。REVERTED 是交易到了链上并在
那里失败了:余额不足、代币合约拒绝等等。FAILED 是根本没走到那一步。用新的幂等键
重试 FAILED 是合理的;什么都不改就重试 REVERTED,结果还是 revert。
attempt 和 last_error 显示服务已经替你尝试过什么。
6. 目标白名单
钱包只能转账至已登记在白名单中的地址。白名单为空表示该钱包不能向任何地址转账, 而非「不受限制」。每一个目标地址都必须在首次转账前完成登记,否则转账将被拒绝。
这是刻意设计的:Agent 会自己决定何时转账,所以「能转到哪里」这件事必须提前定死, 而不是从 Agent 收到的指令里推断出来。校验在服务端、在构造操作之前执行,所以即使你的 系统被完全攻陷,攻击者也没法把钱转到一个新地址去。
POST /v1/wallets/{wallet_id}/allowlist
GET /v1/wallets/{wallet_id}/allowlist
DELETE /v1/wallets/{wallet_id}/allowlist/{entry_id}
{
"chain_id": 677,
"address": "0x1111111111111111111111111111111111111111",
"label": "结算账户",
"allowed_actions": "TRANSFER",
"created_by": "ops@your-company.example",
"idempotency_key": "allow-payout-1"
}
| 字段 | 必填 | 含义 |
|---|---|---|
chain_id | 是 | 条目按链隔离。968 上的条目不能用于 677 上的转账 |
address | 是 | 目标地址 |
allowed_actions | 是 | TRANSFER、PAYMENT 或 ALL |
created_by | 是 | 审计用,记录是谁批准了这个地址 |
label | 否 | 便于识别的名称 |
idempotency_key | 是 |
条目按 (wallet_id, chain_id, address) 匹配,且必须是 ACTIVE 状态、
allowed_actions 覆盖转账。被拒时返回信息会说明是哪一条不满足:压根没有条目、
条目已吊销、还是 allowed_actions 不含转账。这是三种不同的问题,对应三种不同的
处理,所以要读拒绝原因,而不是直接把地址再加一遍。
初始化钱包时顺序很重要:先加目标地址,再往里打款。一个有钱但没有白名单条目的钱包 付不了任何人。
7. 暂停钱包
POST /v1/wallets/{wallet_id}/status
{ "status": "PAUSED" }
PAUSED 会停止新的操作,可以由我们解除。REVOKED 是永久的。
这个接口刻意没有提供「恢复」。让一个已停止的账户重新运行是一个运营决定,而不是停止 操作的机械反向操作。需要恢复请联系我们。
8. 回调(Webhook)
如果你填了 callback_url,状态变化会以 POST 推送过去,带 X-Callback-Token 请求头。
接收端有四条规则:
- 先校验
X-Callback-Token,用常数时间比较,通过后再处理请求体。 - 尽快返回
2xx,真正的处理放到异步去做。 - 要能处理重复投递。 用对象 ID 加
updated_at做幂等键。 - 重试会导致乱序,
updated_at比已存记录旧的payload 直接忽略。
回调是对轮询的优化,不是替代。你的接收端挂掉时,轮询依然能立刻拿到当前状态。
9. 错误处理
错误返回里带一个可供程序判断的错误码:
{ "error_code": "WHITELIST_DENIED", "error_message": "…" }
请根据 error_code 分支,不要匹配错误文案。
| HTTP | error_code | 含义 | 可重试? |
|---|---|---|---|
400 | INVALID_REQUEST | 请求格式错误,或某个字段没通过校验 | 否。改请求 |
401 | — | API Key 缺失或无效 | 否 |
403 | WHITELIST_DENIED | 目标地址没有生效中的白名单条目 | 否。先加白名单 |
403 | TOKEN_NOT_ALLOWED | 该钱包不允许转出这个代币 | 否 |
404 | NOT_FOUND | 钱包不存在,或不属于你的项目 | 否 |
409 | WALLET_NOT_READY | 尚未部署、已暂停或已吊销 | 看情况,见下 |
409 | IDEMPOTENCY_KEY_CONFLICTS | 同一个幂等键用于了不同内容 | 否 |
422 | INSUFFICIENT_BALANCE | 余额不足 | 否。先充值 |
501 | NOT_IMPLEMENTED | 当前环境未启用该能力 | 否 |
5xx | INTERNAL_ERROR | 我们这边出错 | 是,退避后重试 |
WALLET_NOT_READY 一个码覆盖了三种情况,error_message 里的 status= 和
deployment= 会说明是哪一种:
deployment=PREDICTED或DEPLOYING→ 部署完成后重试,是临时状态。status=PAUSED→ 等钱包恢复后再重试。status=REVOKED→ 永久失败,不要重试。
只看到 409 就当成永久冲突,会把正在部署中的钱包误判为失败丢掉。
10. 环境
| 项目 | BOT 主网 | Bohr 测试网 |
|---|---|---|
| Chain ID | 677 | 968 |
| RPC | https://rpc.botchain.ai | https://rpc.bohr.life |
| 区块浏览器 | https://scan.botchain.ai | https://scan.bohr.life |
| Bundler | https://bundler.botchain.ai/rpc | https://bundler.bohr.life/rpc |
| EntryPoint | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 | 同左 |
| 账户类型 | Kernel v0.3.3 | 同左 |
EntryPoint 是标准的 ERC-4337 v0.7 单例合约,所以现有的 ERC-4337 工具链无需改造
就能配合这些账户使用。
下一步:身份
钱包给了 Agent 一个账户。身份让这个 Agent 可被验证:任何人都能 直接从链上查到它归谁所有、通过哪个钱包行动。
注册身份需要用到刚创建的钱包里的三个值:
| 身份接口字段 | 取自 |
|---|---|
wallet_id | wallet.wallet_id |
wallet_address | wallet.account_address |
owner_address | wallet.owner_address |