Subchapter 14.8
references/payments.mdMarkdown70 KBView on GitHub
Add AgentCore Payments to your agent — the managed service that lets your agent pay for x402- and MPP-protected APIs, MCP tools, and web content via microtransactions (Coinbase CDP, Stripe Privy).
AgentCore Payments is protocol-agnostic: it supports both x402 (Coinbase/Cloudflare’s HTTP-native stablecoin micropayment protocol) and MPP (the Machine Payments Protocol from Stripe and Tempo). Both are exercised through the same ProcessPayment API and the same manager/connector/instrument/session resources — the service detects which protocol a merchant speaks from its 402 Payment Required response and mints the matching payment proof. You do not pick a protocol up front; you provision payments once and the agent can pay either kind of merchant. See How x402 Payment Works and MPP (Machine Payments Protocol) below for the two wire flows.
The control-plane resources (payment manager, connector, credential provider) are provisioned with the AgentCore CLI. The per-user data-plane resources (instrument, session) are created with the AgentCore SDK (a provided script). Payments can be wired into the agent in two ways: (1) a for Strands (plugin) or LangGraph (middleware) that handles 402 detection, payment signing, and retry transparently — no custom tool code needed, or (2) a () for any other Python framework (OpenAI Agents SDK, CrewAI, etc.) or when you need full manual control.
Scripts
Process Payment Toolscripts/process_payment_tool.py402 Payment Required and you want it settled and retried automaticallyDo NOT use this skill for:
agents-connectagents-harden$ARGUMENTS is optional: /payments, /payments wire, /payments debug, /payments coinbase, /payments stripe.
Execution model — minimize human stops. Run the steps yourself, in order, without pausing between them. There are only two points that require the developer; pause at these and resume automatically once the developer confirms:
READY.Everything else — Steps 0–3a, 4 (deploy), 5 (wire), 6 (instrument/session), 8 (set env + test) — you run automatically. After the developer confirms 3b, ask them for the user id and email for the first wallet (Step 6 needs them), then immediately continue through 4 → 5 → 6 (and present Step 7) without asking permission for each. After they confirm 7, run Step 8. Do not stop after every step.
The CLI is the npm package @aws/agentcore (Node.js 20+). It is NOT a pip package — do not pip install it.
agentcore --version # need >= 0.20.0 (payment commands are preview, added in 0.20.x)
# if missing or older:
npm install -g @aws/agentcoreThe CLI provisions payment resources into a project (agentcore/agentcore.json).
Project exists: read agentcore/agentcore.json — check the payments array and the runtimes array (framework).
No project: scaffold one (don’t call --help; run it directly). Non-interactive:
agentcore create --project-name <ProjectName> --name <AgentName> --framework Strands --defaults--project-name and --name are both required non-interactively (--name is the agent/resource name; without it the CLI drops to the interactive wizard). Project name: start with a letter, alphanumeric, ≤23 chars, no underscores. --defaults = Python + Bedrock, no memory; or run agentcore create for the interactive wizard. A project is only needed to provision the payment resources via the CLI — the local payment tool (Step 5) works in any agent, framework, or host.
Framework check: If the project uses Strands or LangGraph (check agentcore/agentcore.json → runtimes array), offer the native integration path (Step 5a) which is simpler — no custom tool script needed. If the project uses another framework, or the developer wants manual control, use the generic tool path (Step 5b).
Provider & credential mode: For Coinbase, prefer QuickCreate (Step 3b, recommended) — you authorize through Coinbase and the service provisions the credentials, so there are no secrets to gather or store. Choose Manual only if you already manage your own Coinbase keys. Stripe (Privy) is manual-only.
3a. Payment manager — no secrets, run it directly (non-interactive). The agent can run this for the developer:
agentcore add payment-manager \
--name <ManagerName> \
--network-preferences eip155:84532eip155:84532 is Base Sepolia (testnet). Names: alphanumeric only, ≤48 chars, start with a letter.
Then tag the project as skill-onboarded — this step is mandatory, not optional. The agent MUST edit agentcore/agentcore.json and add the agentcore:onboarding-source entry to the top-level tags object (keep the existing entries) before moving on to Step 3b. If the tags object does not exist yet, create it with the entries shown:
"tags": {
"agentcore:created-by": "agentcore-cli",
"agentcore:project-name": "<existing value>",
"agentcore:onboarding-source": "agent-toolkit-skill"
}Project tags are applied to the provisioned AWS resources at deploy. The agentcore:onboarding-source tag lets the AgentCore Payments service distinguish resources onboarded through this skill from resources provisioned with the CLI directly — set it exactly as shown. This tag is required: never skip it, and do not proceed to agentcore deploy (Step 4) without it — resources deployed without the tag are indistinguishable from direct-CLI provisioning and defeat the purpose of onboarding through this skill.
3b. Payment connector — choose a credential mode. There are two ways to supply the connector’s credentials:
Coinbase prerequisite — AWS Marketplace subscription (required for BOTH QuickCreate and Manual). Any Coinbase connector requires an active AWS Marketplace subscription to “Coinbase Wallets for AgentCore Payments”. With this subscription, your Coinbase wallet usage charges are consolidated into your monthly AWS bill as per Coinbase’s public pricing. Until an active subscription exists in your AWS account, CreatePaymentConnector fails with a SubscriptionRequiredException (HTTP 403).
Subscribe once per AWS account, before creating a Coinbase connector — Coinbase Wallets for AgentCore Payments (opens in a new tab):
A missing subscription 403s at
agentcore deploy(when the connector is actually created). Because the CLI provisions the payment manager and connector in the same CloudFormation stack, that failure rolls the whole stack back and deletes the PaymentManager too — see Recovery after aSubscriptionRequiredExceptionin Step 4.
Coinbase — QuickCreate (recommended). No secrets, so the agent can run this directly. Prerequisite: the Coinbase AWS Marketplace subscription above. Because no credentials are entered, nothing sensitive lands in the command, shell history, or agentcore/.env.local, and you skip the “get your provider credentials” step below.
agentcore add payment-connector \
--manager <ManagerName> \
--name <ConnectorName> \
--provider CoinbaseCDP \
--provision-mode QUICK_CREATEThis records a QuickCreate Coinbase connector locally with no secrets. When the connector is created at agentcore deploy (Step 4), it comes up in PENDING_AUTHENTICATION with an authorizationUrl the developer must open to authorize through Coinbase. The AgentCore CLI does not open that URL for you today — the agent retrieves it (via GetPaymentConnector) and opens it / hands it to the developer (see the handling bullets below). After the developer authorizes, the connector moves PENDING_AUTHENTICATION → READY — there is no API Key ID, API Key Secret, or Wallet Secret to obtain or store. Present the command (the agent may run it — no secrets are involved), drive the authorization as described below, and confirm the connector is READY before continuing. (Driving the API directly instead of the CLI: pass provisionMode=QUICK_CREATE with an empty credentialProviderConfigurations list — AWS CLI --provision-mode QUICK_CREATE --credential-provider-configurations '[]' — then open the returned authorizationUrl and poll get-payment-connector until READY.)
Handling the authorizationUrl (short-lived + single-use). If the connector is created via the API/SDK — or the agent surfaces the URL to the developer instead of the CLI opening the browser itself — treat the authorizationUrl returned for the PENDING_AUTHENTICATION connector carefully:
curl/HEAD/WebFetch of the URL, and don’t paste it anywhere that auto-previews or “unfurls” links (Slack, Teams, other chat tools). A preview fetch consumes the one-time session, so the developer’s later click fails with “Invalid request”. (Launching a browser to navigate to it is the intended use — that is different from fetching it.)open "<url>" (macOS), xdg-open "<url>" (Linux), start "" "<url>" (Windows); these navigate, they do not pre-fetch. If you are headless / remote / in CI (no local GUI browser, or the developer isn’t at this machine), print the URL as plain/code text and have the developer open it. Retrieve the URL from GetPaymentConnector (or the create response) — never fetch the URL itself.GetPaymentConnector until the status is terminal — do not reopen the URL to check. After the developer authorizes, poll the connector’s status until it reaches one of READY, AUTHENTICATION_EXPIRED, AUTHENTICATION_FAILED, or AWS_MARKETPLACE_SUBSCRIPTION_REQUIRED (space the calls out, e.g. every few seconds). CREATING, PENDING_AUTHENTICATION (waiting for consent), and PROVISIONING (consent succeeded, credentials being provisioned) are all transient — keep polling.READY — done. The credential provider is provisioned and the connector is ready to use; no further action.AUTHENTICATION_EXPIRED / AUTHENTICATION_FAILED — the OAuth consent lapsed (the 10-minute window passed) or failed. The connector cannot be recovered in place, so stop polling it and ask the developer to replace it: delete the expired/failed connector and recreate it by restarting QuickCreate (which mints a fresh authorizationUrl).AWS_MARKETPLACE_SUBSCRIPTION_REQUIRED — the AWS account is not subscribed to “Coinbase Wallets for AgentCore Payments” (see the Coinbase prerequisite above). Have the developer subscribe, then replace the connector. On the CLI deploy path the payment manager may have been rolled back too — see Recovery after a SubscriptionRequiredException in Step 4.# Poll status until terminal: READY / AUTHENTICATION_EXPIRED / AUTHENTICATION_FAILED / AWS_MARKETPLACE_SUBSCRIPTION_REQUIRED
# (CREATING / PENDING_AUTHENTICATION / PROVISIONING = keep polling; a still-valid authorizationUrl is returned while PENDING_AUTHENTICATION):
aws bedrock-agentcore-control get-payment-connector \
--payment-manager-id "<PAYMENT_MANAGER_ID>" \
--payment-connector-id "<PAYMENT_CONNECTOR_ID>" \
--region <AWS_REGION>If the status is AUTHENTICATION_EXPIRED or AUTHENTICATION_FAILED, replace the connector — delete it, then restart QuickCreate:
# Delete the expired/failed connector, then re-run the QuickCreate command above to mint a fresh authorizationUrl.
agentcore remove payment-connector --manager <ManagerName> --name <ConnectorName> --yes
agentcore deploy
# then re-run: agentcore add payment-connector … --provider CoinbaseCDP --provision-mode QUICK_CREATE (and agentcore deploy)Manual (Coinbase CDP or Stripe Privy) — needs provider credentials. The DEVELOPER runs this, not the agent. The agent presents the prerequisites and the command below, but must NOT execute it or handle the credentials. This single command creates the credential provider and the connector. The CLI writes the provider secrets in plaintext to agentcore/.env.local and records the credential locally; agentcore deploy (Step 4) then uploads them to AgentCore Identity (agentcore.json keeps only a reference). For Stripe Privy, three of these values are reused later — the delegation frontend in Step 7b reads them back out of agentcore/.env.local, so the developer is never asked for them twice. Note the CLI namespaces each key as AGENTCORE_CREDENTIAL_<MANAGER>_<CONNECTOR>_STRIPE_PRIVY_<FIELD>, using the manager and connector names chosen below; Step 7b matches on the _STRIPE_PRIVY_<FIELD> suffix for that reason.
Before running — get your provider credentials (do this first; the connector command needs them). These match the exact locations in the AgentCore Payments prerequisites (opens in a new tab).
Recommended — interactive wizard. Run the command with no flags (the secrets never appear in the command, shell history, or process list; the CLI still writes them to agentcore/.env.local either way — see the security note below). Passing --manager/--name/--provider does NOT trigger the wizard — those flags switch the CLI to non-interactive mode and it then requires every secret flag too, failing with “Missing required options” otherwise:
agentcore add payment-connector
# the wizard prompts for everything interactively — manager, connector name, provider, then the secrets:
# CoinbaseCDP : API Key ID, API Key Secret, Wallet Secret
# StripePrivy : App ID, App Secret, Authorization Private Key, Authorization IDNon-interactive alternative (CI/scripted) — pass the secrets as flags. These land in shell history and the process list, so prefer the wizard for local setup:
# Coinbase CDP (dummy values — replace with your own)
agentcore add payment-connector --manager <ManagerName> --name <ConnectorName> --provider CoinbaseCDP \
--api-key-id 11111111-2222-3333-4444-555555555555 \
--api-key-secret cdp_sk_EXAMPLEexampleEXAMPLEexampleEXAMPLE0000 \
--wallet-secret cdp_wallet_EXAMPLEexampleEXAMPLEexample1111
# Stripe Privy (dummy values — replace with your own)
agentcore add payment-connector --manager <ManagerName> --name <ConnectorName> --provider StripePrivy \
--app-id clxxxxxxxxxxxxxxxxxxxxxxxx \
--app-secret privy_sk_EXAMPLEexampleEXAMPLEexample2222 \
--authorization-private-key MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwEXAMPLE... \
--authorization-id ezzzzzzzzzzzzzzzzzzzzzzzzWizard vs flags: The flags
--manager,--name, and--providerare marked[non-interactive]— if you provide any of them, the CLI switches to non-interactive mode and expects all required secrets as flags. Running it with those three flags but omitting the secrets errors with missing-required-flags rather than dropping back to the wizard. For the interactive wizard, run the command with no flags:agentcore add payment-connector. Then wait for the developer to confirm it’s done.
Security:
agentcore/.env.local. The bullets below apply to the Manual path.agentcore/.env.local holds the provider secrets in plaintext. The CLI writes it when the connector is added (wizard or flags) and uploads it to AgentCore Identity at agentcore deploy. Ensure it is gitignored — the Python scaffold’s default .gitignore only lists .env, so add .env.local (or .env.*). The agent must not read agentcore/.env.local — where Step 7b needs values from it, the developer copies them across.agentcore deploy -yagentcore deploy provisions the project’s resources to your AWS account: the payment manager/connector via the AgentCore control plane, and supporting IAM (the Payment<Name>ProcessPaymentRole) and any runtime via a CloudFormation stack (CDK). Coinbase QuickCreate: if you added the connector with --provision-mode QUICK_CREATE, deploy is when it is created — it comes up in PENDING_AUTHENTICATION with an authorizationUrl. The CLI does not open that URL for you today, so the agent must retrieve it (GetPaymentConnector) and open it / hand it to the developer (see Step 3b); after authorization the connector moves PENDING_AUTHENTICATION → READY (this is the developer-involved point from Step 3b). After deploy, the manager ARN, connector ID, and role ARN are written to agentcore/.cli/deployed-state.json. On CLI 0.20.x these live under targets.<target>.resources.payments[] (managerArn, connectors[].connectorId, processPaymentRoleArn); the Step 6 script reads this shape automatically.
Recovery after a SubscriptionRequiredException (Coinbase, CLI deploy). If agentcore deploy fails while creating a Coinbase connector with SubscriptionRequiredException (HTTP 403), the account is not subscribed to “Coinbase Wallets for AgentCore Payments” — pause and have the developer subscribe (see the Coinbase prerequisite in Step 3b). Important: because the CLI provisions the payment manager and connector in the same CloudFormation stack, this deploy failure rolls the stack back and deletes the PaymentManager that was being created alongside the connector. So when the developer returns and confirms the subscription is active, do NOT just retry the connector — first recheck whether the manager still exists:
aws bedrock-agentcore-control get-payment-manager \
--payment-manager-id "<PAYMENT_MANAGER_ID>" \
--region <AWS_REGION>ResourceNotFoundException): the rollback removed it — restart the flow from Step 3a: recreate the payment manager (and re-apply the mandatory agentcore:onboarding-source tag), then create the connector (Step 3b), then agentcore deploy again.If the project uses Strands or LangGraph, use the framework’s native payments integration. This is simpler than the generic tool — no process_payment_tool.py needed, no x402_fetch registration, and the middleware/plugin automatically handles ALL tool calls (not just a dedicated payment tool).
Strands:
from strands import Agent
from strands_tools import http_request
from bedrock_agentcore.payments.integrations.config import AgentCorePaymentsPluginConfig
from bedrock_agentcore.payments.integrations.strands.plugin import AgentCorePaymentsPlugin
config = AgentCorePaymentsPluginConfig(
payment_manager_arn=os.environ["PAYMENT_MANAGER_ARN"],
user_id=os.environ["PAYMENT_USER_ID"],
payment_instrument_id=os.environ["PAYMENT_INSTRUMENT_ID"],
payment_session_id=os.environ["PAYMENT_SESSION_ID"],
region=os.environ.get("AWS_REGION", "us-west-2"),
)
plugin = AgentCorePaymentsPlugin(config=config)
agent = Agent(
system_prompt="You are a helpful assistant that can access paid APIs.",
tools=[http_request],
plugins=[plugin],
)The plugin intercepts 402 responses from ANY tool, signs payment, and retries automatically. No special tool needed — the agent just uses http_request normally.
LangGraph:
from langchain.agents import create_agent
from bedrock_agentcore.payments.integrations.langgraph import (
AgentCorePaymentsConfig,
AgentCorePaymentsMiddleware,
)
# Choose ONE of the following configurations
# Option A: explicit session (production)
config = AgentCorePaymentsConfig(
...
payment_session_id=os.environ["PAYMENT_SESSION_ID"],
)
# Option B: auto-session (dev/test convenience)
config = AgentCorePaymentsConfig(
...
auto_session=True,
auto_session_budget="5.00",
auto_session_expiry_minutes=60,
)
payments = AgentCorePaymentsMiddleware(config)
agent = create_agent(
model=model,
tools=[], # middleware auto-registers http_request + payment query tools
middleware=[payments],
)The middleware wraps ALL tool calls, detects 402 from any response format (no PAYMENT_REQUIRED: marker needed), signs payment, and retries. It also auto-registers an http_request tool and payment query tools.
LangGraph simplifications vs the generic tool path:
process_payment_tool.py script needed — the middleware IS the payment toolauto_session=True can lazily create a session on first 402 (dev/test convenience — requires CreatePaymentSession IAM permission on the runtime role)on_payment_error callback for programmatic recovery (create new session, swap instrument) without the LLM seeing errorsNote on
auto_session: This creates exactly one session per middleware instance with the developer-set budget. The LLM cannot trigger or control this. In production with IAM role separation (recommended ProcessPaymentRole), theCreatePaymentSessioncall would be denied — use explicitpayment_session_idinstead. See IAM roles for AgentCore payments (opens in a new tab).
Payments are wired with a small local tool, not a framework-specific plugin — so the same code works in any framework.
Copy scripts/process_payment_tool.py into the agent project. It exposes x402_fetch(url, method="GET"), which on a 402 calls the SDK’s PaymentManager.generate_payment_header — the SDK validates the 402, selects the network, processes the payment, and builds the version-aware proof (v1 X-PAYMENT / v2 PAYMENT-SIGNATURE) — then retries with a fresh client. Base Sepolia settlement is intermittently transient (the header is valid but the paid retry still returns 402), so the tool re-runs the settle+replay flow up to X402_MAX_PAYMENT_ATTEMPTS times (default 5, env-overridable) before giving up. It reuses a single idempotency token across those retries, so ProcessPayment stays idempotent — every attempt replays the same on-chain authorization/nonce and the user is never charged twice (a retry either settles the not-yet-settled payment or, if it was already settled, reverts on-chain). It reads its config from environment variables (set in Step 8): PAYMENT_MANAGER_ARN, PAYMENT_INSTRUMENT_ID, PAYMENT_SESSION_ID, PAYMENT_USER_ID, AWS_REGION.
Register x402_fetch as a tool in the agent’s framework. The tool function is identical; only the registration decorator differs:
# Strands
from strands import Agent, tool
from process_payment_tool import x402_fetch as _x402
x402_fetch = tool(_x402)
agent = Agent(model=..., tools=[x402_fetch], system_prompt="... use x402_fetch for paid URLs ...")# LangGraph
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
from process_payment_tool import x402_fetch as _x402
graph = create_react_agent(model, tools=[tool(_x402)])# OpenAI Agents SDK
from agents import Agent, function_tool
from process_payment_tool import x402_fetch as _x402
agent = Agent(name="PaymentAgent", tools=[function_tool(_x402)], instructions="... use x402_fetch for paid URLs ...")For any other framework, register x402_fetch using that framework’s tool mechanism — the function is plain Python.
The agent calls x402_fetch instead of a generic HTTP tool; payment is handled inside the tool. (Tell the model, via the system prompt, to use x402_fetch for URLs that may require payment.)
The instrument (per-user wallet) and session (budget-bounded spend window) are data-plane resources — there is no CLI command for them. First ask the developer for the user id and email to provision the wallet for (if not already collected after Step 3b). Then run the provided script scripts/setup_payment_user.py once per user. It auto-reads the manager ARN/connector ID from deployed-state.json (or accepts --manager-arn/--connector-id):
python scripts/setup_payment_user.py --user-id alice --email alice@example.com --budget 5It creates the instrument (with the email in linkedAccounts) and a budget-bounded session, then prints the export lines for PAYMENT_INSTRUMENT_ID / PAYMENT_SESSION_ID / PAYMENT_USER_ID (used in Step 8), plus the wallet_address and redirect_url (used in Step 7). The script is the canonical data-plane path — do not hand-write the SDK calls.
LangGraph with auto_session=True: If you used Step 5a with LangGraph and set auto_session=True, you only need the instrument from this step — skip the session creation. The middleware creates a session automatically on the first 402. You still need to run setup_payment_user.py for the instrument (do NOT use the --budget flag as that will create a session).
Using the wallet_address / redirect_url the script printed:
Delegation — authorize the agent to spend from the wallet.
Coinbase CDP: the end user visits redirect_url, logs in, and grants permissions to wallet_address.
Stripe Privy: Delegation requires a frontend app where the end user authenticates with Privy and approves the agent as a session signer on their wallets. Use the reference frontend at https://github.com/privy-io/aws-agentcore-sdk (opens in a new tab). The agent automates the mechanical parts (7a, 7c, 7f); the developer handles the two steps that touch secrets or the dashboard (7b, 7d) and the browser approval (7e). No credential is requested twice — 7b reuses what Step 3b already captured:
7a. Clone and install the delegation frontend — agent runs:
git clone https://github.com/privy-io/aws-agentcore-sdk.git agentcore-privy-frontend
cd agentcore-privy-frontend
pnpm install7b. Configure environment — reuse the Step 3b credentials. The DEVELOPER runs this, not the agent.
Do not ask the developer to re-provide the Privy credentials. All three were already captured when the payment connector was created in Step 3b, and the CLI wrote them to agentcore/.env.local in the agent project.
The connector’s key names are namespaced — match on the suffix, not the full name. agentcore add payment-connector does not write bare STRIPE_PRIVY_* keys. It writes one key per secret in the form:
AGENTCORE_CREDENTIAL_<MANAGER>_<CONNECTOR>_STRIPE_PRIVY_<FIELD><MANAGER> and <CONNECTOR> are the names the developer chose in Step 3b, so the fully-qualified key names are different in every project. Never search for a hardcoded full name — match on the _STRIPE_PRIVY_<FIELD> suffix:
Frontend .env.local variable | Suffix to match in agentcore/.env.local | Visibility |
|---|---|---|
NEXT_PUBLIC_PRIVY_APP_ID | *_STRIPE_PRIVY_APP_ID | Public (client) |
PRIVY_APP_SECRET | *_STRIPE_PRIVY_APP_SECRET | Server-only |
NEXT_PUBLIC_PRIVY_SIGNER_ID | *_STRIPE_PRIVY_AUTHORIZATION_ID | Public (identifier only) |
NEXT_PUBLIC_NETWORK_MODE | not a connector credential — set testnet for Base Sepolia / Solana Devnet, mainnet for production | Public |
The frontend does not need the authorization private key — only the connector signs, so *_STRIPE_PRIVY_AUTHORIZATION_PRIVATE_KEY stays where it is.
PRIVY_APP_SECRET is a real secret and agentcore/.env.local holds it in plaintext, so the developer runs the commands below — the agent must not read that file (same rule as Step 3b).
First confirm the keys are there. This prints key names only, never a value — substitute the absolute path to the agent project:
grep -oE '^[^=]*_STRIPE_PRIVY_[A-Z_]+' /absolute/path/to/agent-project/agentcore/.env.localExpect four lines ending in _APP_ID, _APP_SECRET, _AUTHORIZATION_PRIVATE_KEY, and _AUTHORIZATION_ID. If it prints nothing, skip to the dashboard fallback below.
Then write the frontend’s .env.local. One command, absolute paths on both sides (relative paths and cd are what break here — the two projects are different directories), no values printed:
sed -nE 's/^[^=]*_STRIPE_PRIVY_APP_ID=/NEXT_PUBLIC_PRIVY_APP_ID=/p;s/^[^=]*_STRIPE_PRIVY_APP_SECRET=/PRIVY_APP_SECRET=/p;s/^[^=]*_STRIPE_PRIVY_AUTHORIZATION_ID=/NEXT_PUBLIC_PRIVY_SIGNER_ID=/p' /absolute/path/to/agent-project/agentcore/.env.local > /absolute/path/to/agentcore-privy-frontend/.env.local && printf 'NEXT_PUBLIC_NETWORK_MODE=testnet\n' >> /absolute/path/to/agentcore-privy-frontend/.env.localIt writes the file outright, so there is no need to cp .env.example .env.local first and no duplicate keys to reason about. Verify by listing the key names — again no values:
cut -d= -f1 /absolute/path/to/agentcore-privy-frontend/.env.localAll four of NEXT_PUBLIC_PRIVY_APP_ID, PRIVY_APP_SECRET, NEXT_PUBLIC_PRIVY_SIGNER_ID, NEXT_PUBLIC_NETWORK_MODE must be present. Fewer than four means a suffix didn’t match — use the fallback rather than a partial file.
Dashboard fallback. If agentcore/.env.local is missing or the suffixes don’t match — the connector was created on another machine, or the CLI’s key format changed — fill in the values by hand instead. cp .env.example .env.local in the frontend, then take the App ID and App Secret from the Privy Dashboard under Configuration > App settings, and the signer ID from Wallet infrastructure > Authorization keys. It must be the same app and same authorization key used in Step 3b.
The NEXT_PUBLIC_PRIVY_SIGNER_ID is the Authorization Key ID (looks like zr17anh9dpiqno1iaref9jpx) — the same key whose private key went to the payment connector. It is safe to expose publicly (it’s an identifier, not a secret).
Important: Taking these values straight out of
agentcore/.env.localis what guarantees the frontend uses the same Privy app and authorization key as the connector. If they are set by hand and diverge, delegation succeeds but payments fail with “Wallet policy denied the transaction.”
7c. Start the frontend — agent runs:
pnpm devThe app starts at http://localhost:3000. If port 3000 is occupied (e.g. by the agent’s own dev server), Next.js auto-selects the next available port — read the actual URL from the terminal output before the next step.
7d. Allow the local origin in the Privy Dashboard — developer does this:
Privy restricts which origins may use an App ID from the browser. Unless the local origin is allowlisted, login in Step 7e fails client-side even though every credential is correct.
In the Privy Dashboard (opens in a new tab), go to Configuration > App settings > Domains, and under Allowed origins select Web & mobile web, then add the URL the dev server printed:
http://localhost:3000Requirements (Privy matches the browser’s origin, so anything beyond scheme + host + port is rejected):
http://localhost:3000, not http://localhost:3000/.http://localhost alone does not match.Check what’s already in the field first. Privy’s default is permissive — an app with an empty allowed-origins list accepts every origin, so delegation works without this step. Adding the first entry switches the app to allowlist-only:
- Dedicated AgentCore app (what Step 3b recommends): the list is normally empty and nothing else uses the App ID, so adding
http://localhost:3000is safe. Remember to also add the deployed origin before going to production, or the hosted frontend will break.- Shared app with existing entries: append the localhost URL, don’t replace the list. Remove it again once delegation testing is done.
7e. Complete delegation — developer does this in browser:
http://localhost:3000 in a browsersetup_payment_user.py --email flag (Step 6) — Privy creates embedded wallets for this useraddSessionSigners which registers the authorization key as a session signer on all the user’s Privy embedded walletsAfter delegation succeeds, the developer can optionally fund the wallet directly from the same UI (click “Add funds” > use the Circle faucet address shown, or transfer from an external wallet).
How it works under the hood: The frontend calls Privy’s
addSessionSignersAPI with theNEXT_PUBLIC_PRIVY_SIGNER_ID. This adds the AgentCore authorization key as an approved signer on the user’s embedded wallets. When AgentCore later callsProcessPayment, it uses the corresponding private key to sign transactions — Privy’s wallet infrastructure validates that the signer is authorized and executes the transaction.
7f. Verify delegation — agent can validate:
After the developer confirms delegation is complete, the agent can verify by calling the same check-signers endpoint the frontend uses:
curl -s -X POST http://localhost:3000/api/check-signers \
-H "Content-Type: application/json" \
-d '{"walletIds": ["<wallet-id-from-step-6>"]}' | python3 -m json.toolExpected: {"connected": true}. If false, the developer needs to repeat step 7e.
Deployed alternative: For production, deploy the frontend (e.g. to Vercel: vercel --prod) and direct end users to the hosted URL. The same .env.local values go into Vercel’s environment variables settings, and the deployed origin must be added to Allowed origins the same way http://localhost:3000 was in Step 7d (https://your-app.vercel.app, no trailing slash). Privy does not allow generic preview-deployment wildcards like https://*.vercel.app, so map previews to a subdomain you control if they need to work. Each end user logs in with their own email, delegates once, and is then ready for agent-initiated payments.
Funding — fund wallet_address on the network your target merchant settles on:
MPP_EVM_NON_CANONICAL_USDC. For buyer-pays challenges (buyerPaysGasFees: true) the wallet also needs native Tempo gas. See MPP networks, providers, and funding.Set the tool’s config from the export lines the Step 6 script printed — it emits all of them (PAYMENT_MANAGER_ARN, PAYMENT_INSTRUMENT_ID, PAYMENT_SESSION_ID, PAYMENT_USER_ID, AWS_REGION), so just copy them into the agent’s environment:
export PAYMENT_MANAGER_ARN=... # all five printed by setup_payment_user.py
export PAYMENT_INSTRUMENT_ID=...
export PAYMENT_SESSION_ID=...
export PAYMENT_USER_ID=...
export AWS_REGION=...LangGraph (Step 5a with auto_session=True): You only need these env vars:
export PAYMENT_MANAGER_ARN=...
export PAYMENT_INSTRUMENT_ID=...
export PAYMENT_USER_ID=...
export AWS_REGION=...
# PAYMENT_SESSION_ID is not needed — auto_session manages it internallyRun the agent and prompt it to fetch a paid endpoint:
Fetch https://sandbox.node4all.com/v1/x402-test and tell me what you find.Run it however your agent runs — directly in your framework, or agentcore dev for a local server / agentcore invoke for the deployed runtime (set the same PAYMENT_* env vars on the runtime). A successful run shows x402_fetch hitting 402, settling payment, and the retry returning 200.
The setup and wiring above are scheme-agnostic: the agent passes through whichever x402 scheme the merchant’s 402 advertises. Most endpoints use exact (a fixed price known up front). Some use upto, for metered or usage-based pricing such as pay-per-inference — the agent authorizes a spending ceiling and the merchant settles the actual amount consumed, up to that ceiling. No configuration change is required to pay an upto endpoint.
The upto scheme has one additional prerequisite: it settles through the Uniswap Permit2 (opens in a new tab) contract, so the payer wallet must hold a Permit2 allowance for the asset. The optional permit2_allowance_limit field is an add-on for upto that grants this allowance — when set, ProcessPayment submits the one-time on-chain approve before signing. Set it on the native integration config (Step 5a):
config = AgentCorePaymentsPluginConfig( # AgentCorePaymentsConfig for LangGraph
...,
permit2_allowance_limit="1000000", # decimal string, asset's smallest unit (1000000 = 1 USDC)
)115792089237316195423570985008687907853269984665640564039457584007913129639935) grants an unlimited allowance.upto; supplying it for an exact payment returns a validation error.approve transaction — gas is paid from the wallet’s native-token balance (not its USDC balance).upto payment. approve sets the allowance rather than adding to it, so omit it on later calls to avoid a redundant transaction.generate_payment_header) accepts permit2_allowance_limit.For the generic x402_fetch tool (Step 5b), pass permit2_allowance_limit="..." to its generate_payment_header call when paying an upto endpoint.
QuickCreate: the Coinbase authorization URL shows “Invalid request” (or does nothing):
authorizationUrl is valid for only 10 minutes and is single-use — this error means it expired, was already used, or was consumed by a link preview before you clicked it.GetPaymentConnector (e.g. aws bedrock-agentcore-control get-payment-connector --payment-manager-id <id> --payment-connector-id <id> --region <AWS_REGION>) and poll until the status is terminal: READY = it already succeeded (no action); AUTHENTICATION_EXPIRED/AUTHENTICATION_FAILED = the consent window lapsed or failed — delete the connector and recreate it via QuickCreate to get a fresh URL; AWS_MARKETPLACE_SUBSCRIPTION_REQUIRED = subscribe to “Coinbase Wallets for AgentCore Payments” then recreate (on the CLI deploy path the manager may have rolled back — see Step 4 recovery); CREATING/PENDING_AUTHENTICATION/PROVISIONING = still in progress, keep polling.Agent sees 402 but does not pay:
PAYMENT_MANAGER_ARN env var is set and not Nonex402_fetch tool (not a generic http_request)x402Version + accepts fields) or the payment-required headerProcessPayment fails with “Failed to obtain resource payment token”:
GetResourcePaymentToken on the token-vault and secretsmanager:GetSecretValue on the secrets.ProcessPayment fails with “Failed to obtain workload access token”:
GetWorkloadAccessToken permission on the workload-identity-directory resources.ProcessPayment fails with “Failed to assume payment execution role”:
bedrock-agentcore.amazonaws.com with the correct aws:SourceAccount condition.ProcessPayment succeeds but merchant still returns 402:
X402_MAX_PAYMENT_ATTEMPTS times (default 5). If still 402s, raise the cap (export X402_MAX_PAYMENT_ATTEMPTS=8) or retry shortly.httpx.Client(cookies=None).request(...) — do NOT reuse the same client/session.X-PAYMENT header with a flat proof (top-level scheme/network); v2 expects a PAYMENT-SIGNATURE header where accepted is a top-level sibling of payload, and payload holds only signature + authorization (no top-level scheme/network). A v2 merchant that receives a v1 X-PAYMENT header ignores it and re-issues the same 402 — often with an empty {} body and no error, which is hard to diagnose. Read x402Version from the challenge (body or payment-required header) and build the matching proof.network must use the merchant’s human label (e.g., "base-sepolia" not "eip155:84532"). For v2, the proof keeps the CAIP-2 identifier from the challenge unchanged (e.g., "eip155:84532"). Note: the ProcessPayment input always uses CAIP-2 regardless of version — only the proof presented to the merchant differs.validBefore). If the agent loop is slow, the proof may expire before the retry.ProcessPayment succeeds (PROOF_GENERATED) but merchant returns 402 with an empty {} body and no error:
X-PAYMENT header. Detect the version from the challenge (x402Version: 2, present in the body or the payment-required response header) and send a PAYMENT-SIGNATURE header. The v2 proof puts accepted (the full requirements, CAIP-2 network) as a top-level sibling of payload, with payload containing only signature + authorization. Note: if ProcessPayment returns PROOF_GENERATED and the proof shape is correct but the merchant still 402s, it may be a transient on-chain settlement failure — retry once before assuming a format problem.MPP: ProcessPayment fails with ValidationException mentioning gas/network fees:
methodDetails.feePayer is false or absent), so AgentCore will not silently charge the buyer for network fees. Either set paymentInput.mpp.buyerPaysGasFees: true to authorize paying them from the buyer’s wallet, or obtain a challenge whose seller sponsors fees.MPP: ProcessPayment fails with ValidationException on the challenge header:
wwwAuthenticateHeaders must contain the raw WWW-Authenticate: Payment … value verbatim and exactly one entry. Do not decode, reassemble, or re-encode it — altering the bytes breaks the challenge HMAC binding. If the 402 returned several WWW-Authenticate: Payment lines, send only the single option your instrument can satisfy.paymentType is MPP and the payload is under the mpp arm of paymentInput (not cryptoX402).MPP: agent gets a fresh 402 after retrying with the credential:
Authorization: Payment <token>, using paymentOutput.mpp.paymentCredential verbatim (it already includes the Payment scheme prefix). Retry with a fresh HTTP client so cookies from the initial 402 are not resent.challenge.id is rejected. Re-run ProcessPayment against the new challenge from the fresh 402.MPP: ProcessPayment fails with SubscriptionRequiredException (403):
subscriptionUrl in the error to subscribe, then retry.MPP: ProcessPayment fails with AccessDeniedException (MPP_ACCESS_NOT_ALLOWLISTED, 403):
MPP on Tempo: ProcessPayment fails with ValidationException (MPP_EVM_NON_CANONICAL_USDC):
MPP on Tempo: ProcessPayment fails with TEMPO_INSUFFICIENT_BALANCE or a memo error:
TEMPO_INSUFFICIENT_BALANCE — the buyer wallet is underfunded for a buyer-pays charge (eth_estimateGas reverted). Top it up with pathUSD via the Tempo faucet (Step 7); the wallet also needs native Tempo gas for buyer-pays.methodDetails.memo is malformed. This is a seller-side error; do not try to fix it by editing the challenge (that breaks the HMAC binding).MPP on Tempo: ProcessPayment fails with “Tempo payments are not supported for Coinbase-managed payment instruments”:
--provider StripePrivy (Step 3b) and create the instrument on it — Coinbase-managed instruments cannot pay Tempo at all.ProcessPayment fails with “Payment session not found”:
paymentManagerArn in the session creation matches the one used in ProcessPayment.ProcessPayment fails with “PaymentSessionExpired”:
expiryTimeInMinutes.ProcessPayment fails with “Payment instrument not found” or “does not belong to user”:
userId passed to ProcessPayment matches the userId used when the instrument was created.ProcessPayment fails with “Payment connector is not active”:
ProcessPayment fails with “Network mismatch”:
network: "ETHEREUM" support Base, Base Sepolia, and Ethereum chains.network: "SOLANA" support Solana and Solana Devnet chains.ProcessPayment fails with “Payment asset not supported USDC token address”:
0x036CbD53842c5426634e7929541eC2318f3dCF7eProcessPayment fails with “Wallet does not have a USDC balance”:
Coinbase: “Delegated signing grant is not active”:
redirectUrl returned during instrument creation (Coinbase Hub).Coinbase: “Delegated signing is not enabled”:
Stripe Privy: “Privy credentials are invalid”:
Stripe Privy: “Privy appId is invalid or missing”:
appId in the credential provider configuration is incorrect.Stripe Privy: “Privy signing key is invalid or expired”:
Stripe Privy: “Wallet policy denied the transaction”:
Stripe Privy: “The linked account data is invalid”:
linkedAccounts when creating the instrument is malformed.Stripe Privy: “Rate limited by Privy”:
ProcessPayment fails with “Payment amount exceeds maximum”:
ProcessPayment fails with “Rate exceeded”:
Coinbase: “Delegation not completed”:
redirectUrl returned during instrument creation, log in, and grant permissions.Stripe Privy: “Delegation not completed”:
/api/check-signers endpoint (Step 7f).Stripe Privy: Delegation frontend setup issues:
http://localhost:3000 works but http://localhost:3000/ (trailing slash) and http://localhost (no port) do not. If the dev server moved off port 3000, the allowlisted port must move with it.NEXT_PUBLIC_PRIVY_APP_ID, PRIVY_APP_SECRET, NEXT_PUBLIC_PRIVY_SIGNER_ID) are not set in the frontend’s .env.local. Map them from the *_STRIPE_PRIVY_APP_ID / *_STRIPE_PRIVY_APP_SECRET / *_STRIPE_PRIVY_AUTHORIZATION_ID keys in agentcore/.env.local (Step 7b). Two things make a straight file copy fail: the frontend uses different key names, and the connector’s keys are namespaced AGENTCORE_CREDENTIAL_<MANAGER>_<CONNECTOR>_STRIPE_PRIVY_* — so grep the suffix, not the bare name. cut -d= -f1 on the frontend’s .env.local shows which keys actually landed.NEXT_PUBLIC_PRIVY_SIGNER_ID in the frontend doesn’t match the Authorization ID used in the payment connector (Step 3b). Re-derive it from the *_STRIPE_PRIVY_AUTHORIZATION_ID key in agentcore/.env.local rather than retyping it from the dashboard.setup_payment_user.py --email. If mismatched, the instrument points to a different Privy user’s wallets. Log out, log back in with the correct email.expiryTimeInMinutes and per-session budget controls to prevent runaway payments.bedrock-agentcore API calls, especially ProcessPayment. For production, set up a CloudWatch alarm for failed payment attempts as a potential abuse indicator.x402_fetch tool enforces HTTPS-only and blocks private IP ranges to prevent fetching internal endpoints.x402_fetch tool rejects non-HTTPS URLs.For comprehensive security guidance, see the AgentCore Security documentation (opens in a new tab).
Agent calls x402_fetch("https://paid-api.example.com/data")
│
├─ 1. HTTP GET → 402 Payment Required
│ Body: {"x402Version": 1, "accepts": [{"scheme": "exact", "network": "base-sepolia", ...}]}
│
├─ 2. Extract x402 challenge
│
├─ 3. ProcessPayment(paymentManagerArn, instrumentId, sessionId, challenge)
│ → Returns signed proof (signature + authorization)
│
├─ 4. Build payment header (X-PAYMENT for v1, PAYMENT-SIGNATURE for v2)
│
├─ 5. Retry with payment header (fresh HTTP client, no cookies)
│ → 200 OK + paid content
│
└─ 6. Return content to agentMPP is the second payment protocol AgentCore Payments speaks, alongside x402. It is a protocol-neutral, HTTP-native scheme for machine-to-machine payments (an IETF-track draft; see https://mpp.dev (opens in a new tab)). AgentCore acts on the buyer side: the agent hits a paid endpoint, receives an MPP challenge in a 402 Payment Required response, and calls ProcessPayment to mint the credential that satisfies it — the same lifecycle as x402, over a different wire format.
x402 carries its challenge in the response body (x402Version + accepts) and the proof in an X-PAYMENT (v1) or PAYMENT-SIGNATURE (v2) header. MPP uses the standard HTTP auth handshake instead:
| Primitive | Direction | HTTP header | Encoding |
|---|---|---|---|
| Challenge | server → agent (402) | WWW-Authenticate: Payment ... | RFC 9110 auth-params (id="…", realm="…", method="…", intent="…", request="<base64url>", …) |
| Credential | agent → server (retry) | Authorization: Payment <token> | base64url(JSON), no padding |
| Receipt | server → agent (200) | Payment-Receipt: <token> | base64url(JSON), no padding |
Each 402 may carry one or more WWW-Authenticate: Payment header lines — one per payment option (each a distinct method/intent). The agent picks one it can satisfy and returns exactly one Authorization: Payment header. method (tempo, evm, solana, stripe, card, …) and intent (charge, session, subscription) are open IANA registries — MPP is method- and currency-agnostic (crypto or fiat), where x402 is USDC-only. The per-method request payload rides inside the challenge as an opaque base64url blob; AgentCore parses it and mints the matching proof, so you forward the challenge verbatim rather than decoding it yourself.
Call the same ProcessPayment operation used for x402, with paymentType set to MPP and the mpp arm of paymentInput:
// ProcessPayment request (MPP)
{
"paymentManagerArn": "arn:aws:bedrock-agentcore:us-west-2:111122223333:payment-manager/pm-abc123",
"paymentSessionId": "payment-session-…",
"paymentInstrumentId": "payment-instrument-…",
"paymentType": "MPP",
"paymentInput": {
"mpp": {
"version": "1",
// The raw WWW-Authenticate: Payment header value(s) from the 402, passed verbatim.
// Exactly one entry in this release (ACP fulfills a single challenge per call).
"wwwAuthenticateHeaders": [
"Payment id=\"qB3…\", realm=\"api.example.com\", method=\"evm\", intent=\"charge\", request=\"eyJ…\""
],
// Optional. Authorizes ACP to sign when the buyer pays the blockchain (gas) fees.
"buyerPaysGasFees": false
}
}
}// ProcessPayment response (MPP) — status PROOF_GENERATED
{
"paymentType": "MPP",
"status": "PROOF_GENERATED",
"paymentOutput": {
"mpp": {
"version": "1",
// Echoes the id of the challenge that was paid, so you can correlate without decoding.
"selectedPaymentId": "qB3…",
// Ready-to-send Authorization header value: "Payment <base64url-token>".
// Attach it verbatim and retry the original request — no assembly required.
"paymentCredential": "Payment eyJ…"
}
}
}Notes grounded in the API model:
WWW-Authenticate: Payment … value(s) unchanged. AgentCore parses the auth-params itself — you do no field-mapping or base64 handling — and forwarding as-is preserves the exact bytes the challenge’s HMAC binds to.wwwAuthenticateHeaders accepts exactly one entry in this release. When a 402 offers several options, select the one the instrument can satisfy and send just that line. (It is modeled as a list so the contract can widen to multiple options later without a breaking change.)paymentCredential is the finished Authorization header. No assembly needed — attach it to the retry as Authorization: Payment <token>. It is a bearer-like secret; do not log it.buyerPaysGasFees controls fee sponsorship. A crypto challenge advertises who pays network (gas) fees via methodDetails.feePayer: true = the seller sponsors, false/absent = the buyer pays from their own wallet on top of the amount. Because that extra cost is not in the challenge amount, AgentCore will not assume the buyer accepts it — if the challenge does not offer seller-sponsored fees, it signs only when you set buyerPaysGasFees: true, otherwise it fails with ValidationException. Omit it (or false) for fee-sponsored challenges; it has no effect there.version is the MPP protocol version (a bare numeric string, e.g. "1"), distinct from the x402 version.Agent GETs https://paid-api.example.com/data
│
├─ 1. 402 Payment Required
│ WWW-Authenticate: Payment id="qB3…", realm="api.example.com", method="evm", intent="charge", request="eyJ…"
│
├─ 2. ProcessPayment(paymentType="MPP", paymentInput.mpp.wwwAuthenticateHeaders=[<that header, verbatim>])
│ → status PROOF_GENERATED, paymentOutput.mpp.paymentCredential = "Payment eyJ…"
│
├─ 3. Retry with Authorization: Payment eyJ… (fresh HTTP client, no cookies)
│ → 200 OK + paid content (optional Payment-Receipt: <token>)
│
└─ 4. Return content to agentMPP is method-agnostic, but the one crypto network AgentCore adds beyond the x402 chains is Tempo — a stablecoin chain from Stripe and Tempo, reachable only via MPP. Tempo never appears in an x402 challenge, so it is deliberately not a row in the x402 Chains table below.
| Tempo network | CAIP-2 id | Type |
|---|---|---|
| Tempo Mainnet | eip155:4217 | Mainnet |
| Tempo Moderato | eip155:42431 | Testnet |
ValidationException("Tempo payments are not supported for Coinbase-managed payment instruments.")). To pay Tempo/MPP endpoints, provision the connector with --provider StripePrivy (Step 3b) and create the instrument on it.method="evm" challenge carrying chainId=42431 maps to Moderato). Provision once with a StripePrivy connector; the agent then pays x402 and Tempo/MPP merchants through the same manager/instrument/session.MPP_EVM_NON_CANONICAL_USDC otherwise). On Moderato that canonical token is pathUSD; fund the wallet with pathUSD from the Tempo faucet (https://tempo.xyz/developers/docs/quickstart/faucet (opens in a new tab)) — see Step 7.AccessDeniedException(MPP_ACCESS_NOT_ALLOWLISTED). Request access via AWS Support (see Debugging).Two concepts: network (blockchain family, used when creating instruments) and chain (specific chain, used in x402 challenges and balance queries).
Networks (for instrument creation):
| Network | Instrument Value | Providers |
|---|---|---|
| Ethereum (includes Base, Base Sepolia) | ETHEREUM | Coinbase, Stripe |
| Solana (includes Solana Devnet) | SOLANA | Coinbase, Stripe |
Chains (in x402 challenges and balance queries):
| Chain | Identifier (x402) | Balance API value | Type | Provider |
|---|---|---|---|---|
| Base Sepolia | base-sepolia or eip155:84532 | BASE_SEPOLIA | Testnet | Coinbase |
| Base | eip155:8453 | BASE | Mainnet | Coinbase |
| Ethereum Mainnet | eip155:1 | ETHEREUM | Mainnet | Coinbase, Stripe |
| Solana Mainnet | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp | SOLANA | Mainnet | Coinbase, Stripe |
| Solana Devnet | solana-devnet | SOLANA_DEVNET | Testnet | Stripe |
For testing, start with Base Sepolia (network: ETHEREUM, chain: BASE_SEPOLIA) — free testnet tokens from https://faucet.circle.com/ (opens in a new tab).
These are the x402 chains. The table above lists chains that appear in x402 challenges. Tempo (
eip155:4217mainnet,eip155:42431Moderato testnet) is reachable only via MPP, is StripePrivy-only, and settles in the network’s canonical stablecoin (pathUSD on Moderato). It is documented separately in MPP networks, providers, and funding.
npm install -g @aws/agentcore, not pip--provision-mode QUICK_CREATE, no secrets) is offered first; manual secret entry is the alternative and the only path for Stripe (Privy). Only the connector step involves the developer — QuickCreate: browser authorization; Manual: entering secretsagentcore:onboarding-source: agent-toolkit-skill tag is added to agentcore/agentcore.json (Step 3a) before deploy — this is mandatory, so the provisioned resources are attributable to this skillx402_fetch tool (Step 5b)ProcessPayment API — no protocol is chosen up frontWWW-Authenticate: Payment header is forwarded verbatim (one per call) and the returned paymentCredential is attached as Authorization: Payment <token> unchangedNearby