StableOps

快速开始

从运营者视角配置支付轨道,让自主 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 KeySTABLEOPS_API_KEY,用于配置支付轨道,绝不能下发给 Agent 运行时)。
  • 测试网络为 Base Sepolia(eip155:84532),资产为官方测试 USDC(0x036CbD53842c5426634e7929541eC2318f3dCF7e,6 位小数),协议为 x402 v2 exact + 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/日。

执行授权公钥不是付款钱包公钥,也不是收款钱包公钥。用户应从 StableOps 沙盒接入资料取得当前密钥标识、公钥文件和指纹。没有匹配的公钥时不要继续,更不能用任意公钥代替。

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-sdkAgent 运行时在策略控制下发起支付,暴露最小权限工具

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 不得超过 perPaymentLimitAtomicperPaymentLimitAtomic 不得超过沙盒平台单笔上限 1000000agentDailyLimitAtomic 不得超过 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 客户端也不能同时配置 apiKeyaccessToken。不要缓存管理员访问令牌,也不要把它交给 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、审批与结算状态。准备主网时,先按介绍中的支持范围核对网络、合约、位数、付款方式和燃料费要求。

这篇文档怎么样?

最后更新

本页内容