2026-11-03 · AI · AWS

AgentCore Payments × x402 × Privy:本地复现

这篇记录怎么让一个 AI agent 请求付费 API 时自动付款:服务端返回 402,agent 用 AgentCore Payments 托管的 Privy 钱包签名,带着签名重试,拿到内容。链用 Base Sepolia 测试网,币是测试 USDC。

所有代码都在本地跑,AWS 上只需要 2 个 IAM role 和 AgentCore Payments 的几个资源。完整代码在 domechn/agentcore-x402-payment-demo,文中只贴关键部分。

组件

  • x402:基于 HTTP 402 的付款协议,规定了 PAYMENT-REQUIRED 和 PAYMENT-SIGNATURE 请求头的格式
  • seller.py:卖家,/premium 收 0.001 USDC,本地 :4021
  • agent.py:Strands agent 加 AgentCorePaymentsPlugin,402 → 付款 → 重试全自动,不占端口
  • Privy 前端:Privy 官方的 Next.js 应用,用来登录、授权 agent、充值,本地 :3000,只在准备阶段用
  • Bedrock:提供 Claude Sonnet 5,us-west-2
  • AgentCore Payments:控制 session 预算,调用钱包签名,us-west-2
  • AgentCore Identity:保管 Privy 凭证,底层是 Secrets Manager,us-west-2
  • Privy:托管嵌入式钱包私钥,代为签名
  • x402.org facilitator:替卖家验签、提交链上交易、支付 gas
  • Base Sepolia:USDC 实际转账的链,eip155:84532

一次付款发生了什么

一次 x402 付款的流程
  1. agent 请求 GET /premium
  2. 卖家返回 402 和 PAYMENT-REQUIRED(base64 JSON,写明付多少、哪条链、付给谁)
  3. 插件调用 AgentCore Payments 的 ProcessPayment。它先检查 session 有没有过期、预算够不够,再用 PaymentsServiceRole 取出 Privy 凭证,让 Privy 用钱包签一个 EIP-3009 授权。这一步还没有转账
  4. agent 带着 PAYMENT-SIGNATURE 再请求一次 /premium
  5. 卖家把签名交给 facilitator 的 /verify,通过后再调 /settle 上链
  6. 卖家返回 200、内容和 PAYMENT-RESPONSE(里面有交易哈希)

LLM 只决定“去请求这个 URL”。识别 402、付款、重试都由插件完成,模型不经手钱。

谁拿着什么

  • 钱包私钥:Privy 托管,本地电脑和 AWS 都不持有
  • Privy 凭证:存在 AgentCore Identity,只有 PaymentsServiceRole 能读
  • agent 的权限:AgentRole 只有 ProcessPayment 和只读权限,不能自己建 session 给自己加预算
  • 签名内容:签名里写死了收款人和金额,facilitator 只能原样提交。卖家验证时传的是自己的 requirement,所以买家没法少付

准备

  • AWS:一个管理员 profile,能用 CloudFormation、IAM 和 bedrock-agentcore,区域 us-west-2
  • Bedrock:us-west-2 已开通 Claude Sonnet 5 的模型访问(us.anthropic.claude-sonnet-5)
  • 本地:Python 3.10+,Node 22+ 和 pnpm(跑 Privy 前端)
  • Privy:建一个专用 app

在 Privy Dashboard 里:

  1. Settings → API keys:拿到 App ID 和 App secret
  2. Wallet infrastructure → Authorization → New key:Key ID 是 PRIVY_AUTHORIZATION_ID,Private key 是 PRIVY_AUTHORIZATION_PRIVATE_KEY。私钥只在创建时显示一次,格式是 wallet-auth:...
  3. App Settings → Basics → Domains:加上 http://localhost:3000

Privy 方案不需要订阅 AWS Marketplace,只有 Coinbase 方案需要。

仓库里的文件:

  • infra/infra.yaml:CloudFormation,2 个 IAM role
  • src/setup_payments.py:一次性配置,结果写入 payments.json(manager、钱包、session 的 ID)
  • src/seller.py:卖家(x402 v2),:4021
  • src/agent.py:买家 agent
  • .env、payments.json:已在 .gitignore 里。所有命令都从项目根目录运行

.env(仓库里有 .env.example):

PRIVY_APP_ID=...
PRIVY_APP_SECRET=...
PRIVY_AUTHORIZATION_ID=...
PRIVY_AUTHORIZATION_PRIVATE_KEY=...   # 带不带 wallet-auth: 前缀都行
WALLET_EMAIL=you@example.com          # 在 Privy 前端登录用的邮箱
SELLER_ADDRESS=0x...                  # 卖家收款地址,任意一个你自己的 EVM 地址,不要填 agent 的钱包地址

步骤

1. 部署 IAM

export AWS_REGION=us-west-2 AWS_PROFILE=<管理员profile>
aws cloudformation deploy --stack-name agentcore-x402-payment-demo --template-file infra/infra.yaml --capabilities CAPABILITY_IAM

infra.yaml 里是两个 role:

  • PaymentsServiceRole:信任 bedrock-agentcore.amazonaws.com,并用 aws:SourceAccount 和 aws:SourceArn(payment-manager/*)限制来源。权限有四组:workload identity 的创建、删除和取 token;GetResourcePaymentToken(token-vault/*);创建和读取 payment credential provider;读取 Secrets Manager 里 bedrock-agentcore-identity!* 的 secret
  • AgentRole:信任本账号。权限是调用 Bedrock 模型,加上 ProcessPayment、GetPaymentInstrument、ListPaymentInstruments、GetPaymentInstrumentBalance、GetPaymentSession,资源范围限定在 payment-manager/*。它不能建 session,也不能建钱包

2. 配置 Payments

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python src/setup_payments.py

src/setup_payments.py 分 5 步,每一步做完会存进 payments.json,重跑时跳过已完成的步骤:

ctrl = boto3.client("bedrock-agentcore-control", region_name=REGION)  # 控制面
dp   = boto3.client("bedrock-agentcore",         region_name=REGION)  # 数据面

# 1. 凭证存入 AgentCore Identity
cp = ctrl.create_payment_credential_provider(
    name="agentcore-payment-x402-demo-privy", credentialProviderVendor="StripePrivy",
    providerConfigurationInput={"stripePrivyConfiguration": {
        "appId": os.environ["PRIVY_APP_ID"],
        "appSecret": os.environ["PRIVY_APP_SECRET"],
        "authorizationId": os.environ["PRIVY_AUTHORIZATION_ID"],
        "authorizationPrivateKey": os.environ["PRIVY_AUTHORIZATION_PRIVATE_KEY"].removeprefix("wallet-auth:"),
    }})

# 2. Payment Manager(名字只能用字母和数字),等状态变为 READY
m = ctrl.create_payment_manager(name="x402demo", authorizerType="AWS_IAM",
                                roleArn=outputs["PaymentsServiceRoleArn"])

# 3. Connector,等状态变为 READY
c = ctrl.create_payment_connector(
    paymentManagerId=m["paymentManagerId"], name="privy", type="StripePrivy",
    credentialProviderConfigurations=[{"stripePrivy": {"credentialProviderArn": cp["credentialProviderArn"]}}])

# 4. 嵌入式钱包(ETHEREUM 涵盖 Base 等 EVM 链)
i = dp.create_payment_instrument(
    userId="demo-user", paymentManagerArn=m["paymentManagerArn"], paymentConnectorId=c["paymentConnectorId"],
    paymentInstrumentType="EMBEDDED_CRYPTO_WALLET",
    paymentInstrumentDetails={"embeddedCryptoWallet": {
        "network": "ETHEREUM",
        "linkedAccounts": [{"email": {"emailAddress": os.environ["WALLET_EMAIL"]}}]}},
)["paymentInstrument"]
# → 在 Privy 前端授权并充值

# 5. Session:预算上限(只支持 USD)和有效期(最长 480 分钟)
s = dp.create_payment_session(
    userId="demo-user", paymentManagerArn=m["paymentManagerArn"],
    limits={"maxSpendAmount": {"value": "1.00", "currency": "USD"}},
    expiryTimeInMinutes=480)["paymentSession"]

脚本跑到第 4 步会停下来等你。这时另开一个终端,启动 Privy 前端:

git clone https://github.com/privy-io/aws-agentcore-sdk.git privy-agentcore-frontend
cd privy-agentcore-frontend
cat > .env.local <<'EOF'
NEXT_PUBLIC_PRIVY_APP_ID=<App ID>
PRIVY_APP_SECRET=<App secret>
NEXT_PUBLIC_PRIVY_SIGNER_ID=<Authorization ID>
NEXT_PUBLIC_NETWORK_MODE=testnet
EOF
pnpm install && pnpm dev          # 启动后访问 http://localhost:3000

然后在浏览器里依次操作:

  1. 打开 http://localhost:3000,用 WALLET_EMAIL 登录,输入邮箱收到的 6 位验证码
  2. 点 Connect agent → Give access
  3. 打开 faucet.circle.com,选 Base Sepolia,给脚本打印的钱包地址领 USDC。不需要 ETH,gas 由 facilitator 付
  4. 回到脚本终端按回车。脚本创建 session 后结束

Privy 前端只在这一步用到。授权在撤销之前一直有效,做完可以关掉。

Privy 前端要自己跑,是因为 Privy 不提供托管的授权页面:钱包按 app 隔离,登录页必须跑在 App ID 允许的域名上,前端的服务端接口还要用 App Secret。要给别人用,就把它部署到 Amplify、Vercel 或 ECS,再把正式域名加进 Privy 的 Domains。

3. 运行

src/seller.py 的核心逻辑:

FACILITATOR = "https://www.x402.org/facilitator"
REQUIREMENT = {
    "scheme": "exact",
    "network": "eip155:84532",                              # Base Sepolia
    "amount": "1000",                                       # 0.001 USDC(6 位小数)
    "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",  # Base Sepolia 上的 USDC 合约
    "payTo": os.environ["SELLER_ADDRESS"],
    "maxTimeoutSeconds": 60,
    "extra": {"name": "USDC", "version": "2"},
}

def facilitator(path, payment):
    # 传的是自己的 REQUIREMENT,由 facilitator 按它强制校验金额和收款人
    body = json.dumps({"x402Version": 2, "paymentPayload": payment, "paymentRequirements": REQUIREMENT}).encode()
    req = urllib.request.Request(FACILITATOR + path, body,
                                 {"Content-Type": "application/json", "User-Agent": "agentcore-payment-x402-demo"})
    ...

def do_GET(self):
    signature = self.headers.get("PAYMENT-SIGNATURE")
    if not signature:                                   # 没付款:返回 402,告诉对方要付什么
        return self.reply(402, {...}, {"PAYMENT-REQUIRED": b64({
            "x402Version": 2, "resource": {...}, "accepts": [REQUIREMENT]})})

    payment = json.loads(base64.b64decode(signature))
    if not facilitator("/verify", payment).get("isValid"):   # 验证签名、金额、余额
        return self.reply(402, ...)
    settled = facilitator("/settle", payment)                # 提交上链
    if not settled.get("success"):
        return self.reply(402, ...)
    self.reply(200, CONTENT, {"PAYMENT-RESPONSE": b64(settled)})

src/agent.py:

import json
import os
import sys
from pathlib import Path

from bedrock_agentcore.payments.integrations.config import AgentCorePaymentsPluginConfig
from bedrock_agentcore.payments.integrations.strands.plugin import AgentCorePaymentsPlugin
from strands import Agent
from strands.models import BedrockModel

state = json.loads(Path("payments.json").read_text())
url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:4021/premium"

payments = AgentCorePaymentsPlugin(config=AgentCorePaymentsPluginConfig(
    payment_manager_arn=state["paymentManagerArn"],
    user_id=state["userId"],
    payment_instrument_id=state["paymentInstrumentId"],
    payment_session_id=state["paymentSessionId"],
    region=state["region"],
))

agent = Agent(
    model=BedrockModel(model_id=os.environ.get("MODEL_ID", "us.anthropic.claude-sonnet-5"), region_name=state["region"]),
    system_prompt="You fetch paid web content for the user with http_request. Payments are handled for you.",
    plugins=[payments],   # 插件自带 http_request 工具,自动处理 402 → ProcessPayment → 带签名重试
)
agent(f"Fetch {url} and tell me what it says.")

插件使用进程的默认 AWS 凭证,所以用 AWS_PROFILE=x402-agent 就能以 AgentRole 身份运行。

先启动卖家,再以 AgentRole 身份跑 agent:

# 终端 1
.venv/bin/python src/seller.py

# 终端 2(profile 只需配置一次)
aws configure set profile.x402-agent.role_arn $(aws cloudformation describe-stacks --stack-name agentcore-x402-payment-demo \
  --query "Stacks[0].Outputs[?OutputKey=='AgentRoleArn'].OutputValue" --output text)
aws configure set profile.x402-agent.source_profile <管理员profile>
aws sts get-caller-identity --profile x402-agent     # 应该显示 assumed-role/...AgentRole...

AWS_PROFILE=x402-agent .venv/bin/python src/agent.py

验证:卖家终端会打印 https://sepolia.basescan.org/tx/0x...,这就是链上结算记录。

session 8 小时后过期。用管理员身份重新跑一遍 src/setup_payments.py,已经完成的步骤会跳过,只新建一个 session。

踩过的坑

  • CreatePaymentInstrument: Failed to obtain resource payment token:payments.json 里残留了另一个钱包提供方的旧 credential provider,connector 指向了错误的凭证。删掉 AWS 上错误的 connector 和 credential provider,再从 payments.json 里删掉对应的键,重跑。现在脚本在 payments.json 里用 privyCredentialProviderArn 这个键记录自己建的 credential provider,不会再误用
  • facilitator 返回 HTTP 403: error code: 1010:x402.org 前面的 Cloudflare 拦截了 Python urllib 默认的 User-Agent,请求里加 User-Agent 头就好
  • localhost:3000 打不开:Privy 前端没启动,执行 pnpm dev
  • Privy 登录失败或报 CORS 错误:Domains 里没有 localhost,在 Privy Dashboard 加上 http://localhost:3000
  • 创建凭证时报私钥格式错误:Dashboard 给的私钥带 wallet-auth: 前缀,脚本已经自动去掉
  • 付款时报 instrument 未激活:没有完成 Connect agent,或者钱包没充值
  • Session expired or budget exceeded:session 过期或预算用完,重跑 src/setup_payments.py
  • 页面控制台里有 WalletConnect 的 CSP 报错:Privy 前端的 CSP 策略拦了 WalletConnect,不影响邮箱登录

没验证的部分

  • 只在 Base Sepolia 测试网跑过,没有在主网跑过
  • infra.yaml 里 Secrets Manager 的权限范围(bedrock-agentcore-identity!*)是按命名规律推断的。src/setup_payments.py 第 1 步会打印真实的 secret ARN,名字对不上会给出警告。上生产时要改成精确的 ARN
  • 不要把权限很少的 assumed role 用作 source_profile,它可能连 CloudFormation 都调不了
  • Privy 前端是官方的参考实现,仓库自己写明不是生产级代码

清理

用管理员身份执行,ID 都在 payments.json 里:

R=--region=us-west-2
aws bedrock-agentcore delete-payment-instrument $R --payment-manager-arn <paymentManagerArn> --payment-connector-id <paymentConnectorId> --payment-instrument-id <paymentInstrumentId> --user-id demo-user
aws bedrock-agentcore-control delete-payment-connector $R --payment-manager-id <paymentManagerId> --payment-connector-id <paymentConnectorId>
aws bedrock-agentcore-control delete-payment-manager $R --payment-manager-id <paymentManagerId>
aws bedrock-agentcore-control delete-payment-credential-provider $R --name agentcore-payment-x402-demo-privy
aws cloudformation delete-stack $R --stack-name agentcore-x402-payment-demo
rm payments.json

参考

← 全部文章