
Discover Transactional Agents with Amazon Bedrock AgentCore Payments
When we build AI agents that consume third-party APIs, the main challenge is often not the model's reasoning capabilities it's payments.
An agent may discover that an endpoint costs 0.0005 USDC, but someone still needs to sign that transaction. Traditionally, that means placing a private key somewhere inside the agent's code.
That is exactly what we want to avoid.
This is where the x402 protocol comes in. It reuses an HTTP status code that almost nobody used before —
402 Payment Required — allowing a server to respond with something equivalent to:"This costs X. Pay for it and try again."
Amazon Bedrock AgentCore Payments handles the other side of the transaction. It manages the wallet and signs transactions server-side, which means the agent never sees a private key and can only spend within the budget we assign to it.
In this article, we will deploy a complete end-to-end flow:
- A seller that charges for content behind CloudFront.
- An AI agent that detects an HTTP
402response and pays for the requested resource. - A web interface where we can see the entire process in action.
Everything will run on AWS.
Requirements
- Node.js 24
- Python 3.11 or later
- AWS CLI 2.x, with
aws sts get-caller-identityworking correctly - AWS CDK 2.x
- Docker running — not just installed. The agent is deployed as a container image that we build locally and push to ECR.
AWS account
For this example, we will use the
us-east-1 region.Your AWS credentials should have sufficient permissions for services such as:
- Amazon CloudFront
- Amazon CloudWatch
- Amazon S3
- Amazon Bedrock AgentCore
- AWS Lambda
You also need access to Bedrock models enabled in your account.
It is a good idea to verify this before deploying anything:
1
2
3
4
aws bedrock-runtime converse \
--model-id us.anthropic.claude-sonnet-4-5-20250929-v1:0 \
--messages '[{"role":"user","content":[{"text":"hello"}]}]' \
--inference-config '{"maxTokens":10}'If this returns an
AccessDeniedException mentioning aws-marketplace:Subscribe, model access has not been enabled for the account.You can fix this from the AWS console under:
Bedrock → Model access
You also need to bootstrap CDK once per AWS account and region:
1
npx cdk bootstrap aws://<ACCOUNT_ID>/us-east-1Coinbase Developer Platform
AgentCore Payments signs transactions through a Coinbase CDP project, so we need to configure one before getting started.
You will need:
- An account at
portal.cdp.coinbase.com - An API key:
CDP_API_KEY_IDCDP_API_KEY_SECRET
- A wallet secret:
CDP_WALLET_SECRET
- Delegated signing enabled in your project policies.
Delegated signing is probably the easiest requirement to overlook.
Without it, every
ProcessPayment request will fail with:1
Delegated signing is not enabled for your Coinbase project
You will also need access to an email address.
When the payment instrument is created, CDP sends a wallet activation link to that email address. Until the activation link is opened, the wallet cannot sign transactions.
Test funds
We will use the Circle faucet at
faucet.circle.com.This allows us to fund our wallet on the testnet.
Proposed Architecture
There are two CloudFront distributions.
One exposes our frontend, while the other exposes the backend API protected by the payment paywall that the agent consumes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Browser
|
v
CloudFront + S3 (web interface)
|
v
API Gateway (hard 29-second limit)
|
v
Lambda (proxy: InvokeAgentRuntime)
|
v
AgentCore Runtime (container running the Strands agent)
|
+--> AgentCore Payments --> Coinbase CDP (wallet, signing)
|
+--> CloudFront + Lambda@Edge (seller: verifies x402, returns 402 or content)
|
v
S3 (paid content)The important part of this design is understanding which component knows what.
- The browser does not know the seller's URL. It asks the agent for a path such as
/api/weather-data, and the agent resolves that path against its ownSELLER_API_URL. This prevents the payment origin from being manipulated by the client. - The agent does not have access to private keys. It calls
ProcessPaymentand receives a payment proof. - The payment session defines the available budget. The agent operates under an IAM role called
ProcessPaymentRole, which can only execute payments within that limit. It cannot create payment sessions or modify the budget.
The flow consists of three steps, and each one is a separate call to the agent:
request_content("/api/weather-data")returns a402response containing thex402_payload.process_payment(x402_payload)returns thePROOF_GENERATEDstatus.request_content_with_payment("/api/weather-data")retries the request using thePAYMENT-SIGNATUREheader and receives a200response containing the requested content.
We separate these actions for a practical reason:
API Gateway terminates requests after 29 seconds.
If we ask the agent to perform all three steps within a single prompt, the HTTP response may time out even though the agent eventually completes the operation.
The seller's content comes from two different sources.
Three endpoints return data embedded directly inside the Lambda@Edge function, while another three retrieve their content from S3:
/api/weather-data— 0.0005 USDC, embedded/api/premium-article— 0.001 USDC, embedded/api/market-analysis— 0.002 USDC, embedded/api/tutorial— 0.003 USDC, S3/api/research-report— 0.005 USDC, S3/api/dataset— 0.01 USDC, S3
Walkthrough
Step 1: Deploy the Seller
First, we create:
- The CloudFront distribution
- The x402 verifier running as Lambda@Edge
- The S3 bucket containing the paid content
1
2
cd seller-infrastructure
cp .env.example .envInside the
.env file, configure:1
PAYMENT_RECIPIENT_ADDRESSThis is the wallet that will receive the payments.
The stack intentionally fails if this variable is not defined.
Deploying a payment paywall without explicitly configuring the recipient could result in payments being sent to the wrong destination.
Now deploy the stack:
1
2
npm install
npx cdk deployDeploying CloudFront together with Lambda@Edge is relatively slow.
A deployment time of 10 to 15 minutes is normal.
From the CloudFormation output, we are interested in:
1
DistributionUrlThis is the seller's URL.
Next, upload the content for the three endpoints backed by S3:
1
2
./scripts/upload-content.sh $(aws cloudformation describe-stacks --stack-name X402SellerStack \
--query "Stacks[0].Outputs[?OutputKey=='ContentBucketName'].OutputValue" --output text)Now verify that the paywall is working:
1
curl -s -o /dev/null -w '%{http_code}\n' https://<seller>.cloudfront.net/api/weather-dataThe response should be:
1
402If it returns
200, you are probably pointing to the wrong CloudFront distribution.Step 2: Deploy the Payer IAM Roles
The payment resources we create in Step 3 require the ARNs of the IAM roles created by this stack.
1
2
3
cd ../payer-infrastructure
npm install
npx cdk deploy X402PayerAgentStackWe need to specify the stack explicitly because the CDK application defines two stacks.
Running:
1
cdk deployby itself will fail and ask which stack you want to deploy.
The second stack is:
1
X402ObservabilityStackIt creates CloudWatch dashboards and alarms and is optional.
From the stack output, save the following values:
1
2
3
ProcessPaymentRoleArn
ResourceRetrievalRoleArn
AgentRuntimeRoleArnStep 3: Create the AgentCore Payments Resources
A single script creates:
- The credential provider
- The payment manager
- The connector
- The payment instrument — our wallet
- The payment session with its associated budget
Run:
1
2
3
4
5
6
7
8
9
10
11
12
cd ../payer-agent
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
export CDP_API_KEY_ID=...
export CDP_API_KEY_SECRET=...
export CDP_WALLET_SECRET=...
export RESOURCE_RETRIEVAL_ROLE_ARN=<ResourceRetrievalRoleArn from step 2>
export USER_EMAIL=you@example.com
export USER_ID=x402-demo-user
python scripts/setup_payments.pyThe script prints a wallet activation URL.
You must open that URL and complete the activation process. Otherwise, the wallet will not be able to sign transactions.
The script then prints the values that we need to add to the
.env file in the next step.Before continuing, fund the wallet with Base Sepolia USDC using the Circle faucet.
The wallet address appears in the script output.
Step 4: Configure and Deploy the Agent
Create the environment file:
1
cp .env.example .envAt minimum, configure:
1
2
3
4
5
6
7
8
AWS_REGION=us-east-1
BEDROCK_MODEL_ID=us.anthropic.claude-sonnet-4-5-20250929-v1:0
SELLER_API_URL=https://<seller>.cloudfront.net
MANAGER_ARN=<from step 3>
PAYMENT_SESSION_ID=<from step 3>
PAYMENT_INSTRUMENT_ID=<from step 3>
PROCESS_PAYMENT_ROLE_ARN=<ProcessPaymentRoleArn from step 2>
USER_ID=x402-demo-userSELLER_API_URL should not include a trailing slash.The agent's content tools receive a path and append it to this base URL.
Leave:
1
AGENT_RUNTIME_ARNempty.
The deployment script writes the runtime ARN back into the file after the deployment completes.
Now deploy the agent:
1
python scripts/deploy_to_agentcore.pyThis script:
- Builds the container image.
- Pushes it to Amazon ECR.
- Creates or updates the AgentCore Runtime.
An important detail here is that only environment variables included in the deployment script's allowlist are passed to the runtime.
In practice, this means that if you add a new variable to
.env but do not also add it to that allowlist, the agent will never see it.
Step 5: Deploy the Web Interface
Move to the web infrastructure:
1
2
3
cd ../web-ui-infrastructure
npm install
cd ../web-ui && npm install && cd ../web-ui-infrastructureThen deploy:
1
2
3
WALLET_ADDRESS=<wallet from step 3> \
AGENT_RUNTIME_ARN=<from the .env file in step 4> \
./scripts/deploy.shThe deployment script:
- Deploys the infrastructure stack.
- Retrieves the API Gateway URL.
- Embeds that URL into the frontend build.
- Uploads the frontend to S3.
Both environment variables are important.
AGENT_RUNTIME_ARN identifies the AgentCore Runtime invoked by the Lambda function.WALLET_ADDRESS is displayed in the wallet panel of the frontend.Open the URL returned as:
1
WebUiUrlNow we can test the entire flow.
Choose one of the available pieces of content.
Clicking Request Content should return the
402 Payment Required response along with the payment details.Then clicking Confirm Payment should execute the payment and return the protected content.
You can also test the agent directly through the API:
1
2
3
curl -s -X POST https://<api-id>.execute-api.us-east-1.amazonaws.com/prod/invoke \
-H 'Content-Type: application/json' \
-d '{"message":"Use the request_content tool to fetch content from /api/weather-data"}'Example
First, we add funds to our wallet.

From the frontend, we can verify the wallet address and available balance.
Next, we select the premium content that we want the agent to retrieve.
Finally, we proceed with the payment.

Common Errors and How to Debug Them
These are some of the issues that took real time to troubleshoot, along with the error message and underlying cause.
Received error (422) from runtime
CloudWatch may show nothing other than the
422.This usually means that
invoke_agent_runtime was called without specifying contentType.That parameter is optional in boto3, so the request reaches the container without a JSON content type.
FastAPI then rejects the request body before our handler is executed.
That is why there are no application logs: the framework itself generates the
422.The fix is to explicitly provide:
1
contentType='application/json'Invocation of model ID ... with on-demand throughput isn't supported
In this case,
BEDROCK_MODEL_ID is pointing directly to a foundation model ID.You need to use a cross-region inference profile instead — in this case, the same model ID with the
us. prefix.You can check which inference profiles are currently available:
1
2
aws bedrock list-inference-profiles \
--query "inferenceProfileSummaries[?contains(inferenceProfileId,'sonnet')].inferenceProfileId"AccessDeniedException ... not authorized to perform: bedrock-agentcore:InvokeAgentRuntime on resource: .../runtime/<id>/runtime-endpoint/DEFAULT
The IAM policy may grant access to the runtime ARN itself, but authorization occurs against the
runtime-endpoint subresource.You need to allow both:
1
<runtime-arn>and:
1
<runtime-arn>/*Missing or invalid paymentManagerArn / paymentSessionId / paymentInstrumentId
The values may exist in your local
.env file but never have reached the actual runtime environment.Check the runtime using
get-agent-runtime and inspect the contents of:1
environmentVariablesA 403 response containing HTML instead of JSON when retrying with the payment proof
The retry was probably made using
POST.The
/api/* CloudFront behavior only allows:1
2
3
HEAD
GET
OPTIONSCloudFront therefore rejects the HTTP method before the x402 verifier is ever executed.
The clue is the response format.
If the error is HTML instead of JSON, it was probably generated by CloudFront rather than by our Lambda function.
The .env file appears to be ignored
load_dotenv() does not overwrite variables that already exist in the process environment.An old
export command in your terminal silently takes precedence over the .env value.An even more confusing situation occurs when an environment variable contains the literal value:
1
NoneIn Python, that is still a non-empty string and therefore evaluates as truthy, making the variable appear to be configured.
Check your environment with:
1
env | grep AGENT_Endpoint request timed out from the browser
This is caused by API Gateway's 29-second timeout limit.
The agent may still be working successfully in the background.
What was lost is the HTTP response.
This is why the frontend performs one request per step instead of trying to execute the entire payment flow in one API call.
Inspecting the Agent Runtime Logs
To see what is actually happening inside the agent:
1
2
aws logs tail /aws/bedrock-agentcore/runtimes/<runtime-id>-DEFAULT --since 10m --format short \
| grep -v "GET /ping"Filtering out
/ping is not merely cosmetic.AgentCore performs a container health check roughly twice per second, and those log lines can quickly bury everything else.
Recommendations
- Treat the payment session as what it really is: an expiring budget.
It is created usingmaxSpendAmountandexpiryTimeInMinutes.
Once it expires, you need to create a new session and updatePAYMENT_SESSION_ID.
It is also worth checkingavailableLimitsbefore assuming that a payment failed for some unrelated reason. - Do not allow the frontend to construct payment URLs.
Let the frontend submit paths and have the agent resolve those paths against its own configuration.
If the agent accepts arbitrary absolute URLs, someone could redirect the payment flow toward another origin. - Keep the agent's IAM role limited to
ProcessPayment.
If a backend needs to query balances or inspect payment instruments, use separate credentials rather than expanding the agent's payment role. - Update the AgentCore Runtime instead of recreating it.
If a deployment deletes and recreates the runtime, its ARN changes.
Everything referencing the previous ARN — such as the frontend Lambda or the.envfile — will continue pointing to a resource that no longer exists. - Always start on a testnet with small budgets.
An agent that successfully executes a payment but later fails to retrieve the protected content has still spent the money.
The complete project code is available in the repository, together with a
QUICKSTART.md that covers both deployment and cleanup of resources that CDK does not manage directly, including:- The AgentCore Runtime
- The ECR container image
- The AgentCore Payments resources
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article