快速开始
从运营者视角配置支付轨道,让自主 Agent 在策略、预算和人工审批约束下发起稳定币支付。
Agent Payments 把两件事分开:
- 运营者(你) 提前把 Agent 的“支付轨道”配置好:登记付款钱包、写死支出策略、设置预算,最后签发一把受限的 Agent Key。你也是审批人。
- Agent 运行时(你的自主程序) 只拿到这把 Agent Key、本地签名器地址和认证令牌。它只能调用四个最小权限工具,无法修改钱包、策略、预算或审批,也拿不到私钥。
每一笔支付都会经过:策略校验(来源、收款地址、金额)、组织与 Agent 两级日预算、必要时的人工审批,最后由你自己部署的签名器对一份精确绑定的短时执行授权签名。StableOps 不代理业务请求、不广播交易、不持有私钥。
/agent-payments/dashboard 点选完成(创建 Agent、登记钱包、写策略、设预算、签发 Agent Key、审批)。下面用管理 SDK 演示同一套流程,便于纳入基础设施代码。准备条件
- Node.js 20 或更高版本。本 SDK 运行在服务端 Node.js 中,不支持浏览器或边缘运行时。
- 一个 StableOps 组织,以及一把沙盒 Agent Payments 管理 API Key(
STABLEOPS_API_KEY,用于配置支付轨道,绝不能下发给 Agent 运行时)。 - 测试网络为 Base Sepolia(
eip155:84532),资产为官方测试 USDC(0x036CbD53842c5426634e7929541eC2318f3dCF7e,6 位小数),协议为 x402 v2exact + GET。 - 一个专用的 Base Sepolia 测试钱包私钥,钱包内有足够的测试 USDC。当前 USDC EIP-3009 流程由结算服务(Facilitator)提交交易,付款钱包不需要为了本流程持有测试 ETH。
- StableOps 提供的沙盒执行授权公钥(Ed25519 公钥)及其密钥标识,供签名器校验授权来源。
金额一律用最小单位字符串表示。USDC 为 6 位小数,因此
1000000= 1 USDC,100000= 0.1 USDC。沙盒平台上限:单笔 ≤ 1 USDC(1000000)、单 Agent ≤ 10 USDC/日、组织 ≤ 100 USDC/日。
1. 安装软件包
三个包按角色划分,各自独立发布:
pnpm add @stableops/agent-payments-api-sdk @stableops/agent-sdk @stableops/agent-signer viem| 软件包 | 由谁运行 | 作用 |
|---|---|---|
@stableops/agent-payments-api-sdk | 运营者后台 | 配置 Agent、钱包、策略和预算,查询审批与支付 |
@stableops/agent-signer | 你的签名器 | 校验执行授权并用本地私钥或 AWS KMS 签名 |
@stableops/agent-sdk | Agent 运行时 | 在策略控制下发起支付,暴露最小权限工具 |
1.1 准备环境变量
先创建仅供服务端读取的 .env.local。不要提交这个文件:
STABLEOPS_API_URL=https://api.stableops.dev
STABLEOPS_API_KEY=sk_sandbox_替换为Agent_Payments管理密钥
X402_RESOURCE_URL=https://x402-base-sepolia-resource.vercel.app/api/x402/resource
BASE_SEPOLIA_TEST_PRIVATE_KEY=0x替换为64位十六进制私钥
EXPECTED_WALLET_ADDRESS=0x替换为付款钱包地址
STABLEOPS_GRANT_KEY_ID=替换为StableOps提供的密钥标识
STABLEOPS_GRANT_PUBLIC_KEY_FILE=./grant-public.pem
STABLEOPS_SIDECAR_URL=http://127.0.0.1:8789
STABLEOPS_SIDECAR_TOKEN=替换为高熵随机令牌
# 完成 2.5 后再填写。
STABLEOPS_AGENT_KEY=生成伴随服务令牌:
openssl rand -hex 32把 StableOps 提供的 Ed25519 公钥 PEM 原文保存到 STABLEOPS_GRANT_PUBLIC_KEY_FILE 指向的文件。建议把沙盒测试程序、环境文件、公钥和签名记录放在不提交到 Git 的独立目录中。
下面的代码统一用这个函数读取必填变量,避免缺失值静默关闭安全措施:
function required(name: string): string {
const value = process.env[name]?.trim()
if (!value) throw new Error(`缺少环境变量 ${name}`)
return value
}2. 配置支付轨道(运营者)
以下代码运行在运营者后台,不属于 Agent 运行时。先读取真实 402 报价、校验钱包与余额,再创建任何平台资源。
2.1 读取报价并校验付款钱包
import {
createPublicClient,
erc20Abi,
formatUnits,
getAddress,
http,
} from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { baseSepolia } from 'viem/chains'
import {
parseX402Requirement,
SafeHttpsRequester,
} from '@stableops/agent-sdk'
const BASE_SEPOLIA_USDC = getAddress(
'0x036CbD53842c5426634e7929541eC2318f3dCF7e',
)
const resourceUrl = new URL(required('X402_RESOURCE_URL'))
const rawPrivateKey = required('BASE_SEPOLIA_TEST_PRIVATE_KEY')
const privateKey = (rawPrivateKey.startsWith('0x')
? rawPrivateKey
: `0x${rawPrivateKey}`) as `0x${string}`
if (!/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
throw new Error('BASE_SEPOLIA_TEST_PRIVATE_KEY 必须是 32 字节十六进制私钥')
}
const account = privateKeyToAccount(privateKey)
if (account.address !== getAddress(required('EXPECTED_WALLET_ADDRESS'))) {
throw new Error('私钥推导地址与 EXPECTED_WALLET_ADDRESS 不一致')
}
const requester = new SafeHttpsRequester()
const challengeResponse = await requester.get(resourceUrl.toString())
if (challengeResponse.status !== 402) {
throw new Error(`资源没有返回 402,实际状态码为 ${challengeResponse.status}`)
}
const requirement = parseX402Requirement(challengeResponse).selected
if (
requirement.scheme !== 'exact' ||
requirement.network !== 'eip155:84532' ||
getAddress(requirement.asset) !== BASE_SEPOLIA_USDC
) {
throw new Error('资源报价不属于当前支持的 Base Sepolia USDC exact 范围')
}
const publicClient = createPublicClient({
chain: baseSepolia,
transport: http(),
})
const usdcBalance = await publicClient.readContract({
address: BASE_SEPOLIA_USDC,
abi: erc20Abi,
functionName: 'balanceOf',
args: [account.address],
})
if (usdcBalance < BigInt(requirement.amount)) {
throw new Error(
`测试 USDC 余额不足:余额 ${formatUnits(usdcBalance, 6)},报价 ${formatUnits(BigInt(requirement.amount), 6)}`,
)
}
console.log({
walletAddress: account.address,
origin: resourceUrl.origin,
payTo: getAddress(requirement.payTo),
amountAtomic: requirement.amount,
})这一步只读取报价和链上余额,不会创建 Intent、生成签名或付款。不要跳过地址、网络、资产和余额检查。
2.2 创建管理客户端和 Agent
环境由管理 API Key 决定,baseUrl 指向 StableOps API:
import { StableOpsAgentPayments } from '@stableops/agent-payments-api-sdk'
const management = new StableOpsAgentPayments({
apiKey: required('STABLEOPS_API_KEY'),
baseUrl: required('STABLEOPS_API_URL'),
})
const agent = await management.agents.create({
name: 'research-agent',
description: '按需购买付费研究数据',
})2.3 登记并绑定付款钱包
钱包登记是一次性的“地址所有权证明”:管理端下发一段质询文本,你用钱包私钥签名,StableOps 校验通过后登记地址、记录签名器类型,并把它设为该 Agent 在该网络的默认钱包。私钥全程不出你本地。
// 1) 领取质询
const challenge = await management.wallets.createPairingChallenge({
network: 'eip155:84532',
address: account.address,
})
// 2) 用钱包私钥对质询文本签名(EIP-191 personal_sign)
const signature = await account.signMessage({ message: challenge.message })
// 3) 登记钱包(沙盒用本地测试签名器)
const wallet = await management.wallets.register({
challengeId: challenge.challengeId,
signature,
signerType: 'LOCAL_TEST',
signerKeyId: `local:${account.address.toLowerCase()}`,
})
// 4) 绑定为该 Agent 的默认付款钱包
await management.wallets.bind(agent.id, {
agentWalletId: wallet.id,
network: 'eip155:84532',
})平台只保存已验证的公开地址、签名器类型,以及“这个 Agent 可以使用这个钱包”的授权关系。没有默认钱包,后续任何支付都无法发起。
2.4 创建并激活支出策略
这一步决定了 Agent 能自动付什么、什么必须人工点头。 新 Agent 的默认策略把白名单留空、自动阈值设为 0,也就是任何支付都要人工审批。要让 Agent 在限定范围内自动付款,就得写一份显式策略并激活它。策略版本不可变,改规则等于新建版本再激活。
const version = await management.agents.createPolicyVersion(agent.id, {
network: 'eip155:84532',
asset: {
symbol: 'USDC',
contractAddress: '0x036CbD53842c5426634e7929541eC2318f3dCF7e',
},
// 首次测试只允许真实 402 报价中的来源、收款地址和金额。
allowedOrigins: [resourceUrl.origin],
allowedPayTo: [getAddress(requirement.payTo)],
automaticPaymentThresholdAtomic: requirement.amount,
perPaymentLimitAtomic: requirement.amount,
agentDailyLimitAtomic: requirement.amount,
requireApprovalForUnknownOrigin: true,
requireApprovalForUnknownPayTo: true,
})
await management.agents.activatePolicyVersion(agent.id, version.id)激活后,每一笔支付请求都按下面三条判定:
| 情况 | 结果 |
|---|---|
金额 > perPaymentLimitAtomic | 直接拒绝,不可支付 |
来源在 allowedOrigins、收款地址在 allowedPayTo,且金额 ≤ automaticPaymentThresholdAtomic | 自动放行,Agent 无需等待 |
| 其它(未知来源、未知收款地址,或金额超过自动阈值但未超单笔上限) | 进入人工审批(默认 30 分钟内有效) |
约束:
automaticPaymentThresholdAtomic不得超过perPaymentLimitAtomic。perPaymentLimitAtomic不得超过沙盒平台单笔上限1000000。agentDailyLimitAtomic不得超过10000000。来源必须是不带路径的 HTTPS origin。需要允许多次购买时,应明确提高策略日限额,而不是扩大来源或收款地址白名单。
2.5 设置预算(可选)与签发 Agent Key
沙盒下组织和 Agent 都已带默认日预算(100 / 10 USDC),无需配置即可开跑。需要设定自己的限额时再调用,但不能超过平台上限:
await management.budgets.updateOrganization('50000000') // 组织 50 USDC/日
await management.budgets.updateAgent(agent.id, '5000000') // 该 Agent 5 USDC/日最后签发 Agent Key。明文只在这一次返回,请存进 Agent 运行时的密钥管理,它是运行时唯一持有的凭证:
const credential = await management.agents.createKey(agent.id, {
name: 'research-agent-runtime',
})
if (!credential.secret) throw new Error('API 没有返回一次性 Agent Key')
console.log(`STABLEOPS_AGENT_KEY=${credential.secret}`) // 仅在本地首次配置时输出。立即把明文写入 Agent 运行时的密钥管理或本地 .env.local。如果丢失,撤销这把 Key 并重新签发。不能从 API 再次读取明文。
3. 运行签名器伴随服务
签名器伴随服务是你自己部署的小进程,只监听本地回环地址。它逐字段校验 StableOps 下发的执行授权(Agent、钱包、网络、资产、收款地址、金额、随机数、有效期),全部一致才用私钥签名。任意签名请求一律拒绝。后文简称“伴随服务”。
用 2.3 里同一把私钥启动本地签名器:
import { readFileSync } from 'node:fs'
import {
FileGrantAuthorizationStore,
LocalTestSigner,
startSignerSidecar,
} from '@stableops/agent-signer'
const rawPrivateKey = required('BASE_SEPOLIA_TEST_PRIVATE_KEY')
const privateKey = (rawPrivateKey.startsWith('0x')
? rawPrivateKey
: `0x${rawPrivateKey}`) as `0x${string}`
if (!/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
throw new Error('BASE_SEPOLIA_TEST_PRIVATE_KEY 必须是 32 字节十六进制私钥')
}
const signer = new LocalTestSigner({
privateKey,
environment: 'SANDBOX',
network: 'eip155:84532',
grantVerification: {
publicKeys: {
[required('STABLEOPS_GRANT_KEY_ID')]: readFileSync(
required('STABLEOPS_GRANT_PUBLIC_KEY_FILE'),
'utf8',
),
},
},
store: new FileGrantAuthorizationStore('./data/grant-authorizations.json'),
})
const { url } = await startSignerSidecar({
signer,
host: '127.0.0.1',
port: 8789,
authToken: required('STABLEOPS_SIDECAR_TOKEN'),
})
console.log(`签名器伴随服务已启动:${url}`)另开一个终端检查健康状态:
curl -sS http://127.0.0.1:8789/health看到 {"ok":true} 后再启动 Agent。不要把伴随服务暴露到公网。生产环境改用 AwsKmsSigner.create(),私钥留在 AWS KMS,用法见签名器。
4. 接入 Agent 运行时
在 Agent 进程里,用 Agent Key 连接控制面、用本地地址连接伴随服务,组装出 StableOpsAgent。这里没有私钥、管理 API Key 或钱包管理权限。
import {
AgentPaymentsControlClient,
HttpAgentSignerSidecar,
SafeHttpsRequester,
StableOpsAgent,
SettlementUnknownError,
} from '@stableops/agent-sdk'
const agent = new StableOpsAgent({
control: new AgentPaymentsControlClient({
agentKey: required('STABLEOPS_AGENT_KEY'),
baseUrl: required('STABLEOPS_API_URL'),
}),
sidecar: new HttpAgentSignerSidecar({
url: required('STABLEOPS_SIDECAR_URL'),
authToken: required('STABLEOPS_SIDECAR_TOKEN'),
}),
requester: new SafeHttpsRequester(),
})SafeHttpsRequester 强制使用 HTTPS、屏蔽内网地址并限制重定向。只有回环地址上的伴随服务使用 HTTP。
把四个工具定义暴露给你的模型,并在工具调用时转发到上面的实例:
import { agentPaymentTools } from '@stableops/agent-sdk'
// 将这个与框架无关的工具定义数组传给你的模型。
const toolDefinitions = agentPaymentTools
async function runTool(name: string, args: Record<string, unknown>) {
switch (name) {
case 'stableops_get_budget':
return agent.getBudget()
case 'stableops_x402_fetch':
return agent.x402Fetch(args.url as string, {
idempotencyKey: args.idempotencyKey as string | undefined,
resumeIntentId: args.resumeIntentId as string | undefined,
})
case 'stableops_get_payment':
return agent.getPayment(args.intentId as string)
case 'stableops_list_recent_payments':
return agent.listRecentPayments((args.limit as number) ?? 20)
default:
throw new Error(`未知工具:${name}`)
}
}把 toolDefinitions 传给所用模型框架,并把模型返回的工具名和参数交给 runTool。不要向模型暴露管理客户端、私钥或任意网络请求工具。
5. 发起一笔受控支付
当 Agent 需要一个付费资源时,它调用 stableops_x402_fetch。SDK 会先请求资源。遇到 HTTP 402 后解析 x402 报价、创建支付 Intent、执行策略校验和签名,再携带支付凭证请求同一资源:
let result: Awaited<ReturnType<typeof agent.x402Fetch>>
try {
result = await agent.x402Fetch(required('X402_RESOURCE_URL'), {
// 同一笔业务购买的安全重试必须复用这个值。
idempotencyKey: 'quickstart:x402-resource:1',
})
} catch (error) {
if (error instanceof SettlementUnknownError) {
const payment = await agent.getPayment(error.intentId)
console.error({
message: '结算结果未知,不得创建新的支付授权',
intentId: error.intentId,
authorizationId: error.authorizationId,
currentStatus: payment.status,
})
}
throw error
}
if (result.status === 'paid') {
const body = await result.response.text()
const payment = await agent.getPayment(result.intentId)
console.log({
httpStatus: result.response.status,
body,
intentId: result.intentId,
paymentStatus: payment.status,
resultReported: result.resultReported,
})
} else if (result.status === 'not_required') {
console.log(await result.response.text())
} else if (result.status === 'awaiting_approval') {
console.log({
message: '等待人工审批',
intentId: result.intentId,
approvalId: result.approvalId,
approvalExpiresAt: result.approvalExpiresAt,
})
}status: 'paid' 表示付费请求已经得到可靠的 HTTP 响应,不代表控制面已经确认链上状态为 settled。以 getPayment(intentId).status 为准。若为 settlement_unknown,或抛出 SettlementUnknownError,只能查询并核对原 Intent,绝不能换新的 Intent、授权或幂等键再次付款。需要人工审批时
awaiting_approval 不是失败,而是策略在起作用。审批人应优先在控制台审批页处理。下面的客户端模式只适用于已经接入 StableOps 登录态的控制台代码;accessToken 必须来自当前已登录的组织管理员会话,是短时令牌,不能作为环境变量长期保存:
async function approveFromDashboardSession(
accessToken: string,
approvalId: string,
) {
const dashboard = new StableOpsAgentPayments({
accessToken,
environment: 'sandbox',
baseUrl: required('STABLEOPS_API_URL'),
})
await dashboard.approvals.approve(approvalId, '预算内的常规数据购买')
}management 使用的是管理 API Key,不能调用审批操作。同一个 StableOpsAgentPayments 客户端也不能同时配置 apiKey 和 accessToken。不要缓存管理员访问令牌,也不要把它交给 Agent 运行时。
审批通过后,读取先前保存的 Intent ID,并用同一个 ID 恢复这笔支付,完成签名与结算:
const approvedIntentId = 'pint_...' // 读取 awaiting_approval 阶段保存的 ID。
const resumed = await agent.x402Fetch(required('X402_RESOURCE_URL'), {
resumeIntentId: approvedIntentId,
})
console.log(resumed)你刚刚完成
- 登记并绑定了 Agent 的默认付款钱包,私钥留在本地。
- 写了一份显式支出策略并激活,界定了自动放行、人工审批与直接拒绝的边界。
- 签发了运行时唯一持有的 Agent Key,并部署了自持私钥的签名器伴随服务。
- 让 Agent 在策略、预算与审批约束下请求真实 x402 资源,并查询支付的最终状态。
下一步:用管理 SDK把配置纳入基础设施代码,用签名器切到 AWS KMS,或接入 Webhook实时跟踪 Intent、审批与结算状态。准备主网时,先按介绍中的支持范围核对网络、合约、位数、付款方式和燃料费要求。
这篇文档怎么样?
最后更新