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,本地:4021agent.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
一次付款发生了什么
- agent 请求
GET /premium - 卖家返回
402和PAYMENT-REQUIRED(base64 JSON,写明付多少、哪条链、付给谁) - 插件调用 AgentCore Payments 的
ProcessPayment。它先检查 session 有没有过期、预算够不够,再用PaymentsServiceRole取出 Privy 凭证,让 Privy 用钱包签一个 EIP-3009 授权。这一步还没有转账 - agent 带着
PAYMENT-SIGNATURE再请求一次/premium - 卖家把签名交给 facilitator 的
/verify,通过后再调/settle上链 - 卖家返回
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 里:
- Settings → API keys:拿到 App ID 和 App secret
- Wallet infrastructure → Authorization → New key:Key ID 是
PRIVY_AUTHORIZATION_ID,Private key 是PRIVY_AUTHORIZATION_PRIVATE_KEY。私钥只在创建时显示一次,格式是wallet-auth:... - App Settings → Basics → Domains:加上
http://localhost:3000
Privy 方案不需要订阅 AWS Marketplace,只有 Coinbase 方案需要。
仓库里的文件:
infra/infra.yaml:CloudFormation,2 个 IAM rolesrc/setup_payments.py:一次性配置,结果写入payments.json(manager、钱包、session 的 ID)src/seller.py:卖家(x402 v2),:4021src/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!*的 secretAgentRole:信任本账号。权限是调用 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
然后在浏览器里依次操作:
- 打开
http://localhost:3000,用WALLET_EMAIL登录,输入邮箱收到的 6 位验证码 - 点 Connect agent → Give access
- 打开 faucet.circle.com,选 Base Sepolia,给脚本打印的钱包地址领 USDC。不需要 ETH,gas 由 facilitator 付
- 回到脚本终端按回车。脚本创建 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 拦截了 Pythonurllib默认的 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