1
0
mirror of synced 2026-08-05 11:07:15 +00:00

docs(08-agents-that-transact): convert payments getting-started to AgentCore CLI + SDK (#1767)

---------

Co-authored-by: Anil Nadiminti <anilnadi@amazon.com>
Co-authored-by: deepaxs <deepaxs@amazon.com>
This commit is contained in:
Anil Nadiminti
2026-07-15 06:46:00 -04:00
committed by GitHub
parent 790838a830
commit eda4f2bb09
49 changed files with 2223 additions and 1744 deletions
@@ -0,0 +1,34 @@
# AgentCore payments getting-started — local artifacts to keep out of git.
# (Secrets like .env, plus .venv / __pycache__, are already covered by the repo-root .gitignore;
# this file adds the scaffolded AgentCore CLI project directories these tutorials generate.)
# Scaffolded AgentCore CLI projects (created by `agentcore create --name <X>`).
# These contain generated CDK/Docker/config and an .env.local with provider credentials —
# never commit them.
PaymentSetup/
PaymentAgent/
PaymentOrchestrator/
BazaarAgent/
# Any other agentcore project scaffold (agentcore.json marks the project root) + its local config.
**/agentcore.json
**/aws-targets.json
**/.env.local
.bedrock_agentcore/
.bedrock_agentcore.yaml
cdk.out/
# Local test scratch (venv, caches, secret .env, and any .env backups) — belt-and-suspenders
# even though the repo root already ignores these.
.venv/
__pycache__/
*.pyc
.env
.env.*
!.env.sample
!.env.coinbase.sample
!.env.privy.sample
# Local-only authoring/review tooling — never part of the published tutorials.
.preview_server.py
REVIEW_NOTES.md
@@ -27,9 +27,7 @@ AWS_REGION=us-west-2
# ── Coinbase CDP Credentials ─────────────────────────────────────
# TUTORIAL ONLY: For local testing, store credentials in .env (git-ignored).
# DEPLOYED WORKLOADS: Use AWS Secrets Manager or Systems Manager Parameter Store.
# See: https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html
# Get these from portal.cdp.coinbase.com (see providers/coinbase_cdp_account_setup.ipynb)
# Get these from portal.cdp.coinbase.com (see providers/coinbase_cdp_account_setup.py)
# API Key ID + Secret: from project API key creation
# Wallet Secret: from Wallet → ServerWallet
COINBASE_API_KEY_ID=<your-coinbase-api-key-id>
@@ -14,7 +14,7 @@
#
# .env lives in the parent 00-getting-started/ directory, shared across all tutorials.
#
# Note: The Privy provider setup notebook (providers/stripe_privy_account_setup.ipynb)
# Note: The Privy provider setup script (providers/stripe_privy_account_setup.py)
# writes most of these values automatically via input prompts. You only need to set
# AWS_REGION, NETWORK, LINKED_EMAIL, and USER_ID manually.
@@ -31,10 +31,8 @@ AWS_REGION=us-west-2
# ── Privy Credentials ────────────────────────────────────────────
# TUTORIAL ONLY: For local testing, store credentials in .env (git-ignored).
# DEPLOYED WORKLOADS: Use AWS Secrets Manager or Systems Manager Parameter Store.
# See: https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html
# Get these from dashboard.privy.io (see providers/stripe_privy_account_setup.ipynb)
# The provider notebook writes these automatically — only fill manually if skipping it.
# Get these from dashboard.privy.io (see providers/stripe_privy_account_setup.py)
# The provider setup script writes these automatically — only fill manually if skipping it.
PRIVY_APP_ID=<your-privy-app-id>
PRIVY_APP_SECRET=<your-privy-app-secret>
PRIVY_AUTHORIZATION_ID=<p256-authorization-key-id>
@@ -22,6 +22,6 @@
# Then fill in your credentials and email address.
#
# For multi-provider setup (Tutorial 07), run both provider setup
# notebooks — they write prefixed keys (COINBASE_*, PRIVY_*) into
# scripts — they write prefixed keys (COINBASE_*, PRIVY_*) into
# the same .env without conflicts.
#
@@ -1,214 +1,381 @@
# Tutorial 00 — Setting Up Amazon Bedrock AgentCore payments
# Tutorial 00 — Set Up AgentCore payments
| Information | Details |
|:--------------------|:---------------------------------------------------------------------|
| Tutorial type | Task-based setup |
| Tutorial components | IAM roles, Payment Manager, Connector, Embedded Wallet, Session |
| Wallet providers | Coinbase CDP and/or Stripe (Privy) |
| Networks | Base Sepolia (ETHEREUM), Solana Devnet (SOLANA) |
| SDK used | boto3, bedrock-agentcore |
| Example complexity | Easy |
| Information | Details |
|:--------------------|:---------------------------------------------------------------------------------|
| Tutorial type | One-time setup (every downstream tutorial builds on it) |
| Agent type | None — this tutorial provisions payment infrastructure |
| Framework | Strands (scaffolded project host; no agent code runs here) |
| LLM model | None |
| Components | AgentCore CLI (`create` / `add payment-manager` / `add payment-connector` / `deploy`) + AgentCore SDK (`PaymentManager.create_payment_instrument` / `create_payment_session`) |
| Example complexity | Beginner |
> **Complementary tools.** The AgentCore CLI provisions your shared payment infrastructure
> (credential provider, payment manager, connector, IAM roles) in one `agentcore deploy`. The AgentCore
> SDK (`PaymentManager`) then creates the per-user wallet (instrument) and spending session,
> scoped to the user you serve. **Reads** provider credentials from `../.env`; **writes**
> `PAYMENT_MANAGER_ARN`, `PAYMENT_MANAGER_ID`, `PAYMENT_CONNECTOR_ID`, `CREDENTIAL_PROVIDER_TYPE`,
> `USER_ID`, `NETWORK`, `INSTRUMENT_ID`, `WALLET_ADDRESS`, `SESSION_ID` back to `../.env`.
> → [How the pieces fit together](../README.md#cli-vs-sdk)
## Overview
Building payment infrastructure for AI agents means solving secure wallet management, deterministic payment limits enforcement, and payment orchestration across evolving protocols. A single agent task may hit dozens of x402 endpoints, each with different payment requirements, while the agent makes non-deterministic decisions about which to call next.
This is the one-time setup that Tutorials 0107 build on. You provision one **PaymentManager**, one
**PaymentConnector** (Coinbase CDP or Stripe/Privy), a **PaymentCredentialProvider**, and the runtime
execution + resource-retrieval **IAM roles** — all with the AgentCore CLI in a single deploy. Then you
create a per-user **PaymentInstrument** (embedded crypto wallet) and a budgeted **PaymentSession** with
the AgentCore SDK (`PaymentManager`). Wallets and sessions are per-user, so you create one of each scoped
to the user you serve — the way you'd build a payment-enabled application. Every resource ID is
written to the shared `../.env` so downstream tutorials pick them up unchanged.
**Amazon Bedrock AgentCore payments** handles all of this. This tutorial creates the complete payment stack that all downstream tutorials depend on: IAM roles with least-privilege separation, a Payment Manager, a Payment Connector, an embedded USDC wallet, and a budgeted Payment Session.
> **Billable resources.** `agentcore deploy` creates real AWS resources (IAM roles, credential
> provider, payment manager/connector). See
> [AgentCore pricing](https://aws.amazon.com/bedrock/agentcore/pricing/). First-time deploy takes a
> few minutes (IAM role propagation); run [Clean Up](#clean-up) when you finish all tutorials.
![AgentCore payments Overview](images/main-image.png)
> **Testnet only.** Base Sepolia (`NETWORK=ETHEREUM`) or Solana Devnet (`NETWORK=SOLANA`), with free
> USDC from [faucet.circle.com](https://faucet.circle.com/). Testnet USDC has no monetary value.
> **Supported regions:** `us-east-1`, `us-west-2`, `eu-central-1`, `ap-southeast-2`. Set `AWS_REGION`
> in `../.env` to one of these.
## Architecture
```
┌───────────────────────────────────────────────────────────┐
│ Developer / Admin │
(ControlPlaneRole)
CredentialProvider → PaymentManager → PaymentConnector
│ One-time setup. Creates the payment stack. │
└─────────────────────────────┬─────────────────────────────┘
┌───────────────────────────────────────────────────────────┐
Application Backend
│ (ManagementRole) │
│ │
CreateInstrument (wallet) → CreateSession (budget)
│ Cannot call ProcessPayment. │
└─────────────────────────┬─────────────────────────────────┘
│ passes sessionId + instrumentId
┌───────────────────────────────────────────────────────────┐
│ Agent runtime │
│ (ProcessPaymentRole) │
│ │
│ ProcessPayment only. Cannot create sessions/instruments. │
└───────────────────────────────────────────────────────────┘
Developer machine AgentCore CLI AWS
│ agentcore create ─────────────────────────►│ (scaffolds PaymentSetup/)
│ agentcore add payment-manager ────────────►│
│ agentcore add payment-connector ──────────►│ (patches project locally,
│ stores creds in .env.local)
│ agentcore deploy -y ──────────────────────►│───────────────────────────►│ Creates:
│ │ │ - ProcessPaymentRole
│ - ResourceRetrievalRole
│ │ │ - PaymentCredentialProvider
│ │ │ - PaymentManager
│ - PaymentConnector
│ PaymentManager.create_payment_instrument() ───────────────────────────►│ Creates embedded wallet
│ PaymentManager.create_payment_session() ──────────────────────────────►│ Creates budgeted session
│ writes PAYMENT_MANAGER_ARN, INSTRUMENT_ID, SESSION_ID, … → ../.env
```
The **ResourceRetrievalRole** is a service role assumed by AgentCore at runtime — you never call it directly.
![Resource Hierarchy](images/resource_hierarchy.png)
### Role Separation
The role separation enforces a security boundary between the application backend and the runtime process:
- **ControlPlaneRole** — Creates and manages Managers, Connectors, Credential Providers. Admin setup.
- **ManagementRole** — Creates Instruments and Sessions with spending limits. Has an explicit IAM **Deny** on `ProcessPayment`. The code that sets budgets never processes payments.
- **ProcessPaymentRole** — Calls `ProcessPayment` only. Cannot create sessions, override limits, or provision wallets.
- **ResourceRetrievalRole** — Service role assumed by AgentCore at runtime to access credentials from Secrets Manager. You never call this directly.
This is structural separation enforced by IAM, not application logic.
![Role Separation](images/role_separation.png)
## x402 Payment Flow
![x402 Payment Flow](images/x402_payment_flow.png)
The x402 protocol embeds payment requirements inside HTTP responses. The agent reads the requirement, calls `ProcessPayment` to generate a signed proof, and retries the request with the proof attached. AgentCore handles signing and budget enforcement — the agent code never touches wallet credentials.
## Supported Provider and Network Combinations
AgentCore payments is provider-agnostic and network-agnostic:
| Provider | Network setting | Faucet network |
|----------|-----------------|----------------|
| Coinbase CDP | `ETHEREUM` | Base Sepolia |
| Coinbase CDP | `SOLANA` | Solana Devnet |
| Stripe (Privy) | `ETHEREUM` | Base Sepolia |
| Stripe (Privy) | `SOLANA` | Solana Devnet |
All combinations use `EMBEDDED_CRYPTO_WALLET` with `linkedAccounts` for user identity. Set `CREDENTIAL_PROVIDER_TYPE` and `NETWORK` in `.env` to choose. Agent code in downstream tutorials is identical regardless of provider or network.
## API Reference
| Operation | Plane | Client | Description |
|:----------|:------|:-------|:------------|
| `CreatePaymentCredentialProvider` | Control | `bedrock-agentcore-control` | Stores wallet credentials securely |
| `CreatePaymentManager` | Control | `bedrock-agentcore-control` | Top-level payment configuration |
| `CreatePaymentConnector` | Control | `bedrock-agentcore-control` | Links Manager to Credential Provider |
| `CreatePaymentInstrument` | Data | `bedrock-agentcore` | Provisions a crypto wallet |
| `CreatePaymentSession` | Data | `bedrock-agentcore` | Time-bounded spending authorization |
The AgentCore CLI provisions the shared stack once, and `agentcore deploy` mints the runtime
execution and resource-retrieval roles for you. Every command in this tutorial runs under your
ambient AWS credentials, which is ideal for learning and prototyping. For the stricter ControlPlane /
Management / ProcessPayment / ResourceRetrieval **4-role separation**, see the boto3
`setup_agentcore_payments.py` reference under [Alternatives](#alternatives).
## Prerequisites
- Python 3.10+
- AWS CLI configured (`aws sts get-caller-identity` to verify)
- AWS account with access to AgentCore payments
- Wallet provider credentials:
- **Coinbase CDP:** See [`providers/coinbase_cdp_account_setup.py`](providers/coinbase_cdp_account_setup.py)
- **Stripe (Privy):** See [`providers/stripe_privy_account_setup.py`](providers/stripe_privy_account_setup.py)
- `.env` configured: copy a provider sample to the parent directory and fill in values, e.g. `cp .env.coinbase.sample ../.env` (for Coinbase CDP) or `cp .env.privy.sample ../.env` (for Stripe/Privy). The `.env` file lives in the parent `00-getting-started/` directory and is shared by every tutorial.
- **AWS account + region** where AgentCore payments is available (`us-east-1`, `us-west-2`,
`eu-central-1`, `ap-southeast-2`), and AWS CLI configured (`aws sts get-caller-identity`).
- **Python 3.10+** and dependencies:
```bash
pip install -r requirements.txt
```
- **AgentCore CLI** (Node.js 20+) — this tutorial runs `agentcore` commands. These tutorials were
validated against the 1.0.0-preview line; run `agentcore --version` to check yours:
```bash
npm install -g @aws/agentcore
```
- **Wallet-provider credentials in `../.env`** — captured in Step 1 by running one of the
`providers/*_account_setup.py` scripts. Coinbase CDP needs `COINBASE_API_KEY_ID`,
`COINBASE_API_KEY_SECRET`, `COINBASE_WALLET_SECRET`; Stripe/Privy needs `PRIVY_APP_ID`,
`PRIVY_APP_SECRET`, `PRIVY_AUTHORIZATION_ID`, `PRIVY_AUTHORIZATION_PRIVATE_KEY`.
- **A funded testnet wallet** — you fund it in Step 4, after the instrument is created.
## Running the Python Scripts
The shared `.env` lives at `../.env` (one directory up, `00-getting-started/.env`). A starter is
provided: `cp .env.coinbase.sample ../.env` (or `.env.privy.sample`).
## Walkthrough
Run these steps top to bottom. Steps 12 use the AgentCore CLI to provision your shared payment
infrastructure; Steps 34 use the AgentCore SDK (`PaymentManager`) to create the per-user wallet and
session, then fund the wallet.
### Step 1 — Capture wallet-provider credentials (pick ONE provider)
These scripts walk you through the provider portal and write the credential keys into `../.env`.
```bash
pip install -r requirements.txt
# Step 1: Provider setup (choose one)
python providers/coinbase_cdp_account_setup.py
# OR
python providers/stripe_privy_account_setup.py
# Step 2: Create the full payment stack
python setup_agentcore_payments.py
# Optional: Multi-provider setup (both Coinbase + Privy on one Manager)
python multi_provider_setup.py
python providers/coinbase_cdp_account_setup.py # Coinbase CDP
# or
python providers/stripe_privy_account_setup.py # Stripe (Privy)
```
## Security Best Practice: Restrict Secret Access
Then set `AWS_REGION`, `CREDENTIAL_PROVIDER_TYPE` (`CoinbaseCDP` or `StripePrivy`), `USER_ID`,
`LINKED_EMAIL` (a real inbox — used for the wallet and provider OTP), and `NETWORK`
(`ETHEREUM` or `SOLANA`) in `../.env`.
After setup, lock down the credential provider secrets in Secrets Manager so only the `ResourceRetrievalRole` can read them. In the [Secrets Manager console](https://console.aws.amazon.com/secretsmanager/), find secrets prefixed with `bedrock-agentcore-identity` and add a resource policy that denies `GetSecretValue` to all principals except `ResourceRetrievalRole`.
### Step 2 — Provision the shared stack with the AgentCore CLI
See the [documentation](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments-iam-roles.html) for the full policy template and [Configure credential provider](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/resource-providers.html) for more details.
Scaffold a project to hold the payment configuration, add the payment manager and connector, then
deploy. Because the manager and connector are added *before* `agentcore deploy`, the CLI creates the
execution role with `ProcessPayment` permissions already attached — no separate IAM patch step.
## Two Identities in Play
```bash
# 1. Scaffold a project to hold the payment configuration
agentcore create --name PaymentSetup --framework Strands --protocol HTTP --model-provider Bedrock --memory none
cd PaymentSetup
These tutorials have you play two roles simultaneously:
# 2. Add a payment manager
agentcore add payment-manager --name MyPaymentManager --auto-payment true --default-spend-limit 1.00
```
| identity | Who | What they do |
|----------|-----|-------------|
| **Developer** | Your AWS credentials | Call AgentCore APIs, create managers/connectors/sessions |
| **End user** | `LINKED_EMAIL` identity | Log into WalletHub or Privy frontend, fund wallet, grant signing permission |
Add a payment connector for the provider you chose in Step 1 — run **one** of these:
In a real application these are different people — your backend code uses AWS credentials, while end users interact via your app's UI. For tutorial simplicity you play both roles with the same email.
```bash
# Coinbase CDP:
agentcore add payment-connector \
--manager MyPaymentManager \
--name MyCoinbaseConnector \
--provider CoinbaseCDP \
--api-key-id <YOUR_CDP_API_KEY_ID> \
--api-key-secret <YOUR_CDP_API_KEY_SECRET> \
--wallet-secret <YOUR_CDP_WALLET_SECRET>
```
### Funding and Delegated Signing
```bash
# OR Stripe/Privy:
agentcore add payment-connector \
--manager MyPaymentManager \
--name MyPrivyConnector \
--provider StripePrivy \
--app-id <YOUR_PRIVY_APP_ID> \
--app-secret <YOUR_PRIVY_APP_SECRET> \
--authorization-id <YOUR_PRIVY_AUTHORIZATION_ID> \
--authorization-private-key <YOUR_PRIVY_PRIVATE_KEY_BASE64>
```
Before the agent can produce x402 payments, two things must happen:
Validate the payment configuration, then deploy. `validate` catches a missing or bad credential value
early instead of letting deploy fail partway through. `deploy` provisions the IAM roles, credential
provider, manager, and connector.
1. **Fund the wallet** with testnet USDC at [faucet.circle.com](https://faucet.circle.com/) — request 20 USDC (covers all tutorials)
2. **Delegate signing** — the end user grants AgentCore permission to sign transactions on their wallet
```bash
# 3. Sanity-check the payment config, then deploy
agentcore validate
agentcore deploy -y
| | Coinbase CDP | Stripe (Privy) |
|---|---|---|
| **Signing grant** | CDP Portal → Wallets → Embedded Wallet → Policies → enable Delegated Signing | Privy reference frontend → log in as `LINKED_EMAIL`**Connect agent → Give access** |
| **Fund + delegate UI** | Coinbase WalletHub (`redirectUrl` in the instrument response) | Privy reference frontend at `http://localhost:3000` |
| **Scope** | All wallets under the project | Per-wallet |
# 4. Read back the created resource ARNs/IDs
agentcore status --type payment
```
## Key Concepts
From the `agentcore status --type payment` output, copy the **Payment Manager ARN** and **Payment
Connector ID** and export them (you'll pass them to the commands in Step 3):
**PaymentManager** — The top-level resource. All payment operations reference the Manager ARN. Created once; shared across all downstream tutorials.
```bash
export PAYMENT_MANAGER_ARN="arn:aws:bedrock-agentcore:...:payment-manager/..."
export PAYMENT_CONNECTOR_ID="payment-connector-..."
```
**PaymentConnector** — Links the Manager to a wallet provider (Coinbase CDP or Stripe/Privy). One Connector per provider; multiple Connectors on one Manager enables multi-provider scenarios (see `multi_provider_setup.py`).
Also write `PAYMENT_MANAGER_ARN`, `PAYMENT_MANAGER_ID` (the ARN's last path segment), and
`PAYMENT_CONNECTOR_ID` to `../.env` — downstream tutorials read them from there.
**PaymentInstrument** — An embedded crypto wallet provisioned for a specific `userId`. The wallet address is assigned when the instrument becomes `ACTIVE`.
> `--default-spend-limit 1.00` applies **only** to `agentcore invoke --auto-session`, where the CLI
> mints a session from the manager's default. In Step 3 you create an explicit budgeted session so
> you have a `SESSION_ID` the downstream tutorials can use.
**PaymentSession** — Time-bounded spending authorization with a `maxSpendAmount` limit. Agents cannot exceed this budget. The session expires after `expiryTimeInMinutes` or when the budget is exhausted, whichever comes first.
### Step 3 — Create the per-user wallet and session with the AgentCore SDK
**Delegated Signing** — Required for `ProcessPayment` to succeed. The end user grants AgentCore permission to sign transactions on their behalf. For Coinbase CDP, this is enabled at the project level in the CDP Portal. For Stripe/Privy, it is done through the Privy reference frontend (localhost:3000) after the wallet is created.
A wallet (instrument) and spending session are per-user resources, so you create one of each for your
`USER_ID` with the AgentCore SDK's `PaymentManager`. Use the same `USER_ID` you set in `../.env` in
Step 1 — every downstream tutorial reads that same value, so they all resolve to this wallet and
session. Pick the `network` that matches your `NETWORK` (`ETHEREUM` → Base Sepolia,
`SOLANA` → Solana Devnet), and use a real inbox for the linked email. Run this from a Python shell (or
paste into a script) with `PAYMENT_MANAGER_ARN` and `PAYMENT_CONNECTOR_ID` from Step 2 in your
environment:
```python
import os, uuid
from bedrock_agentcore.payments import PaymentManager
REGION = os.environ["AWS_REGION"]
PAYMENT_MANAGER_ARN = os.environ["PAYMENT_MANAGER_ARN"]
PAYMENT_CONNECTOR_ID = os.environ["PAYMENT_CONNECTOR_ID"]
USER_ID = os.environ["USER_ID"] # the same USER_ID from ../.env (Step 1)
NETWORK = os.environ.get("NETWORK", "ETHEREUM")
EMAIL = os.environ["LINKED_EMAIL"] # a real inbox — used for the wallet and provider OTP
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
# Create the embedded wallet (instrument). Save paymentInstrumentId, walletAddress, and redirectUrl.
instrument = manager.create_payment_instrument(
user_id=USER_ID,
payment_connector_id=PAYMENT_CONNECTOR_ID,
payment_instrument_type="EMBEDDED_CRYPTO_WALLET",
payment_instrument_details={
"embeddedCryptoWallet": {
"network": NETWORK,
"linkedAccounts": [{"email": {"emailAddress": EMAIL}}],
}
},
client_token=str(uuid.uuid4()),
)
INSTRUMENT_ID = instrument["paymentInstrumentId"]
WALLET_ADDRESS = instrument["paymentInstrumentDetails"]["embeddedCryptoWallet"]["walletAddress"]
REDIRECT_URL = instrument.get("redirectUrl") # WalletHub link used in Step 4 (Coinbase)
print("INSTRUMENT_ID:", INSTRUMENT_ID)
print("WALLET_ADDRESS:", WALLET_ADDRESS)
print("REDIRECT_URL:", REDIRECT_URL)
# Create a budgeted, time-bounded session. Save paymentSessionId.
session = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "1.00", "currency": "USD"}},
expiry_time_in_minutes=60,
)
SESSION_ID = session["paymentSessionId"]
print("SESSION_ID:", SESSION_ID)
```
Write `INSTRUMENT_ID`, the wallet address (`WALLET_ADDRESS`), and `SESSION_ID` (the
`paymentSessionId` from the session response) into `../.env` alongside the manager and connector
IDs. Downstream tutorials read them all via `utils.load_tutorial_env()`.
### Step 4 — Fund the wallet and grant delegated signing (once per user)
1. Fund the wallet at [faucet.circle.com](https://faucet.circle.com/) — pick **Base Sepolia** for
`ETHEREUM`, **Solana Devnet** for `SOLANA` (~20 USDC covers all tutorials). Verify at
`https://sepolia.basescan.org/address/<WALLET_ADDRESS>` for Ethereum.
2. Grant delegated signing so the agent can pay on the user's behalf:
- **Coinbase** — open the WalletHub `REDIRECT_URL` printed in Step 3, sign in as
`LINKED_EMAIL`, and grant signing.
- **Stripe/Privy** — open the Privy reference frontend (`http://localhost:3000`), log in as
`LINKED_EMAIL`, and choose **Connect agent → Give access**.
Until delegated signing is granted, payment attempts report
`Delegated signing grant is not active for the end user wallet.`
## What this setup does
All resource IDs now live in the shared `../.env`, so Tutorials 0107 reuse the same manager,
connector, wallet, and session with no changes.
## Inspect / verify
```bash
# Run from inside PaymentSetup/ — lists managers, connectors, and live status
cd PaymentSetup
agentcore status --type payment
```
Confirm `../.env` now contains `PAYMENT_MANAGER_ARN`, `PAYMENT_MANAGER_ID`, `PAYMENT_CONNECTOR_ID`,
`CREDENTIAL_PROVIDER_TYPE`, `USER_ID`, `NETWORK`, `INSTRUMENT_ID`, `WALLET_ADDRESS`, and `SESSION_ID`.
The SDK `PaymentManager` covers both the session spend detail and the wallet balance:
```python
import os
from bedrock_agentcore.payments import PaymentManager
REGION = os.environ["AWS_REGION"]
PAYMENT_MANAGER_ARN = os.environ["PAYMENT_MANAGER_ARN"]
PAYMENT_CONNECTOR_ID = os.environ["PAYMENT_CONNECTOR_ID"]
INSTRUMENT_ID = os.environ["INSTRUMENT_ID"]
SESSION_ID = os.environ["SESSION_ID"]
NETWORK = os.environ["NETWORK"]
USER_ID = os.environ["USER_ID"]
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
# Session spend detail — remaining vs. maximum
session = manager.get_payment_session(user_id=USER_ID, payment_session_id=SESSION_ID)
print("available:", session["availableLimits"]["availableSpendAmount"])
print("max:", session["limits"]["maxSpendAmount"])
# Wallet balance — get_payment_instrument_balance takes the chain + token
chain = "BASE_SEPOLIA" if NETWORK == "ETHEREUM" else "SOLANA_DEVNET"
balance = manager.get_payment_instrument_balance(
payment_connector_id=PAYMENT_CONNECTOR_ID,
payment_instrument_id=INSTRUMENT_ID,
chain=chain,
token="USDC",
user_id=USER_ID,
)
micro = int(balance["tokenBalance"]["amount"]) # micro-USDC
print(f"balance: {micro / 1_000_000:.2f} USDC")
```
> Payment sessions expire after `expiry_time_in_minutes` (60 min here). If this read returns a
> not-found error, the session expired — create a fresh one with `manager.create_payment_session(...)`.
> Re-running setup (or switching providers) can leave older `INSTRUMENT_ID` / `SESSION_ID` /
> `PAYMENT_CONNECTOR_ID` values in `../.env`. `load_tutorial_env()` resolves the active wallet from
> `CREDENTIAL_PROVIDER_TYPE`, so downstream tutorials use the right one — but if you want a clean
> slate, clear the stale keys.
## Troubleshooting
### CreatePaymentManager returns ConflictException
A Manager with the same name already exists. Either use `idempotent_create()` (already done in the script) or delete the existing Manager first. The script appends a UUID suffix to ensure uniqueness on fresh runs.
### Instrument status stays CREATING
`EMBEDDED_CRYPTO_WALLET` provisioning is asynchronous. The script uses `wait_for_status()` which polls every 5 seconds for up to 5 minutes. If it times out, check the Coinbase CDP or Privy dashboard for errors. Make sure `LINKED_EMAIL` is a real email address.
### ProcessPayment fails with signing error (Tutorial 01)
Delegated signing is not configured. For Coinbase CDP: enable Delegated Signing under CDP Portal → Wallets → Embedded Wallet → Policies. For Stripe/Privy: open the Privy reference frontend at `http://localhost:3000`, log in as `LINKED_EMAIL`, and choose **Connect agent**.
### LINKED_EMAIL not accepted
`LINKED_EMAIL` must be a real, reachable email address. It cannot be a placeholder like `user@example.com` or `<your email>`. The script validates this and exits if not set.
### Missing IAM role permissions
If you see `AccessDenied` errors, run `setup_agentcore_payments.py` from an IAM identity with `iam:CreateRole` and `iam:PutRolePolicy` permissions. The `setup_payment_roles()` call in Step 0a creates the four tutorial roles. You do not need admin access after that.
| Error | Cause | Fix |
|---|---|---|
| `agentcore: command not found` | CLI not installed | `npm install -g @aws/agentcore` |
| `add payment-connector` fails on a missing credential | Required provider flag not provided | Re-check the credential keys in `../.env`; re-run with all flags |
| Payment Manager stuck in `CREATING` | IAM propagation | Wait ~2 min; if `CREATE_FAILED`, check the service role |
| Instrument status stays `CREATING` | Wallet provisioning is async | Ensure `LINKED_EMAIL` is a real address; keep polling |
| `Delegated signing grant is not active` | Consent step not completed | Do Step 4 (funding + signing) |
| Deploy fails with CDK bootstrap error | Account/region not bootstrapped | `cdk bootstrap aws://<account-id>/<region>` |
## Clean Up
> **Warning:** Cleanup is irreversible. Run only after completing all downstream tutorials.
> **Warning:** irreversible. Only after finishing all downstream tutorials.
Uncomment and run the cleanup section at the bottom of `setup_agentcore_payments.py`. It deletes resources in dependency order:
1. Delete the payment instrument (wallet) with the SDK `PaymentManager`:
1. Payment Sessions (data plane)
2. Payment Instruments (data plane)
3. Payment Connectors (control plane)
4. Payment Manager (control plane)
5. Credential Provider (control plane)
```python
import os
from bedrock_agentcore.payments import PaymentManager
After running the script, also:
manager = PaymentManager(
payment_manager_arn=os.environ["PAYMENT_MANAGER_ARN"],
region_name=os.environ["AWS_REGION"],
)
manager.delete_payment_instrument(
payment_instrument_id=os.environ["INSTRUMENT_ID"],
payment_connector_id=os.environ["PAYMENT_CONNECTOR_ID"],
user_id=os.environ["USER_ID"],
)
```
- Delete the four IAM roles from the IAM console.
- Delete CloudWatch log groups: `/aws/vendedlogs/bedrock-agentcore/<manager-id>`.
- For runtime deployments: `agentcore remove all -y` in the agent directory.
2. Remove the connector + manager from the CLI project, then deploy to tear down in AWS:
## Next Steps
```bash
cd PaymentSetup
agentcore remove payment-connector --manager MyPaymentManager --name MyCoinbaseConnector -y # or MyPrivyConnector
agentcore remove payment-manager --name MyPaymentManager -y
agentcore deploy -y # `remove` only updates local config; deploy tears down the AWS resources
```
After completing Tutorial 00, continue to any of the downstream tutorials in any order:
3. Remove the scaffolded runtime project:
- **Tutorial 01** — `../01-agents-payments-and-limits/` — Strands and LangGraph agents with automatic x402 payments
- **Tutorial 02** — `../02-deploy-to-agentcore-runtime/` — Deploy a payment agent to AgentCore runtime
- **Tutorial 03** — `../03-user-onboarding-wallet-funding/` — Wallet lifecycle, funding, delegation
- **Tutorial 04** — `../04-agent-with-coinbase-bazaar-via-gateway/` — Discover paid MCP tools via AgentCore gateway
- **Tutorial 05** — `../05-agent-with-browser-tool-pay-for-content/` — Browser + paywall payment pattern
- **Tutorial 06** — `../06-research-agent-with-payment-memory/` — Recall past data and skip redundant paid calls with AgentCore Memory
- **Tutorial 07** — `../07-multi-agent-payment-orchestrator/` — Multi-agent orchestration with per-agent budgets
```bash
agentcore remove all -y
```
Payment sessions expire automatically. If you used the boto3 `setup_agentcore_payments.py` path
instead, run its cleanup section and delete the four IAM roles + `/aws/vendedlogs/bedrock-agentcore/*`
log groups from the console.
## Alternatives
- **Raw boto3 reference (4-role IAM separation):** `python setup_agentcore_payments.py` provisions the
identical stack via `bedrock-agentcore-control` and writes the **same `../.env` keys**, so
downstream tutorials are unaffected. Use it to see the explicit ControlPlane / Management /
ProcessPayment / ResourceRetrieval role model.
- **Multi-provider (Tutorial 07):** `multi_provider_setup.py` creates one manager with **both**
connectors and writes `COINBASE_`/`PRIVY_`-prefixed `INSTRUMENT_ID` / `CONNECTOR_ID` /
`WALLET_ADDRESS` keys. It reads the three IAM role ARNs (`CONTROL_PLANE_ROLE_ARN`,
`MANAGEMENT_ROLE_ARN`, `RESOURCE_RETRIEVAL_ROLE_ARN`) that the boto3 `setup_agentcore_payments.py`
writes to `../.env`. So for Tutorial 07: run **both** provider scripts in Step 1, then run **`python
setup_agentcore_payments.py` first** (it creates the four IAM roles and writes their ARNs), and
**only then** `python multi_provider_setup.py`.
## Next steps
You're ready for the downstream tutorials — all read the shared `../.env`:
- **[Tutorial 01 — Agents, payments, and limits](../01-agents-payments-and-limits/)** (start here)
- **[Tutorial 02 — Deploy to AgentCore Runtime](../02-deploy-to-agentcore-runtime/)**
- **[Tutorial 07 — Multi-agent payment orchestrator](../07-multi-agent-payment-orchestrator/)**
(needs the multi-provider setup above)
- Full list: the [getting-started index](../README.md#tutorials).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

@@ -42,7 +42,7 @@ print("""
║ Provider Setup: Coinbase Developer Platform (CDP) ║
╚══════════════════════════════════════════════════════════════════════════════╝
MANUAL STEPS (complete in your browser before running the credential cells):
MANUAL STEPS (complete in your browser before running the credential prompts):
Step 1 — Create a Coinbase Account (skip if you have one)
──────────────────────────────────────────────────────────
@@ -57,30 +57,30 @@ Step 2 — Enable Coinbase Developer Platform
────────────────────────────────────────────
1. Go to https://portal.cdp.coinbase.com/
2. Sign in with your Coinbase account.
3. Create a Project (or select an existing one).
3. A default project is created on first sign-in (use it, or create a new one).
Step 3a — Create an API Key
────────────────────────────
1. In the CDP Portal, navigate to your project.
2. Choose Create API key (or New key).
3. Enter a descriptive name (e.g. agentcore-payments-tutorial).
4. Choose Create or Save.
5. Download the keys when prompted.
Step 3a — Create a Secret API Key
──────────────────────────────────
1. In the CDP Portal, go to Settings (bottom-left) → API Keys.
2. Select the **Secret API keys** tab (not Client API key).
3. Choose "Create secret API key".
4. Enter a descriptive name (e.g. agentcore-payments-tutorial).
5. Check "Opt-out of IP allowlisting" (required to enable the Create button).
6. Leave Advanced settings at defaults (View read-only is sufficient).
7. Choose Create.
→ Gives you: API Key ID → COINBASE_API_KEY_ID
API Key Secret → COINBASE_API_KEY_SECRET
Step 3b — Get the Wallet Secret
────────────────────────────────
1. In the CDP Portal, go to Wallets → ServerWallet.
2. Copy the Wallet Secret (sometimes called "wallet passphrase").
Step 3b — Wallet Secret + Delegated Signing
────────────────────────────────────────────
Both are on the same page:
1. In the CDP Portal sidebar, go to Wallets → Non-custodial Wallet → Security tab.
(Direct link: https://portal.cdp.coinbase.com/wallets/non-custodial/security)
2. Under "Generate Wallet Secret": click Generate new and save the secret immediately.
→ Gives you: Wallet Secret → COINBASE_WALLET_SECRET
⚠️ The Wallet Secret is shown only ONCE. Save it immediately.
Step 6 — Enable Delegated Signing (REQUIRED before Tutorial 00)
────────────────────────────────────────────────────────────────
1. In CDP Portal → Wallets → Embedded Wallet → Policies tab.
2. Enable Delegated Signing.
Once enabled, all embedded wallets in this project support delegated signing.
3. Under "Delegated signing": toggle it ON (same page, below the Wallet Secret).
This allows embedded wallets in this project to sign transactions on the user's behalf.
""")
# ── Step 4: Paste credentials and save to .env ────────────────────────────────
@@ -42,7 +42,7 @@ print("""
║ Provider Setup: Stripe (Privy) ║
╚══════════════════════════════════════════════════════════════════════════════╝
MANUAL STEPS (complete in your browser before running the credential cells):
MANUAL STEPS (complete in your browser before running the credential prompts):
Step 1 — Create a Privy App
────────────────────────────
@@ -86,9 +86,9 @@ print("""
MANUAL STEPS in the Privy dashboard:
1. In the Privy dashboard, make sure your app is selected in the left sidebar.
2. Go to Wallet Infrastructure → Authorization.
3. Choose New key.
4. Enter a name (e.g. Demo app key).
2. Go to Wallet infrastructure → Keys and quorums.
3. Choose New key (or "Create key").
4. Enter a name (e.g. agentcore-payment-key).
5. Choose Continue. Privy generates a P-256 keypair and displays the ID + Private Key.
6. Copy BOTH values.
7. Choose Save and close.
@@ -193,7 +193,7 @@ print("""
3. Enter http://localhost:3000
4. Choose Save.
PRODUCTION NOTE: Use HTTPS for production origins. Never reuse dev apps for prod.
NOTE: For a deployed app, use HTTPS origins and a separate Privy app from your dev one.
3e. Log in to verify the Privy reference frontend works:
─────────────────────────────────────────────────────────
@@ -4,6 +4,5 @@ botocore>=1.43.5
bedrock-agentcore>=1.9.0
# Utilities
pexpect
python-dotenv>=1.0.0
requests>=2.31.0
@@ -1,5 +1,5 @@
"""
Set Up Amazon Bedrock AgentCore Payments
Set Up Amazon Bedrock AgentCore payments
Creates the complete payment stack that all downstream tutorials depend on:
IAM roles → CredentialProvider → PaymentManager → PaymentConnector
@@ -243,6 +243,10 @@ resp = idempotent_create(
if resp:
CREDENTIAL_PROVIDER_ARN = resp["credentialProviderArn"]
print(f" credentialProviderArn: {CREDENTIAL_PROVIDER_ARN}")
else:
# Re-run: the credential provider already exists, so reuse the ARN from .env.
CREDENTIAL_PROVIDER_ARN = os.environ["CREDENTIAL_PROVIDER_ARN"]
print(f" Reusing existing credential provider: {CREDENTIAL_PROVIDER_ARN}")
# Security best practice: After setup, lock down the credential provider secrets
# in Secrets Manager so only the ResourceRetrievalRole can read them.
@@ -284,6 +288,11 @@ if resp:
"NETWORK": NETWORK,
},
)
else:
# Re-run: the manager already exists, so reuse the IDs a prior run wrote to .env.
MANAGER_ARN = os.environ["PAYMENT_MANAGER_ARN"]
MANAGER_ID = os.environ.get("PAYMENT_MANAGER_ID", MANAGER_ARN.split("/")[-1])
print(f" Reusing existing PaymentManager: {MANAGER_ID}")
# ── Step 6 — Create Payment Connector ─────────────────────────────────────────
# Links the Manager to the Credential Provider.
@@ -314,6 +323,10 @@ if resp:
)
print(" ✅ PaymentConnector is READY")
update_env_file(ENV_FILE, {"PAYMENT_CONNECTOR_ID": CONNECTOR_ID})
else:
# Re-run: the connector already exists, so reuse the id a prior run wrote to .env.
CONNECTOR_ID = os.environ["PAYMENT_CONNECTOR_ID"]
print(f" Reusing existing PaymentConnector: {CONNECTOR_ID}")
# ── Step 7 — Create Payment Instrument (Embedded Wallet) ──────────────────────
# Provisions an embedded USDC wallet linked to a user identity.
@@ -1,22 +1,44 @@
# Tutorial 01 — Enable Payment Limits on an Agent
# Tutorial 01 — Agents, Payments, and Limits
| Information | Details |
|:--------------------|:-------------------------------------------------------------------|
| Tutorial type | Conversational |
| Agent type | Single |
| Agentic Frameworks | Strands Agents, LangGraph |
| LLM model | Anthropic Claude Sonnet 4.6 |
| Tutorial components | PaymentManager, AgentCorePaymentsPlugin, x402 endpoints, sessions |
| Example complexity | Easy |
| Agent type | Single, payment-enabled |
| Frameworks | Strands Agents, LangGraph |
| LLM model | Anthropic Claude Sonnet 4.6 (`us.anthropic.claude-sonnet-4-6`) |
| Components | `PaymentManager`, `AgentCorePaymentsPlugin`, x402 endpoints, sessions |
| Complexity | Easy |
> **Reads** the shared `.env` from Tutorial 00 (`PAYMENT_MANAGER_ARN`, `USER_ID`, `INSTRUMENT_ID`;
> `NETWORK` optional). **Does** run two local agents that each create a per-run spending session
> in-code with the SDK and pay x402 endpoints automatically under a budget — nothing new is deployed.
> → [How the pieces fit together](../README.md#cli-vs-sdk)
## Overview
This tutorial shows how to build payment-enabled AI agents using the AgentCore payments SDK. Two agent frameworks are demonstrated:
The shared payment stack — payment manager, connector, IAM roles, and a funded wallet (instrument) —
is already provisioned from [Tutorial 00](../00-setup-agentcore-payments/). Here your agent code
uses the AgentCore SDK to open a **spending session** (a per-request budget you set per user) and pay
each HTTP 402 automatically. You run two agents that both call x402-protected endpoints under a
`maxSpendAmount` budget:
- **Strands** — Uses `AgentCorePaymentsPlugin` that intercepts 402 responses and pays automatically. Zero payment logic in agent code.
- **LangGraph** — Uses a `wrap_with_auto_402()` function that wraps any HTTP tool to detect 402 responses, calls `PaymentManager.generate_payment_header()`, and retries. The LLM never sees the 402.
- **Strands** — `AgentCorePaymentsPlugin` intercepts 402 responses from the `http_request` tool and
pays automatically. Zero payment logic in the agent code.
- **LangGraph** — a `wrap_with_auto_402()` wrapper detects a 402, calls
`PaymentManager.generate_payment_header()`, and retries with the proof header. The LLM never sees
the 402.
Both approaches work with either wallet provider (Coinbase CDP or Stripe/Privy) and either network (Ethereum Base Sepolia or Solana Devnet) — the only difference is the instrument ID from Tutorial 00.
Both scripts read the same `PaymentManager` ARN and instrument from `.env`, and work with either
wallet provider (Coinbase CDP or Stripe/Privy) and either network (Ethereum Base Sepolia or Solana
Devnet) — the only thing that changes is the instrument ID from Tutorial 00.
> **Billable resources.** Each successful x402 call spends testnet USDC from your funded wallet and
> is metered by AgentCore payments. See [AgentCore pricing](https://aws.amazon.com/bedrock/agentcore/pricing/).
> **Testnet only.** Use Base Sepolia (network `ETHEREUM`) or Solana Devnet (network `SOLANA`) with
> free USDC from [faucet.circle.com](https://faucet.circle.com/). Testnet USDC has no monetary value.
> **Supported regions:** `us-east-1`, `us-west-2`, `eu-central-1`, `ap-southeast-2`.
## Architecture
@@ -42,8 +64,6 @@ Agent (Strands + http_request tool)
└─► Agent summarizes results for the user
```
![Strands Agent Payment Flow](images/strands_agent_payment_flow.png)
### LangGraph
![LangGraph Payment Flow](images/langgraph_payment_flow.png)
@@ -57,101 +77,190 @@ LangGraph ReAct Agent
└── Returns content to agent (LLM never sees the 402)
```
![LangGraph Agent Payment Flow](images/langgraph_agent_payment_flow.png)
## What You'll Learn
- How `AgentCorePaymentsPlugin` auto-handles x402 in a Strands agent (Strands)
- How to wrap any LangGraph tool with auto-402 payment handling (LangGraph)
- How to create sessions with `maxSpendAmount` budgets
- How budget enforcement works at the infrastructure level
- How to check remaining spend with `get_payment_session`
- How the plugin's built-in tools (`get_payment_session`, `get_payment_instrument`, `list_payment_instruments`) let the agent reason about its own budget
## Payment Limits Patterns
| Pattern | Budget | Expiry | Use Case |
|---------|--------|--------|----------|
| Quick lookup | $0.10 | 5 min | Single API call, price check |
| Research task | $1.00 | 60 min | Multi-endpoint research session |
| Deep analysis | $5.00 | 480 min | Extended multi-tool workflow |
| No budget cap | omit `limits` | 60 min | Trusted internal agents (use with caution) |
### How limits are enforced
| Dimension | How It Works |
|-----------|-------------|
| **Cumulative tracking** | Service sums ALL ProcessPayment calls in the session — not per-call |
| **Rejection** | When cumulative spend + next payment would exceed `maxSpendAmount`, ProcessPayment returns an error |
| **Time expiry** | After `expiryTimeInMinutes`, ProcessPayment fails even if budget remains |
| **IAM enforcement** | Agent role (ProcessPaymentRole) cannot create sessions, modify budgets, or extend expiry |
| **Per-user isolation** | Sessions scoped to `userId` — different users have independent budgets |
| **Budget optional** | Omit `limits` for uncapped sessions — spend tracked via `availableLimits.availableSpendAmount` |
## Prerequisites
- Tutorial 00 completed (`.env` has manager ARN, connector, instrument, session)
- Wallet funded with testnet USDC from [faucet.circle.com](https://faucet.circle.com/)
- Python 3.10+
- **Tutorial 00 completed** — the shared `.env` (one directory up, at
[`00-getting-started/.env`](../)) must contain `PAYMENT_MANAGER_ARN`, `USER_ID`, and
`INSTRUMENT_ID` (`NETWORK` is optional and defaults to `ETHEREUM`). The scripts read these via
`utils.load_tutorial_env()`.
- **Funded wallet with delegated signing granted** — the instrument's wallet must hold testnet USDC
([faucet.circle.com](https://faucet.circle.com/)) and have delegated signing enabled (done in
Tutorial 00). Without it, the 402 payment step fails.
- **Python 3.10+** and AWS credentials configured (`aws sts get-caller-identity`).
- **Python deps:**
```bash
pip install -r requirements.txt
```
- **AgentCore CLI (optional)** — only needed for the inspect step below
(`agentcore status --type payment`). Install with `npm install -g @aws/agentcore` (Node.js 20+).
Everything the agents do in this tutorial is pure SDK.
## Running the Python Scripts
## Walkthrough
### Step 1 — Confirm Tutorial 00 populated the shared `.env`
The agents load their configuration from the shared `.env` one directory up. Confirm the keys they
read are present:
```bash
pip install -r requirements.txt
grep -E 'PAYMENT_MANAGER_ARN|INSTRUMENT_ID|USER_ID|NETWORK' ../.env
```
```bash
# Strands agent with AgentCorePaymentsPlugin
python strands_payment_agent.py
If any of `PAYMENT_MANAGER_ARN`, `INSTRUMENT_ID`, or `USER_ID` is missing, re-run Tutorial 00
([`../00-setup-agentcore-payments/`](../00-setup-agentcore-payments/)) to write the resource IDs
before continuing. (`NETWORK` is optional.)
# LangGraph agent with wrap_with_auto_402()
### Step 2 — Run the Strands agent
```bash
python strands_payment_agent.py
```
The script loads the manager ARN and instrument from `.env`, creates a per-run spending session
in-code with the SDK (`manager.create_payment_session(...)`, budget set by the `SESSION_BUDGET`
constant near the top of the script), wires up `AgentCorePaymentsPlugin`, and asks the agent to fetch
the paid weather endpoint — the plugin settles each HTTP 402 automatically within the session budget.
(This is the flow in the **Strands Payment Flow** diagram under [Architecture](#architecture) above.)
### Step 3 — Run the LangGraph agent
```bash
python langgraph_payment_agent.py
```
## Key Concepts
Same `.env` and instrument; it creates its own in-code session the same way. The script builds the
`wrap_with_auto_402()` wrapper around
a `requests`-based `http_request` tool and runs a streaming ReAct agent against the x402 endpoint
`/api/market-news`. On each 402 it calls
`manager.generate_payment_header()` to sign the proof and retries.
(This is the flow in the **LangGraph Payment Flow** diagram under [Architecture](#architecture) above.)
**AgentCorePaymentsPlugin (Strands)** — A Strands plugin that intercepts 402 responses from any tool, calls `ProcessPayment` via AgentCore to sign the transaction and generate a proof, then retries the original request with the payment proof. The developer writes zero payment logic.
## Try different budgets (payment limits)
**generate_payment_header() (LangGraph)** — A `PaymentManager` method that takes a 402 response and returns an HTTP header containing the payment proof. Used to build `wrap_with_auto_402()` for any LangGraph tool.
Budget enforcement lives on the session, which each agent creates in-code with the SDK. Change the
budget by editing the `SESSION_BUDGET` constant near the top of the script, then re-run the agent.
For example, set a tiny budget smaller than the API cost:
**Session budget** — Created by the app backend (ManagementRole). The agent (ProcessPaymentRole) can only spend within the session budget. The session budget is always the tighter constraint — if your wallet has 10 USDC but the session budget is $0.50, the agent can only spend $0.50.
```python
# strands_payment_agent.py / langgraph_payment_agent.py — near the top
SESSION_BUDGET = {"maxSpendAmount": {"value": "0.0001", "currency": "USD"}}
```
**CAIP-2 network preferences** — Chain identifiers like `eip155:84532` (Base Sepolia) or `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` (Solana Devnet) passed to the plugin via `network_preferences_config`. Tells the plugin which chain to prefer when a merchant supports multiple chains.
Re-run `python strands_payment_agent.py` — the payment is rejected because the $0.0001 budget is
smaller than the API cost. Enforcement is structural (service-level), not agent logic.
To compare two budgets in one run, create a second session in-code with a tiny budget:
```python
tiny = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.0001", "currency": "USD"}},
expiry_time_in_minutes=60,
)
```
Pass `tiny["paymentSessionId"]` to the plugin (or the wrapper) instead of the $1.00 session and
re-run. Omit `limits` entirely from `create_payment_session` for an uncapped session (spend tracked
but not capped). Read a session's remaining budget in-code with the SDK:
```python
sess = manager.get_payment_session(user_id=USER_ID, payment_session_id=SESSION_ID)
print(sess["availableLimits"]["availableSpendAmount"])
```
This is the workshop's division of labor: infrastructure is provisioned once with the agentcore CLI,
each per-user session is created in-code with the SDK, and the agent's `AgentCorePaymentsPlugin` /
`generate_payment_header()` handles the pay-and-retry at request time.
## What the agents do
Each script's default run does the **happy path** — one paid call under a $1.00 session. Strands calls
the weather endpoint; LangGraph calls `/api/market-news`. The remaining scenarios below are exercised
by changing `SESSION_BUDGET` (or creating a second session) and re-running, as described in
[Try different budgets](#try-different-budgets-payment-limits) above:
| Scenario | How to run it | What it shows |
|----------|---------------|---------------|
| Happy path | Default run ($1.00 session) | The 402 → sign → retry → 200 flow, fully automatic |
| Budget session | Set `SESSION_BUDGET` to `$0.50`, re-run | Remaining spend after a paid call (`get_payment_session`) |
| Budget exceeded | Set `SESSION_BUDGET` to `$0.0001` (below API cost), re-run | ProcessPayment rejects the payment at the infra level |
| Built-in tools (Strands) | Default run — agent answers "how much budget is left?" | Plugin tools `get_payment_session` / `get_payment_instrument` / `list_payment_instruments` |
| Uncapped session | Create a session with no `limits` | Spend tracked but not capped — for trusted agents only |
Budget enforcement is cumulative and server-side: the service sums all `ProcessPayment` calls in a
session and rejects the next payment once `maxSpendAmount` would be exceeded, or once the session
expires. The agent role cannot raise its own budget.
## Inspect / verify
```bash
# Live view of managers, connectors, and payment status (requires the AgentCore CLI).
# `status --type payment` reads a scaffolded project's config — run it from the Tutorial 00 project dir:
cd ../00-setup-agentcore-payments/PaymentSetup && agentcore status --type payment
# Confirm the keys the scripts read are present
grep -E 'PAYMENT_MANAGER_ARN|PAYMENT_CONNECTOR_ID|INSTRUMENT_ID|USER_ID|NETWORK' ../.env
```
The scripts read a specific session's remaining spend in-code with the SDK
(`manager.get_payment_session(...)`). Do the same from a quick Python one-liner:
```python
from bedrock_agentcore.payments import PaymentManager
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
sess = manager.get_payment_session(user_id=USER_ID, payment_session_id=SESSION_ID)
print(sess["availableLimits"]["availableSpendAmount"]) # remaining spend
```
Check the funded wallet's balance with the SDK (`chain` and `token` are required; map `NETWORK` to the
chain):
```python
from bedrock_agentcore.payments import PaymentManager
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
chain = "BASE_SEPOLIA" if NETWORK == "ETHEREUM" else "SOLANA_DEVNET"
bal = manager.get_payment_instrument_balance(
payment_connector_id=PAYMENT_CONNECTOR_ID,
payment_instrument_id=INSTRUMENT_ID,
chain=chain,
token="USDC",
user_id=USER_ID,
)
print(bal["tokenBalance"]["amount"] / 1_000_000, "USDC") # micro-USDC → USDC
```
## Troubleshooting
### Agent gets 402 but payment fails
| Symptom | Cause | Fix |
|---|---|---|
| `load_tutorial_env()` raises `FileNotFoundError`, or `PaymentManager` fails on a `None` ARN | Tutorial 00 didn't finish — `../.env` is missing or has no resource IDs | Re-run Tutorial 00 to write the resource IDs to `../.env` |
| Agent gets 402 but payment fails | Delegated signing not granted for the wallet | Coinbase CDP: enable Delegated Signing in CDP Portal → Wallets → Embedded Wallet → Policies. Stripe/Privy: open the Privy reference frontend at `http://localhost:3000`, log in as `LINKED_EMAIL`, choose **Connect agent** |
| Budget exceeded immediately | Session budget below API cost, or wallet has insufficient USDC | Expected for the $0.0001 demo; otherwise fund the wallet at [faucet.circle.com](https://faucet.circle.com/) |
| `invalid_exact_evm_transaction_failed` / settlement failure | Transient on-chain failure (e.g. back-to-back payments) | Retry — funds are not debited on a failed attempt |
| `agentcore: command not found` | CLI not installed (only needed for the inspect step) | `npm install -g @aws/agentcore` |
Delegated signing is not configured. For Coinbase CDP: enable Delegated Signing in CDP Portal → Wallets → Embedded Wallet → Policies. For Stripe/Privy: open the Privy reference frontend at `http://localhost:3000`, log in as `LINKED_EMAIL`, and choose **Connect agent**.
## Clean Up
### Session budget exceeded immediately
This tutorial provisions nothing durable — payment **sessions expire automatically** at
`expiryTimeInMinutes`, so there is nothing to tear down here. The shared manager/connector/instrument
and any deployed runtimes are cleaned up in their owning tutorials. To remove the shared stack from
Tutorial 00 (delete the per-user instrument with the SDK first — see Tutorial 00's Clean Up):
The wallet may have insufficient USDC. Check the balance with `GetPaymentInstrumentBalance` (Tutorial 00 Step 7c). Fund the wallet at [faucet.circle.com](https://faucet.circle.com/).
```bash
cd ../00-setup-agentcore-payments/PaymentSetup
agentcore remove payment-connector --manager MyPaymentManager --name MyCoinbaseConnector -y
agentcore remove payment-manager --name MyPaymentManager -y
agentcore deploy -y # applies the removal in AWS
agentcore remove all -y # removes the scaffolded runtime project
```
### load_tutorial_env() raises KeyError
## Next steps
Tutorial 00 did not complete successfully or `.env` is missing. Re-run `setup_agentcore_payments.py` to create all required resources and write their IDs to `.env`.
## Wallet-Agnostic, Network-Agnostic Design
The agent code in this tutorial does not change based on:
- **Which wallet provider** — Coinbase CDP or Stripe (Privy)
- **Which blockchain network** — Ethereum (Base Sepolia) or Solana (Solana Devnet)
The only thing that changes is the `INSTRUMENT_ID` and `PAYMENT_CONNECTOR_ID` in `.env` — set by Tutorial 00.
| Layer | Network-aware? | What it knows |
|-------|---------------|---------------|
| PaymentManager | No | Authorization policy only |
| PaymentConnector | No | Which provider (Coinbase/Privy), not which chain |
| PaymentInstrument | **Yes** | `network: ETHEREUM` or `network: SOLANA` — set at creation |
| ProcessPayment | **Yes** | Merchant's 402 payload specifies the chain |
| Agent code | **No** | Passes the instrument ID to the plugin |
The control plane (manager, connector, credentials) is set up once and works across all networks. To switch networks, create a new instrument — one data plane API call, no control plane changes.
## Next Steps
- **Tutorial 02** — Deploy this agent to AgentCore runtime with proper role separation using the AgentCore CLI
- **Tutorial 03** — Wallet operations: delegation, funding, balance checks, multi-session patterns
- **Tutorial 04** — Discover and call paid MCP tools on Coinbase Bazaar through AgentCore gateway
- **[Tutorial 02](../02-deploy-to-agentcore-runtime/)** — deploy this agent to AgentCore Runtime with
role separation using the AgentCore CLI.
- **[Tutorial 03](../03-user-onboarding-wallet-funding/)** — per-user wallet onboarding, funding,
delegation, and balance checks.
- **[Tutorial 04](../04-agent-with-coinbase-bazaar-via-gateway/)** — discover and call paid MCP tools
on Coinbase Bazaar through an AgentCore Gateway.
@@ -13,6 +13,11 @@ Payment flow:
├── Retries with proof header
└── Returns content to agent (LLM never sees the 402)
The spending session is created in-code with the AgentCore SDK
(`PaymentManager.create_payment_session`) — one session per agent role, budgeted with
`maxSpendAmount`. To try a different budget, change SESSION_BUDGET below or create a second
session with a tiny budget and re-run (see the README).
Usage:
python langgraph_payment_agent.py
@@ -36,7 +41,7 @@ from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils import load_tutorial_env
from utils import client_token, load_tutorial_env
# ── Load config from Tutorial 00 .env ────────────────────────────────────────
ENV_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
@@ -52,14 +57,17 @@ PAYMENT_MANAGER_ARN = config["payment_manager_arn"]
REGION = config["region"]
USER_ID = config["user_id"]
if config.get("multi_provider"):
INSTRUMENT_ID = config["instruments"][list(config["instruments"].keys())[0]]["instrument_id"]
else:
INSTRUMENT_ID = config["instrument_id"]
# load_tutorial_env resolves instrument_id to the configured provider
# (CREDENTIAL_PROVIDER_TYPE), so single- and multi-provider .env files both work.
INSTRUMENT_ID = config["instrument_id"]
MODEL_ID = os.environ.get("MODEL_ID", "us.anthropic.claude-sonnet-4-6")
NETWORK = os.environ.get("NETWORK", "ETHEREUM")
# Per-run spending budget for this agent's session. Change this value (or create a second
# session with a tiny budget) to watch server-side enforcement — see the README.
SESSION_BUDGET = {"maxSpendAmount": {"value": "1.00", "currency": "USD"}}
# CAIP-2 chain identifiers for network preference
NETWORK_PREFS = (
["eip155:84532", "base-sepolia"] if NETWORK == "ETHEREUM" else ["solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"]
@@ -69,7 +77,11 @@ print(f"Manager: {PAYMENT_MANAGER_ARN}")
print(f"Instrument: {INSTRUMENT_ID}")
print(f"Network: {NETWORK}")
# ── Step 2: Create PaymentManager and Session ─────────────────────────────────
# ── Step 2: Create the payment session; PaymentManager signs each 402 ─────────
# A spending session is the per-user budget the agent spends within. Create one in-code with
# the AgentCore SDK (PaymentManager.create_payment_session). The same PaymentManager then signs
# the x402 proof header for each 402 the agent hits (manager.generate_payment_header). Omit
# `limits` for an uncapped session (spend tracked but not capped).
from bedrock_agentcore.payments import PaymentManager # noqa: E402
payment_manager = PaymentManager(
@@ -77,15 +89,14 @@ payment_manager = PaymentManager(
region_name=REGION,
)
# Create a fresh session — $1.00 budget, 60 minutes
session_response = payment_manager.create_payment_session(
sess = payment_manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "1.00", "currency": "USD"}},
limits=SESSION_BUDGET,
expiry_time_in_minutes=60,
client_token=client_token(),
)
SESSION_ID = session_response["paymentSessionId"]
print("PaymentManager ready")
print(f"Session created: {SESSION_ID} ($1.00 USD, 60 min)")
SESSION_ID = sess["paymentSessionId"]
print(f"Created payment session: {SESSION_ID} (budget {SESSION_BUDGET['maxSpendAmount']['value']} USD)")
# ── Step 3: Build the Auto-402 Tool Wrapper ───────────────────────────────────
@@ -211,7 +222,7 @@ for chunk, metadata in agent.stream(
"messages": [
(
"user",
"Access this paid weather API and tell me what data you get back: "
"Access this paid market-news API and tell me what data you get back: "
"https://x402-test.genesisblock.ai/api/market-news "
"Report the data and how much it cost.",
)
@@ -244,97 +255,8 @@ for i, resp in enumerate(collected_tool_responses):
print(f"Response #{i + 1}: {str(resp)[:500]}")
# ── Step 6: Payment Limits ────────────────────────────────────────────────────
print("\n── Step 6a: Budget session ($0.50) ──")
budget_session = payment_manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.50", "currency": "USD"}},
expiry_time_in_minutes=60,
)
budget_session_id = budget_session["paymentSessionId"]
print(f"Budget session: {budget_session_id} ($0.50 USD, 60 min)")
budget_http = wrap_with_auto_402(http_tool, payment_manager, USER_ID, INSTRUMENT_ID, budget_session_id, NETWORK_PREFS)
budget_agent = create_agent(model, [budget_http], system_prompt=SYSTEM_PROMPT)
for chunk, metadata in budget_agent.stream(
{
"messages": [
(
"user",
"Access this CDP discovery endpoint, pull one result and show the content: "
"https://api.cdp.coinbase.com/platform/v2/x402/discovery/search?query=market-news&network=base-sepolia",
)
]
},
stream_mode="messages",
):
if chunk.type == "AIMessageChunk":
if isinstance(chunk.content, list):
for block in chunk.content:
if isinstance(block, dict) and block.get("type") == "text":
print(block["text"], end="", flush=True)
elif isinstance(chunk.content, str) and chunk.content:
print(chunk.content, end="", flush=True)
print()
# Check remaining budget
session_info = payment_manager.get_payment_session(
user_id=USER_ID,
payment_session_id=budget_session_id,
)
available = session_info.get("availableLimits", {}).get("availableSpendAmount", {})
limit = session_info.get("limits", {}).get("maxSpendAmount", {})
print(f"Budget: ${limit.get('value', 'N/A')} {limit.get('currency', '')}")
print(f"Remaining: ${available.get('value', 'N/A')} {available.get('currency', '')}")
print("\n── Step 6b: Budget exceeded demo ($0.0001 — less than API cost) ──")
tiny_session = payment_manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.0001", "currency": "USD"}},
expiry_time_in_minutes=60,
)
tiny_session_id = tiny_session["paymentSessionId"]
print(f"Tiny session: {tiny_session_id} (budget: $0.0001 USD)")
tiny_http = wrap_with_auto_402(http_tool, payment_manager, USER_ID, INSTRUMENT_ID, tiny_session_id, NETWORK_PREFS)
tiny_agent = create_agent(model, [tiny_http], system_prompt=SYSTEM_PROMPT)
try:
for chunk, metadata in tiny_agent.stream(
{
"messages": [
(
"user",
"Access this CDP discovery search, pull one result and show the content: "
"https://api.cdp.coinbase.com/platform/v2/x402/discovery/search?query=market-news&network=base-sepolia",
)
]
},
stream_mode="messages",
):
if chunk.type == "AIMessageChunk":
if isinstance(chunk.content, list):
for block in chunk.content:
if isinstance(block, dict) and block.get("type") == "text":
print(block["text"], end="", flush=True)
elif isinstance(chunk.content, str) and chunk.content:
print(chunk.content, end="", flush=True)
print()
except Exception as e:
print("\nBudget exceeded — payment rejected by the service:")
print(f" {e}")
print("\n Expected: budget ($0.0001) is smaller than API cost.")
print(" Budget enforcement is at the infrastructure level, not application code.")
print("\n── Step 6c: Uncapped session (no spending limit) ──")
uncapped_session = payment_manager.create_payment_session(
user_id=USER_ID,
expiry_time_in_minutes=60,
# No limits — spend is tracked but not capped
)
uncapped_id = uncapped_session["paymentSessionId"]
print(f"Uncapped session: {uncapped_id}")
print("No budget limit — spend tracked but not enforced")
print("Use with caution — only for trusted internal agents")
print("\nDone. Next: python ../02-deploy-to-agentcore-runtime/deploy_payment_agent.py")
# To try smaller/uncapped budgets and watch server-side enforcement, edit SESSION_BUDGET above —
# see the README "Try different budgets" section.
print("\nDone. Change SESSION_BUDGET (see the README's limits exercise) to watch budget")
print("enforcement, or continue: follow ../02-deploy-to-agentcore-runtime/README.md to deploy")
print("payment_agent.py to AgentCore Runtime with the AgentCore CLI.")
@@ -21,11 +21,16 @@ What happens under the hood:
└─► Agent summarizes results for the user
The spending session is created in-code with the AgentCore SDK
(`PaymentManager.create_payment_session`) — one session per agent role, budgeted with
`maxSpendAmount`. To try a different budget (for example the budget-exceeded demo), change
SESSION_BUDGET below or create a second session with a tiny budget and re-run (see the README).
Usage:
python strands_payment_agent.py
Prerequisites:
- Tutorial 00 completed (.env has manager ARN, connector, instrument, session)
- Tutorial 00 completed (.env has manager ARN, connector, instrument)
- Wallet funded with testnet USDC from https://faucet.circle.com/
- pip install -r requirements.txt
"""
@@ -37,7 +42,7 @@ import boto3
from dotenv import load_dotenv
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils import load_tutorial_env, print_summary
from utils import client_token, load_tutorial_env, print_summary
# ── Load config from Tutorial 00 .env ────────────────────────────────────────
ENV_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
@@ -54,18 +59,17 @@ PAYMENT_MANAGER_ARN = config["payment_manager_arn"]
REGION = config["region"]
USER_ID = config["user_id"]
# Handle both single-provider and multi-provider configs
if config.get("multi_provider"):
PROVIDER = list(config["instruments"].keys())[0]
INSTRUMENT_ID = config["instruments"][PROVIDER]["instrument_id"]
CONNECTOR_ID = config["instruments"][PROVIDER]["connector_id"]
else:
INSTRUMENT_ID = config["instrument_id"]
CONNECTOR_ID = config.get("connector_id")
PROVIDER = config.get("provider_type", "unknown")
# load_tutorial_env resolves instrument_id to the provider you configured
# (CREDENTIAL_PROVIDER_TYPE), so single- and multi-provider .env files both work here.
INSTRUMENT_ID = config["instrument_id"]
PROVIDER = config.get("active_provider") or config.get("provider_type", "unknown")
NETWORK = os.environ.get("NETWORK", "ETHEREUM")
# Per-run spending budget for this agent's session. Change this value (or create a second
# session with a tiny budget) to watch server-side enforcement — see the README.
SESSION_BUDGET = {"maxSpendAmount": {"value": "1.00", "currency": "USD"}}
# CAIP-2 chain identifiers for network preference
NETWORK_PREFS = (
["eip155:84532", "base-sepolia"] if NETWORK == "ETHEREUM" else ["solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"]
@@ -78,7 +82,10 @@ print_summary(
instrument_id=INSTRUMENT_ID,
)
# ── Step 2: Create Payment Session and Plugin ─────────────────────────────────
# ── Step 2: Create the payment session and configure the plugin ───────────────
# A spending session is the per-user budget the agent spends within. Create one in-code with
# the AgentCore SDK (PaymentManager.create_payment_session). The plugin then settles each 402
# within this budget. Omit `limits` for an uncapped session (spend tracked but not capped).
from bedrock_agentcore.payments import PaymentManager # noqa: E402
from bedrock_agentcore.payments.integrations.strands import ( # noqa: E402
AgentCorePaymentsPlugin,
@@ -86,17 +93,16 @@ from bedrock_agentcore.payments.integrations.strands import ( # noqa: E402
)
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
# Create a fresh session for this agent run — $1.00 budget, 60 minutes
session_response = manager.create_payment_session(
sess = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "1.00", "currency": "USD"}},
limits=SESSION_BUDGET,
expiry_time_in_minutes=60,
client_token=client_token(),
)
SESSION_ID = session_response["paymentSessionId"]
print(f"Session created: {SESSION_ID} ($1.00 USD, 60 min)")
SESSION_ID = sess["paymentSessionId"]
print(f"Created payment session: {SESSION_ID} (budget {SESSION_BUDGET['maxSpendAmount']['value']} USD)")
# Configure the payment plugin with the fresh session
# Configure the payment plugin with the in-code session
payment_plugin = AgentCorePaymentsPlugin(
config=AgentCorePaymentsPluginConfig(
payment_manager_arn=PAYMENT_MANAGER_ARN,
@@ -139,112 +145,41 @@ result = agent(
)
print(result.message)
def _abort_if_payment_blocked(res):
"""Stop cleanly if a payment couldn't settle.
On a failed payment the AgentCorePaymentsPlugin raises an interrupt rather than
returning a normal answer. The most common cause is that delegated signing has not
been granted for the wallet yet. Surface that clearly instead of continuing (which
would crash the next agent() call trying to resume an unhandled interrupt).
"""
if getattr(res, "stop_reason", None) == "interrupt" or getattr(res, "interrupts", None):
print(
"\n⚠️ The payment did not settle, so the run can't continue.\n"
" Most likely cause: delegated signing isn't active for this wallet yet.\n"
" Grant it (once per wallet), then re-run:\n"
" • Coinbase — open the WalletHub redirect URL from Tutorial 00 Step 3 and grant signing\n"
" • Stripe/Privy — open http://localhost:3000, log in, choose 'Connect agent → Give access'\n"
" See Tutorial 00 Step 4 or Tutorial 03 for the full delegation walkthrough."
)
sys.exit(1)
_abort_if_payment_blocked(result)
# ── Step 5: Payment Limits ────────────────────────────────────────────────────
print("\n── Step 5: Payment Limits ──")
# Create a session with $0.50 budget
new_session = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.50", "currency": "USD"}},
expiry_time_in_minutes=60,
)
new_session_id = new_session["paymentSessionId"]
print(f"Budget session: {new_session_id} ($0.50 USD, 60 min)")
budget_plugin = AgentCorePaymentsPlugin(
config=AgentCorePaymentsPluginConfig(
payment_manager_arn=PAYMENT_MANAGER_ARN,
user_id=USER_ID,
payment_instrument_id=INSTRUMENT_ID,
payment_session_id=new_session_id,
region=REGION,
network_preferences_config=NETWORK_PREFS,
)
)
budget_agent = Agent(
model=BedrockModel(model_id=MODEL_ID, streaming=True),
tools=[http_request],
plugins=[budget_plugin],
system_prompt=SYSTEM_PROMPT,
)
result = budget_agent(
"Access this paid weather API and summarize the data: https://x402-test.genesisblock.ai/api/weather"
)
# To try smaller/uncapped budgets and watch server-side enforcement, edit SESSION_BUDGET above —
# see the README "Try different budgets" section.
#
# The plugin also registers built-in tools the agent can call to reason about its own budget:
print("\n── Step 5: Budget-aware tools ──")
result = agent("How much budget do I have left in my current session?")
print(result.message)
# Check remaining budget
session_info = manager.get_payment_session(
user_id=USER_ID,
payment_session_id=new_session_id,
)
available = session_info.get("availableLimits", {}).get("availableSpendAmount", {})
print_summary(
"Budget Status",
session_id=new_session_id,
remaining_budget=f"${available.get('value', 'N/A')} {available.get('currency', '')}",
budget_limit=session_info.get("limits", {}).get("maxSpendAmount", "N/A"),
)
# ── Step 5b: Budget exceeded demo ─────────────────────────────────────────────
print("\n── Step 5b: Budget Exceeded Demo ──")
tiny_session = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.0001", "currency": "USD"}},
expiry_time_in_minutes=60,
)
tiny_session_id = tiny_session["paymentSessionId"]
print(f"Tiny session: {tiny_session_id} (budget: $0.0001 USD — less than API cost)")
tiny_plugin = AgentCorePaymentsPlugin(
config=AgentCorePaymentsPluginConfig(
payment_manager_arn=PAYMENT_MANAGER_ARN,
user_id=USER_ID,
payment_instrument_id=INSTRUMENT_ID,
payment_session_id=tiny_session_id,
region=REGION,
network_preferences_config=NETWORK_PREFS,
)
)
tiny_agent = Agent(
model=BedrockModel(model_id=MODEL_ID, streaming=True),
tools=[http_request],
plugins=[tiny_plugin],
system_prompt=SYSTEM_PROMPT,
)
try:
result = tiny_agent("Access this paid weather API: https://x402-test.genesisblock.ai/api/weather")
print(result.message)
except Exception as e:
print("Budget exceeded — payment rejected by the service:")
print(f" {e}")
print("\n Expected: the budget ($0.0001) is smaller than the API cost.")
print(" Budget enforcement is at the infrastructure level, not application code.")
# ── Step 5c: Plugin built-in tools ────────────────────────────────────────────
print("\n── Step 5c: Built-in Payment Tools ──")
# The plugin registers get_payment_session, get_payment_instrument, list_payment_instruments
result = budget_agent("How much budget do I have left in my current session?")
result = agent("What payment instruments (wallets) do I have available?")
print(result.message)
result = budget_agent("What payment instruments (wallets) do I have available?")
print(result.message)
# ── Step 5d: Uncapped session ─────────────────────────────────────────────────
print("\n── Step 5d: Uncapped Session ──")
uncapped_session = manager.create_payment_session(
user_id=USER_ID,
expiry_time_in_minutes=60,
# No limits — spend is tracked but not capped
)
uncapped_id = uncapped_session["paymentSessionId"]
print(f"Uncapped session: {uncapped_id}")
print("No budget limit — spend tracked but not enforced")
print("Use with caution — only for trusted internal agents")
# ── Step 6: Observability ─────────────────────────────────────────────────────
print("\n── Step 6: Observability ──")
PAYMENT_MANAGER_ID = os.environ.get("PAYMENT_MANAGER_ID", PAYMENT_MANAGER_ARN.split("/")[-1])
@@ -252,4 +187,5 @@ print(f"CloudWatch Logs: /aws/vendedlogs/bedrock-agentcore/{PAYMENT_MANAGER_ID}"
print(f"Console: https://{REGION}.console.aws.amazon.com/cloudwatch/home?region={REGION}#logsV2:log-groups")
print(f"X-Ray: https://{REGION}.console.aws.amazon.com/cloudwatch/home?region={REGION}#xray:traces")
print("\nDone. Next: python ../02-deploy-to-agentcore-runtime/deploy_payment_agent.py")
print("\nDone. Next: follow ../02-deploy-to-agentcore-runtime/README.md to deploy payment_agent.py")
print("to AgentCore Runtime with the AgentCore CLI.")
@@ -1,157 +1,272 @@
# Tutorial 02 — Deploy Payment Agent to AgentCore runtime
# Tutorial 02 — Deploy a Payment Agent to AgentCore Runtime
| Information | Details |
|:--------------------|:-------------------------------------------------------------------|
| Tutorial type | runtime deployment |
| Agent type | Single, payment-enabled |
| Agentic Framework | Strands Agents |
| LLM model | Anthropic Claude Sonnet 4.6 |
| Tutorial components | AgentCore runtime, AgentCorePaymentsPlugin, ProcessPaymentRole |
| Example complexity | Intermediate |
| Information | Details |
|:--------------------|:-----------------------------------------------------------------------------|
| Tutorial type | Runtime deployment |
| Agent type | Single, payment-enabled |
| Agentic Framework | Strands Agents |
| LLM model | Anthropic Claude Sonnet 4.6 (`us.anthropic.claude-sonnet-4-6`) |
| Components | AgentCore CLI (`create` / `deploy` / `status` / `invoke` / `logs` / `remove`), AgentCore Runtime, `AgentCorePaymentsPlugin`, AgentCore SDK `PaymentManager` (`create_payment_session` / `get_payment_session`) |
| Example complexity | Intermediate |
> **Reads** `PAYMENT_MANAGER_ARN`, `USER_ID`, `INSTRUMENT_ID`, `AWS_REGION` from the shared `.env`. **Does** deploy `payment_agent.py` to AgentCore Runtime with the CLI, mint a budgeted session with the AgentCore SDK `PaymentManager`, and invoke the deployed agent over HTTPS — writing `AGENT_RUNTIME_ARN` back to `.env`. → [How the pieces fit together](../README.md#cli-vs-sdk)
## Overview
Tutorial 01 ran a payment-enabled agent locally. This tutorial deploys the same agent to **AgentCore runtime** so it can be invoked over HTTPS with SigV4 auth from any AWS-authenticated client.
Tutorial 01 ran a payment-enabled Strands agent locally. Here you deploy the **same agent** to **AgentCore Runtime** with the **AgentCore CLI** so it can be invoked over HTTPS with SigV4 auth from any AWS-authenticated client. You use two complementary tools: the **AgentCore CLI** provisions and runs the runtime (`agentcore create``deploy``invoke``logs`), and the **AgentCore SDK** `PaymentManager` (`manager.create_payment_session(...)`) mints each request's budgeted payment session before you invoke. The Payment Manager, connector, and wallet instrument already exist from Tutorial 00; this tutorial reads their IDs from the shared `.env` and reuses them.
The deployed agent runs under a dedicated execution role. The `AgentCorePaymentsPlugin` handles 402 responses automatically — the LLM never calls payment APIs directly. All payment context (manager ARN, session, instrument) arrives in the invocation payload, keeping the agent stateless.
The deployed agent runs under its own execution role. Its `AgentCorePaymentsPlugin` intercepts HTTP 402 responses and calls `ProcessPayment` within the session budget, so the LLM never calls payment APIs directly. All payment context (manager ARN, session, instrument, user) arrives in the invocation payload, keeping the agent stateless — the same deployed binary can serve different users with different budgets.
> **Billable resources.** `agentcore deploy` creates real AWS resources (AgentCore Runtime, IAM execution role, CloudWatch). See [AgentCore pricing](https://aws.amazon.com/bedrock/agentcore/pricing/). First deploy takes a few minutes for IAM role propagation; later deploys are faster.
> **Testnet only.** The agent pays x402 endpoints on Base Sepolia (network `ETHEREUM`) with free testnet USDC from [faucet.circle.com](https://faucet.circle.com/). Testnet USDC has no monetary value.
> **Supported regions:** `us-east-1`, `us-west-2`, `eu-central-1`, `ap-southeast-2`. Set `AWS_REGION` in the shared `.env` to one of these.
## Architecture
![High-Level Architecture](images/high_level_architecture.png)
![Architecture](images/architecture.png)
![Architecture](images/high_level_architecture.png)
```
App Backend AgentCore runtime
App Backend AgentCore Runtime
│ ┌──────────────────────────┐
│ create_session(budget=$0.50) │ Payment Agent │
│ (execution role) │
│ create_payment_session($0.50) │ Payment Agent │
(PaymentManager SDK) │ (execution role) │
│── invoke(session, instrument) ──►│ Plugin: ProcessPayment │
│ Cannot: CreateSession
│◄── weather data + cost ─────────│ Cannot: Override budget
└──────────────────────────┘
│ get_session(check spend)
(agentcore invoke) │ Scope: ProcessPayment
│◄── weather data + cost ─────────│ only — spends within the
│ session budget set by │
│ get_payment_session(check spend) │ the backend │
│ (PaymentManager SDK) └──────────────────────────┘
```
![Payment Flow](images/payment_flow.png)
### How the agent code works (`payment_agent.py`)
![Payment Flow Sequence](images/payment_flow_sequence.png)
### How the Agent Code Works (`payment_agent.py`)
1. **`BedrockAgentCoreApp` + `@app.entrypoint`** — the standard AgentCore runtime service contract
2. **Payload-driven config** — ALL payment context comes from the invocation payload. The agent does not read payment config from environment variables. This keeps the agent stateless.
3. **`AgentCorePaymentsPlugin`** — intercepts HTTP 402 responses and calls `ProcessPayment` automatically within the session budget
1. **`BedrockAgentCoreApp` + `@app.entrypoint`** — the standard AgentCore Runtime service contract. This file is copied into the scaffolded project as `app/PaymentAgent/main.py`.
2. **Payload-driven config** — the agent reads all payment context (`payment_manager_arn`, `user_id`, `payment_session_id`, `payment_instrument_id`) from the invocation payload. This keeps the agent stateless.
3. **`AgentCorePaymentsPlugin`** — built per request from the payload context (network preferences `eip155:84532` / `base-sepolia`); it intercepts HTTP 402 responses and calls `ProcessPayment` automatically within the session budget.
## Prerequisites
- Tutorial 00 completed (`.env` with payment manager, instrument, etc.)
- Tutorial 01 completed (understand the local agent + plugin flow)
- Wallet funded with testnet USDC from [faucet.circle.com](https://faucet.circle.com/)
- Python 3.10+
- Node.js 20+ and the AgentCore CLI: `npm install -g @aws/agentcore`
- AWS CDK: `npm install -g aws-cdk`
- AWS CLI configured
## CLI Commands
- **Tutorial 00 completed** — the shared `.env` (one directory up, `00-getting-started/.env`) is populated with `PAYMENT_MANAGER_ARN`, `USER_ID`, `INSTRUMENT_ID`, `AWS_REGION`, etc.
- **Tutorial 01 completed** — you understand the local agent + plugin flow.
- **Funded wallet** — the instrument from Tutorial 00 has testnet USDC and delegated signing granted ([faucet.circle.com](https://faucet.circle.com/)).
- **AgentCore CLI** (Node.js 20+): `npm install -g @aws/agentcore`
- **AWS CDK** (used by `agentcore deploy`): `npm install -g aws-cdk`
- **Python 3.10+** and AWS CLI configured (`aws sts get-caller-identity`).
```bash
# Install AgentCore CLI
npm install -g @aws/agentcore
# Install Python dependencies
pip install -r requirements.txt
```
## Walkthrough
Run these steps top to bottom. Steps 15 provision and deploy the runtime with the CLI; steps 68 mint a budgeted session with the AgentCore SDK and invoke the deployed agent.
### Step 1 — (Optional) test locally before deploying
Confirm the agent starts cleanly on your machine first.
```bash
# Test locally before deploying
python payment_agent.py
# In another terminal: curl -s http://localhost:8080/ping
# In another terminal:
curl -s http://localhost:8080/ping
# Stop the agent (Ctrl+C) before continuing.
```
### Step 2 — Scaffold the AgentCore project
`agentcore create` generates a runtime project (CDK app, Dockerfile, and an `app/PaymentAgent/` package) wired for a Strands agent served over HTTP with a Bedrock model and no memory.
```bash
# Scaffold the AgentCore project
agentcore create --name PaymentAgent --framework Strands --protocol HTTP --model-provider Bedrock --memory none
```
```bash
# Deploy to AgentCore runtime (~2-3 minutes first time)
cd PaymentAgent
agentcore deploy -y
```
### Step 3 — Copy the agent into the project
Replace the scaffold's placeholder entrypoint with your payment agent.
```bash
# Check deployment status
cp ../payment_agent.py app/PaymentAgent/main.py
```
### Step 4 — Set the project dependencies
The runtime build installs from `app/PaymentAgent/pyproject.toml`. `payment_agent.py` imports `strands_tools`, `dotenv`, and the payments plugin (`bedrock_agentcore.payments.integrations.strands`), so list those libraries in `[project].dependencies`, then remove the stale lock so the build regenerates it with the new deps.
Edit `app/PaymentAgent/pyproject.toml` so its `[project]` dependencies include:
```toml
[project]
name = "payment-agent"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"bedrock-agentcore[strands-agents]>=1.9.0",
"strands-agents>=1.0.0",
"strands-agents-tools>=0.2.0",
"python-dotenv>=1.0.0",
"boto3>=1.43.5",
]
```
Then remove the old lock so it is regenerated with these dependencies:
```bash
rm -f app/PaymentAgent/uv.lock
```
### Step 5 — Deploy and attach payment permissions
`agentcore deploy` builds the image and provisions the Runtime, its execution role, and CloudWatch (~23 min on first run while IAM roles propagate).
```bash
agentcore deploy -y
agentcore status
```
Tutorial 00 already provisioned the Payment Manager and connector, so this project deploys a plain runtime. After the deploy, attach the payment data-plane permissions the agent needs at request time — `ProcessPayment`, `GetPaymentInstrument`, and `GetPaymentSession` — to the auto-created execution role. Find the role name in the `agentcore status` output (it contains `PaymentAgent` and `Execution`), then attach an inline policy scoped to your payment manager:
```bash
# Invoke the deployed agent (create a session first — see deploy_payment_agent.py)
agentcore invoke '{"prompt": "Access https://x402-test.genesisblock.ai/api/weather and report the data", "payment_manager_arn": "<ARN>", "user_id": "test-user-001", "payment_session_id": "<SESSION_ID>", "payment_instrument_id": "<INSTRUMENT_ID>"}'
# Replace <EXECUTION_ROLE_NAME> with the PaymentAgent execution role from `agentcore status`,
# and <REGION>/<ACCOUNT_ID> with your values.
aws iam put-role-policy \
--role-name <EXECUTION_ROLE_NAME> \
--policy-name PaymentDataPlaneAccess \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"bedrock-agentcore:ProcessPayment",
"bedrock-agentcore:GetPaymentInstrument",
"bedrock-agentcore:GetPaymentSession"
],
"Resource": "arn:aws:bedrock-agentcore:<REGION>:<ACCOUNT_ID>:payment-manager/*"
}]
}'
```
### Step 6 — Mint a budgeted session (AgentCore SDK)
Sessions are per-request and carry a spend budget, so you create one before each invoke, scoped to the user you serve and the budget you want to allow. The **simplest path** is to let the CLI manage the session for you: `agentcore invoke --auto-session` creates (and reuses) a session with the manager's default spend limit, so you can skip straight to Step 7. When you want to **control the budget explicitly**, mint the session with the AgentCore SDK `PaymentManager` and pass its ID to `agentcore invoke`.
Run this short script from the tutorial directory to mint a $0.50 session and print its ID:
```python
# mint_session.py — creates a budgeted payment session for the invoke in Step 7
import os
from pathlib import Path
from dotenv import load_dotenv
from bedrock_agentcore.payments import PaymentManager
# Load the shared .env (one directory up, at 00-getting-started/.env)
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
REGION = os.environ["AWS_REGION"]
PAYMENT_MANAGER_ARN = os.environ["PAYMENT_MANAGER_ARN"]
USER_ID = os.environ["USER_ID"]
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
session = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.50", "currency": "USD"}},
expiry_time_in_minutes=60,
)
print(session["paymentSessionId"])
```
```bash
# Stream logs
SESSION_ID=$(python mint_session.py)
echo "$SESSION_ID"
```
Copy the printed `paymentSessionId` for the invoke in Step 7.
### Step 7 — Invoke the deployed agent
This agent reads its payment context from the payload, so pass the manager ARN, user, session, and instrument in the JSON — the agent requires all four. Use the `SESSION_ID` you minted in Step 6. `agentcore invoke` is project-scoped, so run it from inside the scaffolded `PaymentAgent/` directory (where `agentcore.json` lives):
```bash
cd PaymentAgent # agentcore invoke reads the project config here
agentcore invoke '{"prompt": "Access this paid weather API and tell me what data you get back: https://x402-test.genesisblock.ai/api/weather", "payment_manager_arn": "<MANAGER_ARN>", "user_id": "<USER_ID>", "payment_session_id": "<SESSION_ID>", "payment_instrument_id": "<INSTRUMENT_ID>"}'
```
If you skipped the explicit session in Step 6, let the CLI manage it instead: add `--auto-session` (with `--payment-user-id <USER_ID>`) and `agentcore invoke` creates or reuses a session with the manager's default spend limit, so you omit `payment_session_id` from the payload.
> **This agent is payload-driven.** `handle_request` reads `payment_manager_arn`, `user_id`, `payment_session_id`, and `payment_instrument_id` from the payload dict and returns `{"error": "Missing required fields in payload: ..."}` if any are absent.
### Step 8 — Check the spend (AgentCore SDK)
Confirm the paid call debited the session as expected. This reads the session back with the SDK and prints the remaining budget:
```python
# check_spend.py — reads the remaining budget for the session from Step 6
import os, sys
from pathlib import Path
from dotenv import load_dotenv
from bedrock_agentcore.payments import PaymentManager
# Load the shared .env (one directory up, at 00-getting-started/.env)
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
REGION = os.environ["AWS_REGION"]
PAYMENT_MANAGER_ARN = os.environ["PAYMENT_MANAGER_ARN"]
USER_ID = os.environ["USER_ID"]
SESSION_ID = sys.argv[1] # pass the session ID from Step 6
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
session = manager.get_payment_session(user_id=USER_ID, payment_session_id=SESSION_ID)
print("Max budget: ", session["limits"]["maxSpendAmount"])
print("Available budget:", session["availableLimits"]["availableSpendAmount"])
```
```bash
python check_spend.py "$SESSION_ID"
```
## What the agent does
The agent calls the paid weather endpoint with `http_request`. The endpoint returns HTTP 402; the `AgentCorePaymentsPlugin` intercepts it, settles the payment via `ProcessPayment` against the instrument within the session budget, retries the request, and returns the weather data plus the cost. The agent's scope is `ProcessPayment` only — it spends within the session budget the backend set, and it does not create sessions, override the budget, or provision wallets.
The full request path — invoke → 402 → `ProcessPayment` → retry → `200` + data — looks like this:
![Payment Flow Sequence](images/payment_flow_sequence.png)
## Inspect / verify
Run these from the scaffolded project directory (`PaymentAgent/`), where the AgentCore project config lives.
```bash
# Runtime status (deployed agent)
agentcore status
# Stream runtime logs
agentcore logs
```
```bash
# Clean up all deployed resources
agentcore remove all -y
```
Or run the full automated script:
Confirm the Runtime ARN was written to the shared `.env`:
```bash
python deploy_payment_agent.py
grep AGENT_RUNTIME_ARN ../../.env
```
## Running the Python Scripts
```bash
pip install -r requirements.txt
```
```bash
# Automated deploy + invoke script
python deploy_payment_agent.py
# Start agent locally for testing
python payment_agent.py
```
## Key Concepts
**Stateless agent**`payment_agent.py` reads all payment context from the invocation payload, not from environment variables. This means the same agent binary can serve different users with different budgets — the app backend controls what each user can spend by creating a session with the appropriate budget before invoking.
**ProcessPaymentRole** — The execution role the agent runs under. It has `ProcessPayment` permission and explicit denies on `CreatePaymentSession`, `CreatePaymentInstrument`, and all control-plane operations. The agent cannot create sessions, override budgets, or provision wallets.
**Payload-driven sessions** — The app backend creates a fresh session with a budget before every invocation, then passes the `payment_session_id` in the payload. The agent cannot reuse sessions from previous invocations or extend their expiry.
CloudWatch GenAI observability dashboard: `https://<region>.console.aws.amazon.com/cloudwatch/home?region=<region>#gen-ai-observability/agent-core`
## Troubleshooting
### agentcore create fails with "command not found"
Install Node.js 20+ and the AgentCore CLI:
```bash
npm install -g @aws/agentcore
agentcore --version
```
### Deploy fails with CDK bootstrap error
Bootstrap CDK in your account/region first:
```bash
cdk bootstrap aws://<account-id>/<region>
```
### Agent returns "Missing required fields in payload"
The invocation payload must include `payment_manager_arn`, `user_id`, `payment_session_id`, and `payment_instrument_id`. All four come from the app backend — not from `.env`. Check that the session was created and its ID was passed in the payload.
### Payment permissions error after deploy
The agentcore CLI creates an execution role with Bedrock + CloudWatch permissions. Add payment data-plane permissions manually or let `deploy_payment_agent.py` do it automatically via `iam.put_role_policy`.
| Error | Cause | Fix |
|---|---|---|
| `agentcore: command not found` | AgentCore CLI not installed | `npm install -g @aws/agentcore` (Node.js 20+) |
| Deploy build fails on import (`strands_tools` / `dotenv` / payments plugin) | Project `pyproject.toml` missing the agent's dependencies | Add the Step 4 dependencies and `rm -f app/PaymentAgent/uv.lock`, then redeploy |
| Deploy fails with CDK bootstrap error | Account/region not bootstrapped | `cdk bootstrap aws://<account-id>/<region>` |
| `Missing required fields in payload` | Payload missing one of `payment_manager_arn`, `user_id`, `payment_session_id`, `payment_instrument_id` | Include all four fields in the invoke JSON (this agent is payload-driven) |
| Access-denied on `ProcessPayment` after deploy | Execution role lacks payment data-plane permissions | Attach `ProcessPayment`/`GetPaymentInstrument`/`GetPaymentSession` to the execution role with the `aws iam put-role-policy` command in Step 5 |
| `Instrument not found` at invoke | `user_id` doesn't match the instrument's owner | Use the exact `USER_ID` from Tutorial 00's `.env` |
| `Delegated signing grant is not active` | Wallet consent not completed | Complete the funding/delegation step from Tutorial 00 / 03 |
## Clean Up
@@ -162,11 +277,33 @@ cd PaymentAgent
agentcore remove all -y
```
This deletes the AgentCore runtime deployment and associated AWS resources (CDK stack, CloudWatch logs). Payment sessions expire automatically.
This deletes the AgentCore Runtime deployment and its AWS resources (CDK stack, CloudWatch logs). Payment sessions expire automatically. The shared Payment Manager, connector, and instrument from Tutorial 00 remain in place — tear those down with Tutorial 00's cleanup. The connector and manager teardown uses the AgentCore CLI (`agentcore remove payment-connector` / `remove payment-manager`, then `agentcore deploy -y` to apply). The instrument is deleted with the AgentCore SDK `PaymentManager` — pass the connector id and user id alongside the instrument id (all read from the shared `.env`):
To also delete payment resources (Manager, Connector, Instrument), run the cleanup cell in Tutorial 00.
```python
import os
from pathlib import Path
from dotenv import load_dotenv
from bedrock_agentcore.payments import PaymentManager
## Next Steps
# Load the shared .env (one directory up, at 00-getting-started/.env)
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
- **Tutorial 03** — `../03-user-onboarding-wallet-funding/` — Wallet lifecycle, funding, delegation, balance checks
- **Tutorial 04** — `../04-agent-with-coinbase-bazaar-via-gateway/` — Discover paid MCP tools via AgentCore gateway
REGION = os.environ["AWS_REGION"]
PAYMENT_MANAGER_ARN = os.environ["PAYMENT_MANAGER_ARN"]
PAYMENT_CONNECTOR_ID = os.environ["PAYMENT_CONNECTOR_ID"]
INSTRUMENT_ID = os.environ["INSTRUMENT_ID"]
USER_ID = os.environ["USER_ID"]
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
manager.delete_payment_instrument(
payment_instrument_id=INSTRUMENT_ID,
payment_connector_id=PAYMENT_CONNECTOR_ID,
user_id=USER_ID,
)
```
## Next steps
- **Tutorial 03** — [`../03-user-onboarding-wallet-funding/`](../03-user-onboarding-wallet-funding/) — Per-user wallet onboarding, funding, delegation, balance checks (SDK).
- **Tutorial 04** — [`../04-agent-with-coinbase-bazaar-via-gateway/`](../04-agent-with-coinbase-bazaar-via-gateway/) — Discover paid MCP tools via AgentCore Gateway (CLI + SDK).
@@ -1,265 +0,0 @@
"""
Deploy Payment Agent to AgentCore Runtime
Deploys the payment-enabled Strands agent to AgentCore Runtime using the
agentcore CLI. After deployment, the agent can be invoked over HTTPS with
SigV4 auth from any AWS-authenticated client.
Architecture:
App Backend AgentCore Runtime
│ ┌──────────────────────────┐
│ create_session(budget=$0.50) │ Payment Agent │
│ │ (execution role) │
│── invoke(session, instrument) ──►│ Plugin: ProcessPayment │
│ │ Cannot: CreateSession │
│◄── weather data + cost ─────────│ Cannot: Override budget │
│ └──────────────────────────┘
│ get_session(check spend)
Usage:
python deploy_payment_agent.py
Prerequisites:
- Tutorial 00 and 01 completed
- Wallet funded with testnet USDC
- Node.js 20+ and agentcore CLI installed: npm install -g @aws/agentcore
- AWS CDK installed: npm install -g aws-cdk
- pip install -r requirements.txt
"""
import json
import os
import re
import shutil
import subprocess
import sys
import boto3
from dotenv import load_dotenv
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from utils import load_tutorial_env, print_summary, update_env_file
ENV_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
load_dotenv(ENV_FILE, override=True)
HERE = os.path.dirname(os.path.abspath(__file__))
# ── Verify AWS credentials ────────────────────────────────────────────────────
session = boto3.Session()
identity = session.client("sts").get_caller_identity()
account_id = identity["Account"]
REGION = session.region_name or os.environ.get("AWS_REGION", "us-west-2")
print(f"Authenticated as: {identity['Arn']}")
print(f"Account: {account_id}")
print(f"Region: {REGION}")
# ── Step 4: Load Payment Config ───────────────────────────────────────────────
config = load_tutorial_env()
PAYMENT_MANAGER_ARN = config["payment_manager_arn"]
USER_ID = config["user_id"]
if config.get("multi_provider"):
PROVIDER = list(config["instruments"].keys())[0]
INSTRUMENT_ID = config["instruments"][PROVIDER]["instrument_id"]
else:
INSTRUMENT_ID = config["instrument_id"]
PROVIDER = config.get("provider_type", "unknown")
print_summary(
"Payment Config",
payment_manager_arn=PAYMENT_MANAGER_ARN,
region=REGION,
user_id=USER_ID,
instrument_id=INSTRUMENT_ID,
provider=PROVIDER,
)
# ── Step 5: Test Locally ──────────────────────────────────────────────────────
print("""
── Step 5: Test Locally (optional) ──
To test locally before deploying:
1. Run in a separate terminal:
python payment_agent.py
2. Health check:
curl -s http://localhost:8080/ping
3. Test invocation:
curl -X POST http://localhost:8080/invocations \\
-H 'Content-Type: application/json' \\
-d '{"prompt": "Hello, what can you do?"}'
4. Stop the agent (Ctrl+C), then continue with deployment.
""")
# ── Step 6: Scaffold the AgentCore Project ────────────────────────────────────
print("── Step 6: Scaffold AgentCore Project ──")
project_dir = os.path.join(HERE, "PaymentAgent")
if not os.path.exists(project_dir):
subprocess.run(
[
"agentcore",
"create",
"--name",
"PaymentAgent",
"--framework",
"Strands",
"--protocol",
"HTTP",
"--model-provider",
"Bedrock",
"--memory",
"none",
],
cwd=HERE,
check=True,
)
print("AgentCore project scaffolded: PaymentAgent/")
else:
print("PaymentAgent/ already exists — skipping create")
# Copy agent code into the project
agent_dest = os.path.join(project_dir, "app", "PaymentAgent", "main.py")
shutil.copy(os.path.join(HERE, "payment_agent.py"), agent_dest)
print(f"Agent code copied to {agent_dest}")
# Update pyproject.toml
pyproject_content = """[project]
name = "payment-agent"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"bedrock-agentcore[strands-agents]>=1.9.0",
"boto3>=1.43.5",
"strands-agents>=1.0.0",
"strands-agents-tools>=0.2.0",
"python-dotenv>=1.0.0",
]
"""
pyproject_path = os.path.join(project_dir, "app", "PaymentAgent", "pyproject.toml")
with open(pyproject_path, "w") as f:
f.write(pyproject_content)
# Remove stale lock file if it exists
lock_file = os.path.join(project_dir, "app", "PaymentAgent", "uv.lock")
if os.path.exists(lock_file):
os.remove(lock_file)
print("pyproject.toml updated")
# ── Step 7: Deploy to AgentCore Runtime ───────────────────────────────────────
print("\n── Step 7: Deploy to AgentCore Runtime ──")
print("This creates billable AWS resources (Lambda, CloudWatch, API Gateway).")
print("First deploy takes ~2-3 minutes...\n")
subprocess.run(["agentcore", "deploy", "-y"], cwd=project_dir, check=True)
# Verify deployment
result = subprocess.run(["agentcore", "status"], cwd=project_dir, capture_output=True, text=True)
print(result.stdout)
# Add payment permissions to the auto-created execution role
print("Adding payment permissions to execution role...")
iam = boto3.client("iam")
roles = iam.list_roles(MaxItems=200)["Roles"]
runtime_roles = [r["RoleName"] for r in roles if "PaymentAgent" in r["RoleName"] and "Execution" in r["RoleName"]]
if not runtime_roles:
runtime_roles = [r["RoleName"] for r in roles if "PaymentAgent" in r["RoleName"]]
if runtime_roles:
RUNTIME_ROLE_NAME = runtime_roles[0]
payment_policy = {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock-agentcore:ProcessPayment",
"bedrock-agentcore:GetPaymentInstrument",
"bedrock-agentcore:ListPaymentInstruments",
"bedrock-agentcore:GetPaymentInstrumentBalance",
"bedrock-agentcore:GetPaymentSession",
"bedrock-agentcore:GetResourcePaymentToken",
],
"Resource": f"arn:aws:bedrock-agentcore:{REGION}:{account_id}:payment-manager/*",
}
],
}
iam.put_role_policy(
RoleName=RUNTIME_ROLE_NAME,
PolicyName="PaymentDataPlaneAccess",
PolicyDocument=json.dumps(payment_policy),
)
print(f"Added payment permissions to: {RUNTIME_ROLE_NAME}")
else:
print("WARNING: Could not find PaymentAgent execution role — add payment permissions manually")
# Extract Runtime ARN and save to .env
status_output = result.stdout + result.stderr
match = re.search(r"arn:aws:bedrock-agentcore:[^\s\"]+", status_output)
if match:
AGENT_RUNTIME_ARN = match.group(0)
update_env_file(ENV_FILE, {"AGENT_RUNTIME_ARN": AGENT_RUNTIME_ARN})
print(f"Runtime ARN: {AGENT_RUNTIME_ARN}")
print("Saved to .env")
else:
print("NOTE: Could not extract Runtime ARN from status output — check agentcore status manually")
# ── Step 8: Invoke the Deployed Agent ────────────────────────────────────────
print("\n── Step 8: Invoke Deployed Agent ──")
from bedrock_agentcore.payments import PaymentManager # noqa: E402
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
fresh_session = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.50", "currency": "USD"}},
expiry_time_in_minutes=60,
)
fresh_session_id = fresh_session["paymentSessionId"]
print(f"Session: {fresh_session_id} (budget: $0.50, expiry: 60 min)")
invoke_payload = json.dumps(
{
"prompt": (
"Access this paid weather API and tell me what data you get back: "
"https://x402-test.genesisblock.ai/api/weather "
"Report the weather data and how much it cost."
),
"payment_manager_arn": PAYMENT_MANAGER_ARN,
"user_id": USER_ID,
"payment_session_id": fresh_session_id,
"payment_instrument_id": INSTRUMENT_ID,
}
)
print("Invoking deployed agent...")
subprocess.run(
["agentcore", "invoke", invoke_payload],
cwd=project_dir,
check=True,
)
# ── Step 9: Verify Session Spend ─────────────────────────────────────────────
print("\n── Step 9: Verify Session Spend ──")
session_info = manager.get_payment_session(
user_id=USER_ID,
payment_session_id=fresh_session_id,
)
available = session_info.get("availableLimits", {}).get("availableSpendAmount", {})
budget = session_info.get("limits", {}).get("maxSpendAmount", {})
print_summary(
"Post-Invocation Session",
session_id=fresh_session_id,
budget_limit=f"${budget.get('value', 'N/A')} {budget.get('currency', '')}",
remaining=f"${available.get('value', 'N/A')} {available.get('currency', '')}",
)
# ── Observability ─────────────────────────────────────────────────────────────
print("\n── Observability ──")
print(
f"GenAI Dashboard: https://{REGION}.console.aws.amazon.com/cloudwatch/home?"
f"region={REGION}#gen-ai-observability/agent-core"
)
print("Stream logs: cd PaymentAgent && agentcore logs")
print("\nDone. To clean up: cd PaymentAgent && agentcore remove all -y")
print("Next: python ../03-user-onboarding-wallet-funding/user_onboarding.py")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

@@ -2,9 +2,9 @@
Payment-enabled Strands Agent for AgentCore Runtime.
This agent uses the AgentCorePaymentsPlugin to automatically handle
x402 payments. When deployed to AgentCore Runtime, it runs under the
ProcessPaymentRole execution role — it can only process payments within
the budget set by the application backend.
x402 payments. When deployed to AgentCore Runtime, it runs under its own
auto-created PaymentAgent execution role — it can only process payments
within the budget set by the application backend.
The app backend passes ALL payment context via the invocation payload:
- payment_manager_arn
@@ -17,7 +17,8 @@ This keeps the agent stateless and enforces that the app backend
controls what the agent can access.
Deployment:
agentcore create --name PaymentAgent --defaults
agentcore create --name PaymentAgent --framework Strands --protocol HTTP \
--model-provider Bedrock --memory none
agentcore deploy
"""
@@ -4,7 +4,7 @@ boto3>=1.43.5
botocore>=1.43.5
# Agent framework
strands-agents[otel]>=1.0.0
strands-agents>=1.0.0
strands-agents-tools>=0.2.0
# Observability (required for Runtime traces)
@@ -1,35 +1,55 @@
# Tutorial 03 — User Onboarding and Backend Wallet Operations
| Information | Details |
|:--------------------|:-----------------------------------------------------------------|
| Tutorial type | Operations / lifecycle |
| Tutorial components | PaymentInstrument, PaymentSession, balance checks, multi-network |
| Wallet providers | Coinbase CDP and Stripe (Privy) |
| Networks | ETHEREUM (Base Sepolia), SOLANA (Solana Devnet) |
| SDK used | bedrock-agentcore, boto3 |
| Example complexity | Intermediate |
| Information | Details |
|:--------------------|:--------------------------------------------------------------------|
| Tutorial type | Backend operations / lifecycle (no agent) |
| Agent type | None — per-user wallet operations only |
| Tooling | AgentCore SDK (`PaymentManager`) for all per-user ops; `user_onboarding.py` shows the full flow |
| LLM model | None |
| Components | PaymentInstrument, PaymentSession, instrument balance, multi-network |
| Example complexity | Intermediate |
> **Reads** the shared `.env` one directory up (`PAYMENT_MANAGER_ARN`, `PAYMENT_CONNECTOR_ID`,
> `USER_ID`, `INSTRUMENT_ID`, `NETWORK`, `LINKED_EMAIL`). **Does** the per-user backend work on top
> of Tutorial 00's shared stack: creates a new user's embedded wallet, checks balances, and mints
> spending sessions with budgets. → [How the pieces fit together](../README.md#cli-vs-sdk)
## Overview
Tutorial 00 created one wallet for the developer. In production, every end user of your application gets their own embedded wallet, and your backend manages the session budgets that govern what the agent spends on their behalf.
Tutorial 00 provisioned your shared payment infrastructure — the payment manager, connector, and
IAM roles — with the AgentCore CLI, and created one wallet for you as the developer. In a real
application, every end user gets their own embedded wallet, and your backend manages the
session budgets that govern what the agent spends on their behalf. Wallets and sessions are
per-user, so you create them in your backend with the AgentCore SDK (`PaymentManager`), scoped to
the individual user you serve.
This tutorial has two parts:
Running `user_onboarding.py` walks that full backend lifecycle: it calls `create_payment_instrument`
to provision a new user's wallet (a `PaymentInstrument`), reads balances via
`get_payment_instrument_balance`, creates three `PaymentSession`s with different budgets and
expiries, and lists instruments per user. The same flow works for both wallet providers — Coinbase
CDP and Stripe (Privy) — with provider-specific onboarding steps (funding + delegated signing)
called out where they differ.
- **Part 1 — Onboarding (per end user):** create the wallet, fund it, delegate signing, and optionally provision wallets on additional chains.
- **Part 2 — Backend operations:** balance checks, session creation with budgets, instrument listing, and remaining-budget queries.
> **Billable resources.** Creating instruments and sessions touches real AgentCore payments
> resources. See [AgentCore pricing](https://aws.amazon.com/bedrock/agentcore/pricing/).
The same flow works for both wallet providers — Coinbase CDP and Stripe (Privy) — with provider-specific steps called out where they differ.
> **Testnet only.** Use Base Sepolia (`NETWORK=ETHEREUM`) or Solana Devnet (`NETWORK=SOLANA`) with
> free USDC from [faucet.circle.com](https://faucet.circle.com/). Testnet USDC has no monetary value.
## Two Personas
> **Supported regions:** `us-east-1`, `us-west-2`, `eu-central-1`, `ap-southeast-2`. Set
> `AWS_REGION` in the shared `.env` to one of these.
## Two personas
| Persona | Who | What they do |
|---------|-----|-------------|
| **Application backend** | You (developer / backend code) | Provisions wallets, checks balances, creates sessions |
| **End user** | User of your app | Funds the wallet, grants consent via WalletHub or Privy reference frontend |
For simplicity, the tutorial reuses the developer's `LINKED_EMAIL` as the end-user identity. In production each user has their own email and their own wallet.
For simplicity, the tutorial reuses the developer's `LINKED_EMAIL` as the end-user identity. In a
real application each user has their own email and their own wallet.
## Onboarding Flow
## Architecture
![Onboarding Flow](images/onboarding_flow.png)
@@ -46,7 +66,11 @@ Backend End User UI (WalletHub / Privy frontend)
└─ ListPaymentInstruments ───── │ (account dashboard)
```
## Session Patterns
### Wallet providers
![Wallet Providers](images/wallet_provider_paths.png)
### Session patterns
| Pattern | Budget | Expiry | Use case |
|---------|--------|--------|----------|
@@ -55,47 +79,7 @@ Backend End User UI (WalletHub / Privy frontend)
| Deep analysis | $5.00 | 480 min | Extended workflow |
| No budget cap | omit `limits` | 60 min | Trusted internal agents |
## Wallet Providers
![Wallet Providers](images/wallet_providers.png)
![Wallet Provider Paths](images/wallet_provider_paths.png)
## Delegation: Grant Signing Permission
Before the agent can sign transactions, the end user grants permission. This is a one-time step per wallet.
| | Coinbase CDP | Stripe (Privy) |
|---|---|---|
| **Mechanism** | Project-level delegated signing | Authorization key as an additional signer on the wallet |
| **Setup** | CDP Portal → Wallets → Embedded Wallet → Policies → enable | Privy reference frontend `addSigners()` call |
| **User action** | Grant consent via WalletHub `redirectUrl` | Log in at `http://localhost:3000`, choose **Connect agent → Give access** |
| **Scope** | All wallets under the project | Per-wallet |
| **Without it** | ProcessPayment fails with signing error | ProcessPayment fails |
## Funding Options
| Method | Use case | Provider |
|--------|----------|----------|
| Circle faucet | Testnet (free) | Both |
| Direct USDC transfer | User sends from external wallet | Both |
| Coinbase Onramp URL | Fiat → crypto (credit card, bank) | Coinbase |
| Stripe Onramp | Fiat → crypto (credit card, bank, Apple Pay) | Privy |
| Coinbase WalletHub | Fund + delegate in one UI (managed by Coinbase) | Coinbase |
| Privy reference frontend | Fund + delegate via your app's UI (self-hosted in prod) | Privy |
![Privy Fund Options](images/03-privy-fund-options.png)
### Fiat-to-Crypto Onramp Glossary
**Fiat** means traditional currency (USD, EUR) paid via credit card, bank transfer, Apple Pay, or Google Pay. An **onramp** converts fiat into stablecoin (USDC) and deposits it into the embedded wallet. Tutorial runs use testnet USDC from the faucet and never touch real money.
| Provider | Onramp |
|----------|--------|
| Coinbase | [Coinbase Onramp](https://docs.cdp.coinbase.com/onramp/coinbase-hosted-onramp/generating-onramp-url) — fiat → crypto via credit card, bank transfer, Apple Pay, Google Pay |
| Stripe (Privy) | [Stripe Onramp](https://docs.stripe.com/crypto/onramp) — fiat → crypto via credit card, bank, Apple Pay |
## Supported Networks
### Supported networks
| Network setting | Chain | CAIP-2 | Faucet |
|----------------|-------|--------|--------|
@@ -104,52 +88,208 @@ Before the agent can sign transactions, the end user grants permission. This is
## Prerequisites
- Tutorial 00 completed (`.env` with `PAYMENT_MANAGER_ARN`, `PAYMENT_CONNECTOR_ID`, `LINKED_EMAIL`)
- Testnet USDC from [faucet.circle.com](https://faucet.circle.com/)
- **Tutorial 00 completed** — the shared `.env` (one directory up, at
`00-getting-started/.env`) is populated with `PAYMENT_MANAGER_ARN`, `PAYMENT_CONNECTOR_ID`,
`USER_ID`, `INSTRUMENT_ID`, `NETWORK`, and `LINKED_EMAIL`. This tutorial reads them via
`utils.load_tutorial_env()`.
- **Testnet USDC** from [faucet.circle.com](https://faucet.circle.com/) for the network in your
`.env` (Base Sepolia for `ETHEREUM`, Solana Devnet for `SOLANA`).
- **Python 3.10+** and AWS CLI configured (`aws sts get-caller-identity`).
- **Python deps:**
```bash
pip install -r requirements.txt
```
- **AgentCore CLI** (used only to inspect the shared stack from Tutorial 00's project dir) —
`npm install -g @aws/agentcore`.
## Running the Python Scripts
## Walkthrough
You own the per-user backend operations — instrument creation, balance checks, and session budgets —
and run them from your backend with the AgentCore SDK (`PaymentManager`), which wraps every payment
data-plane call. Each snippet below is copy-paste ready. (Prefer to
run all of them at once? `python user_onboarding.py` does the same operations end to end — see
[What the script does](#what-the-script-does).)
### Step 1 — Confirm the shared stack from Tutorial 00
The `agentcore status` command reads a scaffolded project's config, so run it from Tutorial 00's
project directory, where that config lives. This confirms the manager and connector you'll build on
are live.
```bash
pip install -r requirements.txt
cd ../00-setup-agentcore-payments/PaymentSetup
agentcore status --type payment
cd - # back to the tutorial-03 folder
```
Load the shared `.env` (one directory up) so the payment IDs are in scope for the commands below:
```bash
python user_onboarding.py
set -a && source ../.env && set +a
```
## Key Concepts
### Step 2 — Run the per-user wallet operations
**Embedded wallet** — A crypto wallet provisioned by the wallet provider (Coinbase CDP or Privy) on behalf of the end user. The user never sees a private key. The wallet is linked to the user's email via `linkedAccounts`.
Work through the SDK snippets in
[Per-user wallet operations](#per-user-wallet-operations) below — create a new user's wallet, fund it,
check its balance, and create a budgeted session. A brand-new wallet starts at `0.00 USDC`, so its
first balance check reads zero until you fund it at [faucet.circle.com](https://faucet.circle.com/);
the already-funded Tutorial 00 instrument shows a non-zero balance.
**Delegated signing** — One-time user consent for the agent to sign transactions. For Coinbase CDP: enabled at the project level in CDP Portal. For Stripe/Privy: end user clicks "Connect agent" in the Privy reference frontend or your production equivalent.
## What the script does
**Session-wallet independence**`CreatePaymentSession` does not take an instrument ID. Sessions are wallet-blind. At `ProcessPayment` time the service picks the user's instrument whose network matches the merchant's x402 challenge. One session can spend across the user's Ethereum and Solana wallets.
`python user_onboarding.py` runs eight sections across two parts. (The **Onboarding Flow** diagram
under [Architecture](#architecture) above shows this backend ↔ end-user lifecycle end to end.)
**`GetPaymentInstrumentBalance`** — Not exposed in the `bedrock_agentcore.payments` SDK; call via boto3 directly.
**Part 1 — Onboarding (per end user)**
1. **Create embedded wallet** — `manager.create_payment_instrument(...)` for a new `user_id`,
printing the wallet address and WalletHub `redirectUrl`.
2. **Fund the wallet** — prints faucet instructions for the configured network (end-user action).
3. **Delegate signing** — prints provider-specific consent steps (Coinbase WalletHub / Privy
frontend). This is what authorizes the agent to sign later.
**Multi-network** — One user, one manager, one connector, multiple instruments. Call `create_payment_instrument` with `ETHEREUM` and again with `SOLANA` — the user gets two wallet addresses, one per chain.
**Part 2 — Backend operations**
4. **Check balance** — `manager.get_payment_instrument_balance(...)` for both the Tutorial 00
instrument and the new one.
5. **Multi-network** — reference snippet showing how to add a second wallet on another chain.
6. **Create sessions** — three `create_payment_session` calls ($0.10/15 min, $1.00/60 min,
$5.00/480 min).
7. **List instruments** — `list_payment_instruments` per user.
8. **Check remaining budget** — `get_payment_session` for each session's available spend.
## Per-user wallet operations
These are the individual building blocks the script runs — each SDK snippet can be run on its own.
Set up the client and config once (the script reads these from `utils.load_tutorial_env()`; here we
read them from the shared `.env`):
```python
import os
from bedrock_agentcore.payments import PaymentManager
from utils import client_token # uuid4 helper
PAYMENT_MANAGER_ARN = os.environ["PAYMENT_MANAGER_ARN"]
REGION = os.environ["AWS_REGION"]
CONNECTOR_ID = os.environ["PAYMENT_CONNECTOR_ID"]
NETWORK = os.environ.get("NETWORK", "ETHEREUM")
NEW_EMAIL = os.environ["LINKED_EMAIL"]
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
```
**Create a per-user embedded wallet (instrument):**
```python
inst = manager.create_payment_instrument(
user_id="tutorial-03-user",
payment_connector_id=CONNECTOR_ID,
payment_instrument_type="EMBEDDED_CRYPTO_WALLET",
payment_instrument_details={
"embeddedCryptoWallet": {
"network": NETWORK, # "ETHEREUM" (Base Sepolia) or "SOLANA" (Solana Devnet)
"linkedAccounts": [{"email": {"emailAddress": NEW_EMAIL}}],
}
},
client_token=client_token(),
)
instrument_id = inst["paymentInstrumentId"]
wallet_address = inst["paymentInstrumentDetails"]["embeddedCryptoWallet"]["walletAddress"]
redirect_url = inst["paymentInstrumentDetails"]["embeddedCryptoWallet"].get("redirectUrl")
```
The response includes `paymentInstrumentId`, the wallet address to fund, and the WalletHub
`redirectUrl` for consent. Set `network` to `ETHEREUM` (Base Sepolia) or `SOLANA` (Solana Devnet).
**Create a spending session with a custom budget and expiry:**
```python
session = manager.create_payment_session(
user_id="tutorial-03-user",
limits={"maxSpendAmount": {"value": "1.00", "currency": "USD"}},
expiry_time_in_minutes=60,
)
session_id = session["paymentSessionId"]
```
Sessions are **wallet-blind** — `create_payment_session` takes no instrument id. At `ProcessPayment`
time the service picks the user's instrument whose network matches the merchant's x402 challenge, so
one session can spend across a user's Ethereum and Solana wallets. Omit `limits` for an uncapped
session.
**Read an instrument balance** with the SDK:
```python
chain = "BASE_SEPOLIA" if NETWORK == "ETHEREUM" else "SOLANA_DEVNET"
resp = manager.get_payment_instrument_balance(
payment_connector_id=CONNECTOR_ID,
payment_instrument_id=instrument_id,
chain=chain,
token="USDC",
user_id="tutorial-03-user",
)
amount = int(resp["tokenBalance"]["amount"]) / 1_000_000 # micro-USDC → USDC
```
## Delegation: grant signing permission
Before the agent can sign transactions, the end user grants permission once per wallet.
| | Coinbase CDP | Stripe (Privy) |
|---|---|---|
| **Mechanism** | Project-level delegated signing | Authorization key as an additional signer on the wallet |
| **User action** | Grant consent via WalletHub `redirectUrl` | Log in at `http://localhost:3000`, choose **Connect agent → Give access** |
| **Scope** | All wallets under the project | Per-wallet |
| **Without it** | `ProcessPayment` fails with a signing error | `ProcessPayment` fails |
## Inspect / verify
To confirm the managers, connectors, and live payment status, re-run the
`agentcore status --type payment` check from [Step 1](#step-1--confirm-the-shared-stack-from-tutorial-00).
The script prints each new instrument id, wallet address, session id, and remaining budget as it runs.
## Troubleshooting
### Wallet balance shows 0
Fund the wallet at [faucet.circle.com](https://faucet.circle.com/). Select the network matching `NETWORK` in `.env` (Base Sepolia for ETHEREUM, Solana Devnet for SOLANA). Paste the wallet address printed by Section 1.
### ProcessPayment fails with signing error (when running Tutorial 01 later)
Delegation was not completed. For Coinbase CDP: check CDP Portal → Wallets → Embedded Wallet → Policies. For Privy: ensure the end user completed "Connect agent → Give access" in the Privy reference frontend.
### `list_payment_instruments` returns empty list
Pass the correct `payment_connector_id`. Instruments are scoped to a specific connector under the manager. Use `CONNECTOR_ID` from `.env`.
| Symptom | Cause | Fix |
|---|---|---|
| `FileNotFoundError: .env not found. Run Tutorial 00 first.` | Tutorial 00 not completed — `load_tutorial_env()` raises when the shared `.env` is missing | Run Tutorial 00 so the shared `.env` is created and populated |
| Wallet balance shows `0.00 USDC` | Wallet not funded (expected for the brand-new Section 1 wallet) | Fund at [faucet.circle.com](https://faucet.circle.com/) for the network in `.env`, paste the address printed in Section 1, then re-run |
| `list_payment_instruments` returns empty | Wrong `payment_connector_id` | Instruments are scoped to a connector — pass `PAYMENT_CONNECTOR_ID` from `.env` |
| `ProcessPayment` fails with a signing error (in a later tutorial) | Delegation not completed | Coinbase: CDP Portal → Wallets → Embedded Wallet → Policies. Privy: complete **Connect agent → Give access** |
| `ImportError: PaymentManager` | Wrong import path | Import from `bedrock_agentcore.payments` (not `...payments.manager`) |
| `agentcore: command not found` | CLI not installed (inspection step only) | `npm install -g @aws/agentcore` |
## Clean Up
Sessions created in this tutorial expire automatically. To delete instruments and the payment manager, run the cleanup section in Tutorial 00.
Sessions created here expire automatically — no teardown needed. Instruments and the shared payment
manager/connector are torn down by Tutorial 00's cleanup. Delete each instrument with the SDK
(`manager.delete_payment_instrument`), then remove the shared stack:
## Next Steps
```python
# Delete an instrument with the SDK:
from bedrock_agentcore.payments import PaymentManager
- **Tutorial 04** — `../04-agent-with-coinbase-bazaar-via-gateway/` — Discover and call paid MCP tools on Coinbase Bazaar through AgentCore gateway
- **Tutorial 05** — `../05-agent-with-browser-tool-pay-for-content/` — Browser + paywall payment pattern
- **Tutorial 06** — `../06-research-agent-with-payment-memory/` — Recall past data and skip redundant paid calls with AgentCore Memory
- **Tutorial 07** — `../07-multi-agent-payment-orchestrator/` — Multi-agent orchestration with per-agent budgets
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
manager.delete_payment_instrument(
payment_instrument_id=INSTRUMENT_ID,
payment_connector_id=CONNECTOR_ID,
user_id=USER_ID,
)
```
Then remove the shared stack with the AgentCore CLI:
```bash
cd ../00-setup-agentcore-payments/PaymentSetup
agentcore remove payment-connector --manager MyPaymentManager --name MyCoinbaseConnector -y # or MyPrivyConnector
agentcore remove payment-manager --name MyPaymentManager -y
agentcore deploy -y # applies the removal in AWS
agentcore remove all -y # removes the scaffolded runtime project
```
## Next steps
- **Tutorial 04** — [`../04-agent-with-coinbase-bazaar-via-gateway/`](../04-agent-with-coinbase-bazaar-via-gateway/) — Discover and call paid MCP tools on Coinbase Bazaar through AgentCore Gateway
- **Tutorial 05** — [`../05-agent-with-browser-tool-pay-for-content/`](../05-agent-with-browser-tool-pay-for-content/) — Browser + paywall payment pattern
- **Tutorial 06** — [`../06-research-agent-with-payment-memory/`](../06-research-agent-with-payment-memory/) — Recall past data and skip redundant paid calls with AgentCore Memory
- **Tutorial 07** — [`../07-multi-agent-payment-orchestrator/`](../07-multi-agent-payment-orchestrator/) — Multi-agent orchestration with per-agent budgets
@@ -34,7 +34,6 @@ Prerequisites:
import os
import sys
import boto3
from dotenv import load_dotenv
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -52,21 +51,14 @@ REGION = config["region"]
USER_ID = config["user_id"]
NETWORK = os.environ.get("NETWORK", "ETHEREUM")
# SDK client for instrument + session operations
# SDK client for all payment data-plane operations (instruments, balances, sessions)
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
# boto3 client for GetPaymentInstrumentBalance (not in SDK)
dp_client = boto3.client("bedrock-agentcore", region_name=REGION)
# Get connector ID
if config.get("multi_provider"):
PROVIDER = list(config["instruments"].keys())[0]
CONNECTOR_ID = config["instruments"][PROVIDER]["connector_id"]
INSTRUMENT_ID = config["instruments"][PROVIDER]["instrument_id"]
else:
CONNECTOR_ID = config.get("connector_id")
INSTRUMENT_ID = config.get("instrument_id")
PROVIDER = config.get("provider_type", "unknown")
# load_tutorial_env resolves connector_id / instrument_id to the provider you
# configured (CREDENTIAL_PROVIDER_TYPE), so single- and multi-provider .env files both work.
CONNECTOR_ID = config.get("connector_id")
INSTRUMENT_ID = config.get("instrument_id")
PROVIDER = config.get("active_provider") or config.get("provider_type", "unknown")
print_summary("Config", provider=PROVIDER, instrument=INSTRUMENT_ID)
@@ -82,7 +74,7 @@ print("\n── Section 1: Create Embedded Wallet ──")
print("Backend operation: provision wallet for a new user")
# For this tutorial, reuse the developer's LINKED_EMAIL as the end-user identity.
# In production, pass each user's real email here.
# In your own application, pass each user's own email here.
NEW_USER_ID = "tutorial-03-user"
NEW_EMAIL = os.environ.get("LINKED_EMAIL", "tutorial03@example.com")
@@ -104,12 +96,10 @@ NEW_WALLET = inst["paymentInstrumentDetails"]["embeddedCryptoWallet"]["walletAdd
if inst.get("status") != "ACTIVE":
print("Waiting for instrument to become ACTIVE...")
wait_for_status(
dp_client.get_payment_instrument,
manager.get_payment_instrument,
"ACTIVE",
paymentManagerArn=PAYMENT_MANAGER_ARN,
paymentConnectorId=CONNECTOR_ID,
paymentInstrumentId=NEW_INSTRUMENT_ID,
userId=NEW_USER_ID,
user_id=NEW_USER_ID,
payment_instrument_id=NEW_INSTRUMENT_ID,
)
print_summary(
@@ -154,7 +144,7 @@ print()
print("Provider-specific steps:")
print()
print(" Coinbase CDP:")
print(" 1. Open the WalletHub URL (printed above, or from Setup Tutorial 00 Step 7a)")
print(" 1. Open the WalletHub URL (printed above, or from Setup Tutorial 00 Step 3)")
print(" 2. Log in with LINKED_EMAIL")
print(" 3. Consent to delegated signing")
print(" (Or: CDP Portal → Wallets → Embedded Wallet → Policies → Enable Delegated Signing)")
@@ -184,13 +174,12 @@ for label, inst_id, user_id in [
("New instrument", NEW_INSTRUMENT_ID, NEW_USER_ID),
]:
try:
resp = dp_client.get_payment_instrument_balance(
paymentManagerArn=PAYMENT_MANAGER_ARN,
paymentConnectorId=CONNECTOR_ID,
paymentInstrumentId=inst_id,
userId=user_id,
resp = manager.get_payment_instrument_balance(
payment_connector_id=CONNECTOR_ID,
payment_instrument_id=inst_id,
chain=chain,
token="USDC",
user_id=user_id,
)
balance = resp.get("tokenBalance", {})
amount = int(balance.get("amount", "0")) / 1_000_000
@@ -1,39 +1,54 @@
# Tutorial 04 — Agent with Coinbase Bazaar via AgentCore gateway
# Tutorial 04 — Agent with Coinbase Bazaar via AgentCore Gateway
| Information | Details |
|:--------------------|:---------------------------------------------------------------------|
| Tutorial type | Feature integration |
| Agent type | Single, discovery-driven |
| Agentic Framework | Strands Agents |
| LLM model | Anthropic Claude Sonnet 4.6 |
| Tutorial components | AgentCore gateway, Coinbase x402 Bazaar, AgentCorePaymentsPlugin |
| Example complexity | Intermediate |
| Information | Details |
|:--------------------|:---------------------------------------------------------------------------------|
| Tutorial type | Feature integration |
| Agent type | Single, discovery-driven |
| Agentic Framework | Strands Agents |
| LLM model | Anthropic Claude Sonnet 4.6 (`us.anthropic.claude-sonnet-4-6`) |
| Components | AgentCore Gateway (MCP target), Coinbase x402 Bazaar, `AgentCorePaymentsPlugin` |
| Example complexity | Intermediate |
> **Reads** the shared `.env` from Tutorial 00 (`PAYMENT_MANAGER_ARN`, `USER_ID`, `INSTRUMENT_ID`)
> plus `GATEWAY_URL`, and `AWS_REGION` (default `us-west-2`). **Does** — provisions an AgentCore
> Gateway fronting the Coinbase x402 Bazaar with the CLI, then runs a Strands agent that discovers
> and pays for Bazaar tools with the SDK. → [How the pieces fit together](../README.md#cli-vs-sdk)
## Overview
The **Coinbase x402 Bazaar** is an MCP marketplace where paid tools are listed with semantic descriptions, pricing, and input/output schemas. Agents discover tools via `search_resources` and call them via `proxy_tool_call` — the Bazaar handles 402 detection and payment routing.
The **Coinbase x402 Bazaar** is an MCP marketplace where paid tools are listed with semantic
descriptions, pricing, and input/output schemas. In this tutorial you use the **AgentCore CLI** to
provision one shared piece of infrastructure — a Gateway named `BazaarGateway` with a target named
`CoinbaseBazaar` that fronts the Bazaar's MCP endpoint — and return its Gateway URL. Then your agent
backend uses the **AgentCore SDK** (`PaymentManager`) to create a per-request spending session and
run a Strands agent that discovers tools at runtime via `search_resources` and calls (and pays for)
them via `proxy_tool_call`. The payment manager, connector, and instrument all come from Tutorial 00.
This tutorial connects the Bazaar to an **AgentCore gateway** as a native MCP target, then builds a Strands agent that discovers and calls paid tools with automatic payment handling.
The step forward from Tutorial 01 is discovery: the agent doesn't know which
URLs to call. It searches the Bazaar, picks a tool, and the `AgentCorePaymentsPlugin` handles the 402
automatically.
The key difference from Tutorial 01: the agent doesn't know which URLs to call. It discovers tools at runtime via `search_resources`, then pays and calls them via `proxy_tool_call`. The developer doesn't pre-configure which tools exist — the agent finds them at runtime.
> **Billable resources.** `agentcore deploy` creates a real AgentCore Gateway (billed per request +
> data transfer). See [AgentCore pricing](https://aws.amazon.com/bedrock/agentcore/pricing/). Run
> Clean Up when finished.
> **Testnet only.** Uses Base Sepolia (`network ETHEREUM`) with free USDC from
> [faucet.circle.com](https://faucet.circle.com/). Testnet USDC has no monetary value.
> **Supported regions:** `us-east-1`, `us-west-2`, `eu-central-1`, `ap-southeast-2`. Use the same
> region as your Tutorial 00 stack.
## Architecture
![Architecture Overview](images/architecture_overview.png)
![Architecture](images/architecture.png)
![Payment Sequence](images/payment_sequence.png)
![Request Payment Sequence](images/request_payment_sequence.png)
```
Developer Code
Strands Agent
+ AgentCorePaymentsPlugin
+ MCPClient (streamable HTTP)
│ MCP protocol
AgentCore gateway
AgentCore Gateway
Target: Coinbase x402 Bazaar
Coinbase x402 Bazaar
@@ -44,146 +59,150 @@ Developer Code
PaymentManager → ProcessPayment (sign + proof)
```
## Wallet-Agnostic by Design
The agent code is the same whether you configured Coinbase CDP or Stripe (Privy) in Tutorial 00. The `AgentCorePaymentsPlugin` receives a `payment_instrument_id` — AgentCore payments knows which wallet provider backs that instrument based on the PaymentConnector. The same code, same gateway, same Bazaar tools — only the `.env` values differ.
`bazaar_gateway_agent.py` connects to the Gateway with the MCP streamable-HTTP transport, lists the
Bazaar tools, and hands them to a Strands agent with an `AgentCorePaymentsPlugin`. The plugin
intercepts any 402 the Bazaar surfaces, signs the payment, and retries — no payment code in the agent
loop. It is **wallet-agnostic**: the same code works whether Tutorial 00 configured Coinbase CDP or
Stripe/Privy, because AgentCore payments resolves the wallet provider from the instrument's
PaymentConnector. Only the `.env` values differ.
## Prerequisites
- Tutorial 00 completed (`.env` with payment manager, instrument, session)
- Wallet funded with testnet USDC from [faucet.circle.com](https://faucet.circle.com/)
- AgentCore gateway created with Coinbase x402 Bazaar as a target (see Step 1 below)
- `GATEWAY_URL` set in `.env` (from gateway creation)
- AgentCore CLI (for CLI method): `npm install -g @aws/agentcore`
- **Tutorial 00 completed** — the shared `.env` (one directory up, `00-getting-started/.env`) is
populated with `PAYMENT_MANAGER_ARN`, `USER_ID`, `INSTRUMENT_ID`, and region.
- **Funded wallet with delegated signing** — the instrument must be `ACTIVE` (funded with testnet
USDC and delegated-signing granted in Tutorial 00 or Tutorial 03). The script asserts this.
- **AgentCore CLI** (this tutorial provisions the Gateway with it): `npm install -g @aws/agentcore`
(Node.js 20+).
- **AWS CLI configured**: `aws sts get-caller-identity`.
- **Region** — the SDK reads the region from the `AWS_REGION` env var (default `us-west-2`). Set
`AWS_REGION` (in your shell or the shared `.env`) to your Tutorial 00 stack region so
`PaymentManager` targets the right one.
- **Python deps**:
```bash
pip install -r requirements.txt
```
## gateway Setup (Step 1 — Manual)
## Walkthrough
Create an AgentCore gateway with the Coinbase x402 Bazaar as a target using one of these methods:
### Step 1 — Provision the Gateway + Bazaar target with the CLI
### Option A: AgentCore Console
1. Open the [Amazon Bedrock AgentCore console](https://console.aws.amazon.com/bedrock-agentcore/)
2. Navigate to gateway → Create gateway → Add Target
3. Target type: **Integrations**
4. Select **Coinbase x402 Bazaar**
5. No outbound auth needed
### Option B: AgentCore CLI
Scaffold a small project to hold the Gateway, add the Gateway and the Coinbase x402 Bazaar as an
`mcp-server` target, deploy, then fetch the Gateway URL and auth. Run these from this tutorial folder;
`agentcore create` drops you into a new `BazaarAgent/` project directory.
```bash
agentcore create --name BazaarAgent --defaults
# Scaffold a project to hold the Gateway
agentcore create --name BazaarAgent --framework Strands --protocol HTTP --model-provider Bedrock --memory none
cd BazaarAgent
# Add the Gateway and the Coinbase x402 Bazaar as an mcp-server target
agentcore add gateway --name BazaarGateway
agentcore add gateway-target \
--name CoinbaseBazaar \
--type mcp-server \
--endpoint https://api.cdp.coinbase.com/platform/v2/x402/discovery/mcp \
--gateway BazaarGateway
# Deploy the Gateway, then fetch its URL + auth
agentcore deploy -y
agentcore fetch access --name BazaarGateway --type gateway
```
After creating the gateway, add its URL to `.env`:
The `fetch access` output includes the Gateway URL you'll use in Step 2. If your Gateway uses
`CUSTOM_JWT` inbound auth, the same output also lists `CLIENT_ID`, `CLIENT_SECRET`, and `TOKEN_URL`.
> **Prefer the Console?** Gateway → Create Gateway → Add Target → target type **Integrations** →
> **Coinbase x402 Bazaar** (no outbound auth needed) configures the same Bazaar MCP server — the
> result is equivalent to the CLI path above.
### Step 2 — Put the Gateway URL in `.env`
Take the URL from `agentcore fetch access` and add it to the shared `.env`
(`00-getting-started/.env`):
```
GATEWAY_URL=https://<gateway-id>.gateway.bedrock-agentcore.<region>.amazonaws.com/mcp
```
For CUSTOM_JWT auth, also add `CLIENT_ID`, `CLIENT_SECRET`, `TOKEN_URL` from `agentcore fetch access` output.
If your Gateway uses `CUSTOM_JWT` inbound auth, also add `CLIENT_ID`, `CLIENT_SECRET`, and
`TOKEN_URL` from the `agentcore fetch access` output. If it uses `NONE` auth (the default), leave them
unset — the script auto-detects which to use.
## Running the Python Scripts
```bash
pip install -r requirements.txt
```
### Step 3 — Run the discovery-driven agent
```bash
python bazaar_gateway_agent.py
```
## What the Agent Does
The script verifies the Tutorial 00 instrument is `ACTIVE`, **creates a $1.00 / 60-min spending
session in-code with the SDK** (`manager.create_payment_session(...)`), connects to the Gateway over
MCP, and lets the agent discover and pay for Bazaar tools — every payment draws down the session
budget, and the `AgentCorePaymentsPlugin` handles each 402 automatically.
1. **Discovers tools** — Calls `search_resources` with queries like "market news", "weather data" to find available paid tools on the Bazaar
2. **Compares prices** — Lists tools across categories with their costs before deciding
3. **Makes budget-aware decisions** — Checks remaining session budget before selecting an expensive tool
4. **Calls multiple tools in sequence** — Tracks cumulative spend across multiple Bazaar calls within one session
## What the agent does
## Key Concepts
`bazaar_gateway_agent.py`, in order:
**Endpoint discoverability** — The Bazaar exposes 10,000+ pay-per-use x402 endpoints. The agent doesn't need to know endpoint URLs at build time — it searches and discovers at runtime.
1. **Verifies AWS credentials** and loads the shared `.env` via `utils.load_tutorial_env()`, which
derives the region from the `AWS_REGION` env var (default `us-west-2`).
2. **Checks the instrument** is `ACTIVE` (funded + delegated) and **creates a $1.00 payment session**
with the SDK.
3. **Connects to the Gateway** over MCP streamable HTTP (auto-detecting `NONE` vs `CUSTOM_JWT` auth)
and builds a Strands agent with the Bazaar tools + `AgentCorePaymentsPlugin`.
4. Runs four discovery scenarios: discover-and-call a paid tool, compare prices across categories,
make a budget-aware selection under $0.10, and chain multiple paid calls in one session.
5. **Prints session spend** (budget, remaining, spent) and a CloudWatch traces link.
**`search_resources`** — MCP tool exposed by the Bazaar. Accepts a `query` string and `network` parameter. Returns a list of paid tools with descriptions, pricing, and call schemas.
The discover → call → 402 → pay → retry sequence across the Gateway and Bazaar looks like this:
**`proxy_tool_call`** — MCP tool exposed by the Bazaar. Calls a discovered tool by name, passing arguments. If the tool returns a 402, the Bazaar detects it and the `AgentCorePaymentsPlugin` handles payment automatically.
![Payment Sequence](images/payment_sequence.png)
**MCPClient with streamable HTTP** — Connects the Strands agent to the AgentCore gateway using the MCP streamable HTTP transport. The `with mcp_client:` context manager maintains the connection.
## Inspect / verify
**gateway auth** — If your gateway uses CUSTOM_JWT auth, set `CLIENT_ID`, `CLIENT_SECRET`, `TOKEN_URL` in `.env`. If NONE auth (default), leave them unset. The script detects which auth to use automatically.
- Confirm the Gateway deployed: `agentcore status` (run from the `BazaarAgent/` project dir created in
Step 1).
- Inspect payment resources: `agentcore status --type payment` (from the same project dir).
- Confirm `.env` has `GATEWAY_URL` set (and `CLIENT_ID`/`CLIENT_SECRET`/`TOKEN_URL` if `CUSTOM_JWT`).
- The script itself prints per-session spend and remaining budget after the Bazaar calls.
## Troubleshooting
### GATEWAY_URL not set
Create the gateway as described in Step 1 above, then add `GATEWAY_URL=<url>` to your `.env` file. The URL format is `https://<gateway-id>.gateway.bedrock-agentcore.<region>.amazonaws.com/mcp`.
### MCP connection fails
Verify the gateway was deployed successfully with `agentcore status`. Check that your AWS credentials have gateway invoke permissions. If using CUSTOM_JWT auth, verify `CLIENT_ID`, `CLIENT_SECRET`, `TOKEN_URL` are set in `.env`.
### search_resources returns no results
The Bazaar indexes new tools periodically. Try broader search terms like "market" or "weather". The Bazaar endpoint must be reachable from the gateway — verify the target configuration in the console.
### Payment fails on proxy_tool_call
Verify the wallet has USDC (Tutorial 03 Section 4) and delegated signing is configured (Tutorial 00 Step 7b or Tutorial 03 Section 3).
## Summary
| AgentCore payments feature | What this tutorial demonstrates |
|---------------------------|-------------------------------|
| Endpoint discoverability | Agent connects to one gateway URL and discovers tools across multiple categories at runtime |
| Payment processing | `AgentCorePaymentsPlugin` handles 402 → sign → retry for every Bazaar tool call with no developer payment code |
| Payment limits | Session budget tracks cumulative spend across multiple paid tool calls from different merchants |
| Wallet integration | Same code works with Coinbase CDP or Stripe (Privy) — only `.env` values from Tutorial 00 differ |
## Role Separation for Deployed Agents
This tutorial runs locally under your AWS credentials. When deployed, the runtime process runs under **ProcessPaymentRole** — the plugin calls `ProcessPayment` on behalf of the agent within the budget set by the app backend. The runtime cannot create sessions, modify limits, or provision wallets. The agent (LLM) never calls `ProcessPayment` directly.
To test role separation locally, pass an assumed-role session to the SDK client:
```python
from utils import assume_role
import boto3
# App backend (ManagementRole) creates the session
manager = PaymentManager(payment_manager_arn=ARN, region_name=REGION)
session = manager.create_payment_session(user_id=USER_ID, ...)
# Agent runs under ProcessPaymentRole — can only ProcessPayment
agent_session = assume_role(boto3.Session(), PROCESS_PAYMENT_ROLE_ARN, 'agent')
agent_manager = PaymentManager(payment_manager_arn=ARN, boto3_session=agent_session)
# Pass agent_manager to the plugin — restricted credentials
```
See Tutorial 02 for the full role separation implementation.
| Symptom | Cause | Fix |
|---|---|---|
| `GATEWAY_URL not set in .env` | Gateway URL missing | Complete Step 1, then add `GATEWAY_URL=<url>` to the shared `.env` (Step 2) |
| `agentcore: command not found` | AgentCore CLI not installed | `npm install -g @aws/agentcore` |
| MCP connection fails / 401 / 403 | Gateway not deployed, or wrong/missing auth | `agentcore status` to confirm deploy; for `CUSTOM_JWT`, set `CLIENT_ID`/`CLIENT_SECRET`/`TOKEN_URL` from `agentcore fetch access` |
| `AssertionError: Instrument is ... — fund and delegate` | Instrument not `ACTIVE` | Fund the wallet with testnet USDC and grant delegated signing (Tutorial 00 or Tutorial 03) |
| `search_resources` returns no results | Bazaar index / narrow query | Try broader terms like "market" or "weather"; verify the target endpoint is reachable |
| `duplicate key: Set-Cookie` (or repeated transport errors) on `search_resources` / `proxy_tool_call` | Transient Coinbase Bazaar infrastructure issue — not your code or wallet | Re-run in a few minutes. The agent is instructed to stop after one retry rather than loop; no funds are charged on a failed settlement |
| Payment fails on `proxy_tool_call` | Wallet unfunded or delegation missing | Verify USDC balance and delegated signing (Tutorial 00 Step 4 / Tutorial 03) |
## Clean Up
Sessions expire automatically. To remove the gateway:
Payment **sessions expire automatically** (60 min).
> **Note:** `agentcore create --name BazaarAgent` scaffolds an agent **project**, so `agentcore
> deploy -y` provisions a billable **AgentCore Runtime** alongside the Gateway — even though this
> tutorial runs the agent **locally** (`python bazaar_gateway_agent.py`) and never invokes that
> Runtime. `agentcore remove all -y` reclaims the scaffolded Runtime **and** the Gateway in one step.
```bash
# Keep payment resources — only remove the gateway
agentcore remove gateway --name BazaarGateway -y
cd BazaarAgent
# Or remove all AgentCore project resources
# Reclaim all AgentCore project resources (the scaffolded Runtime + the Gateway)
agentcore remove all -y
# Granular option — remove only the Gateway, keep the rest
agentcore remove gateway --name BazaarGateway -y
```
Payment resources (Manager, Connector, Instrument): run the cleanup section in Tutorial 00.
The payment manager, connector, and instrument are shared across tutorials — tear them down with the
Clean Up section in **Tutorial 00** (SDK instrument delete, then `agentcore remove payment-connector`
/ `remove payment-manager` / `deploy -y`).
## Next Steps
## Next steps
- **Tutorial 05** — `../05-agent-with-browser-tool-pay-for-content/` — Browser + paywall payment pattern
- **Tutorial 06** — `../06-research-agent-with-payment-memory/` — Recall past data and skip redundant paid calls with AgentCore Memory
- **Tutorial 07** — `../07-multi-agent-payment-orchestrator/` — Multi-agent orchestration with per-agent budgets
- **Tutorial 05** — [`../05-agent-with-browser-tool-pay-for-content/`](../05-agent-with-browser-tool-pay-for-content/) — pay 402 paywalls inside a browser session.
- **Tutorial 06** — [`../06-research-agent-with-payment-memory/`](../06-research-agent-with-payment-memory/) — add AgentCore Memory to skip redundant paid calls.
- **Tutorial 07** — [`../07-multi-agent-payment-orchestrator/`](../07-multi-agent-payment-orchestrator/) — multiple agents with per-agent budgets.
@@ -57,31 +57,12 @@ print(f"Authenticated as: {identity['Arn']}")
print(f"Account: {identity['Account']}")
print(f"Region: {session.region_name}")
# ── Step 1: Gateway Setup (manual) ───────────────────────────────────────────
# ── Step 1: Gateway must already exist ───────────────────────────────────────
print("""
── Step 1: Create AgentCore Gateway with Bazaar Target (manual) ──
Option A: AgentCore Console (recommended)
1. Open https://console.aws.amazon.com/bedrock-agentcore/
2. Navigate to Gateway → Create Gateway → Add Target
3. Target type: Integrations
4. Select: Coinbase x402 Bazaar
5. No outbound auth needed
Option B: AgentCore CLI
agentcore create --name BazaarAgent --defaults
agentcore add gateway --name BazaarGateway
agentcore add gateway-target \\
--name CoinbaseBazaar \\
--type mcp-server \\
--endpoint https://api.cdp.coinbase.com/platform/v2/x402/discovery/mcp \\
--gateway BazaarGateway
agentcore deploy -y
agentcore fetch access --name BazaarGateway --type gateway
Save the Gateway URL to .env:
GATEWAY_URL=https://<gateway-id>.gateway.bedrock-agentcore.<region>.amazonaws.com/mcp
(Optional, for CUSTOM_JWT auth: CLIENT_ID, CLIENT_SECRET, TOKEN_URL)
── Step 1: Confirm your Gateway is set up ──
This script expects GATEWAY_URL in the shared .env. If you have not created the
Gateway yet, follow Step 1 of this tutorial's README (it provisions the Gateway and
Coinbase x402 Bazaar target with the agentcore CLI), then re-run this script.
""")
# ── Step 2: Load Payment Config ───────────────────────────────────────────────
@@ -90,12 +71,10 @@ PAYMENT_MANAGER_ARN = config["payment_manager_arn"]
REGION = config["region"]
USER_ID = config["user_id"]
if config.get("multi_provider"):
PROVIDER = list(config["instruments"].keys())[0]
INSTRUMENT_ID = config["instruments"][PROVIDER]["instrument_id"]
else:
INSTRUMENT_ID = config["instrument_id"]
PROVIDER = config.get("provider_type", "unknown")
# load_tutorial_env resolves instrument_id to the configured provider
# (CREDENTIAL_PROVIDER_TYPE), so single- and multi-provider .env files both work.
INSTRUMENT_ID = config["instrument_id"]
PROVIDER = config.get("active_provider") or config.get("provider_type", "unknown")
GATEWAY_URL = os.environ.get("GATEWAY_URL", "")
if not GATEWAY_URL:
@@ -121,15 +100,20 @@ print_summary(
gateway_url=GATEWAY_URL,
)
# ── Step 3: Create Payment Session ────────────────────────────────────────────
# ── Step 3: Create the payment session ────────────────────────────────────────
# The spending session is the per-request budget the agent draws down on each
# Bazaar payment. The agentcore CLI provisions the shared Gateway; the SDK is the
# application backend that mints each session — so the script creates it in-code
# via the PaymentManager it already has (matches the AWS devguide SDK path).
print("\n── Step 3: Create Payment Session ──")
sess_resp = manager.create_payment_session(
SESSION_BUDGET = {"maxSpendAmount": {"value": "1.00", "currency": "USD"}}
payment_session = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "1.00", "currency": "USD"}},
limits=SESSION_BUDGET,
expiry_time_in_minutes=60,
)
SESSION_ID = sess_resp["paymentSessionId"]
print(f"Session: {SESSION_ID} (budget: $1.00)")
SESSION_ID = payment_session["paymentSessionId"]
print(f"Created payment session: {SESSION_ID} (budget ${SESSION_BUDGET['maxSpendAmount']['value']} / 60 min)")
# ── Step 4: Connect to Gateway and Create Agent ───────────────────────────────
print("\n── Step 4: Connect to Gateway and Create Agent ──")
@@ -188,6 +172,15 @@ When asked to find information:
- Then call the most relevant tool
- Report what you found and what it cost
Handling failures — stop instead of looping:
- If search_resources or proxy_tool_call returns a transport/infrastructure error
(for example "duplicate key: Set-Cookie", a 5xx, or a connection error), retry the
SAME call at most ONCE. If it fails again, treat it as a temporary Bazaar-side outage:
stop retrying, report the error and which step failed, and move on. Do NOT keep calling
the same tool over and over — a persistent transport error will not clear by retrying.
- A tool priced above the session budget, or a payment rejection, is final: report it and
stop; do not attempt workarounds or alternative endpoints.
Always be transparent about payments."""
with mcp_client:
@@ -209,6 +202,15 @@ with mcp_client:
)
print(result.message)
# If a payment failed, the plugin raises an interrupt — stop cleanly instead of
# crashing the next agent() call with a TypeError.
if getattr(result, "stop_reason", None) == "interrupt" or getattr(result, "interrupts", None):
print(
"\n⚠️ A payment did not settle (likely delegated signing not granted for this wallet)."
"\n Skipping remaining scenarios. Grant delegation and re-run."
)
sys.exit(1)
# ── Step 5b: Multi-Tool Discovery ────────────────────────────────────────
print("\n── Step 5b: Multi-Tool Discovery — Compare Prices Across Categories ──")
result = agent(
@@ -219,6 +221,10 @@ with mcp_client:
)
print(result.message)
if getattr(result, "stop_reason", None) == "interrupt" or getattr(result, "interrupts", None):
print("\n⚠️ A payment did not settle. Skipping remaining scenarios.")
sys.exit(1)
# ── Step 5c: Budget-Aware Tool Selection ─────────────────────────────────
print("\n── Step 5c: Budget-Aware Tool Selection ──")
mid_session = manager.get_payment_session(
@@ -236,6 +242,10 @@ with mcp_client:
)
print(result.message)
if getattr(result, "stop_reason", None) == "interrupt" or getattr(result, "interrupts", None):
print("\n⚠️ A payment did not settle. Skipping remaining scenarios.")
sys.exit(1)
# ── Step 5d: Multiple Bazaar Calls in One Session ─────────────────────────
print("\n── Step 5d: Multiple Bazaar Calls in One Session ──")
result = agent(
@@ -247,6 +257,9 @@ with mcp_client:
)
print(result.message)
if getattr(result, "stop_reason", None) == "interrupt" or getattr(result, "interrupts", None):
print("\n⚠️ A payment did not settle in Step 5d. Continuing to spend report.")
# ── Step 6: Check Session Spend ──────────────────────────────────────────────
print("\n── Step 6: Check Session Spend ──")
session_info = manager.get_payment_session(
@@ -1,34 +1,47 @@
# Tutorial 05 — Strands Agent with Browser Tool Accesses Paid Content
# Tutorial 05 — Agent with Browser Tool Pays for Content
| Information | Details |
|:--------------------|:-----------------------------------------------------------------------|
| Tutorial type | Task-based |
| Agent type | Single |
| Agentic Framework | Strands Agents |
| LLM model | Anthropic Claude Sonnet 4.6 |
| Tutorial components | AgentCore Browser, Playwright, PaymentManager, x402 |
| SDK used | bedrock-agentcore (BrowserClient + PaymentManager), Strands, Playwright |
| Example complexity | Intermediate |
| Information | Details |
|:--------------------|:------------------------------------------------------------------------|
| Tutorial type | Task-based |
| Agent type | Single, payment-enabled |
| Agentic Framework | Strands Agents |
| LLM model | Anthropic Claude Sonnet 4.6 |
| Components | AgentCore Browser (`BrowserClient`), Playwright, `PaymentManager`, x402 |
| Example complexity | Intermediate |
> **Reads** the shared stack from [Tutorial 00](../00-setup-agentcore-payments/) — `PAYMENT_MANAGER_ARN`,
> `USER_ID`, `INSTRUMENT_ID`, `AWS_REGION`. **Does:** runs one SDK script that drives a managed AgentCore
> Browser session, creates a per-user payment session, and pays x402 paywalls inside the browser.
> Provisioning: **SDK**. → [How the pieces fit together](../README.md#cli-vs-sdk)
## Overview
In Tutorial 01, the `AgentCorePaymentsPlugin` handled x402 payments at the tool output level — working for API endpoints where the plugin can intercept and retry. For **browser-rendered content** (paywalled articles, content sites), the agent needs to detect the 402 inside the browser session, pay, and retry with proof headers — all within the same Playwright session.
In this tutorial your agent drives a managed cloud browser and pays x402 paywalls **inside the browser
session**. You build a custom Strands tool, `browse_with_payment`, that reuses the **PaymentManager** and
**payment instrument (wallet)** you provisioned in Tutorial 00. When `page.goto(url)` returns HTTP 402,
the tool reads the x402 requirements, calls `PaymentManager.generate_payment_header()` for a signed proof,
injects that header on the retry navigation, and returns the unlocked content — all without leaving the
same Playwright session, so cookies, auth tokens, and DOM context are preserved.
This tutorial builds a custom `browse_with_payment` tool that uses:
- **AgentCore Browser** (`BrowserClient`) for managed cloud Chromium
- **Playwright** as the browser automation library (connects to AgentCore Browser via WebSocket)
- **AgentCore payments** (`PaymentManager.generate_payment_header()`) for x402 signing
The resources involved: the **PaymentManager** and **payment instrument (wallet)** from Tutorial 00, a
short-lived **payment session** the script creates at runtime (`$1.00` budget, 60-minute expiry), and
an **AgentCore Browser** session for managed Chromium. A spending session is per-user, so the SDK
(`PaymentManager`) mints one here in your application code, scoped to the user you serve — the wallet
itself is the one you provisioned in Tutorial 00.
> **Note:** This pattern requires an x402-enabled endpoint returning HTTP 402. A future use case sample will provide a deployable x402 paywall server for full end-to-end testing.
> **Billable resources.** Running the script starts an AgentCore Browser session and can settle a
> real (testnet) x402 payment. See [AgentCore pricing](https://aws.amazon.com/bedrock/agentcore/pricing/).
> **Testnet only.** Wallets use Base Sepolia (Ethereum) or Solana Devnet with free USDC from
> [faucet.circle.com](https://faucet.circle.com/). Testnet USDC has no monetary value.
> **Supported regions:** `us-east-1`, `us-west-2`, `eu-central-1`, `ap-southeast-2`. Set `AWS_REGION`
> in the shared `.env` to one of these.
## Architecture
![Architecture](images/architecture.png)
![Payment Flow](images/payment_flow.png)
![Payment Flow Sequence](images/payment_flow_sequence.png)
```
Strands Agent
└── browse_with_payment tool
@@ -38,101 +51,180 @@ Strands Agent
├── 3. page.goto(url) → response interceptor detects 402
├── 4. Extract x402 requirements from response
├── 5. PaymentManager.generate_payment_header() → signed proof
├── 6. page.route() injects proof header
├── 6. page.route() injects proof header on the navigation request
├── 7. page.goto(url) retries → 200 + content
└── 8. Return content to agent
```
## Two Payment Patterns Compared
### Two payment patterns compared
| Pattern | Tool | Payment handling | Best for |
|---------|------|-----------------|----------|
|---------|------|------------------|----------|
| **Plugin (Tutorial 01)** | `http_request` | Plugin intercepts tool output, retries externally | API endpoints, MCP tools |
| **Browser (this tutorial)** | Custom `browse_with_payment` | Tool handles 402 internally, retries in same session | Browser-rendered content, paywalls |
| **Browser (this tutorial)** | Custom `browse_with_payment` | Tool handles 402 internally, retries in the same session | Browser-rendered content, paywalls |
Use the plugin pattern for API calls. Use the browser pattern when you need to maintain session state (cookies, auth tokens, DOM context) across the payment retry.
## Role Separation for Deployed Agents
This tutorial runs locally under your AWS credentials. When deployed to AgentCore runtime, the runtime process runs under **ProcessPaymentRole** — the tool calls `generate_payment_header()` within the budget set by the app backend. The runtime cannot create sessions, modify limits, or provision wallets.
The app backend passes all payment context (`payment_manager_arn`, `user_id`, `payment_instrument_id`, `payment_session_id`) via the invocation payload — the agent is stateless and wallet-agnostic.
To test role separation locally, pass an assumed-role session:
```python
from utils import assume_role
import boto3
# App backend creates the session
manager = PaymentManager(payment_manager_arn=ARN, region_name=REGION)
session = manager.create_payment_session(user_id=USER_ID, ...)
# Tool runs under ProcessPaymentRole
agent_session = assume_role(boto3.Session(), PROCESS_PAYMENT_ROLE_ARN, 'agent')
agent_manager = PaymentManager(payment_manager_arn=ARN, boto3_session=agent_session)
# Pass agent_manager to generate_payment_header() — restricted credentials
```
See Tutorial 02 for the full deployed implementation.
Use the plugin pattern for API calls. Use the browser pattern when you must keep session state
(cookies, auth tokens, DOM context) across the payment retry.
## Prerequisites
- Tutorials 00 and 01 completed (`.env` exists with payment manager, instrument)
- Wallet funded with testnet USDC
- Python 3.10+
- Playwright Chromium installed:
- **Tutorial 00 completed** — the shared `.env` (one directory up, at `00-getting-started/.env`) is
populated with `PAYMENT_MANAGER_ARN`, `USER_ID`, `INSTRUMENT_ID`, and `AWS_REGION`. This tutorial reads
them via `utils.load_tutorial_env()`.
- **Wallet funded + delegated signing granted** — the instrument must be `ACTIVE` (the script asserts
this before starting the browser). Fund with testnet USDC and grant delegated signing in Tutorial 00
(or Tutorial 03).
- **Python 3.10+** and AWS CLI configured (`aws sts get-caller-identity`).
- **Python deps + Playwright Chromium:**
```bash
pip install -r requirements.txt
python -m playwright install chromium
```
## Running the Python Scripts
No AgentCore CLI install is required to run this tutorial — the SDK does the work here. The CLI is used
only in the optional inspect step below, to confirm the shared stack from Tutorial 00 is live.
## Walkthrough
### Step 1 — Confirm Tutorial 00 is done
The shared `.env` at `00-getting-started/.env` must already contain `PAYMENT_MANAGER_ARN`, `USER_ID`,
`INSTRUMENT_ID`, and `AWS_REGION`, and the instrument must be `ACTIVE` (funded + delegated). If you have
not done this, run [Tutorial 00](../00-setup-agentcore-payments/) first.
### Step 2 — Install dependencies and the browser binary
```bash
pip install -r requirements.txt
python -m playwright install chromium
```
`python -m playwright install chromium` downloads the Chromium build Playwright uses to connect to the
managed AgentCore Browser session.
### Step 3 — Run the script
```bash
python browser_paywall_payments.py
```
## Key Concepts
That single command runs the whole flow. In order, the script:
**AgentCore Browser (`BrowserClient`)** — Managed cloud Chromium service. Provides a WebSocket endpoint that Playwright connects to via `connect_over_cdp()`. Handles browser lifecycle, scaling, and cleanup automatically.
1. Loads config from the shared `.env` via `load_tutorial_env()` (`PAYMENT_MANAGER_ARN`, `USER_ID`,
`INSTRUMENT_ID`, `AWS_REGION`).
2. Verifies the instrument is `ACTIVE`, then mints a per-user payment session in-code via
`manager.create_payment_session()` (`$1.00` budget, 60-minute expiry).
3. Builds the `browse_with_payment` tool and a Strands agent around it.
4. Asks the agent to browse the target x402 endpoint; the tool navigates, pays on 402, and retries in the
same session.
5. Prints remaining session spend and a CloudWatch traces link.
**Why the payment logic is inside the tool** — The `AgentCorePaymentsPlugin` from Tutorial 01 handles 402 at the tool output level. But for browser sessions, the browser state (cookies, auth tokens, opened tabs) must persist between the initial request and the payment retry. The tool must remain in control of the browser session throughout.
Default target URL (a Coinbase CDP x402 discovery endpoint used as a demonstration):
`https://api.cdp.coinbase.com/platform/v2/x402/discovery/search?query=technology+trends&limit=3`
**`page.route()` for header injection** — Playwright's route interception injects the payment proof header only on navigation requests. Sub-resources (images, CSS, analytics) don't receive the payment header, preventing credential leakage to third-party origins.
## How the tool pays inside the browser session
**`generate_payment_header()`** — Unlike the plugin's `ProcessPayment` call (which handles the full retry), `generate_payment_header()` just returns the signed header. The tool then uses Playwright to inject it and retry in the same browser session.
![Payment Flow Sequence](images/payment_flow_sequence.png)
The shared payment stack (manager + connector) comes from Tutorial 00, and the `SESSION_ID` is created
in-code via `manager.create_payment_session()` (Step 3, above). The one thing that must happen *inside*
the browser tool is signing the 402 proof for the current navigation — the agent needs the signed header
in-process to inject it on the retry. The tool does that with `PaymentManager.generate_payment_header()`
(repo import convention shown):
```python
from bedrock_agentcore.payments import PaymentManager
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
# Sign the 402 for the SESSION_ID created in-code via manager.create_payment_session — returns just
# the proof header; the tool injects it and retries the navigation.
payment_header = manager.generate_payment_header(
user_id=USER_ID,
payment_instrument_id=INSTRUMENT_ID,
payment_session_id=SESSION_ID, # created in-code via manager.create_payment_session
payment_required_request={"statusCode": 402, "headers": resp_headers, "body": body},
)
```
`generate_payment_header()` signs **just the proof header** — it does not perform the retry itself. The
tool then uses Playwright's `page.route()` to inject that header on the **navigation request only**;
sub-resources (images, CSS, analytics) never receive the payment header, so the proof is not leaked to
third-party origins. This is what makes the browser pattern well suited to paywalled, browser-rendered
content: the retry happens in the same session with all its state intact.
## What the script does
`browser_paywall_payments.py` connects Playwright to the managed AgentCore Browser over CDP
(`connect_over_cdp`), signs and retries the 402 in that same session, and always calls
`browser_client.stop()` in a `finally` block.
## Inspect / verify
The script prints remaining budget after the run. To check the same session's remaining spend
yourself, pass the session id the script printed (`Created payment session <SESSION_ID>`) to the SDK:
```python
from bedrock_agentcore.payments import PaymentManager
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
session_info = manager.get_payment_session(user_id=USER_ID, payment_session_id=SESSION_ID)
print(session_info["availableLimits"]["availableSpendAmount"], "of", session_info["limits"]["maxSpendAmount"])
```
To check the wallet's on-chain balance directly, use `PaymentManager.get_payment_instrument_balance()`
with the instrument id and connector id from the shared `.env`:
```python
from bedrock_agentcore.payments import PaymentManager
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
chain = "BASE_SEPOLIA" if NETWORK == "ETHEREUM" else "SOLANA_DEVNET"
balance = manager.get_payment_instrument_balance(
payment_connector_id=PAYMENT_CONNECTOR_ID,
payment_instrument_id=INSTRUMENT_ID,
chain=chain,
token="USDC",
user_id=USER_ID,
)
print(balance["tokenBalance"]["amount"] / 1_000_000, "USDC") # amount is micro-USDC
```
To confirm the shared payment manager/connector from Tutorial 00 are live, use the AgentCore CLI.
`agentcore status` is project-scoped, so run it from Tutorial 00's scaffolded project directory:
```bash
cd ../00-setup-agentcore-payments/PaymentSetup && agentcore status --type payment
```
Confirm the shared `.env` (`00-getting-started/.env`) still has `PAYMENT_MANAGER_ARN`, `USER_ID`,
`INSTRUMENT_ID`, and `AWS_REGION`. `load_tutorial_env()` raises `FileNotFoundError` only if the `.env`
file itself is missing; individual missing keys come back as `None`. The hard stop at startup is the
instrument-`ACTIVE` assertion — the script will not start a browser session against a non-active wallet.
## Troubleshooting
### Playwright Chromium not installed
Run `python -m playwright install chromium`. This downloads the Chromium binary needed for `connect_over_cdp()`.
### BrowserClient connection timeout
AgentCore Browser requires the managed browser service to be available. Check that your AWS region supports AgentCore Browser. Verify the `REGION` in `.env` matches a supported region.
### 402 not detected (endpoint returns 200 without payment)
The target URL must return HTTP 402 with an x402 payment payload to trigger the payment flow. The Coinbase discovery endpoint used in this tutorial may return 200 — in that case `paid=False` in the result, which is expected.
### Payment proof rejected (HTTP 402 on retry)
The wallet may be unfunded or delegated signing is not configured. Verify the wallet has USDC (Tutorial 03 Section 4) and delegation is set up (Tutorial 00 Step 7b).
| Error | Cause | Fix |
|---|---|---|
| `Instrument is <status> — fund and delegate…` | Instrument not `ACTIVE` | Fund the wallet + grant delegated signing in Tutorial 00 or 03 |
| Playwright Chromium not found | Browser binary not installed | `python -m playwright install chromium` |
| `BrowserClient` connection timeout | AgentCore Browser unavailable in region | Set `AWS_REGION` to a supported region (`us-east-1`, `us-west-2`, `eu-central-1`, `ap-southeast-2`) |
| Endpoint returns 200 without payment (`Paid: False`) | Target didn't return HTTP 402 | Expected for the demo discovery endpoint; use an x402 endpoint that returns 402 to exercise the pay path |
| Payment proof rejected (402 on retry) | Wallet unfunded or delegation not active | Verify USDC balance and delegated signing (Tutorial 00 / 03) |
| `FileNotFoundError` for `.env` at startup | Tutorial 00 not run / no shared `.env` | Run Tutorial 00 first to create and populate the shared `.env` |
## Clean Up
Browser sessions expire automatically. Payment sessions expire after their configured `expiryTimeInMinutes`. No manual cleanup needed for this tutorial.
No teardown is required for this tutorial: the AgentCore Browser session is closed by
`browser_client.stop()` on every run, and the payment session expires automatically after its 60-minute
`expiry_time_in_minutes`. Nothing here provisions new shared infrastructure.
For full payment resource cleanup, run the cleanup section in Tutorial 00.
To tear down the shared stack (manager, connector, IAM roles) and delete the instrument, run the
**Clean Up** section in [Tutorial 00](../00-setup-agentcore-payments/).
## Next Steps
## Next steps
- **Tutorial 06** — `../06-research-agent-with-payment-memory/` — Recall past data and skip redundant paid calls with AgentCore Memory
- **Tutorial 07** — `../07-multi-agent-payment-orchestrator/` — Multi-agent orchestration with per-agent budgets and provider-separated wallets
- **Use case: Browser paywall** — `../../02-use-cases/pay-for-content-browser-use/` — End-to-end use case with a deployable x402 paywall server on AgentCore runtime
- **Tutorial 06** — [`../06-research-agent-with-payment-memory/`](../06-research-agent-with-payment-memory/) — add AgentCore Memory to recall past data and skip redundant paid calls.
- **Tutorial 07** — [`../07-multi-agent-payment-orchestrator/`](../07-multi-agent-payment-orchestrator/) — multiple agents, separate wallets, per-agent budgets.
@@ -32,14 +32,13 @@ Usage:
python browser_paywall_payments.py
Prerequisites:
- Tutorials 00 and 01 completed (.env exists)
- Tutorial 00 completed (.env exists with the payment manager + instrument)
- Wallet funded with testnet USDC
- pip install -r requirements.txt
- python -m playwright install chromium
"""
import asyncio
import json
import os
import sys
import time
@@ -58,17 +57,17 @@ PAYMENT_MANAGER_ARN = config["payment_manager_arn"]
REGION = config["region"]
USER_ID = config["user_id"]
if config.get("multi_provider"):
PROVIDER = list(config["instruments"].keys())[0]
INSTRUMENT_ID = config["instruments"][PROVIDER]["instrument_id"]
CONNECTOR_ID = config["instruments"][PROVIDER]["connector_id"]
else:
INSTRUMENT_ID = config["instrument_id"]
CONNECTOR_ID = config.get("connector_id")
PROVIDER = config.get("provider_type", "unknown")
# load_tutorial_env resolves instrument_id to the configured provider
# (CREDENTIAL_PROVIDER_TYPE), so single- and multi-provider .env files both work.
INSTRUMENT_ID = config["instrument_id"]
PROVIDER = config.get("active_provider") or config.get("provider_type", "unknown")
MODEL_ID = os.environ.get("MODEL_ID", "us.anthropic.claude-sonnet-4-6")
# Per-user spending budget for the browser tool's payment session.
SESSION_BUDGET_USD = "1.00"
SESSION_EXPIRY_MINUTES = 60
print_summary(
"Config",
payment_manager_arn=PAYMENT_MANAGER_ARN,
@@ -85,15 +84,17 @@ manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=RE
instr = manager.get_payment_instrument(user_id=USER_ID, payment_instrument_id=INSTRUMENT_ID)
instr_status = instr.get("status", "UNKNOWN")
assert instr_status == "ACTIVE", f"Instrument is {instr_status} — fund and delegate in Tutorial 00/03 first"
session_resp = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "1.00", "currency": "USD"}},
expiry_time_in_minutes=60,
)
SESSION_ID = session_resp["paymentSessionId"]
print(f"Instrument {INSTRUMENT_ID} is {instr_status}")
print(f"Session: {SESSION_ID} (budget: $1.00, expiry: 60 min)")
# A spending session is per-user, so the SDK mints one here in application code,
# scoped to the user we serve and capped at SESSION_BUDGET_USD for SESSION_EXPIRY_MINUTES.
session = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": SESSION_BUDGET_USD, "currency": "USD"}},
expiry_time_in_minutes=SESSION_EXPIRY_MINUTES,
)
SESSION_ID = session["paymentSessionId"]
print(f"Created payment session {SESSION_ID} (budget ${SESSION_BUDGET_USD}, {SESSION_EXPIRY_MINUTES} min)")
# ── Step 3-4: Build the browse_with_payment Tool ──────────────────────────────
from playwright.async_api import async_playwright # noqa: E402
@@ -102,15 +103,6 @@ from strands import tool # noqa: E402
from bedrock_agentcore.tools.browser_client import BrowserClient # noqa: E402
def extract_x402_requirements(headers, body):
"""Parse x402 payment requirements from a 402 response."""
try:
return json.loads(body)
except (json.JSONDecodeError, TypeError):
pass
return {"headers": headers, "body": body}
def _format_result(status: int, content: str, paid: bool, url: str) -> str:
"""Render the tool result as a single text string for Strands."""
header = f"URL: {url}\nHTTP status: {status}\nPaid: {paid}"
@@ -264,4 +256,4 @@ print(
f"region={REGION}#gen-ai-observability/agent-core"
)
print("\nDone. Sessions expire automatically.")
print("Next: python ../07-multi-agent-payment-orchestrator/multi_agent_payments.py")
print("Next: python ../06-research-agent-with-payment-memory/research_agent_with_memory.py")
@@ -5,20 +5,42 @@
| Tutorial type | Conversational |
| Agent type | Single |
| Agentic Framework | Strands Agents |
| LLM model | Anthropic Claude Sonnet 4.6 |
| Tutorial components | AgentCore payments, AgentCore Memory, AgentCorePaymentsPlugin |
| LLM model | Anthropic Claude Sonnet 4.6 (`us.anthropic.claude-sonnet-4-6`) |
| Components | AgentCore payments, AgentCore Memory, `AgentCorePaymentsPlugin` |
| Example complexity | Intermediate |
| SDK used | bedrock-agentcore SDK, Strands Agents SDK |
> **Reads** the shared `.env` from Tutorial 00 (`PAYMENT_MANAGER_ARN`, `INSTRUMENT_ID`, `PAYMENT_CONNECTOR_ID`, `USER_ID`, `NETWORK`, `AWS_REGION`). **Does**: creates an AgentCore Memory resource, mints a payment session, runs a Strands research agent that recalls prior research and skips re-paying, then deletes the Memory resource on exit. → [How the pieces fit together](../README.md#cli-vs-sdk)
## Overview
In previous tutorials the agent is stateless — every session starts from scratch and pays from scratch. This tutorial adds **AgentCore Memory** so the agent builds intelligence across sessions:
In earlier tutorials the agent is stateless — every session starts from scratch and pays from scratch. This tutorial adds **AgentCore Memory** so a Strands agent builds intelligence across sessions: it recalls topics it already paid to research (and skips re-paying), learns user preferences (budget tolerance, topic interests), and tracks which endpoints were useful versus expensive.
- Remembers what topics it already paid to research (avoids re-paying for the same data)
- Learns user preferences (budget tolerance, topic interests)
- Tracks which endpoints were useful versus expensive
The runnable script creates one AgentCore **Memory** resource (semantic strategy, namespace `/actor/{USER_ID}/facts/`), a **payment session** ($0.20 / 60 min) via `PaymentManager`, and a Strands `Agent` wired with a `recall_user_context` tool, `http_request`, and the `AgentCorePaymentsPlugin`. Before each paid call the agent recalls prior research, decides per topic whether to reuse memory or pay for fresh data, and reports cost transparently. The shared payment stack (manager, connector, instrument) comes from [Tutorial 00](../00-setup-agentcore-payments/).
The agent recalls past research before each request, decides per-topic whether to reuse memory or pay for fresh data, and reports cost transparently.
> **Billable resources.** AgentCore Memory incurs AWS charges for storage and retrieval, and each x402 call spends testnet USDC from your session budget. See [AgentCore pricing](https://aws.amazon.com/bedrock/agentcore/pricing/). The script deletes the Memory resource on exit (including on crash).
> **Testnet only.** Uses Base Sepolia (`NETWORK=ETHEREUM`) or Solana Devnet (`NETWORK=SOLANA`) with free USDC from [faucet.circle.com](https://faucet.circle.com/). Testnet USDC has no monetary value.
> **Supported regions:** `us-east-1`, `us-west-2`, `eu-central-1`, `ap-southeast-2`. Set `AWS_REGION` in the shared `.env` to one of these.
## Architecture
```
Strands Agent
+ recall_user_context (@tool)
+ http_request
+ AgentCorePaymentsPlugin
│ │
│ │
AgentCore AgentCore payments
Memory ProcessPayment
(recall) Session budget
Wallet Provider
Coinbase CDP — or — Stripe Privy
```
Workflow per request: **RECALL** (search memory) → **DECIDE** (pay or skip) → **FETCH** (plugin handles the 402) → **REPORT** (cost transparency).
### How Payments + Memory work together
@@ -38,47 +60,39 @@ Session 1 (new user) Session 2 (returning user)
• renewable energy researched ($0.05)
```
## Architecture
```
Strands Agent
+ recall_user_context (@tool)
+ http_request
+ AgentCorePaymentsPlugin
│ │
│ │
AgentCore AgentCore payments
Memory ProcessPayment
(recall) Session budget
Wallet Provider
Coinbase CDP — or — Stripe Privy
```
Workflow per request: **RECALL** (search memory) → **DECIDE** (pay or skip) → **FETCH** (plugin handles 402) → **REPORT** (cost transparency).
## Two Layers of Budget Control
### Two layers of budget control
| Layer | Controls | Enforced by |
|-------|----------|-------------|
| **Session budget** ($0.20, 60 min expiry) | Hard ceiling — cannot exceed | AgentCore payments service |
| **Memory intelligence** | Soft optimization — skip redundant calls | Agent logic (system prompt + recall tool) |
| **Session budget** ($0.20, 60 min expiry) | Hard ceiling — cannot be exceeded | AgentCore payments service (IAM + API) |
| **Memory intelligence** | Soft optimization — skip redundant paid calls | Agent logic (system prompt + recall tool) |
Budget enforcement is structural (IAM + API). Memory is additive intelligence on top.
Budget enforcement is structural; memory is additive intelligence layered on top. The agent code is wallet-agnostic — the same code runs whether Tutorial 00 configured Coinbase CDP or Stripe/Privy; only the `.env` values differ.
## Wallet-Agnostic by Design
## Choosing how to provision Memory
The agent code is the same whether you configured Coinbase CDP or Stripe (Privy) in Tutorial 00. The plugin receives a `payment_instrument_id` — AgentCore payments knows which wallet provider backs that instrument based on the PaymentConnector. Same code, same memory, same agent — only the `.env` values differ.
AgentCore gives you two ways to create the Memory resource, and both are first-class:
- **AgentCore CLI** — `agentcore add memory --name ResearchFacts --strategies SEMANTIC --expiry 30` provisions a semantic memory in one line. This is the right choice for most agents.
- **AgentCore SDK** — `MemoryControlPlaneClient.create_memory(...)` lets you set a **custom per-user `namespaceTemplates`** (`/actor/{USER_ID}/facts/`) and hands you the returned `MEMORY_ID` to use with `MemoryClient` for `batch_create_memory_records` and `retrieve_memory_records`.
This tutorial needs that fine-grained namespace control (memory is scoped per end user, `/actor/{USER_ID}/facts/`), so it uses the SDK path. The payment session is minted with the `PaymentManager` SDK for the same reason — sessions are per-user, so your backend creates them scoped to the user you serve, with a custom budget and expiry.
## Prerequisites
- Tutorial 00 completed (`.env` with `PAYMENT_MANAGER_ARN`, `PAYMENT_INSTRUMENT_ID`, `PAYMENT_CONNECTOR_ID`)
- Wallet funded with testnet USDC from [faucet.circle.com](https://faucet.circle.com/)
- IAM permissions for AgentCore Memory in addition to the payments permissions from Tutorial 00 — see below
- **Tutorial 00 completed** — the shared `.env` (one directory up, at `00-getting-started/.env`) is populated with `PAYMENT_MANAGER_ARN`, `INSTRUMENT_ID`, `PAYMENT_CONNECTOR_ID`, `USER_ID`, `NETWORK`, and `AWS_REGION`. This tutorial reads them via `utils.load_tutorial_env()`.
- **Funded wallet** — the instrument from Tutorial 00 is `ACTIVE` and holds testnet USDC from [faucet.circle.com](https://faucet.circle.com/). (The script asserts `status == ACTIVE` and exits early otherwise.)
- **IAM permissions for AgentCore Memory** in addition to Tutorial 00's payments permissions (see below).
- **Python deps** for this tutorial:
```bash
pip install -r requirements.txt
```
This tutorial's runnable flow is pure Python/SDK — the shared payment infrastructure already exists from Tutorial 00, so no `agentcore` CLI commands are needed to run it.
### IAM permissions for AgentCore Memory
The caller identity that runs this script needs AgentCore Memory permissions in addition to the payments permissions from Tutorial 00. On a local laptop with an admin profile this is automatic. On SageMaker or other restricted environments, attach the following policy. Scope the resource ARN to your AWS account and region. `CreateMemory` and `ListMemories` are account-level actions and require `Resource: "*"` — narrow them further with condition keys (e.g. `aws:RequestTag`) if needed.
The caller identity running the script needs Memory permissions on top of Tutorial 00's payments permissions. On a laptop with an admin profile this is automatic. On SageMaker or other restricted environments, attach the policy below (scope the resource ARN to your account/region). `CreateMemory` and `ListMemories` are account-level actions and require `Resource: "*"`.
```json
[
@@ -105,94 +119,192 @@ The caller identity that runs this script needs AgentCore Memory permissions in
Without these, `create_memory` returns `AccessDeniedException` in Step 3.
## Setup
## Walkthrough
This tutorial declares its own dependencies in `requirements.txt`. Install once per tutorial — Tutorial 00's environment does not cover them.
### Step 1 — Run the research agent
```bash
pip install -r requirements.txt
```
## Running the Python Script
Everything the agent needs already exists from Tutorial 00, so the whole flow runs with one command:
```bash
python research_agent_with_memory.py
```
The script runs end to end without prompts: creates memory, hydrates it with simulated prior research, runs four queries against the agent (memory hit, budget recall, partial hit, recap), prints session spend, demonstrates budget enforcement with a tiny session, and deletes the memory resource at the end.
The rest of this section describes exactly what the script does, in order; the SDK snippets
match the code and use the repo import conventions `from bedrock_agentcore.payments import PaymentManager`
and `from bedrock_agentcore.memory import MemoryClient, MemoryControlPlaneClient`.
The script loads the resource IDs Tutorial 00 wrote to `.env` (`PAYMENT_MANAGER_ARN`, `INSTRUMENT_ID`,
`USER_ID`, region; `MODEL_ID` is optional, defaulting to `us.anthropic.claude-sonnet-4-6`), verifies
the instrument is `ACTIVE`, then continues with the session and Memory:
If a Python dependency is missing, the script prints the exact `pip install` command and exits — before any AWS resources are created.
### Step 2 — Verify the instrument and create the session
## What the Script Does
After confirming the instrument is `ACTIVE`, the script creates a $0.20 / 60-minute session in-code
with the `PaymentManager` SDK and uses the returned `paymentSessionId` for the rest of the run:
| Step | Action | Notes |
|------|--------|-------|
| 1 | Load config from `.env` | Reads `PAYMENT_MANAGER_ARN`, instrument, connector, region, model |
| 2 | Verify instrument and create session | $0.20 budget, 60 min expiry |
| 3 | Create AgentCore Memory | Semantic strategy, polls until ACTIVE |
| 4 | Hydrate memory | 4 records: profile, two past research entries, tool preferences |
| 5 | Build agent | Strands `Agent` with `recall_user_context`, `http_request`, payments plugin |
| 6 | Run 4 queries | Memory hit, budget recall, partial hit (the payoff), session recap |
| 7 | Check session spend | `get_payment_session` shows remaining budget |
| 8 | Budget enforcement demo | Tiny $0.0001 session — payment rejected at API level |
| 9 | View payment traces | Prints CloudWatch GenAI Observability Dashboard URL |
| Cleanup | Delete memory resource | Sessions expire automatically; payment manager belongs to Tutorial 00 |
```python
from bedrock_agentcore.payments import PaymentManager
## Key Concepts
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
assert manager.get_payment_instrument(user_id=USER_ID, payment_instrument_id=INSTRUMENT_ID)["status"] == "ACTIVE"
**AgentCore Memory** — A managed memory service for AI agents. Records are indexed and retrieved semantically. The script uses one *semantic strategy* with one namespace per user (`/actor/{USER_ID}/facts/`). Memory must reach `ACTIVE` status before record operations work.
session = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.20", "currency": "USD"}},
expiry_time_in_minutes=60,
)
session_id = session["paymentSessionId"]
```
**Memory namespace** — A path-like key that scopes records. The script uses `/actor/{USER_ID}/facts/` so multiple users could share the same Memory resource without colliding.
### Step 3 — Create AgentCore Memory (custom per-user namespace)
**Hydration** — Pre-populating memory with simulated prior research so the demo doesn't need a multi-session run to show value. The hydrated records use `yesterday`'s date so the agent's freshness rule (7 days) treats them as authoritative.
Because memory here is scoped per end user, create it with the AgentCore SDK's `MemoryControlPlaneClient.create_memory` so you can set a custom `namespaceTemplates` and use the returned `MEMORY_ID` directly. `wait_for_active=True` blocks until the resource is `ACTIVE` (typically 3090s), so downstream record operations are safe to run:
**Two-step paid-call pattern** — The Coinbase x402 *discovery search* endpoint is a free catalog. Calling it does not cost anything. To actually obtain research data the agent must call one of the `resource` URLs the catalog returns; that's where the 402 → payment → retry flow runs. The system prompt encodes this pattern so the agent's spend reports stay honest.
```python
from bedrock_agentcore.memory import MemoryControlPlaneClient
**Hard limit vs soft optimization** — The session's `maxSpendAmount` is enforced by AgentCore payments at the API level — the LLM cannot exceed it. Memory is an additive optimization that lets the agent stretch the same budget further by skipping redundant calls.
memory_ctl = MemoryControlPlaneClient(region_name=REGION)
memory = memory_ctl.create_memory(
name=f"research_memory_{...}",
description="Research agent memory - tracks topics, costs, and preferences",
event_expiry_days=30,
strategies=[{
"semanticMemoryStrategy": {
"name": "ResearchFacts",
"namespaceTemplates": [f"/actor/{USER_ID}/facts/"],
}
}],
wait_for_active=True,
)
memory_id = memory["id"] # used by batch_create_memory_records / retrieve_memory_records
```
**`recall_user_context` tool** — A `@tool`-decorated function that wraps `RetrieveMemoryRecords`. The agent's system prompt mandates a recall before any paid call.
> For a standard agent that doesn't need a custom namespace, the AgentCore CLI one-liner is the simpler path — see [Choosing how to provision Memory](#choosing-how-to-provision-memory).
### Step 4 — Hydrate memory (simulate a returning user)
Pre-populate the memory with four records via `batch_create_memory_records` so the agent behaves like it has research history — **two prior-research entries dated yesterday** (Seattle weather $0.05, renewable energy outlook $0.05), plus a **user-profile** record (interests, budget preference) and a **tool-preference** record (which endpoints to prefer or avoid). Yesterday's date keeps the cached research inside the 7-day freshness window so the agent treats it as authoritative. The records write into `/actor/{USER_ID}/facts/`; after the batch, wait ~25s for indexing before semantic search returns them.
### Step 5 — Build the Strands agent
Wire up an `Agent` with three capabilities: `recall_user_context` (searches memory before paying), `http_request` (calls paid endpoints), and `AgentCorePaymentsPlugin` (settles the x402 402 → payment → retry automatically). The plugin is configured with the manager ARN, user, instrument, session, region, and network preferences:
```python
from bedrock_agentcore.payments.integrations.strands import (
AgentCorePaymentsPlugin,
AgentCorePaymentsPluginConfig,
)
from strands import Agent
from strands.models import BedrockModel
payment_plugin = AgentCorePaymentsPlugin(
config=AgentCorePaymentsPluginConfig(
payment_manager_arn=PAYMENT_MANAGER_ARN,
user_id=USER_ID,
payment_instrument_id=INSTRUMENT_ID,
payment_session_id=session_id,
region=REGION,
network_preferences_config=["eip155:84532", "base-sepolia"], # or Solana devnet
)
)
agent = Agent(
model=BedrockModel(model_id=model_id, streaming=True),
tools=[recall_user_context, http_request],
plugins=[payment_plugin],
system_prompt=SYSTEM_PROMPT,
)
```
### Step 6 — Run four queries
The agent runs four queries that demonstrate memory-aware spending:
1. **Familiar topic** — asks for Seattle weather; expects a memory hit and reuses prior research instead of paying.
2. **Budget recall** — asks what it has researched, its budget preference, and last session's spend; answered entirely from memory.
3. **Partial memory hit (the payoff)** — asks for two topics; renewable energy is in memory (reused free), the new topic is fetched via a paid x402 call. The agent reports source, resource URL, and price per topic, then totals savings.
4. **Session recap with savings** — enumerates each request, marks memory vs paid, and compares total spend to the prior session and to fetching everything fresh.
### Step 7 — Check session spend
Read the remaining budget with the `PaymentManager` SDK:
```python
info = manager.get_payment_session(user_id=USER_ID, payment_session_id=session_id)
print(info["availableLimits"]["availableSpendAmount"], "of", info["limits"]["maxSpendAmount"])
```
### Step 8 — Budget enforcement (try it)
To prove the budget is a structural hard ceiling, set `SESSION_BUDGET = "0.0001"` near the top of
`research_agent_with_memory.py` (smaller than any priced x402 resource) and re-run the agent. The
session is minted in-code by `manager.create_payment_session`, so no extra setup is needed — the
paid resource call is rejected at the service level regardless of what the LLM decides:
```python
# research_agent_with_memory.py
SESSION_BUDGET = "0.0001" # was "0.20"; re-run to watch enforcement reject a paid call
```
### Step 9 — View payment traces
The script prints the Amazon CloudWatch GenAI Observability Dashboard URL for your region, where you can inspect payment success rates, session spend, and transaction latency.
### Step 10 — Cleanup (in `finally`)
The Memory resource is deleted whether the run succeeds or crashes. Payment sessions expire on their own after their expiry time; the Manager, Connector, and Instrument belong to Tutorial 00.
> If a Python dependency is missing, the script prints the exact `pip install` command and exits before any AWS resources are created.
## What the agent does
- **`recall_user_context` (`@tool`)** wraps `RetrieveMemoryRecords`. The system prompt mandates a recall before any paid call, once per distinct topic.
- **Freshness rule** — a memory hit dated within 7 days is treated as authoritative; the agent only pays when memory is missing, stale, or the user asks for an update.
- **Two-step paid-call pattern** — the Coinbase x402 *discovery search* endpoint is a free catalog; only calling one of the `resource` URLs it returns triggers the 402 → payment → retry flow the plugin handles.
- **Transparent reporting** — per topic, the agent states whether the answer came from memory or a fresh paid call, the resource URL paid, and the actual price, then totals savings.
## Inspect / verify
- **Payment stack & live status** (`agentcore status` is project-scoped — run it from Tutorial 00's scaffolded project dir):
```bash
cd ../00-setup-agentcore-payments/PaymentSetup && agentcore status --type payment
```
- **Session spend** — read the remaining budget for a session with the `PaymentManager` SDK (use the `paymentSessionId` the script printed):
```python
from bedrock_agentcore.payments import PaymentManager
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
info = manager.get_payment_session(user_id=USER_ID, payment_session_id=SESSION_ID)
print(info["availableLimits"]["availableSpendAmount"])
```
- **Traces** — open the CloudWatch GenAI Observability Dashboard URL the script prints.
- **`.env` keys** — confirm `PAYMENT_MANAGER_ARN`, `INSTRUMENT_ID`, `USER_ID`, `NETWORK`, `AWS_REGION` are present (written by Tutorial 00).
## Troubleshooting
### `create_memory` returns AccessDeniedException
Your caller identity is missing AgentCore Memory permissions. Attach the policy from the IAM section above. On SageMaker, add it to the execution role.
### Memory stays in CREATING for a long time
Normal startup is 3090 seconds. The script polls every 10 seconds. If it stays CREATING beyond 5 minutes, call `get_memory` once to inspect `failureReason`. Check CloudWatch logs in the bedrock-agentcore log group for index build errors.
## Role Separation for Deployed Agents
This tutorial runs locally under your AWS credentials. When deployed, the runtime process runs under **ProcessPaymentRole** — the plugin calls `ProcessPayment` on behalf of the agent within the budget set by the app backend. The runtime cannot create sessions, modify limits, or provision wallets. The agent (LLM) never calls `ProcessPayment` directly. The memory tool and payment plugin work the same way when deployed. See Tutorial 02 for the full role separation implementation.
To test role separation locally, pass an assumed-role session to the SDK client:
```python
from utils import assume_role
import boto3
# App backend (ManagementRole) creates the session
manager = PaymentManager(payment_manager_arn=ARN, region_name=REGION)
session = manager.create_payment_session(user_id=USER_ID, ...)
# Agent runs under ProcessPaymentRole — can only ProcessPayment
agent_session = assume_role(boto3.Session(), PROCESS_PAYMENT_ROLE_ARN, "agent")
agent_manager = PaymentManager(payment_manager_arn=ARN, boto3_session=agent_session)
# Pass agent_manager to the plugin — restricted credentials
```
| Symptom | Cause | Fix |
|---|---|---|
| `create_memory` returns `AccessDeniedException` | Caller identity missing Memory permissions | Attach the IAM policy from the Prerequisites section (add to the SageMaker execution role if applicable) |
| Instrument assertion fails (`Instrument is <status>`) | Wallet not `ACTIVE` / not funded or delegated | Complete funding + delegated signing in Tutorial 00/03 |
| Memory stays `CREATING` past ~5 min | Slow index build or failure | Call `get_memory` and inspect `failureReason`; check the `bedrock-agentcore` CloudWatch log group |
| `Delegated signing grant is not active` at a paid call | End user hasn't granted delegated signing | Grant it as in Tutorial 00/03 (WalletHub consent / Privy Connect agent) |
| Budget rejection in Step 8 | Expected — $0.0001 session cannot cover any resource call | This is the enforcement demo, not an error |
| `invalid_exact_evm_transaction_failed` / `Settlement failed` | Transient on-chain failure | Retry — funds aren't debited on a failed attempt |
## Clean Up
The script deletes the memory resource at the end. Payment sessions expire automatically after their `expiryTimeInMinutes`. Payment resources (Manager, Connector, Instrument) belong to Tutorial 00 — run the cleanup section in `setup_agentcore_payments.py` when you are done with all tutorials.
The script deletes the Memory resource in its `finally` block, so normal runs clean up automatically. Payment sessions expire on their own after `expiryTimeInMinutes`.
> **Cost notice:** AgentCore Memory may incur AWS charges based on storage and retrieval usage. The script's automatic cleanup keeps these costs minimal, but if the script aborts before reaching cleanup, delete the memory manually:
>
> ```bash
> aws bedrock-agentcore-control delete-memory --memory-id <id>
> ```
If the script aborts before cleanup, delete the Memory resource by hand with the same AgentCore SDK client the script uses:
## Next Steps
```python
from bedrock_agentcore.memory import MemoryControlPlaneClient
- **Tutorial 07** — `../07-multi-agent-payment-orchestrator/` — Multi-agent orchestration with per-agent budgets and provider-separated wallets
- **Use case: Browser paywall** — `../../02-use-cases/pay-for-content-browser-use/` — End-to-end use case with a deployable x402 paywall server on AgentCore runtime
MemoryControlPlaneClient(region_name=REGION).delete_memory(memory_id="<MEMORY_ID>")
```
The payment **Manager**, **Connector**, and **Instrument** belong to Tutorial 00 — tear them down there (CLI `agentcore remove …` for the manager/connector, SDK instrument delete) when you're finished with all tutorials.
## Next steps
- **Tutorial 07** — [`../07-multi-agent-payment-orchestrator/`](../07-multi-agent-payment-orchestrator/) — multiple agents with per-agent budgets and provider-separated wallets (CLI + SDK; needs multi-provider setup).
- **Use case: Browser paywall** — [`../../02-use-cases/pay-for-content-browser-use/`](../../02-use-cases/pay-for-content-browser-use/) — end-to-end use case with a deployable x402 paywall server on AgentCore Runtime.
@@ -39,15 +39,12 @@ from datetime import datetime, timedelta, timezone
# Importing everything up front so missing dependencies fail in the first
# second instead of after expensive AWS calls in Steps 1-4.
_MISSING = []
try:
import boto3
except ImportError:
_MISSING.append("boto3")
try:
from dotenv import load_dotenv
except ImportError:
_MISSING.append("python-dotenv")
try:
from bedrock_agentcore.memory import MemoryClient, MemoryControlPlaneClient
from bedrock_agentcore.payments import PaymentManager
from bedrock_agentcore.payments.integrations.strands import (
AgentCorePaymentsPlugin,
@@ -85,6 +82,13 @@ ENV_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__
load_dotenv(ENV_FILE, override=True)
# Per-session spending ceiling for this research agent. The session is a hard
# budget enforced by AgentCore payments at the API level, independent of what
# the LLM decides. To watch enforcement reject a paid call, drop this to
# "0.0001" (smaller than any priced x402 resource) and re-run — see Step 8.
SESSION_BUDGET = "0.20"
SYSTEM_PROMPT = """You are a research agent with payment capabilities and persistent memory.
The user pays real money for fresh data, so reusing prior research is part of your job.
@@ -119,14 +123,6 @@ If a paid call fails, report the error — do not attempt workarounds, do not
follow trial/free links from a 402 response body, and do not invent
environment variable names for auth tokens."""
TINY_SYSTEM_PROMPT = """You are a research agent. To fetch paid data on x402:
1. Call the discovery search URL (this is FREE — it just returns a catalog).
2. Pick a resource URL from the catalog and call THAT URL with http_request.
Step 2 is what triggers the 402 → payment flow. The plugin handles payment
automatically — pass only `method` and `url` to http_request.
If a paid call is rejected, report the exact error message verbatim. Do not
attempt workarounds or invent retry strategies."""
def main() -> None:
# ── Step 1: Load Config ───────────────────────────────────────────────
@@ -135,12 +131,10 @@ def main() -> None:
region = config["region"]
user_id = config["user_id"]
if config.get("multi_provider"):
provider = list(config["instruments"].keys())[0]
instrument_id = config["instruments"][provider]["instrument_id"]
else:
instrument_id = config["instrument_id"]
provider = config.get("provider_type", "unknown")
# load_tutorial_env resolves instrument_id to the configured provider
# (CREDENTIAL_PROVIDER_TYPE), so single- and multi-provider .env files both work.
instrument_id = config["instrument_id"]
provider = config.get("active_provider") or config.get("provider_type", "unknown")
model_id = os.environ.get("MODEL_ID", "us.anthropic.claude-sonnet-4-6")
@@ -157,28 +151,38 @@ def main() -> None:
instr_status = instr.get("status", "UNKNOWN")
assert instr_status == "ACTIVE", f"Instrument is {instr_status} — fund and delegate in Tutorial 00/03 first"
sess_resp = manager.create_payment_session(
# Sessions are per-user, so the backend mints one scoped to the user it
# serves, with a custom budget and expiry. Create it in-code via the SDK.
session = manager.create_payment_session(
user_id=user_id,
limits={"maxSpendAmount": {"value": "0.20", "currency": "USD"}},
limits={"maxSpendAmount": {"value": SESSION_BUDGET, "currency": "USD"}},
expiry_time_in_minutes=60,
)
session_id = sess_resp["paymentSessionId"]
session_id = session["paymentSessionId"]
print(f"✅ Instrument {instrument_id} is {instr_status}")
print(f"Session: {session_id} (budget: $0.20)")
print(f"Created payment session {session_id} (budget ${SESSION_BUDGET} / 60 min)")
# ── Step 3: Create Memory ─────────────────────────────────────────────
# AgentCore Memory with a semantic strategy that extracts facts from
# conversations: topics researched, endpoints called and their cost,
# and user preferences expressed during conversation.
memory_ctl = boto3.client("bedrock-agentcore-control", region_name=region)
memory_data = boto3.client("bedrock-agentcore", region_name=region)
# AgentCore SDK Memory clients: control plane for the resource lifecycle
# (create / get / delete), data plane for records (batch create / retrieve).
memory_ctl = MemoryControlPlaneClient(region_name=region)
memory_data = MemoryClient(region_name=region)
memory_name = f"research_memory_{uuid.uuid4().hex[:8]}"
memory_resp = memory_ctl.create_memory(
# create_memory with wait_for_active=True blocks until the resource is
# ACTIVE (usually 30-90s), so downstream record ops are safe to run.
print(
"\n Creating memory and waiting for it to become ACTIVE (usually 30-90s)...",
flush=True,
)
memory = memory_ctl.create_memory(
name=memory_name,
description=("Research agent memory - tracks topics, costs, and preferences"),
eventExpiryDuration=30,
memoryStrategies=[
description="Research agent memory - tracks topics, costs, and preferences",
event_expiry_days=30,
strategies=[
{
"semanticMemoryStrategy": {
"name": "ResearchFacts",
@@ -186,37 +190,16 @@ def main() -> None:
}
}
],
wait_for_active=True,
)
memory_id = memory_resp["memory"]["id"]
print(f"✅ Memory created: {memory_id}")
memory_id = memory["id"]
print(f"✅ Memory created and ACTIVE: {memory_id}")
print(" Strategy: ResearchFacts (semantic extraction)")
print(f" Namespace: /actor/{user_id}/facts/")
# Everything below this point is wrapped in try/finally so that
# memory_id always gets cleaned up even on crash.
try:
# Wait for memory to become ACTIVE. CreateMemory returns immediately
# with status=CREATING; downstream record ops require ACTIVE.
print(
"\n Waiting for memory to become ACTIVE (usually 30-90s)...",
flush=True,
)
elapsed = 0
while True:
status = memory_ctl.get_memory(memoryId=memory_id)["memory"]["status"]
if status == "ACTIVE":
print(f" ✅ Memory is ACTIVE (after {elapsed}s)", flush=True)
break
if status == "FAILED":
reason = memory_ctl.get_memory(memoryId=memory_id)["memory"].get("failureReason", "unknown")
raise RuntimeError(f"Memory creation failed: {reason}")
print(
f" status={status}, elapsed={elapsed}s, polling again in 10s...",
flush=True,
)
time.sleep(10)
elapsed += 10
# ── Step 4: Hydrate Memory (Simulate Returning User) ──────────
# Pre-populate memory to simulate a returning user with research
# history. Two prior research topics ($0.05 each, $0.10 total):
@@ -429,7 +412,7 @@ def main() -> None:
print("Query 3 — Partial memory hit (the payoff)")
print("=" * 70)
result = agent(
"Research two topics for me: (1) renewable energy market outlook and (2) Daily fashion trends. "
"Research two topics for me: (1) renewable energy market outlook and (2) AI market trends. "
"Before paying for anything, check what we already know about each topic from prior sessions — "
"if we have recent research on it, reuse it and don't pay again. "
"For anything we don't already have, find a paid data source by browsing the catalog at "
@@ -473,57 +456,19 @@ def main() -> None:
budget_limit=session_info.get("limits", {}).get("maxSpendAmount", "N/A"),
)
# ── Step 8: Budget Enforcement in Action ──────────────────────
# Memory optimizes spend, but the session budget is the hard
# limit. Let's prove it — create a tiny session ($0.0001) smaller
# than any priced x402 resource, then have the agent actually
# attempt a paid resource fetch. AgentCore payments will reject
# the payment at the API level, regardless of what the LLM
# decides.
#
# Note: discovery search itself is free, so it would always
# succeed. Budget enforcement only triggers when the agent calls
# a *resource* URL returned by discovery — that's where the
# 402 → payment flow runs.
# ── Step 8: Budget Enforcement (README exercise) ─────────────
# Memory optimizes spend, but the session budget is the hard limit —
# enforced by AgentCore payments at the API level, not by agent logic.
# To prove it, set SESSION_BUDGET = "0.0001" near the top of this file
# (smaller than any priced x402 resource) and re-run. The session is
# minted in-code by manager.create_payment_session, so no extra setup
# is needed — the agent still tries to pay, and AgentCore payments
# rejects the paid resource call at the service level.
print("\n" + "=" * 70)
print("Step 8 — Budget Enforcement (tiny $0.0001 session)")
print("Step 8 — Budget enforcement: see the README's tiny-session exercise")
print("=" * 70)
tiny_resp = manager.create_payment_session(
user_id=user_id,
limits={"maxSpendAmount": {"value": "0.0001", "currency": "USD"}},
expiry_time_in_minutes=15,
)
tiny_session_id = tiny_resp["paymentSessionId"]
print(f"Tiny session: {tiny_session_id} (budget: $0.0001)")
tiny_plugin = AgentCorePaymentsPlugin(
config=AgentCorePaymentsPluginConfig(
payment_manager_arn=payment_manager_arn,
user_id=user_id,
payment_instrument_id=instrument_id,
payment_session_id=tiny_session_id,
region=region,
network_preferences_config=["eip155:84532", "base-sepolia"],
)
)
budget_test_agent = Agent(
model=BedrockModel(model_id=model_id, streaming=True),
tools=[http_request],
plugins=[tiny_plugin],
system_prompt=TINY_SYSTEM_PROMPT,
)
print("\nAttempting a paid resource fetch with $0.0001 budget...")
result = budget_test_agent(
"Find a paid weather data resource on x402 by browsing the catalog at "
"https://api.cdp.coinbase.com/platform/v2/x402/discovery/search?query=weather&network=base-sepolia&limit=1 "
"and then actually call the resource URL it returns to fetch the weather data. "
"Report exactly what happened — including any payment errors verbatim."
)
print(result.message)
print("\n✅ Budget enforcement: the $0.0001 session cannot cover any Bazaar resource call.")
print(" This is structural — enforced by AgentCore payments at the API level, not by agent logic.")
print('Set SESSION_BUDGET = "0.0001" at the top of this script and re-run to')
print("watch AgentCore payments reject a paid resource call at the API level.")
# ── Step 9: View Payment Traces ───────────────────────────────
# Every payment produces a trace. Explore the service-generated
@@ -542,11 +487,15 @@ def main() -> None:
# automatically. Payment resources (Manager, Connector, Instrument)
# belong to Tutorial 00 — don't delete them here.
try:
memory_ctl.delete_memory(memoryId=memory_id)
memory_ctl.delete_memory(memory_id=memory_id)
print(f"\n✅ Deleted memory: {memory_id}")
except Exception as e: # noqa: BLE001
print(f"\n⚠️ Could not delete memory {memory_id}: {e}")
print(f" Delete manually with: aws bedrock-agentcore-control delete-memory --memory-id {memory_id}")
print(
" Delete manually with the SDK: "
f"MemoryControlPlaneClient(region_name='{region}')"
f".delete_memory(memory_id='{memory_id}')"
)
if __name__ == "__main__":
@@ -3,94 +3,191 @@
| Information | Details |
|:--------------------|:---------------------------------------------------------------------|
| Tutorial type | Task-based, advanced |
| Agent type | Multi-agent (orchestrator + 2 specialists) |
| Agentic Framework | Strands Agents (agents-as-tools pattern) |
| LLM model | Anthropic Claude Sonnet 4.6 |
| Tutorial components | AgentCore payments (multi-session), AgentCore runtime, AgentCore CLI |
| Agent type | Multi-agent (orchestrator + 2 specialists, agents-as-tools) |
| Agentic Framework | Strands Agents |
| LLM model | Anthropic Claude Sonnet 4.6 (`us.anthropic.claude-sonnet-4-6`) |
| Components | AgentCore payments (multi-session), AgentCore Runtime, AgentCore CLI |
| Example complexity | Advanced |
> **Reads** `PAYMENT_MANAGER_ARN`, `USER_ID`, `COINBASE_INSTRUMENT_ID`, `PRIVY_INSTRUMENT_ID`,
> `COINBASE_CONNECTOR_ID`, `PRIVY_CONNECTOR_ID`, `AWS_REGION` from the shared `.env`. **Does** create
> per-agent spending sessions in-code with the AgentCore SDK, run three local demos, then deploy the
> orchestrator to AgentCore Runtime with online evaluation. → [How the pieces fit together](../README.md#cli-vs-sdk)
## Overview
Build a multi-agent system with per-agent budgets, multi-wallet support, and full spend attribution — then demonstrate intelligent failover when a budget is exhausted.
Build a multi-agent system with per-agent budgets, multi-wallet support, and full spend
attribution — then watch the orchestrator route work between specialists and fail over when one
agent's budget is exhausted. It uses one **PaymentManager** with two connectors (CoinbaseCDP +
StripePrivy), two **payment instruments** (wallets), and two **payment sessions** (one budget each).
### Three Demos
The design gives you structural safety, stated positively: **the orchestrator only routes and
monitors — spending authority lives with the specialist agents that hold the payment plugins.** Each
specialist carries its own `AgentCorePaymentsPlugin` scoped to its own session and wallet, so budgets
stay isolated and every dollar is attributable to exactly one agent.
Two tools do complementary jobs here. The **AgentCore CLI** scaffolds, deploys, and evaluates the
runtime (`agentcore create` / `deploy` / `add online-eval` / `invoke`). The **AgentCore SDK** is your
application backend: `PaymentManager.create_payment_session(...)` mints each per-agent spending
session in-code, and each specialist's `AgentCorePaymentsPlugin` binds to its session ID and settles
the x402 payment at request time.
> **Billable resources.** `agentcore deploy` and online evaluation create real AWS resources billed
> per invocation. See [AgentCore pricing](https://aws.amazon.com/bedrock/agentcore/pricing/). Run
> Clean Up when finished.
> **Testnet only.** Wallets use Base Sepolia (network `ETHEREUM`) with free USDC from
> [faucet.circle.com](https://faucet.circle.com/). Testnet USDC has no monetary value.
> **Supported regions:** `us-east-1`, `us-west-2`, `eu-central-1`, `ap-southeast-2`. Set
> `AWS_REGION` in the shared `.env` to one of these.
### Three demos (`multi_agent_payments.py`)
| Demo | Pattern | What it proves |
|------|---------|---------------|
| **Demo 1** | Spend Attribution | Two wallets, two budgets, full per-agent cost tracking |
| **Demo 2** | Budget Exhaustion + Failover | Orchestrator detects payment rejection, reroutes to healthy agent |
| **Demo 3** | Structural Safety | Orchestrator literally cannot spend — even with http_request |
| **Demo 2** | Budget Exhaustion + Failover | Orchestrator detects a payment rejection and reroutes to the healthy agent |
| **Demo 3** | Structural Safety | Orchestrator has `http_request` but no plugin — spending authority stays with the specialists |
### Key Concepts
| Concept | How it works |
|---------|-------------|
| **Multi-wallet** | One PaymentManager, two connectors (Coinbase + Privy), two instruments |
| **Per-agent budgets** | Separate sessions with independent spend limits |
| **Orchestrator decoupling** | Orchestrator has NO plugin — cannot spend, only routes |
| **Budget exhaustion + failover** | When one agent's budget is rejected, orchestrator reroutes to the other |
| **Role enforcement** | ProcessPaymentRole on runtime — deterministic code processes payments |
![Payment Flow — Three Demos](images/payment_flow_three_demos.png)
## Architecture
![Architecture Overview](images/architecture_overview.png)
![Payment Flow — Three Demos](images/payment_flow_three_demos.png)
```
PaymentManager
├── CoinbaseCDP Connector → Research Agent (Session A, $0.50)
├── CoinbaseCDP Connector → Research Agent (Session A, $0.50)
└── StripePrivy Connector → Discovery Agent (Session B, $0.20)
Orchestrator
├── research_agent.as_tool() → Research Agent (Coinbase plugin)
├── research_agent.as_tool() → Research Agent (Coinbase plugin)
├── discovery_agent.as_tool() → Discovery Agent (Privy plugin)
└── check_budgets → monitors both sessions
[NO PAYMENT PLUGIN — structural safety]
└── check_budgets → monitors both sessions
[routes and monitors only — spending lives with the specialists]
```
## Who Does What
| Who | Does what | Role |
|-----|-----------|------|
| App backend | Verifies wallets, mints per-agent sessions in-code (`manager.create_payment_session`), invokes orchestrator | Your AWS credentials |
| Orchestrator | Routes tasks and monitors budgets | None (routing/monitoring only) |
| Research Agent | Calls paid endpoints, spends from Session A | ProcessPaymentRole (Coinbase wallet) |
| Discovery Agent | Calls paid endpoints, spends from Session B | ProcessPaymentRole (Privy wallet) |
| Who | Does What | Role |
|-----|----------|------|
| App backend | Creates sessions, allocates budgets, invokes orchestrator | ManagementRole |
| Orchestrator | Routes tasks, monitors budgets | NONE (no plugin) |
| Research Agent | Calls paid endpoints, spends from Session A | ProcessPaymentRole (Coinbase) |
| Discovery Agent | Calls paid endpoints, spends from Session B | ProcessPaymentRole (Privy) |
`payment_orchestrator.py` is the runtime entrypoint (`BedrockAgentCoreApp` + `@app.entrypoint`): the
app backend passes two session IDs and two instrument IDs in the invocation payload, and each
specialist receives its own plugin scoped to its own budget.
## Prerequisites
- Tutorial 00b (`multi_provider_setup.py`) completed`.env` has both Coinbase + Privy instruments
- Both wallets funded with testnet USDC from [faucet.circle.com](https://faucet.circle.com/)
- Python 3.10+
- Node.js 20+ and AgentCore CLI for runtime deployment: `npm install -g @aws/agentcore`
- **Tutorial 00 shared stack + multi-provider setup completed.** In
[`../00-setup-agentcore-payments/`](../00-setup-agentcore-payments/), run
`setup_agentcore_payments.py` first (provisions the manager and base infrastructure), then
`multi_provider_setup.py` (adds the second connector — one manager, two connectors: Coinbase +
Privy). Together they write `PAYMENT_MANAGER_ARN`, `USER_ID`, `COINBASE_INSTRUMENT_ID`,
`PRIVY_INSTRUMENT_ID`, `COINBASE_CONNECTOR_ID`, and `PRIVY_CONNECTOR_ID` to the shared `.env` (one
directory up, at `00-getting-started/.env`). This tutorial reads them via
`utils.load_tutorial_env()`.
- **Both wallets funded** with testnet USDC from [faucet.circle.com](https://faucet.circle.com/)
(Base Sepolia), with delegated signing granted — both instruments must be `ACTIVE`.
- **Python 3.10+** and AWS CLI configured (`aws sts get-caller-identity`).
- **Node.js 20+ and the AgentCore CLI** (the runtime deploy uses it):
```bash
npm install -g @aws/agentcore
```
- Python deps:
```bash
pip install -r requirements.txt
```
## Running the Python Scripts
## Walkthrough
### Step 1 — Run the local demos
Run all three demos locally against the live payment infrastructure:
```bash
pip install -r requirements.txt
python multi_agent_payments.py
```
```bash
# Local execution — all three demos
python multi_agent_payments.py
What it does, in order:
# Start agent locally for testing
1. Verifies your AWS identity and loads the multi-provider config from `.env` (fails fast with a
clear `ValueError` if the multi-provider setup hasn't been run).
2. Confirms both instruments are `ACTIVE` via `PaymentManager.get_payment_instrument(...)`.
3. Mints three budgeted sessions in-code via `manager.create_payment_session(...)` — the research
agent's ($0.50), the discovery agent's ($0.20), and a deliberately tiny one ($0.0005) that Demo 2
uses to trigger a budget-exhaustion failover — and binds each to its specialist's
`AgentCorePaymentsPlugin`. The budgets are simple constants near the top of the script.
4. Builds two specialist agents (each with its own `AgentCorePaymentsPlugin`) plus an orchestrator
that routes and monitors, then runs Demo 1 → Demo 2 → Demo 3, printing a per-agent spend report
after each.
The payment **instruments** (wallets) came from Tutorial 00's multi-provider setup; this tutorial
reads their IDs from `.env`. Each spend report reads live balances with
`manager.get_payment_session(user_id=..., payment_session_id=...)` — inspect
`["availableLimits"]["availableSpendAmount"]` for any session the same way in your own code.
### Step 2 — Test the runtime entrypoint locally (optional)
`payment_orchestrator.py` is the **deploy artifact** — the `BedrockAgentCoreApp` + `@app.entrypoint`
you'll ship to the Runtime in Step 3. It reads `PAYMENT_MANAGER_ARN` (and `AWS_REGION`) from the
environment rather than calling `load_dotenv`, which is exactly how it will run in the Runtime. To
exercise the `@app.entrypoint` on a local port, export the two values from the shared `.env` first:
```bash
export $(grep -E '^(PAYMENT_MANAGER_ARN|AWS_REGION)=' ../.env | xargs)
python payment_orchestrator.py
```
## CLI Commands (runtime Deployment)
For the full three-demo walkthrough, use `multi_agent_payments.py` from Step 1 — it loads `.env`
and mints the per-agent sessions in-code.
### Step 3 — Deploy to AgentCore Runtime (CLI)
`agentcore create` scaffolds a runtime project; you wire `payment_orchestrator.py` in as the
entrypoint and declare its dependencies before deploying (this follows the same model as Tutorial 02):
```bash
# Install AgentCore CLI
npm install -g @aws/agentcore
# Create and deploy
agentcore create --name PaymentOrchestrator --defaults
# Scaffold the runtime project
agentcore create --name PaymentOrchestrator --framework Strands --protocol HTTP \
--model-provider Bedrock --memory none
# (agentcore create --name PaymentOrchestrator --defaults is also valid)
cd PaymentOrchestrator
agentcore deploy -y
# Add online evaluation
# Wire our entrypoint into the scaffold (overwrites the generated stub)
cp ../payment_orchestrator.py app/PaymentOrchestrator/main.py
# Declare the entrypoint's dependencies, then refresh the lockfile.
# Edit app/PaymentOrchestrator/pyproject.toml so its [project] dependencies include:
# "bedrock-agentcore[strands-agents]", "strands-agents", "strands-agents-tools", "boto3"
rm -f app/PaymentOrchestrator/uv.lock
```
The entrypoint reads `PAYMENT_MANAGER_ARN` (and `AWS_REGION`) from the runtime environment, so give
the deployed runtime both values. Pull them from the shared getting-started `.env` into the
scaffold's environment file:
```bash
# from inside PaymentOrchestrator/, copy the two values into the scaffold's .env
grep -E '^(PAYMENT_MANAGER_ARN|AWS_REGION)=' ../../.env >> app/PaymentOrchestrator/.env
```
Now deploy — this provisions the IAM roles (`ProcessPaymentRole`, `ResourceRetrievalRole`) and the
runtime:
```bash
agentcore deploy -y
```
First-time deploy takes a few minutes while IAM roles propagate; subsequent deploys are faster.
### Step 4 — Add online evaluation (CLI)
Score every invocation automatically with built-in evaluators, then redeploy to apply:
```bash
agentcore add online-eval \
--name PaymentMonitor \
--runtime PaymentOrchestrator \
@@ -98,98 +195,105 @@ agentcore add online-eval \
--sampling-rate 100 \
--enable-on-create
agentcore deploy -y
# Invoke
agentcore invoke '{"prompt": "Search Bazaar and call endpoints...", "user_id": "test-user-001", "research_session_id": "<ID>", "research_instrument_id": "<ID>", "discovery_session_id": "<ID>", "discovery_instrument_id": "<ID>"}'
# View traces
agentcore traces list --limit 20
# View logs
agentcore logs
# Clean up
agentcore remove all -y
```
## Online evaluation
AgentCore evaluations provides built-in evaluators that score every invocation automatically. For a payment orchestrator, the most relevant are:
| Evaluator | Level | What it checks |
|-----------|-------|---------------|
| `Builtin.GoalSuccessRate` | Session | Did the orchestrator complete both research and discovery tasks? |
| `Builtin.ToolSelectionAccuracy` | Tool | Did it route to the right specialist for each task? |
| `Builtin.Helpfulness` | Trace | Was the spend report clear and useful? |
### Step 5 — Invoke the deployed orchestrator (CLI)
The app backend passes the session and instrument IDs (from Step 1 / your `.env`) in the payload.
The entrypoint unwraps this JSON and hands each specialist its scoped budget:
```bash
# Add online eval with built-in evaluators — evaluate 100% of invocations
agentcore add online-eval \
--name PaymentMonitor \
--runtime PaymentOrchestrator \
--evaluator Builtin.GoalSuccessRate Builtin.ToolSelectionAccuracy Builtin.Helpfulness \
--sampling-rate 100 \
--enable-on-create
agentcore deploy -y
agentcore invoke '{"prompt": "Search Bazaar and call the found endpoints, then report spend.", "user_id": "test-user-001", "research_session_id": "<SESSION_A>", "research_instrument_id": "<COINBASE_INSTRUMENT_ID>", "discovery_session_id": "<SESSION_B>", "discovery_instrument_id": "<PRIVY_INSTRUMENT_ID>"}'
```
## Optional: Route Agents Through AgentCore gateway
## What the agent does
If you completed Tutorial 04 and have a gateway with the Coinbase Bazaar target, you can swap `http_request` for MCP tools. The agents get `search_resources` and `proxy_tool_call` — the payment infrastructure stays the same, only the tool layer changes.
See `payment_orchestrator.py` for the runtime wiring described in the Overview and Architecture
above. Two details worth calling out: the orchestrator exposes each specialist to itself as a tool
via `research_agent.as_tool()` / `discovery_agent.as_tool()`, and it holds no payment plugin of its
own — so the paying capability stays entirely with the specialists (structural safety by
construction).
```python
from datetime import timedelta
from mcp.client.streamable_http import streamablehttp_client
from strands.tools.mcp.mcp_client import MCPClient
## Inspect / verify
GATEWAY_URL = os.environ['GATEWAY_URL'] # From Tutorial 04
```bash
# Managers, connectors, and live payment status (run from the scaffolded project dir)
cd PaymentOrchestrator
agentcore status --type payment
mcp_client = MCPClient(lambda: streamablehttp_client(
GATEWAY_URL, timeout=timedelta(seconds=120),
))
mcp_client.__enter__()
bazaar_tools = mcp_client.list_tools_sync()
# Replace http_request with MCP tools — everything else (plugin, session, wallet) stays the same
research_agent = Agent(
model=model,
tools=bazaar_tools, # search_resources + proxy_tool_call
plugins=[research_plugin], # Same plugin, same session, same wallet
system_prompt='...',
)
# Runtime traces and logs
agentcore traces list
agentcore logs
```
Both agents share the same gateway connection but have separate payment plugins with independent budgets and wallets.
Confirm these keys exist in `00-getting-started/.env`: `PAYMENT_MANAGER_ARN`, `USER_ID`,
`COINBASE_INSTRUMENT_ID`, `PRIVY_INSTRUMENT_ID`, `COINBASE_CONNECTOR_ID`, `PRIVY_CONNECTOR_ID`.
## Troubleshooting
### ValueError: multi_provider config required
Run `multi_provider_setup.py` first. This creates one PaymentManager with two connectors (Coinbase + Privy) and writes `COINBASE_INSTRUMENT_ID`, `PRIVY_INSTRUMENT_ID`, `COINBASE_CONNECTOR_ID`, `PRIVY_CONNECTOR_ID` to `.env`.
### Instrument not ACTIVE
Both wallets must be funded and have delegated signing enabled before the first payment. Fund at [faucet.circle.com](https://faucet.circle.com/) — Base Sepolia for ETHEREUM. For Privy: open localhost:3000, log in as LINKED_EMAIL, choose Connect agent. For Coinbase: enable Delegated Signing in CDP Portal.
### Demo 2 failover doesn't trigger
The $0.0005 budget must be smaller than the API cost. If the Bazaar endpoint costs less than $0.0005, increase the cost by testing against a more expensive endpoint. The demo uses `https://x402-test.genesisblock.ai/api/market-news` which costs ~$0.002.
### Orchestrator pays in Demo 3
This should not happen — the orchestrator has no `AgentCorePaymentsPlugin`. If you see spend, check that `unsafe_orchestrator` was not accidentally given a plugin. The 402 response has no handler without the plugin.
| Symptom | Cause | Fix |
|---|---|---|
| `ValueError: multi-provider config required` | The multi-provider setup hasn't been run | Run `setup_agentcore_payments.py` then `multi_provider_setup.py` in `../00-setup-agentcore-payments/` — they write the Coinbase + Privy IDs to `.env` |
| Instrument not `ACTIVE` | Wallet unfunded or delegated signing not granted | Fund at [faucet.circle.com](https://faucet.circle.com/) (Base Sepolia). Privy: open `localhost:3000`, log in as `LINKED_EMAIL`, choose **Connect agent**. Coinbase: enable Delegated Signing in the CDP Portal |
| Demo 2 failover doesn't trigger | The endpoint cost is below the tiny $0.0005 budget, so no rejection occurs | Point at a pricier endpoint — the demo uses `https://x402-test.genesisblock.ai/api/market-news` (~$0.002) |
| `{"error": "PAYMENT_MANAGER_ARN is not set ..."}` at invoke | The runtime environment is missing the ARN | Add `PAYMENT_MANAGER_ARN` and `AWS_REGION` to `app/PaymentOrchestrator/.env` (Step 3), then `agentcore deploy -y` |
| `agentcore: command not found` | AgentCore CLI not installed | `npm install -g @aws/agentcore` (Node.js 20+) |
## Clean Up
Sessions expire automatically. For full payment resource cleanup:
> **Warning:** irreversible.
```bash
# Remove runtime deployment
# Remove the runtime deployment (from the scaffolded project dir)
cd PaymentOrchestrator && agentcore remove all -y
# Remove payment resources — uncomment cleanup in setup_agentcore_payments.py
```
## Next Steps
Payment **sessions** expire automatically (the demos set a 60-minute expiry). The shared payment
manager, connectors, and instruments are torn down by **Tutorial 00's Clean Up** — see
[`../00-setup-agentcore-payments/`](../00-setup-agentcore-payments/). To delete a payment
**instrument**, call the AgentCore SDK's `PaymentManager.delete_payment_instrument` (repeat for each
wallet, passing that wallet's connector id), then remove the connectors and manager with the
documented `remove` syntax and apply the teardown:
- **Use case: Browser paywall** — `../../02-use-cases/pay-for-content-browser-use/` — End-to-end use case deployed on AgentCore runtime with a CDK content-provider stack
```python
import os
from bedrock_agentcore.payments import PaymentManager
manager = PaymentManager(
payment_manager_arn=os.environ["PAYMENT_MANAGER_ARN"],
region_name=os.environ["AWS_REGION"],
)
USER_ID = os.environ["USER_ID"]
# Delete each payment instrument (one per wallet, each with its own connector id)
for instrument_id, connector_id in (
(os.environ["COINBASE_INSTRUMENT_ID"], os.environ["COINBASE_CONNECTOR_ID"]),
(os.environ["PRIVY_INSTRUMENT_ID"], os.environ["PRIVY_CONNECTOR_ID"]),
):
manager.delete_payment_instrument(
payment_instrument_id=instrument_id,
payment_connector_id=connector_id,
user_id=USER_ID,
)
```
```bash
# Then remove the connectors and manager, and apply the teardown
agentcore remove payment-connector --manager <MANAGER_NAME> --name <CONNECTOR_NAME> -y
agentcore remove payment-manager --name <MANAGER_NAME> -y
agentcore deploy -y # remove updates local config; deploy applies the teardown in AWS
```
## Next steps
- **Provision the shared stack** → [Tutorial 00 — Setup](../00-setup-agentcore-payments/)
- **Discover 10,000+ paid MCP tools via Gateway** → [Tutorial 04](../04-agent-with-coinbase-bazaar-via-gateway/)
- **Skip redundant paid calls with Memory** → [Tutorial 06](../06-research-agent-with-payment-memory/)
- **End-to-end browser paywall use case** → `../../02-use-cases/pay-for-content-browser-use/`
@@ -61,6 +61,11 @@ PRIVY = config["instruments"]["stripe_privy"]
MODEL_ID = os.environ.get("MODEL_ID", "us.anthropic.claude-sonnet-4-6")
# Per-agent budgets (USD). Each specialist gets its own budgeted session, created in-code below.
RESEARCH_BUDGET = "0.50" # research agent (Coinbase wallet)
DISCOVERY_BUDGET = "0.20" # discovery agent (Privy wallet)
FAILOVER_BUDGET = "0.0005" # deliberately tiny — Demo 2 uses this to trigger a budget-exhaustion failover
print_summary(
"Multi-Provider Config",
manager_arn=PAYMENT_MANAGER_ARN,
@@ -82,27 +87,23 @@ for label, instr_id in [
assert status == "ACTIVE", f"{label} instrument {instr_id} is {status} — fund and delegate first"
print(f" {label} instrument {instr_id} is {status}")
# Session A: Research Agent — larger budget, Coinbase wallet
sess_a = manager.create_payment_session(
# Each agent gets its own budgeted session, created in-code via the SDK
# (manager.create_payment_session). One session per agent, each capped to its own budget.
SESSION_A_ID = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.50", "currency": "USD"}},
limits={"maxSpendAmount": {"value": RESEARCH_BUDGET, "currency": "USD"}},
expiry_time_in_minutes=60,
)
SESSION_A_ID = sess_a["paymentSessionId"]
# Session B: Discovery Agent — smaller budget, Privy wallet
sess_b = manager.create_payment_session(
)["paymentSessionId"]
SESSION_B_ID = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.20", "currency": "USD"}},
limits={"maxSpendAmount": {"value": DISCOVERY_BUDGET, "currency": "USD"}},
expiry_time_in_minutes=60,
)
SESSION_B_ID = sess_b["paymentSessionId"]
)["paymentSessionId"]
print_summary(
"Per-Agent Sessions",
session_a=f"{SESSION_A_ID} ($0.50, Coinbase)",
session_b=f"{SESSION_B_ID} ($0.20, Privy)",
total_allocated="$0.70",
session_a=f"{SESSION_A_ID} (Coinbase)",
session_b=f"{SESSION_B_ID} (Privy)",
)
# ── Step 4: Create Plugins and Agents ─────────────────────────────────────────
@@ -258,14 +259,15 @@ print("Demo 2 — Budget Exhaustion + Failover")
print("=" * 60)
print("Tiny research budget ($0.0005) → payment rejected → reroute to discovery agent")
tiny_sess = manager.create_payment_session(
# This demo needs a deliberately tiny research session so the paid call is rejected. Create it
# in-code via the SDK, capped to the tiny FAILOVER_BUDGET.
TINY_SESSION_ID = manager.create_payment_session(
user_id=USER_ID,
limits={"maxSpendAmount": {"value": "0.0005", "currency": "USD"}},
limits={"maxSpendAmount": {"value": FAILOVER_BUDGET, "currency": "USD"}},
expiry_time_in_minutes=60,
)
TINY_SESSION_ID = tiny_sess["paymentSessionId"]
print(f" Tiny research session: {TINY_SESSION_ID} (budget: $0.0005)")
print(f" Discovery session: {SESSION_B_ID} (budget: $0.20)")
)["paymentSessionId"]
print(f" Tiny research session: {TINY_SESSION_ID} (budget: ${FAILOVER_BUDGET})")
print(f" Discovery session: {SESSION_B_ID}")
tiny_research_plugin = AgentCorePaymentsPlugin(
config=AgentCorePaymentsPluginConfig(
@@ -370,11 +372,9 @@ print("Demo 3 — Structural Safety (Orchestrator Cannot Spend)")
print("=" * 60)
print("Orchestrator gets http_request but NO plugin → 402 is a dead end")
from strands_tools import http_request as http_tool # noqa: E402
unsafe_orchestrator = Agent(
model=model,
tools=[http_tool], # Has http_request but NO payment plugin
tools=[http_request], # Has http_request but NO payment plugin
system_prompt=(
"You have http_request available. Try to access this paid endpoint: "
"GET https://x402-test.genesisblock.ai/api/weather. "
@@ -19,6 +19,7 @@ import os
import json
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from bedrock_agentcore.payments import PaymentManager
from bedrock_agentcore.payments.integrations.strands import (
AgentCorePaymentsPlugin,
AgentCorePaymentsPluginConfig,
@@ -27,11 +28,12 @@ from strands import Agent
from strands.models import BedrockModel
from strands.tools import tool
from strands_tools import http_request
import boto3
app = BedrockAgentCoreApp()
PAYMENT_MANAGER_ARN = os.environ["PAYMENT_MANAGER_ARN"]
# Read at import time but do NOT hard-fail here — a missing var must not crash the
# runtime container on start. It is validated per-request in handle_request instead.
PAYMENT_MANAGER_ARN = os.environ.get("PAYMENT_MANAGER_ARN", "")
REGION = os.environ.get("AWS_REGION", "us-west-2")
MODEL_ID = os.environ.get("MODEL_ID", "us.anthropic.claude-sonnet-4-6")
@@ -49,6 +51,19 @@ def handle_request(payload, context=None):
- discovery_session_id: Session B (discovery agent budget)
- discovery_instrument_id: Privy instrument
"""
# agentcore invoke wraps the JSON arg as {"prompt": "<json-string>"} — unwrap it.
raw_prompt = payload.get("prompt", "")
if isinstance(raw_prompt, str) and raw_prompt.strip().startswith("{"):
try:
inner = json.loads(raw_prompt)
if "research_session_id" in inner or "research_instrument_id" in inner:
payload = inner
except json.JSONDecodeError:
pass
if not PAYMENT_MANAGER_ARN:
return {"error": "PAYMENT_MANAGER_ARN is not set in the runtime environment."}
prompt = payload.get("prompt", "Hello")
user_id = payload.get("user_id", "default-user")
@@ -76,6 +91,7 @@ def handle_request(payload, context=None):
payment_instrument_id=research_instrument_id,
payment_session_id=research_session_id,
region=REGION,
network_preferences_config=["eip155:84532", "base-sepolia"],
)
)
@@ -86,12 +102,13 @@ def handle_request(payload, context=None):
payment_instrument_id=discovery_instrument_id,
payment_session_id=discovery_session_id,
region=REGION,
network_preferences_config=["eip155:84532", "base-sepolia"],
)
)
# --- Budget check tool (orchestrator only) ---
dp_client = boto3.client("bedrock-agentcore", region_name=REGION)
manager = PaymentManager(payment_manager_arn=PAYMENT_MANAGER_ARN, region_name=REGION)
@tool
def check_budgets() -> str:
@@ -105,12 +122,10 @@ def handle_request(payload, context=None):
("research_agent", research_session_id),
("discovery_agent", discovery_session_id),
]:
info = dp_client.get_payment_session(
paymentManagerArn=PAYMENT_MANAGER_ARN,
paymentSessionId=sid,
userId=user_id,
sess = manager.get_payment_session(
user_id=user_id,
payment_session_id=sid,
)
sess = info
results[label] = {
"session_id": sid,
"available": sess.get("availableLimits", {}).get("availableSpendAmount", "N/A"),
@@ -1,116 +1,111 @@
# AgentCore payments — Getting Started Tutorials
Step-by-step Python scripts for building payment-enabled AI agents with **Amazon Bedrock AgentCore payments**.
Step-by-step Python tutorials for building payment-enabled AI agents with **Amazon Bedrock
AgentCore payments** — x402 protocol orchestration, configurable spend limits, and third-party
wallet integration (Coinbase CDP, Stripe/Privy).
AgentCore payments handles payment orchestration for the x402 protocol, configurable payment limits, and third-party wallet integration with Coinbase CDP and Stripe (Privy) stablecoin wallets.
> **Testnet only.** All tutorials use Base Sepolia (Ethereum) or Solana Devnet with free USDC from
> [faucet.circle.com](https://faucet.circle.com/). Testnet USDC has no monetary value.
> **Testnet only.** All tutorials use Base Sepolia (Ethereum) or Solana Devnet with free USDC from [faucet.circle.com](https://faucet.circle.com/). Testnet USDC has no real-world value.
## How the pieces fit together <a name="cli-vs-sdk"></a>
## Top-level layout
AgentCore payments gives you two complementary tools, each with a clear job. Learn this once and
every tutorial reads the same way:
| Folder | What's inside |
|--------|---------------|
| [`00-setup-agentcore-payments/`](00-setup-agentcore-payments/) | Create IAM roles, PaymentManager, Connector, embedded wallet, and a budgeted PaymentSession |
| [`01-agents-payments-and-limits/`](01-agents-payments-and-limits/) | Strands and LangGraph agents that pay x402 endpoints automatically with budget enforcement |
| [`02-deploy-to-agentcore-runtime/`](02-deploy-to-agentcore-runtime/) | Package and deploy a payment agent to AgentCore runtime with role separation and observability |
| [`03-user-onboarding-wallet-funding/`](03-user-onboarding-wallet-funding/) | User onboarding, wallet funding, delegation, balance checks, multi-network instruments |
| [`04-agent-with-coinbase-bazaar-via-gateway/`](04-agent-with-coinbase-bazaar-via-gateway/) | Discover 10,000+ paid MCP tools via AgentCore gateway and pay on call |
| [`05-agent-with-browser-tool-pay-for-content/`](05-agent-with-browser-tool-pay-for-content/) | Intercept 402 paywalls in a browser session and pay for web content |
| [`06-research-agent-with-payment-memory/`](06-research-agent-with-payment-memory/) | Combine AgentCore payments with AgentCore Memory so an agent recalls past data and skips redundant paid calls across sessions |
| [`07-multi-agent-payment-orchestrator/`](07-multi-agent-payment-orchestrator/) | Multiple agents with separate wallets, per-agent budgets, and runtime deploy |
> **The AgentCore CLI provisions your shared payment infrastructure. The AgentCore SDK
> (`PaymentManager`) is your application backend — it creates each end user's wallet and spending
> session, and settles x402 payments at request time.**
| Job | Tool | Role |
|---|---|---|
| Provision the payment manager, connector, gateway, runtime, and IAM roles | **AgentCore CLI** (`agentcore add …` / `deploy`) | Set up your shared infrastructure once; every tutorial reuses it |
| Create a per-user **wallet (instrument)** and **spending session**, sign x402 payment headers, check a wallet balance, and delete an instrument | **AgentCore SDK** (`PaymentManager`) | Your backend mints and manages these per end user, scoped to who you serve |
| Let an agent pay a 402 automatically | **AgentCore SDK** (`AgentCorePaymentsPlugin`) | Handles the pay-and-retry at request time, inside your app |
This mirrors how you'd build a real payment-enabled application: infrastructure is provisioned once
with the CLI, and your application backend uses the SDK to give each user a wallet, set their budget,
and pay on their behalf. **Tutorial 00 provisions the shared infrastructure; the later tutorials use
the SDK to build paying agents on top of it.**
## Start here
Start with Tutorial 00; then work through 0107 (in order, or dip into any that interests you — each
stands alone). Each folder's `README.md` is a self-contained walkthrough — you run the `agentcore`
commands and Python snippets yourself and learn each piece as you go.
1. **Install the tools once:**
```bash
pip install -r 00-setup-agentcore-payments/requirements.txt
npm install -g @aws/agentcore # AgentCore CLI (Node.js 20+)
```
2. **Open [Tutorial 00 — Set Up AgentCore payments](00-setup-agentcore-payments/)** and follow it end
to end. You'll capture wallet-provider credentials, provision the shared payment stack with the
`agentcore` CLI, create your first wallet and session with the SDK, and fund the wallet. Tutorial
00 writes the shared resource IDs (`PAYMENT_MANAGER_ARN`, `INSTRUMENT_ID`, …) to the shared `.env`.
Each downstream agent creates its own spending session in-code with the SDK
(`manager.create_payment_session(...)`), so there's no session ID to carry between tutorials.
3. **Then open any downstream tutorial** ([01](01-agents-payments-and-limits/) is the recommended
next step). Each one reads that shared `.env` and builds on it.
Every tutorial's README follows the same shape — a short **Reads / Does** strip, prerequisites, a
numbered walkthrough, an inspect step, troubleshooting, and clean-up — so once you've done Tutorial
00 the rest feel familiar.
## Tutorials
Run Tutorial 00 first; then 0107 in any order. Each folder's README opens with a **Reads / Does**
strip so you can see its inputs and outputs at a glance.
| # | Folder | What you build | Provisioning |
|---|--------|----------------|:------------:|
| 00 | [`00-setup-agentcore-payments/`](00-setup-agentcore-payments/) | Payment manager, connector, IAM roles (CLI) + wallet & session (SDK) | CLI + SDK |
| 01 | [`01-agents-payments-and-limits/`](01-agents-payments-and-limits/) | Strands & LangGraph agents that pay x402 endpoints with budget limits | SDK |
| 02 | [`02-deploy-to-agentcore-runtime/`](02-deploy-to-agentcore-runtime/) | Deploy a payment agent to AgentCore Runtime | CLI |
| 03 | [`03-user-onboarding-wallet-funding/`](03-user-onboarding-wallet-funding/) | Per-user wallet onboarding, funding, delegation, balances | SDK |
| 04 | [`04-agent-with-coinbase-bazaar-via-gateway/`](04-agent-with-coinbase-bazaar-via-gateway/) | Discover 10,000+ paid MCP tools via AgentCore Gateway | CLI + SDK |
| 05 | [`05-agent-with-browser-tool-pay-for-content/`](05-agent-with-browser-tool-pay-for-content/) | Pay 402 paywalls inside a browser session | SDK |
| 06 | [`06-research-agent-with-payment-memory/`](06-research-agent-with-payment-memory/) | Add AgentCore Memory to skip redundant paid calls | SDK |
| 07 | [`07-multi-agent-payment-orchestrator/`](07-multi-agent-payment-orchestrator/) | Multiple agents, separate wallets, per-agent budgets | CLI + SDK |
## Which tutorial do I need?
- **Just paying an API?** → 01 (local), then 02 (deploy).
- **Onboarding real users / managing their wallets?** → 03.
- **Agent discovers tools at runtime?** → 04 (Gateway).
- **Paying for web/article content?** → 05 (Browser).
- **Personalized agentic payments with memory?** → 06 (Memory).
- **Several agents with independent budgets?** → 07 (needs multi-provider setup).
## Shared files
| File | Purpose |
|------|---------|
| `utils.py` | IAM role creation (`setup_payment_roles()`), config persistence, observability setup, display helpers |
| `.env` | Shared config created by Tutorial 00, loaded by all downstream tutorials (git-ignored) |
## Choose your path
### Path A: Single provider (Tutorials 0006)
```
1. Pick ONE provider and run its setup script:
providers/coinbase_cdp_account_setup.py ← writes Coinbase keys to .env
OR providers/stripe_privy_account_setup.py ← writes Privy keys to .env
2. Run Tutorial 00 (setup_agentcore_payments.py)
Creates IAM roles, PaymentManager, Connector, Instrument, Session
Writes resource IDs back to .env
3. Run Tutorials 0106 in any order
Each loads .env and uses the resources Tutorial 00 created
```
### Path B: Multi-provider (Tutorial 07)
```
1. Run BOTH provider setup scripts
2. Run multi_provider_setup.py instead of Tutorial 00
Creates one PaymentManager with two Connectors (Coinbase + Privy)
3. Run Tutorial 07
```
## AgentCore payments features → tutorial mapping
| Feature | Description | Tutorials |
|---------|-------------|-----------|
| Payment processing | x402 protocol orchestration, transaction signing, proof generation | 01, 02, 04, 05, 06, 07 |
| Payment limits | Session budgets (`maxSpendAmount`), expiry, overspend rejection | 00, 01, 03, 06, 07 |
| Wallet integration | Coinbase CDP and Stripe (Privy) embedded wallets, delegation, funding | 00, 03, 07 |
| Endpoint discoverability | Coinbase x402 Bazaar via AgentCore gateway, MCP tool search | 04 |
| Memory | AgentCore Memory for cross-session recall and spend optimization | 06 |
| observability | AgentCore observability (vended logs, traces via CloudWatch) | 00, 02, 07 |
| `utils.py` | IAM role helper (`setup_payment_roles()`), `.env` read/write (`load_tutorial_env`, `update_env_file`), observability setup, display helpers |
| `.env` (git-ignored) | Shared config: Tutorial 00 writes resource IDs; every downstream tutorial reads them |
## Prerequisites
- Python 3.10+
- AWS CLI configured (`aws sts get-caller-identity` to verify)
- AWS account with access to AgentCore payments
- Wallet provider credentials (Coinbase CDP or Stripe/Privy) — see Tutorial 00
## Running the Python Scripts
```bash
pip install -r 00-setup-agentcore-payments/requirements.txt
# Provider setup (pick one)
python 00-setup-agentcore-payments/providers/coinbase_cdp_account_setup.py
# OR
python 00-setup-agentcore-payments/providers/stripe_privy_account_setup.py
# Tutorial 00
python 00-setup-agentcore-payments/setup_agentcore_payments.py
# Tutorial 01
python 01-agents-payments-and-limits/strands_payment_agent.py
python 01-agents-payments-and-limits/langgraph_payment_agent.py
# Tutorial 02
python 02-deploy-to-agentcore-runtime/deploy_payment_agent.py
# Tutorial 03
python 03-user-onboarding-wallet-funding/user_onboarding.py
# Tutorial 04
python 04-agent-with-coinbase-bazaar-via-gateway/bazaar_gateway_agent.py
# Tutorial 05
python 05-agent-with-browser-tool-pay-for-content/browser_paywall_payments.py
# Tutorial 06
python 06-research-agent-with-payment-memory/research_agent_with_memory.py
# Tutorial 07
python 07-multi-agent-payment-orchestrator/multi_agent_payments.py
```
- Python 3.10+ and AWS CLI configured (`aws sts get-caller-identity`)
- AWS account with access to AgentCore payments, in a supported region: `us-east-1`, `us-west-2`,
`eu-central-1`, `ap-southeast-2`
- Node.js 20+ and the AgentCore CLI (`npm install -g @aws/agentcore`) for Tutorials 00, 02, 04, 07
- Wallet-provider credentials (Coinbase CDP or Stripe/Privy) — captured in Tutorial 00
## Cleanup
> **Warning:** Cleanup is irreversible. Run after completing all tutorials.
> **Warning:** irreversible. Run after completing all tutorials.
1. Run the cleanup section in `setup_agentcore_payments.py` to delete the Payment Manager and all child resources.
2. Delete the four IAM roles from the IAM console.
3. Delete CloudWatch log groups: `/aws/vendedlogs/bedrock-agentcore/<manager-id>`.
4. For runtime deployments: `cd PaymentAgent && agentcore remove all -y`
Tutorial 00's cleanup tears down the shared stack:
```bash
cd 00-setup-agentcore-payments/PaymentSetup
agentcore remove payment-connector --manager MyPaymentManager --name MyCoinbaseConnector -y
agentcore remove payment-manager --name MyPaymentManager -y
agentcore deploy -y # applies the removal in AWS
agentcore remove all -y # removes the scaffolded runtime project
```
Delete the payment instrument first with the AgentCore SDK
(`manager.delete_payment_instrument(...)`, see Tutorial 00's Clean Up). Runtime/Gateway
deployments from 02/04/07 are removed with `agentcore remove all -y` in their project dirs. Payment
sessions expire automatically. If you used the boto3 `setup_agentcore_payments.py` reference-setup path instead,
run its cleanup section and delete the four IAM roles + `/aws/vendedlogs/bedrock-agentcore/*` log
groups from the console.
@@ -11,6 +11,11 @@ Only contains what the AgentCore SDK does NOT provide:
- Observability setup (vended logs + X-Ray)
- Privy-specific helpers
- Display helpers
Note on boto3 usage: payment and Memory *data-plane* operations use the AgentCore SDK
(PaymentManager / MemoryClient) in the tutorial scripts. The boto3 calls in THIS module are
deliberately not payments — they cover IAM role provisioning, Cognito (Gateway auth), CloudWatch/
X-Ray observability, STS identity, and Privy REST — none of which have a PaymentManager equivalent.
"""
import json
@@ -35,29 +40,6 @@ RESOURCE_RETRIEVAL_ROLE = "AgentCorePaymentsResourceRetrievalRole"
# ═════════════════════════════════════════════════════════════════
def load_payment_env(env_file=".env"):
"""Load .env file and return config dict."""
load_dotenv(env_file, override=True)
return {
"region": os.environ.get("AWS_REGION", "us-west-2"),
"cp_endpoint": os.environ.get(
"PAYMENTS_CP_ENDPOINT",
f"https://bedrock-agentcore-control.{os.environ.get('AWS_REGION', 'us-west-2')}.amazonaws.com",
),
"dp_endpoint": os.environ.get(
"PAYMENTS_DP_ENDPOINT",
f"https://bedrock-agentcore.{os.environ.get('AWS_REGION', 'us-west-2')}.amazonaws.com",
),
"cred_endpoint": os.environ.get(
"CREDENTIAL_PROVIDER_ENDPOINT",
os.environ.get(
"PAYMENTS_CP_ENDPOINT",
f"https://bedrock-agentcore-control.{os.environ.get('AWS_REGION', 'us-west-2')}.amazonaws.com",
),
),
}
def require_env(key):
"""Get required environment variable or raise with a clear message."""
val = os.environ.get(key, "").strip()
@@ -109,7 +91,7 @@ def assume_role(session, role_arn, session_name="tutorial-session"):
# These roles cover AgentCore payments operations only.
# Infrastructure actions (CloudWatch, X-Ray, Cognito, Gateway) run under
# the caller's default AWS credentials — not these roles.
# See Tutorial 00 Step 8b and Tutorial 04 prerequisites for details.
# See Tutorial 00 setup and Tutorial 04 prerequisites for details.
PAYMENT_ROLE_DEFINITIONS = {
CONTROL_PLANE_ROLE: {
"description": "AgentCore payments: control plane operations",
@@ -181,8 +163,8 @@ def setup_payment_roles(region=None):
Checks if each role exists first. Creates only what's missing.
Idempotent — safe to run multiple times.
Note: Creates IAM roles that persist until explicitly deleted. Run the
cleanup cell in Tutorial 00 to remove them when no longer needed.
Note: Creates IAM roles that persist until explicitly deleted. Run
Tutorial 00's Clean Up to remove them when no longer needed.
Args:
region: AWS region. Defaults to AWS_REGION env var or us-west-2.
@@ -550,11 +532,20 @@ def load_tutorial_env(env_path=None):
Returns:
Dict with: payment_manager_arn, user_id, instrument_id, session_id,
connector_id, region, provider_type, wallet_address.
connector_id, region, provider_type, active_provider, wallet_address.
For multi-provider setups (Tutorial 07), also includes
multi_provider=True and instruments/connectors dicts.
multi_provider=True and an instruments dict keyed by provider.
Missing keys are None (not raised).
Provider selection: when both a Coinbase and a Privy wallet are present
(multi-provider), the top-level instrument_id / connector_id /
wallet_address resolve to the provider named by CREDENTIAL_PROVIDER_TYPE
(StripePrivy -> stripe_privy, CoinbaseCDP -> coinbase). This means
single-provider tutorials (01/03/04/05/06) can read cfg["instrument_id"]
directly and get the wallet the user actually configured — no need to
branch on multi_provider or guess by ordering. Tutorial 07, which uses
both wallets at once, still reads cfg["instruments"][<provider>].
Example:
cfg = load_tutorial_env()
plugin = AgentCorePaymentsPlugin(config=AgentCorePaymentsPluginConfig(
@@ -570,6 +561,8 @@ def load_tutorial_env(env_path=None):
raise FileNotFoundError(f"{os.path.basename(path)} not found. Run Tutorial 00 first.\n Expected at: {path}")
load_dotenv(path, override=True)
provider_type = os.environ.get("CREDENTIAL_PROVIDER_TYPE")
result = {
"payment_manager_arn": os.environ.get("PAYMENT_MANAGER_ARN"),
"payment_manager_id": os.environ.get("PAYMENT_MANAGER_ID"),
@@ -580,7 +573,8 @@ def load_tutorial_env(env_path=None):
"wallet_address": os.environ.get("WALLET_ADDRESS"),
"session_id": os.environ.get("SESSION_ID"),
"region": os.environ.get("AWS_REGION", "us-west-2"),
"provider_type": os.environ.get("CREDENTIAL_PROVIDER_TYPE"),
"provider_type": provider_type,
"active_provider": None,
}
# Multi-provider support (Tutorial 07)
@@ -601,8 +595,20 @@ def load_tutorial_env(env_path=None):
"wallet_address": os.environ.get("PRIVY_WALLET_ADDRESS"),
},
}
# Resolve the single active wallet from the provider the user configured,
# so single-provider tutorials get the right one instead of alphabetical order.
provider_key_map = {"stripeprivy": "stripe_privy", "coinbasecdp": "coinbase"}
active = provider_key_map.get((provider_type or "").strip().lower())
if active not in result["instruments"]:
active = "coinbase" # deterministic default when CREDENTIAL_PROVIDER_TYPE is unset/unknown
result["active_provider"] = active
result["instrument_id"] = result["instruments"][active]["instrument_id"]
result["connector_id"] = result["instruments"][active]["connector_id"]
result["wallet_address"] = result["instruments"][active]["wallet_address"]
else:
result["multi_provider"] = False
# Single-provider: the active provider is whatever CREDENTIAL_PROVIDER_TYPE says.
result["active_provider"] = provider_type
return result
@@ -622,7 +628,7 @@ def pp(label, response):
def print_summary(title, **kwargs):
"""Pretty-print a summary block for notebook output."""
"""Pretty-print a summary block for console output."""
print(f"\n{'=' * 60}")
print(f" {title}")
print(f"{'=' * 60}")
@@ -889,7 +895,7 @@ def render_frontend_env_local(app_id, app_secret, signer_id, network_mode="testn
"""Build the contents of the Privy reference frontend's ``.env.local`` file.
Pure string builder — no filesystem access, no shell instructions. The
notebook prints the returned string for the developer to paste into the
script prints the returned string for the developer to paste into the
Privy reference frontend's ``.env.local`` on their local machine.
Args:
@@ -928,11 +934,15 @@ def save_privy_authorization_key(env_path, authorization_id, authorization_priva
Returns:
The result of :func:`update_env_file`.
"""
prefix = "wallet-auth:"
key = authorization_private_key.strip()
if key.startswith(prefix):
key = key[len(prefix) :].strip()
print(" ️ Stripped 'wallet-auth:' prefix from the private key.")
# Strip known prefixes — Privy displays the key with 'wallet-auth:' but some
# clipboard/copy paths produce just 'auth:'. The AWS API rejects both; it wants
# either the full 'wallet-auth:' prefix or raw base64.
for prefix in ("wallet-auth:", "auth:"):
if key.startswith(prefix):
key = key[len(prefix) :].strip()
print(f" ️ Stripped '{prefix}' prefix from the private key.")
break
return update_env_file(
env_path,
@@ -946,8 +956,8 @@ def save_privy_authorization_key(env_path, authorization_id, authorization_priva
def verify_privy_signer_on_wallet(app_id, app_secret, wallet_address_or_id, quorum_id):
"""Check whether a key quorum is registered as a signer on a Privy wallet.
After the end user grants signer access in the Privy reference frontend (Step 7b of the
main setup notebook), Privy adds the key quorum to the wallet's
After the end user grants signer access in the Privy reference frontend,
Privy adds the key quorum to the wallet's
``additional_signers``. Call this to confirm consent landed before
attempting ``ProcessPayment`` — missing delegation is the single most
common cause of ProcessPayment failures with the StripePrivy provider.
@@ -1003,7 +1013,7 @@ def verify_privy_signer_on_wallet(app_id, app_secret, wallet_address_or_id, quor
raise RuntimeError(
f"Privy wallet not found for {wallet_address_or_id!r}. "
"Check that PRIVY_APP_ID matches the app the wallet was created in, "
"and that the wallet has been provisioned (Step 7 in the main notebook)."
"and that the wallet has been provisioned (Tutorial 00 setup)."
)
if not resp.ok:
raise RuntimeError(f"Privy wallet fetch failed ({resp.status_code}): {resp.text}")