Workshop Duration: 10 minutes
This tutorial walks through the Ampersend Python SDK for integrating x402 payment capabilities into A2A (Agent-to-Agent) applications. The x402 protocol extends HTTP with a standard way for servers to request payment before providing resources, using the 402 "Payment Required" status code. By the end of this tutorial, you'll understand how to build both buyer (client) and seller (server) agents that can exchange micropayments for AI services.
- Overview
- Core Concepts
- Wallets
- Treasurers
- Client Integration (Buyer)
- Server Integration (Seller)
- Complete Example
The Ampersend SDK enables x402 micropayments between AI agents using blockchain technology. This allows AI agents to autonomously pay for services from other agents without human intervention, enabling a new economy of machine-to-machine transactions. Payments are made in USDC stablecoin on Base (an Ethereum L2), providing fast, low-cost transactions with real USD value.
The SDK provides:
-
Wallets – Sign payment authorizations using cryptographic keys. Wallets hold the private keys needed to authorize spending from your blockchain account, supporting both traditional Ethereum accounts (EOA) and advanced smart contract accounts with programmable spending rules.
-
Treasurers – Decide whether to approve or reject payment requests based on your policies. The treasurer acts as a financial gatekeeper, evaluating each payment request against budgets, spend limits, or other business logic before authorizing the wallet to sign.
-
Client Middleware – Automatically intercept and handle 402 responses during A2A communication. When your buyer agent receives a payment request from a seller, the middleware seamlessly coordinates with your treasurer and wallet to create and submit payments without manual intervention.
-
Server Middleware – Require payment before executing agent operations on the seller side. This middleware wraps your agent's execution logic to enforce payment collection, verify payments on-chain, and only proceed with the request once payment is confirmed.
┌─────────────────┐ ┌─────────────────┐
│ Buyer Agent │ │ Seller Agent │
│ │ │ │
│ ┌───────────┐ │ A2A │ ┌───────────┐ │
│ │ Treasurer │◄─┼─────────┼─►│ Executor │ │
│ └─────┬─────┘ │ │ └───────────┘ │
│ │ │ │ │
│ ┌─────▼─────┐ │ │ │
│ │ Wallet │ │ │ │
│ └───────────┘ │ │ │
└─────────────────┘ └─────────────────┘
The diagram above shows the separation of concerns: the Treasurer makes policy decisions about whether to pay, while the Wallet handles the cryptographic signing. This separation allows you to implement sophisticated payment policies without touching cryptographic code.
The X402Wallet protocol defines the interface for creating signed payment payloads from payment requirements. A payment payload contains all the cryptographic data needed for a seller to verify and settle the payment on the blockchain. Different wallet implementations support different account types, but they all produce the same standard payment payload format.
from ampersend_sdk.x402 import X402Wallet
class X402Wallet(Protocol):
def create_payment(
self,
requirements: PaymentRequirements,
) -> PaymentPayload:
"""Create a signed payment from requirements."""
...The PaymentRequirements parameter contains details like the payment amount, recipient address, asset type (USDC), and network. The wallet uses this information to construct an ERC-3009 authorization (a gasless approval) that allows the facilitator to transfer funds on behalf of the payer.
The X402Treasurer protocol separates payment decisions from payment creation, implementing the Strategy pattern for payment authorization. This design allows you to swap out different payment policies (naive, budget-limited, API-controlled) without changing the rest of your code. The treasurer receives the full context of what's being purchased, enabling intelligent decisions about when to pay.
from ampersend_sdk.x402 import X402Treasurer, X402Authorization
class X402Treasurer(ABC):
@abstractmethod
async def onPaymentRequired(
self,
payment_required: x402PaymentRequiredResponse,
context: Dict[str, Any] | None = None,
) -> X402Authorization | None:
"""Authorize or reject a payment. Return None to reject."""
...
@abstractmethod
async def onStatus(
self,
status: PaymentStatus,
authorization: X402Authorization,
context: Dict[str, Any] | None = None,
) -> None:
"""Handle payment status updates (accepted/rejected/error)."""
...The onPaymentRequired method is called when a seller requests payment, giving you the opportunity to inspect the requirements and decide whether to proceed. The onStatus method provides lifecycle notifications so you can track payment outcomes for logging, analytics, or reconciliation purposes.
When a treasurer approves a payment, it returns an X402Authorization named tuple that bundles the signed payment with a tracking identifier. The authorization ID enables correlation between the initial payment decision and subsequent status updates. This is essential for audit trails and debugging payment flows.
from ampersend_sdk.x402 import X402Authorization
authorization = X402Authorization(
payment=payment_payload, # Signed payment data
authorization_id="unique-id" # For tracking status
)The payment payload is a JSON-serializable structure containing the ERC-3009 signature and metadata. The authorization ID should be unique per payment attempt—using uuid.uuid4().hex is a common pattern.
The AccountWallet is designed for externally owned accounts (EOAs), which are standard Ethereum wallets controlled by a single private key. This is the simplest wallet type and is ideal for development, testing, or scenarios where a single key controls all spending. EOA wallets sign payments directly using the private key without any additional smart contract logic.
from eth_account import Account
from ampersend_sdk.x402.wallets.account import AccountWallet
# Create from private key
account = Account.from_key("0x...")
wallet = AccountWallet(account=account)
# Create payment
payment = wallet.create_payment(requirements)The eth_account library handles the low-level cryptographic operations, while AccountWallet adapts it to the x402 payment format. When you call create_payment(), the wallet constructs an ERC-3009 receiveWithAuthorization message and signs it with your private key.
The SmartAccountWallet supports Safe smart accounts with session keys, enabling advanced features like programmable spending limits and multi-signature requirements. Smart accounts are smart contracts that can validate signatures according to custom logic, using the ERC-1271 standard. This wallet type is recommended for production deployments where you need on-chain spending controls.
from ampersend_sdk.smart_account import SmartAccountConfig
from ampersend_sdk.x402.wallets.smart_account import SmartAccountWallet
config = SmartAccountConfig(
session_key="0x...", # Your session key private key
smart_account_address="0x...", # Smart account address
validator_address="0x..." # Optional: OwnableValidator
)
wallet = SmartAccountWallet(config=config)
# Create payment (uses ERC-1271 signatures)
payment = wallet.create_payment(requirements)The session key is a subordinate key that's been granted limited signing authority by the smart account. Unlike a master key, session keys can be revoked and typically have time or amount limits. The validator address specifies which module verifies the signature—the OwnableValidator is the default for Safe accounts.
Smart accounts support:
-
Spend limits enforced on-chain – The blockchain itself enforces maximum spending amounts, providing trustless protection against bugs or compromised keys. Even if your agent software is hacked, on-chain limits prevent excessive spending.
-
Session keys with limited permissions – Delegate signing authority to temporary keys that can be easily rotated or revoked. Session keys can have time-based expiration, ensuring that even leaked keys become useless after a set period.
-
Multi-sig requirements – Require multiple parties to approve large transactions, adding a human-in-the-loop for high-value payments. This is useful for team-managed agents where no single person should have unilateral control.
-
Social recovery – Recover account access through trusted guardians if keys are lost. Unlike EOAs where losing your private key means losing everything, smart accounts can be recovered through a predefined social process.
The NaiveTreasurer auto-approves all payment requests without any checks, making it ideal for testing and development scenarios. It immediately creates a payment for any valid request, which is useful when you trust the seller and want to focus on testing other parts of your integration. Never use this in production with real funds unless you fully trust all possible sellers.
from ampersend_sdk.x402.treasurers import NaiveTreasurer
treasurer = NaiveTreasurer(wallet=wallet)
# Automatically approves all payments
authorization = await treasurer.onPaymentRequired(payment_required)The naive treasurer simply takes the first payment requirement from the list and creates a payment for it. It doesn't implement any budget tracking, rate limiting, or approval logic—it's the simplest possible treasurer implementation.
The AmpersendTreasurer integrates with the Ampersend API to provide enterprise-grade payment controls and monitoring. Before approving any payment, it checks with the Ampersend backend to verify the payment falls within configured spend limits. This provides a centralized dashboard for managing agent spending across your organization.
Integrates with Ampersend API for:
-
Spend limits (daily/monthly) – Configure maximum spending amounts that reset on a daily or monthly basis. The API tracks cumulative spending and rejects payments that would exceed limits, providing budget guardrails for autonomous agents.
-
Payment monitoring – Every payment decision is logged and visible in the Ampersend dashboard. This gives you real-time visibility into what your agents are spending money on, with detailed breakdowns by seller, time period, and payment type.
-
Budget controls – Define different budgets for different agents or use cases, with alerts when spending approaches limits. You can also set up approval workflows for payments above certain thresholds.
from ampersend_sdk.ampersend import (
AmpersendTreasurer,
ApiClient,
ApiClientOptions,
)
# Create API client (handles SIWE auth automatically)
api_client = ApiClient(
options=ApiClientOptions(
base_url="https://api.staging.ampersend.ai",
session_key_private_key="0x...", # For SIWE authentication
)
)
# Create treasurer with spend limit integration
treasurer = AmpersendTreasurer(
api_client=api_client,
wallet=wallet,
)
# Now payments are:
# 1. Authorized against API spend limits
# 2. Tracked and monitored
# 3. Reported for audit trailThe ApiClient handles authentication using Sign-In with Ethereum (SIWE), a standard protocol for proving wallet ownership. Once authenticated, the client can request payment authorizations and report payment events back to the API for tracking.
Creating a custom treasurer lets you implement any payment logic specific to your use case. Below is a budget-tracking treasurer that enforces a daily spending limit locally, without requiring an external API. This pattern is useful for simple scenarios or when you want to keep payment logic entirely within your codebase.
class BudgetTreasurer(X402Treasurer):
def __init__(self, wallet: X402Wallet, daily_limit_usd: float):
self._wallet = wallet
self._daily_limit = daily_limit_usd
self._spent_today = 0.0
async def onPaymentRequired(
self,
payment_required: x402PaymentRequiredResponse,
context: Dict[str, Any] | None = None,
) -> X402Authorization | None:
# Get first requirement
req = payment_required.accepts[0]
amount_usd = int(req.maxAmountRequired) / 1_000_000 # USDC has 6 decimals
# Check budget
if self._spent_today + amount_usd > self._daily_limit:
print(f"Budget exceeded: ${self._spent_today:.2f} + ${amount_usd:.2f} > ${self._daily_limit:.2f}")
return None # Reject
# Approve and create payment
payment = self._wallet.create_payment(req)
self._spent_today += amount_usd
return X402Authorization(
payment=payment,
authorization_id=uuid.uuid4().hex,
)
async def onStatus(
self,
status: PaymentStatus,
authorization: X402Authorization,
context: Dict[str, Any] | None = None,
) -> None:
print(f"Payment {authorization.authorization_id}: {status}")This example demonstrates the key concepts: extracting the payment amount from requirements, checking against a limit, and returning None to reject or an X402Authorization to approve. In production, you might persist the _spent_today value to a database and implement proper daily reset logic.
The X402RemoteA2aAgent extends Google's ADK RemoteA2aAgent to add transparent x402 payment handling. When the remote seller agent responds with a 402 Payment Required status, this agent automatically coordinates with your treasurer to decide on payment and, if approved, retries the request with the payment attached. This means your application code can call the agent like normal, without worrying about payment flow.
from ampersend_sdk.a2a.client import X402RemoteA2aAgent
# Create agent with payment support
agent = X402RemoteA2aAgent(
treasurer=treasurer, # Your treasurer
name="buyer_agent",
agent_card="https://seller.com/.well-known/agent.json",
)
# Call the agent - payments are handled automatically!
result = await agent.run("What is the weather in NYC?")The agent_card parameter points to the seller's A2A agent card, a JSON document that describes the agent's capabilities and endpoint URLs. The SDK fetches this card to discover where to send requests. Your treasurer will be consulted for every payment request, giving you full control over spending.
Understanding the payment flow helps with debugging and designing your payment policies. The flow is designed to be invisible to application code while providing hooks for treasurers at key decision points.
-
Client sends request to seller – Your buyer agent makes a normal A2A request, like asking the seller agent a question. At this point, no payment information is attached.
-
Seller returns
402 Payment Requiredwith requirements – The seller's x402 middleware intercepts the request and responds with payment requirements before executing the agent. Requirements specify the price, accepted assets (USDC), network (Base), and recipient address. -
Client calls
treasurer.onPaymentRequired()– The client middleware extracts the payment requirements and presents them to your treasurer for a decision. The treasurer can inspect the amount, seller, and context before deciding. -
If approved, payment is attached to retry request – The treasurer creates the payment through the wallet, and the middleware automatically retries the original request with the payment in the
x402/paymentheader. -
Seller verifies payment and executes – The seller's middleware validates the payment signature and either settles it on-chain or verifies it will settle. Only after successful verification does the agent execute.
-
Client calls
treasurer.onStatus()with result – Whether the payment was accepted, rejected, or errored, the treasurer receives a status update. This enables logging, budget tracking adjustments, or retry logic.
The easiest way to add payment requirements to your seller agent is through the before_agent_callback hook. This callback runs before every agent execution and can interrupt the flow to request payment. The make_x402_before_agent_callback helper creates a properly configured callback that specifies your payment requirements.
from ampersend_sdk.a2a.server import make_x402_before_agent_callback
from ampersend_sdk.a2a.server.to_a2a import to_a2a
from google.adk import Agent
root_agent = Agent(
name="facts_agent",
before_agent_callback=make_x402_before_agent_callback(
price="$0.001", # Price per request
network="base-sepolia", # Blockchain network
pay_to_address="0x...", # Your wallet address
),
model="gemini-2.5-flash-lite",
description="Agent that provides interesting facts.",
instruction="You are a helpful agent.",
)
# Convert to A2A protocol
a2a_app = to_a2a(root_agent, port=8001)The price parameter accepts human-readable amounts like "$0.001" which get converted to the correct number of USDC base units (1000, since USDC has 6 decimals). The network should match where your wallet is—use "base-sepolia" for testnet or "base" for mainnet. Payments are sent to pay_to_address, which should be a wallet you control.
For more granular control over payment handling, you can use the X402ServerExecutor directly. This gives you access to the full execution lifecycle and allows custom settlement logic. The executor wraps your existing agent executor and intercepts requests that require payment.
from ampersend_sdk.a2a.server import X402ServerExecutor
executor = X402ServerExecutor(
delegate=your_agent_executor,
config=x402ExtensionConfig(
requirements=[payment_requirements],
settle_callback=your_settle_function,
),
)The delegate is your original agent executor that handles the actual work after payment is confirmed. The settle_callback is called with the payment payload when a client submits payment—you can use this to verify and settle the payment on-chain using a facilitator service, or implement custom settlement logic.
This complete example shows a production-ready buyer agent using smart accounts and the Ampersend API for spend limits. The code is structured to read configuration from environment variables, making it easy to deploy across different environments.
import os
from eth_account import Account
from ampersend_sdk.a2a.client import X402RemoteA2aAgent
from ampersend_sdk.ampersend import (
AmpersendTreasurer,
ApiClient,
ApiClientOptions,
)
from ampersend_sdk.smart_account import SmartAccountConfig
from ampersend_sdk.x402.wallets.smart_account import SmartAccountWallet
# Configuration
SMART_ACCOUNT_ADDRESS = os.environ["SMART_ACCOUNT_ADDRESS"]
SESSION_KEY = os.environ["SESSION_KEY_PRIVATE_KEY"]
AMPERSEND_API_URL = os.environ.get(
"AMPERSEND_API_URL",
"https://api.staging.ampersend.ai"
)
# 1. Create wallet
wallet = SmartAccountWallet(
config=SmartAccountConfig(
session_key=SESSION_KEY,
smart_account_address=SMART_ACCOUNT_ADDRESS,
)
)
# 2. Create treasurer with Ampersend integration
treasurer = AmpersendTreasurer(
api_client=ApiClient(
options=ApiClientOptions(
base_url=AMPERSEND_API_URL,
session_key_private_key=SESSION_KEY,
)
),
wallet=wallet,
)
# 3. Create buyer agent
root_agent = X402RemoteA2aAgent(
treasurer=treasurer,
name="buyer_agent",
agent_card="https://seller-agent.example.com/.well-known/agent.json",
)
# 4. Use with ADK
# adk run buyer.pyThis setup provides robust spending controls: the smart account enforces on-chain limits, while the Ampersend API provides off-chain budget management and monitoring. Both layers work together to prevent unauthorized spending.
The seller agent is simpler because it just needs to specify what it charges, not manage any spending logic. This example creates an agent that uses Google Search and charges a small fee per request.
import os
from ampersend_sdk.a2a.server import make_x402_before_agent_callback
from ampersend_sdk.a2a.server.to_a2a import to_a2a
from google.adk import Agent
from google.adk.tools import google_search
root_agent = Agent(
name="facts_agent",
before_agent_callback=make_x402_before_agent_callback(
price="$0.001",
network="base-sepolia",
pay_to_address=os.environ["PAY_TO_ADDRESS"],
),
model="gemini-2.5-flash-lite",
description="Agent that provides interesting facts.",
instruction="You are a helpful agent who provides facts.",
tools=[google_search],
)
a2a_app = to_a2a(root_agent, port=8001)
# Run with: uvicorn seller:a2a_app --port 8001The to_a2a helper wraps your ADK agent in a FastAPI application that speaks the A2A protocol with x402 extensions. This is a complete server that you can deploy to receive payments for your agent's services.
pip install ampersend-sdk
# or
uv add ampersend-sdkThe SDK requires Python 3.10+ and has dependencies on eth-account for Ethereum operations and httpx for async HTTP. Using uv for installation is recommended for faster dependency resolution.
| Variable | Description |
|---|---|
SMART_ACCOUNT_ADDRESS |
Your smart account address (0x...). This is the address that holds funds and authorizes payments. Create one at app.ampersend.ai. |
SESSION_KEY_PRIVATE_KEY |
Session key private key for signing (0x...). This subordinate key is granted limited signing authority by the smart account. |
AMPERSEND_API_URL |
Ampersend API base URL. Defaults to staging (https://api.staging.ampersend.ai). Use https://api.ampersend.ai for production. |
PRIVATE_KEY |
EOA private key (0x...) for simple setups without smart accounts. Only use this for development or if you don't need smart account features. |
PAY_TO_ADDRESS |
Seller's payment recipient address (0x...). Payments from buyers are sent to this address. |
ampersend_sdk/
├── ampersend/ # Ampersend API integration
│ ├── client.py # ApiClient (SIWE auth, authorize, report)
│ ├── treasurer.py # AmpersendTreasurer
│ └── types.py # API types
├── x402/ # Core x402 protocol
│ ├── treasurer.py # X402Treasurer, X402Authorization
│ ├── wallet.py # X402Wallet protocol
│ ├── treasurers/ # Treasurer implementations
│ │ └── naive.py # NaiveTreasurer
│ └── wallets/ # Wallet implementations
│ ├── account/ # AccountWallet (EOA)
│ └── smart_account/ # SmartAccountWallet
├── smart_account/ # Smart account utilities
│ └── sign.py # ERC-1271 signing
└── a2a/ # A2A protocol integration
├── client/ # Buyer side
│ ├── x402_remote_a2a_agent.py
│ └── x402_middleware.py
└── server/ # Seller side
├── x402_server_executor.py
└── to_a2a.py
The package is organized by domain: ampersend/ for API integration, x402/ for core protocol abstractions, smart_account/ for ERC-1271 signing utilities, and a2a/ for A2A protocol-specific code.
-
Get testnet USDC: Visit https://faucet.circle.com/ and select Base Sepolia to receive free test USDC. You'll need to connect a wallet that holds testnet ETH for gas. This USDC has no real value but lets you test the full payment flow.
-
Create an agent: Sign up at https://app.staging.ampersend.ai to create a smart account with spend limits. The dashboard will generate a session key for you and let you configure daily/monthly budgets. Fund your smart account with the testnet USDC from step 1.
-
Run the examples: Execute
uv --directory=examples run -- adk run src/examples/a2a/buyer/adkto start a buyer agent. Make sure to set the required environment variables first. The examples directory includes both buyer and seller agents you can run locally.
Questions? Check the x402 specification for protocol details and the official reference implementation.