1
0
mirror of synced 2026-05-22 22:53:35 +00:00

feat(payments): host browser use case on AgentCore Runtime + scoped IAM + observability (#1495)

---------

Co-authored-by: Guy Bachar <guybac@amazon.com>
This commit is contained in:
Guy Bachar
2026-05-15 16:54:50 -04:00
committed by GitHub
parent e57c48a481
commit 325b964e03
15 changed files with 1485 additions and 438 deletions
+4 -1
View File
@@ -262,4 +262,7 @@ Test-Downloads/
### Claude Code Assistant ###
.claude/
CLAUDE.md
plan/
plan/
### draw.io source files (keep rendered PNG only) ###
*.drawio
@@ -49,3 +49,8 @@ NETWORK=base-sepolia
# Deploy the CDK stack first: cd content-provider && PAY_TO=0x<your-wallet> bash deploy.sh
# Then paste the printed CloudFront URL here.
CONTENT_DISTRIBUTION_URL=https://<your-cloudfront-id>.cloudfront.net
# ── AgentCore Runtime (Step 5) ────────────────────────────────────────────
# Name used when the AgentCore CLI scaffolds and deploys the agent.
# Override only if you want to deploy multiple variants in the same account.
AGENT_NAME=PayForContentBrowserAgent
@@ -11,3 +11,8 @@ content-provider/cdk/node_modules/
content-provider/cdk/dist/
content-provider/cdk/cdk.out/
content-provider/cdk/package-lock.json
# AgentCore CLI scaffold + deploy artifacts (generated by Step 5).
# The agent source of truth is in agent/. The scaffold under payforcontent/
# is reproducible by re-running the notebook.
payforcontent/
@@ -1,4 +1,4 @@
# Pay for Content — Browser Use Case
# Pay for Content — Browser Use Case (AgentCore Runtime)
## Overview
@@ -8,14 +8,15 @@ each transaction."
Without AgentCore payments, an agent that needs to pay for content must either hold
a private key (exposing credentials to the model) or interrupt the user to complete
the payment manually. This use case shows a third path: the agent delegates signing to
AgentCore payments, stays within human-set payment limits, and completes the entire
browse-pay-extract flow autonomously.
the payment manually. This use case shows a third path: the agent **leverages AgentCore
Payments for payment processing**, stays within human-set payment limits, and completes
the entire browse-pay-extract flow autonomously from a managed Runtime container.
The agent uses the **AgentCore Browser Tool** to navigate a paywalled website, reads
the embedded x402 payment requirement from the page DOM, calls `ProcessPayment` to
generate a cryptographic USDC proof, interacts with the paywall UI, and returns the
unlocked content — all without any private key exposure or human intervention.
The agent is **deployed to AgentCore Runtime** under `ProcessPaymentRole`, uses the
**AgentCore Browser Tool** to navigate a paywalled website, reads the embedded x402
payment requirement from the page DOM, calls `ProcessPayment` to generate a payment
proof, interacts with the paywall UI, and returns the unlocked content — without any
private key exposure or human intervention.
### Use Case Details
@@ -23,11 +24,12 @@ unlocked content — all without any private key exposure or human intervention.
|:--------------------|:--------------------------------------------------------------|
| Use case type | Agentic browser automation with autonomous micropayment |
| Agent type | Single |
| Hosting | AgentCore Runtime (managed microVM, role-segregated) |
| Payment protocol | x402 (HTTP 402 Payment Required) |
| Agentic Framework | Strands Agents |
| LLM model | Anthropic Claude Sonnet 4.6 |
| Complexity | Intermediate |
| SDK used | boto3 + AgentCore SDK + AgentCorePaymentsPlugin (Strands) |
| SDK used | boto3 + AgentCore SDK + AgentCorePaymentsPlugin (Strands) + AgentCore CLI |
| Wallet type | Embedded crypto wallet (AgentCore-provisioned, Coinbase CDP) |
| Network | Base Sepolia testnet (`eip155:84532`); Solana Devnet available |
@@ -35,91 +37,123 @@ unlocked content — all without any private key exposure or human intervention.
## Architecture
There are three distinct phases: **resource provisioning** (runs once), **session setup**
(runs before each agent invocation), and **agent runtime** (the live payment flow).
The content provider is operator-deployed infrastructure — it is not created by the notebook.
There are four distinct phases: **resource provisioning** (runs once), **session setup**
(runs before each agent invocation), **deploy** (runs on agent code change), and
**invoke** (the live payment flow). The content provider is a separate piece of
infrastructure that you deploy from this repo's `content-provider/` CDK stack — it
is not created by the notebook.
> **Note on SDK choice:** the notebook uses boto3 clients (`bedrock-agentcore-control`
> and `bedrock-agentcore`) for Payments resource management because the AgentCore
> Python SDK does not yet expose `CreatePaymentManager` / `CreatePaymentSession`
> / `CreatePaymentInstrument`. The agent itself (running on Runtime) uses the
> `bedrock-agentcore[strands-agents]` SDK and `AgentCorePaymentsPlugin` for
> payment processing — that side is fully SDK-driven.
```
RESOURCE PROVISIONING (notebook Step 3, ControlPlaneRole)
─────────────────────────────────────────────────────────────────────────────
┌─────────────────────────────────────────────────────────────────────┐
│ Two-client setup │
│ │
│ cp_client ──► CP endpoint ──► CreatePaymentCredentialProvider │
│ (bedrock-agentcore-control) CreatePaymentManager │
│ CreatePaymentConnector │
│ │
│ mgmt_client ──► DP endpoint ──► CreatePaymentInstrument │
│ (bedrock-agentcore) (EmbeddedCryptoWallet) │
└─────────────────────────────────────────────────────────────────────┘
cp_client ──► bedrock-agentcore-control ──► CreatePaymentCredentialProvider,
CreatePaymentManager,
CreatePaymentConnector
mgmt_client ──► bedrock-agentcore ──► CreatePaymentInstrument
AgentCore provisions the on-chain wallet — no pre-existing CDP wallet required.
Result: CREDENTIAL_PROVIDER_ARN, MANAGER_ARN, PAYMENT_CONNECTOR_ID, PAYMENT_INSTRUMENT_ID
SESSION SETUP (notebook Step 4, ManagementRole)
─────────────────────────────────────────────────────────────────────────────
Notebook / App AgentCore payments
────────────────── ──────────────────────────────
CreatePaymentSession ────────────────► payment limits=$1.00 USD, expiry=60 min
(ManagementRole via STS) paymentSessionId ──────────────► passed to agent
Notebook (ManagementRole) AgentCore payments
───────────────────────── ──────────────────────────────
CreatePaymentSession ────────────────budget=$1.00 USD, expiry=60 min
paymentSessionId
AGENT RUNTIME (notebook Step 6, ProcessPaymentRole)
DEPLOY AGENT TO RUNTIME (notebook Step 5, AgentCore CLI)
─────────────────────────────────────────────────────────────────────────────
User
│ "Retrieve the article and pay for it"
agent/payment_agent.py agentcore CLI AWS
agent/requirements.txt + agentcore deploy (CodeBuild builds
agent/Dockerfile from Dockerfile)
(BedrockAgentCoreApp + ──► create / deploy ──► AgentRuntime
AgentCoreBrowser + (execution role:
process_x402_payment) ProcessPaymentRole)
+ ECR image
+ CodeBuild project
+ CloudWatch logs
INVOKE (notebook Step 6, ManagementRole → AgentCore Runtime)
─────────────────────────────────────────────────────────────────────────────
Notebook (ManagementRole)
│ InvokeAgentRuntime(arn,
│ paywall_url, session_id,
│ instrument_id, manager_arn)
┌──────────────────────────────────────────────────────┐
Strands Agent (Claude Sonnet 4.6)
│ │
Tool 1: AgentCoreBrowser Tool 2: process_x402_payment
(managed cloud Chromium) (calls ProcessPayment API)
└───────────┬────────────────────────────┬─────────────┘
│ HTTPS │ AWS API (ProcessPaymentRole)
▼ ▼
┌───────────────────────┐ ┌───────────────────────────────┐
│ Content Provider │ │ AgentCore payments │
│ (team-hosted demo or │ │ ProcessPayment API │
your own deploy) │
┌────────────────────────┐
HTTP 200 Embedded Wallet
x402 requirement (Coinbase CDP) │ │
in DOM script tag │ │ │ Base Sepolia testnet │ │
└────────────────────────┘
proof submitted via │◄───┤ status: PROOF_GENERATED
paywall UI → unlock │ └───────────────────────────────
┌──────────────────────────────────────────────────────────────
AgentCore Runtime microVM (ProcessPaymentRole)
Strands Agent (Claude Sonnet 4.6)
Tool 1: AgentCoreBrowser ──► managed cloud Chromium │
│ Tool 2: process_x402_payment ──► PaymentManager │
│ Plugin: AgentCorePaymentsPlugin (payment query tools) │
└───────────┬──────────────────────────────┬───────────────────┘
│ HTTPS │ AWS API (ambient creds)
▼ ▼
┌───────────────────────┐ ┌───────────────────────────────┐
Content Provider │ AgentCore payments
(team-hosted demo or │ ProcessPayment API
your own deploy) │
┌────────────────────────┐
HTTP 200 │ │ Embedded Wallet │ │
x402 requirement (Coinbase CDP) │ │
in DOM script tag │ │ │ Base Sepolia testnet
│ │ └────────────────────────┘
│ proof submitted via │ ◄────┤ status: PROOF_GENERATED │
│ paywall UI → unlock │ └───────────────────────────────┘
└───────────────────────┘
│ article text
Agent returns content + amount paid to user
Agent returns content + amount paid to caller
OPERATOR CLEANUP (automatic on session expiry)
OBSERVABILITY (Step 7, automatic)
─────────────────────────────────────────────────────────────────────────────
Session expires automatically after expiryTimeInMinutes elapses.
Agent can no longer spend once the session is past its expiry.
Each invocation emits a CloudWatch GenAI Observability trace covering
the agent loop, tool calls, payment SDK calls, and ProcessPayment API
latency. Metrics in the bedrock-agentcore namespace.
CLEANUP
─────────────────────────────────────────────────────────────────────────────
agentcore remove all -y — tears down Runtime, ECR, log groups
Session expiry — agent can no longer spend after expiry
```
**Key design points:**
- **Embedded wallet:** AgentCore provisions the on-chain wallet — no pre-existing CDP
- **Hosted on AgentCore Runtime.** The agent runs inside a managed microVM under
`ProcessPaymentRole`. Role separation is enforced by infrastructure: the container
assumes `ProcessPaymentRole` directly, and that role has an explicit Deny on session
and instrument management. The agent code never calls `sts:AssumeRole`.
- **Notebook = app backend.** The notebook (under `ManagementRole`) creates the session
with a budget, then calls `InvokeAgentRuntime` with the session/instrument/manager
context in the payload. The agent is stateless and wallet-agnostic — the same
deployment serves any user the backend authorises.
- **Embedded wallet.** AgentCore provisions the on-chain wallet — no pre-existing CDP
wallet or funded account is required. The `linkedAccounts` email field ties the wallet
to a user identity. Coinbase embedded wallets are provisioned synchronously (no OTP step).
- **Two clients, two endpoints:** `cp_client` → CP (`bedrock-agentcore-control.{region}.amazonaws.com`)
for all control-plane operations including `CreatePaymentCredentialProvider`; `mgmt_client` /
`agent_dp_client` → DP (`bedrock-agentcore.{region}.amazonaws.com`) for instrument, session, and payment.
- The notebook (not the agent) creates the session via `ManagementRole`. The agent only
ever uses `ProcessPaymentRole`, which has an explicit IAM Deny on session management.
- The content provider must be deployed to a public HTTPS URL before running the agent —
`AgentCoreBrowser` is a cloud-managed browser and cannot reach `localhost`.
- The agent never holds a private key. Signing is delegated to the AgentCore-managed
embedded wallet. This example uses Coinbase CDP; it can be adapted for Stripe/Privy by
swapping the credential provider configuration in Step 3.
- **Browser tool.** `AgentCoreBrowser` is a managed cloud Chromium session reached over
WebSocket from inside the Runtime container. The content provider must be deployed to
a public HTTPS URL — the browser cannot reach `localhost`.
- **No private keys.** Signing is delegated to the AgentCore-managed embedded wallet.
Coinbase CDP today; swap the credential provider configuration in Step 3 for StripePrivy.
---
@@ -127,9 +161,19 @@ OPERATOR CLEANUP (automatic on session expiry)
- AWS account with Amazon Bedrock AgentCore access
- Python 3.10+ and Jupyter Notebook (or JupyterLab)
- Node.js 20+ (for the AgentCore CLI and content-provider CDK)
- AWS CLI v2 configured with credentials (`aws configure`)
- IAM roles created — run `bash setup_roles.sh` and record the ARNs in `.env`
- Content provider deployed to AWS — run `cd content-provider && PAY_TO=0x<your-wallet> bash deploy.sh` and set `CONTENT_DISTRIBUTION_URL` in `.env` (Node.js 18+ and AWS CDK v2 required; see [content-provider/README.md](content-provider/README.md))
- AWS CDK v2 installed (used by the AgentCore CLI under the hood)
- AgentCore CLI installed: `npm install -g @aws/agentcore`
> **No local Docker required.** Step 5 builds the agent's container image in
> AWS CodeBuild via the CLI's CDK app. You only need Docker if you want to use
> `agentcore dev` for local hot-reload development.
- IAM roles created — run `bash setup_roles.sh` and record the ARNs in `.env`. This
script configures `ProcessPaymentRole` to also serve as the AgentCore Runtime
execution role (ECR pull, CloudWatch logs, X-Ray, Bedrock model invocation,
browser tool), with explicit Deny on session/instrument management. It also adds
`InvokeAgentRuntime` to `ManagementRole` so the notebook can call the deployed agent.
- Content provider deployed to AWS — run `cd content-provider && PAY_TO=0x<your-wallet> bash deploy.sh` and set `CONTENT_DISTRIBUTION_URL` in `.env` (see [content-provider/README.md](content-provider/README.md))
- A Coinbase Developer Platform (CDP) account with an API key
- API key name, private key, and wallet secret are required (see `.env.sample`)
- **Enable Delegated Signing** in your CDP project before running the agent:
@@ -191,16 +235,51 @@ jupyter notebook pay_for_content_browser.ipynb
Run all cells in order. The notebook will:
1. Load configuration and verify environment variables
2. Initialise all three boto3 clients
2. Initialise the two app-backend boto3 clients (`ControlPlaneRole`, `ManagementRole`)
3. Provision the embedded wallet resource stack (once per user):
CredentialProvider → PaymentManager → PaymentConnection → EmbeddedCryptoWallet Instrument
CredentialProvider → PaymentManager → PaymentConnector → EmbeddedCryptoWallet Instrument
— then pause for you to fund the wallet via WalletHub and the Circle faucet
3e. Verify wallet USDC balance via `GetPaymentInstrumentBalance` (ProcessPaymentRole)
4. Create a payment session with payment limits (operator-controlled)
5. Build a Strands agent with `AgentCoreBrowser` and `process_x402_payment`
6. Invoke the agent to retrieve a premium article
7. Verify the spend was recorded via `GetPaymentSession`
8. Cleanup — curtail session with minimum expiry
3e. Verify wallet USDC balance via `GetPaymentInstrumentBalance` (briefly assumes
`ProcessPaymentRole` locally, only for the balance check)
4. Create a payment session with budget and expiry (`ManagementRole`)
4b. **Enable Payment Manager observability** — runs the 4-step vended log
delivery setup (`PutDeliverySource` × 2 → `PutDeliveryDestination` × 2 →
`CreateDelivery` × 2) so the Payment Manager shows up in the AgentCore
Observability → Payments dashboard with sessions, transactions, and
`Agents using Payments` attribution
5. Deploy `agent/payment_agent.py` to AgentCore Runtime via the AgentCore CLI:
`agentcore create` + `agentcore add agent --build Container`, copy in
[`agent/Dockerfile`](agent/Dockerfile), then `agentcore deploy` (CodeBuild builds
the image, pushes to ECR, creates the AgentRuntime). Pinned to Python 3.13,
`ProcessPaymentRole` execution role, 10-min idle / 30-min max lifecycle.
6. Invoke the deployed agent via `InvokeAgentRuntime` with the session/instrument
context in the payload, then verify spend via `GetPaymentSession`
7. View the session trace in CloudWatch GenAI Observability — Runtime, Agent,
Browser-tool, and Payment Manager telemetry all stitched in one dashboard
8. Cleanup — `agentcore remove all` to tear down the Runtime deployment
### Observability coverage
| Layer | How it's enabled | Where you see it |
|---|---|---|
| Runtime | Auto via `agentcore deploy` (`opentelemetry-instrument` CMD) | All-traces dashboard |
| Agent (Strands) | OTEL spans through the Runtime distro | Inside each trace's waterfall |
| Browser tool | Strands `AgentCoreBrowser` emits client-side spans | Inside each trace's waterfall |
| Payment Manager | Vended log delivery (Step 4b) | **Payments tab** of the AgentCore Observability dashboard |
The dashboard's *Agents using Payments* counter increments only when the SDK
sends the `X-Amzn-Bedrock-AgentCore-Payments-Agent-Name` header, which it does
automatically when `PaymentManager` and `AgentCorePaymentsPluginConfig` are
constructed with `agent_name=`. `agent/payment_agent.py` reads `AGENT_NAME` from
the container environment and passes it to both.
> **Browser observability caveat:** the AgentCore Browser service does not
> currently support per-resource vended log delivery. `PutDeliverySource` rejects
> browser ARNs with: *valid resource types are runtime / gateway / memory /
> payment-manager / code-interpreter / workload-identity*. Browser-tool actions
> still appear as spans inside the agent trace via the OTEL distro
> (`browser session start`, `navigate`, `cleanup`), so the *useful* visibility
> is captured — but no separate Browser-service dashboard exists today.
---
@@ -255,32 +334,44 @@ Real x402 sites will have different selectors — the agent discovers payment fo
elements dynamically using semantic cues (button text, input types, aria-labels)
rather than hardcoded IDs.
### Alternative: x402 via AgentCore Gateway
You can also access x402-protected endpoints directly via **Amazon Bedrock AgentCore
Gateway**, which handles the payment header exchange at the API level without a browser.
See the **Pay for Data** use case for that pattern.
---
## IAM Role Design
| Role | Operations | Denied |
|:-----|:-----------|:-------|
| `ControlPlaneRole` | `CreatePaymentCredentialProvider`, `CreatePaymentManager`, `CreatePaymentConnector`, `CreatePaymentInstrument` | `ProcessPayment`, session management |
| `ManagementRole` | `CreatePaymentSession`, `GetPaymentSession` | `ProcessPayment` |
| `ProcessPaymentRole` | `ProcessPayment`, `GetPaymentInstrumentBalance` | All setup and session management ops |
The notebook assumes all three roles via STS at startup. The agent only ever uses
`ProcessPaymentRole` credentials — it cannot modify its own session payment limits, create
new sessions, or access wallet credentials.
| Role | Operations allowed | Denied | Used by |
|:-----|:-------------------|:-------|:--------|
| `ControlPlaneRole` | `CreatePaymentCredentialProvider`, `CreatePaymentManager`, `CreatePaymentConnector`, `CreatePaymentInstrument` | `ProcessPayment`, session management | Notebook (Step 3) |
| `ManagementRole` | `CreatePaymentSession`, `GetPaymentSession`, `InvokeAgentRuntime` | `ProcessPayment` | Notebook (Step 4, Step 6) |
| `ProcessPaymentRole` | `ProcessPayment`, `GetPaymentInstrument`, `GetPaymentInstrumentBalance`, browser tool, ECR pull, CloudWatch logs/metrics, X-Ray, Bedrock model invocation | All setup and session management ops (`CreatePaymentSession`, `CreatePaymentInstrument`, etc.) | **AgentCore Runtime** as execution role |
| `ResourceRetrievalRole` | Service-side payment-token retrieval | n/a (assumed by AWS service) | AgentCore service |
---
## Cleanup
The payment session created in Step 4 expires automatically after `SESSION_EXPIRY_MINUTES`
(60 minutes by default) and stops accepting payments — no API call required.
Tear down in this order when you're done:
To tear down the IAM roles created by `setup_roles.sh`, delete the four
`AgentCorePayments*` roles from the IAM console or via the AWS CLI.
1. **Runtime deployment**`cd PayForContentRuntime && agentcore remove all -y`
(removes the AgentRuntime, the ECR repo, the CodeBuild project, and CloudWatch logs).
2. **Payment session** — expires automatically after `SESSION_EXPIRY_MINUTES`
(60 minutes by default). No API call required to close it.
3. **Payment manager / connector / instrument / credential provider** — delete via the
AWS CLI or boto3 if you want a fully clean account.
4. **Content provider**`cd content-provider && cdk destroy` (removes the CloudFront
distribution and Lambda@Edge function).
5. **IAM roles** — delete the four `AgentCorePayments*` roles from the IAM console
or via the AWS CLI when no longer needed.
---
## Shared responsibility
| Concern | AWS / AgentCore | You (the customer) |
|:------------------------------|:---------------------------------------------------------|:----------------------------------------------------|
| Runtime container isolation | microVM per session, automatic teardown | Set `idleTimeout`, `maxLifetime` to your workload |
| Payment signing keys | Held in AgentCore identity / Coinbase CDP delegated | Enable Delegated Signing in CDP project |
| Spend limits | Service enforces `maxSpendAmount` per session | Set per-session budget appropriate for the task |
| IAM role segregation | Runtime assumes the execution role you specified | Author least-privilege role policies (see `setup_roles.sh`) |
| Observability ingestion | Traces + metrics emitted automatically | Build alarms on the metrics you care about |
| Wallet funding | Embedded wallet provisioned by AgentCore | Fund via faucet (testnet) or onramp (production) |
| Browser session security | Containerized Chromium, ephemeral, optional recording | Avoid logging in to production accounts via the agent |
@@ -0,0 +1,37 @@
FROM public.ecr.aws/docker/library/python:3.13-slim-bookworm
WORKDIR /app
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1 \
PIP_DISABLE_PIP_VERSION_CHECK=1
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m -u 1000 bedrock_agentcore
COPY requirements.txt ./
RUN pip install -r requirements.txt
COPY --chown=bedrock_agentcore:bedrock_agentcore . .
# Playwright bundles a Node.js driver binary; ensure every file in the
# driver directory is executable. Shell globs don't expand inside RUN
# without /bin/bash -c, so we use a python one-liner.
RUN python -c "import playwright, pathlib; d = pathlib.Path(playwright.__file__).parent / 'driver'; print('driver:', d); [p.chmod(0o755) for p in d.rglob('*') if p.is_file()]"
USER bedrock_agentcore
EXPOSE 8080
# AgentCore Runtime monitors container health at the platform level via the
# service contract on :8080; this HEALTHCHECK exists for portability and to
# satisfy security scanners. BedrockAgentCoreApp serves /ping by default.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD curl -fsS http://localhost:8080/ping || exit 1
CMD ["opentelemetry-instrument", "python", "-m", "main"]
@@ -0,0 +1,205 @@
"""
Pay for Content (Browser Use) — Strands agent for AgentCore Runtime.
The agent uses AgentCoreBrowser to navigate a paywalled page, reads the
x402 requirement from the DOM, calls process_x402_payment to generate a
proof, fills it into the paywall UI, and returns the unlocked content.
When deployed to AgentCore Runtime, the container runs under
ProcessPaymentRole. The agent's PaymentManager uses the container's
ambient credentials — there is no sts:AssumeRole inside the agent.
The app backend (notebook) creates the payment session under
ManagementRole and passes all payment context via the invocation payload:
payment_manager_arn — Payment Manager ARN
payment_session_id — fresh session with budget
payment_instrument_id — wallet to pay from
user_id — payment isolation key
paywall_url — page to retrieve
Browser x402 pattern: the requirement is read from a <script> element in
the DOM, not from an HTTP 402 response. The plugin's auto-intercept hook
does not fire — process_x402_payment constructs the synthetic 402 shape
PaymentManager expects.
"""
import json
import os
import uuid
from bedrock_agentcore.payments.integrations.strands import (
AgentCorePaymentsPlugin,
AgentCorePaymentsPluginConfig,
)
from bedrock_agentcore.payments.manager import PaymentManager
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands_tools.browser import AgentCoreBrowser
app = BedrockAgentCoreApp()
REGION = os.environ.get("AWS_REGION", "us-west-2")
MODEL_ID = os.environ.get("MODEL_ID", "us.anthropic.claude-sonnet-4-6")
# Identifier reported in the AgentCore Payments observability dashboard's
# "Agents using Payments" counter and on each payment span's
# `payment_agent_name` attribute. Set via the
# X-Amzn-Bedrock-AgentCore-Payments-Agent-Name HTTP header on every
# data-plane call when PaymentManager is constructed with agent_name=.
AGENT_NAME = os.environ.get("AGENT_NAME", "PayForContentBrowserAgent")
SYSTEM_PROMPT = """\
You are a content retrieval agent with access to Amazon Bedrock AgentCore payments.
You can autonomously browse paywalled websites and pay for premium content using the
x402 micropayment protocol — without any human involvement in the payment step.
When asked to retrieve content from a URL, follow these steps in order:
1. Use the browser tool to navigate to the URL.
2. Find the <script id="x402-requirement"> element and read its JSON content.
3. Call process_x402_payment with the full JSON text of that element.
4. Use the browser tool to interact with the paywall UI:
- Discover payment form elements dynamically using button text, input types,
and aria-labels — do not rely on hardcoded IDs from any particular site.
- On the reference sample content provider the IDs are: pay-btn, proof-input,
verify-btn, content — but real x402 sites will differ.
5. Wait for the content to become visible, then extract and return it.
6. Report the content retrieved and the amount paid in USDC.
Always be transparent about what you paid and what content you retrieved.
"""
@app.entrypoint
def handle_request(payload, context=None):
"""Handle a paywall retrieval request from the app backend.
Args:
payload: dict with:
prompt — natural-language task (may include the URL)
paywall_url — target paywalled page
payment_manager_arn — Payment Manager ARN
user_id — payment isolation key
payment_session_id — fresh session with budget
payment_instrument_id — wallet to pay from
context: AgentCore Runtime context (provides session_id, etc.)
"""
# `agentcore invoke` wraps a 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 "payment_manager_arn" in inner:
payload = inner
except json.JSONDecodeError:
pass
payment_manager_arn = payload.get("payment_manager_arn")
user_id = payload.get("user_id")
session_id = payload.get("payment_session_id")
instrument_id = payload.get("payment_instrument_id")
paywall_url = payload.get("paywall_url")
prompt = payload.get("prompt") or (
f"Please retrieve the premium article from {paywall_url}. "
f"Pay for it using x402 and give me a summary of what it contains."
)
missing = [
name
for name, value in [
("payment_manager_arn", payment_manager_arn),
("user_id", user_id),
("payment_session_id", session_id),
("payment_instrument_id", instrument_id),
("paywall_url", paywall_url),
]
if not value
]
if missing:
return {"error": f"Missing required fields in payload: {', '.join(missing)}"}
# PaymentManager uses the container's ambient credentials (ProcessPaymentRole
# when deployed; whatever role is active when running locally for dev).
# agent_name populates the X-Amzn-Bedrock-AgentCore-Payments-Agent-Name
# header on every data-plane call so AgentCore Payments observability
# can attribute spans/metrics back to this agent.
payment_manager = PaymentManager(
payment_manager_arn=payment_manager_arn,
region_name=REGION,
agent_name=AGENT_NAME,
)
@tool
def process_x402_payment(requirement_json: str) -> dict:
"""Process an x402 v2 payment requirement and return a signed proof.
Args:
requirement_json: JSON string of the x402 requirement read from
the <script id="x402-requirement"> DOM element.
Returns:
dict with proof_b64, amount, and status.
"""
requirement = json.loads(requirement_json)
first_accept = requirement["accepts"][0]
amount_units = int(
first_accept.get("maxAmountRequired") or first_accept.get("amount", 0)
)
# Token's smallest unit (e.g. 1_000_000 for USDC's 6 decimals) — the
# value is reported back to the caller for display, not used for
# routing or settlement.
amount = amount_units / 1_000_000
# generate_payment_header expects an HTTP-402-shaped envelope.
# In the browser pattern the requirement comes from a DOM script tag
# rather than an HTTP 402 response, so we wrap it to match the SDK's
# input contract.
payment_required_request = {
"statusCode": 402,
"headers": {},
"body": requirement,
}
header_dict = payment_manager.generate_payment_header(
user_id=user_id,
payment_instrument_id=instrument_id,
payment_session_id=session_id,
payment_required_request=payment_required_request,
client_token=str(uuid.uuid4()),
)
proof_b64 = list(header_dict.values())[0]
return {
"proof_b64": proof_b64,
"amount": amount,
"status": "PROOF_GENERATED",
}
payments_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,
agent_name=AGENT_NAME,
)
)
agent_core_browser = AgentCoreBrowser(region=REGION)
agent = Agent(
system_prompt=SYSTEM_PROMPT,
tools=[agent_core_browser.browser, process_x402_payment],
plugins=[payments_plugin],
model=MODEL_ID,
)
result = agent(prompt)
text = result.message.get("content", [{}])[0].get("text", str(result))
return {"response": text}
if __name__ == "__main__":
app.run()
@@ -0,0 +1,9 @@
bedrock-agentcore[strands-agents]>=1.9.0
boto3>=1.43.6
botocore>=1.43.6
strands-agents[otel]>=1.0.0
strands-agents-tools>=0.5.0
aws-opentelemetry-distro
python-dotenv>=1.0.0
nest_asyncio>=1.5.0
playwright>=1.40.0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

After

Width:  |  Height:  |  Size: 537 KiB

File diff suppressed because one or more lines are too long
@@ -1,6 +1,8 @@
strands-agents==1.36.0
strands-agents-tools==0.5.0
boto3==1.43.1
botocore==1.43.3
boto3>=1.43.6
botocore>=1.43.6
python-dotenv>=1.0.0
bedrock-agentcore==1.4.4
bedrock-agentcore>=1.9.0
nest_asyncio>=1.5.0
playwright>=1.40.0
@@ -7,10 +7,15 @@
# opening the notebook.
#
# Roles created:
# AgentCorePaymentsControlPlaneRole — provisioning (manager, connector, credential provider)
# AgentCorePaymentsManagementRole — session lifecycle (create/get/update sessions, instruments)
# AgentCorePaymentsProcessPaymentRole — agent runtime (ProcessPayment, GetPaymentInstrument, GetPaymentInstrumentBalance)
# AgentCorePaymentsResourceRetrievalRole — service-side token retrieval (assumed by AgentCore service)
# AgentCorePaymentsControlPlaneRole — provisioning (manager, connector, credential provider)
# AgentCorePaymentsManagementRole — session lifecycle (create/get/update sessions, instruments)
# AgentCorePaymentsProcessPaymentRole — agent runtime; doubles as the AgentCore Runtime
# execution role (ProcessPayment, browser tool, ECR pull,
# CloudWatch logs/metrics, X-Ray, model invocation).
# Explicit Deny on session/instrument management so the
# role-segregation boundary is enforced even though the
# agent runs in a managed container.
# AgentCorePaymentsResourceRetrievalRole — service-side token retrieval (assumed by AgentCore service)
#
# After running, copy the printed ARNs into your .env file.
# =============================================================
@@ -70,9 +75,11 @@ create_or_update_role() {
}
# ── 1. ControlPlaneRole ───────────────────────────────────────────────────────
# Used by notebook Steps 3a3c: CreatePaymentCredentialProvider,
# CreatePaymentManager, CreatePaymentConnector.
# Uses bedrock-agentcore:* for breadth; explicitly denies ProcessPayment.
# Administrator role per the AgentCore payments IAM doc:
# https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments-iam-roles.html
# Manages payment managers, connectors, and credential providers — never
# executes payments. Each statement is scoped to the specific resource
# pattern called out in the doc.
CONTROL_PLANE_TRUST=$(cat <<EOF
{
@@ -94,10 +101,46 @@ CONTROL_PLANE_POLICY=$(cat <<EOF
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ControlPlaneOperations",
"Sid": "AllowPaymentManagerOperations",
"Effect": "Allow",
"Action": "bedrock-agentcore:*",
"Resource": "*"
"Action": [
"bedrock-agentcore:CreatePaymentManager",
"bedrock-agentcore:GetPaymentManager",
"bedrock-agentcore:ListPaymentManagers",
"bedrock-agentcore:DeletePaymentManager",
"bedrock-agentcore:UpdatePaymentManager"
],
"Resource": "arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*"
},
{
"Sid": "AllowPaymentConnectorOperations",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreatePaymentConnector",
"bedrock-agentcore:GetPaymentConnector",
"bedrock-agentcore:ListPaymentConnectors",
"bedrock-agentcore:DeletePaymentConnector",
"bedrock-agentcore:UpdatePaymentConnector"
],
"Resource": "arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*/connector/*"
},
{
"Sid": "AllowCredentialProviderOperations",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreatePaymentCredentialProvider",
"bedrock-agentcore:GetPaymentCredentialProvider",
"bedrock-agentcore:ListPaymentCredentialProviders",
"bedrock-agentcore:DeletePaymentCredentialProvider",
"bedrock-agentcore:UpdatePaymentCredentialProvider"
],
"Resource": "arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:token-vault/*/paymentcredentialprovider/*"
},
{
"Sid": "AllowVendedLogDelivery",
"Effect": "Allow",
"Action": "bedrock-agentcore:AllowVendedLogDeliveryForResource",
"Resource": "arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*"
},
{
"Sid": "DenyDataPlanePaymentExecution",
@@ -116,13 +159,21 @@ CONTROL_PLANE_POLICY=$(cat <<EOF
"Resource": "arn:aws:secretsmanager:*:${ACCOUNT_ID}:secret:bedrock-agentcore-identity*"
},
{
"Sid": "PassRoles",
"Sid": "PassResourceRetrievalRole",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": [
"arn:aws:iam::${ACCOUNT_ID}:role/AgentCorePaymentsResourceRetrievalRole",
"arn:aws:iam::${ACCOUNT_ID}:role/AgentCorePaymentsManagementRole"
]
"Resource": "arn:aws:iam::${ACCOUNT_ID}:role/AgentCorePaymentsResourceRetrievalRole",
"Condition": {
"StringEquals": {
"iam:PassedToService": "bedrock-agentcore.amazonaws.com"
}
}
},
{
"Sid": "PassManagementRole",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::${ACCOUNT_ID}:role/AgentCorePaymentsManagementRole"
}
]
}
@@ -167,7 +218,7 @@ MANAGEMENT_POLICY=$(cat <<EOF
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InstrumentAndSessionManagement",
"Sid": "AllowPaymentManagement",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:CreatePaymentInstrument",
@@ -176,9 +227,21 @@ MANAGEMENT_POLICY=$(cat <<EOF
"bedrock-agentcore:DeletePaymentInstrument",
"bedrock-agentcore:CreatePaymentSession",
"bedrock-agentcore:GetPaymentSession",
"bedrock-agentcore:ListPaymentSessions"
"bedrock-agentcore:ListPaymentSessions",
"bedrock-agentcore:UpdatePaymentSession",
"bedrock-agentcore:DeletePaymentSession"
],
"Resource": "*"
"Resource": [
"arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*",
"arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*/instrument/*",
"arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*/session/*"
]
},
{
"Sid": "InvokeDeployedAgent",
"Effect": "Allow",
"Action": "bedrock-agentcore:InvokeAgentRuntime",
"Resource": "arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:runtime/*"
},
{
"Sid": "DenyProcessPayment",
@@ -213,6 +276,20 @@ PROCESS_PAYMENT_TRUST=$(cat <<EOF
"Effect": "Allow",
"Principal": { "AWS": ${CLIENT_PRINCIPAL} },
"Action": "sts:AssumeRole"
},
{
"Sid": "AllowAgentCoreRuntimeAssume",
"Effect": "Allow",
"Principal": { "Service": "bedrock-agentcore.amazonaws.com" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "${ACCOUNT_ID}"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:bedrock-agentcore:${REGION}:${ACCOUNT_ID}:runtime/*"
}
}
}
]
}
@@ -226,10 +303,109 @@ PROCESS_PAYMENT_POLICY=$(cat <<EOF
{
"Sid": "AllowProcessPayment",
"Effect": "Allow",
"Action": "bedrock-agentcore:ProcessPayment",
"Resource": [
"arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*",
"arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*/session/*"
]
},
{
"Sid": "AllowPaymentReadOperations",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:ProcessPayment",
"bedrock-agentcore:GetPaymentInstrument",
"bedrock-agentcore:GetPaymentInstrumentBalance"
"bedrock-agentcore:GetPaymentInstrumentBalance",
"bedrock-agentcore:GetPaymentSession"
],
"Resource": [
"arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*",
"arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*/instrument/*",
"arn:aws:bedrock-agentcore:*:${ACCOUNT_ID}:payment-manager/*/session/*"
]
},
{
"Sid": "DenySessionManagement",
"Effect": "Deny",
"Action": [
"bedrock-agentcore:CreatePaymentSession",
"bedrock-agentcore:CreatePaymentInstrument",
"bedrock-agentcore:CreatePaymentManager",
"bedrock-agentcore:CreatePaymentConnector",
"bedrock-agentcore:UpdatePaymentSession",
"bedrock-agentcore:DeletePaymentInstrument",
"bedrock-agentcore:DeletePaymentSession"
],
"Resource": "*"
},
{
"Sid": "RuntimeECRAccess",
"Effect": "Allow",
"Action": [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
"ecr:GetAuthorizationToken"
],
"Resource": "*"
},
{
"Sid": "RuntimeCloudWatchLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:/aws/bedrock-agentcore/runtimes/*",
"arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:*"
]
},
{
"Sid": "RuntimeXRay",
"Effect": "Allow",
"Action": [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets"
],
"Resource": "*"
},
{
"Sid": "RuntimeCloudWatchMetrics",
"Effect": "Allow",
"Action": "cloudwatch:PutMetricData",
"Resource": "*",
"Condition": {
"StringEquals": {
"cloudwatch:namespace": "bedrock-agentcore"
}
}
},
{
"Sid": "BedrockModelInvocation",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
"arn:aws:bedrock:${REGION}:${ACCOUNT_ID}:*"
]
},
{
"Sid": "BrowserToolAccess",
"Effect": "Allow",
"Action": [
"bedrock-agentcore:StartBrowserSession",
"bedrock-agentcore:StopBrowserSession",
"bedrock-agentcore:GetBrowserSession",
"bedrock-agentcore:ListBrowserSessions",
"bedrock-agentcore:UpdateBrowserStream",
"bedrock-agentcore:ConnectBrowserAutomationStream"
],
"Resource": "*"
}