MCP tools calling paid APIs hit HTTP 402 Payment Required and have no way to recover. Google's Agent Payments Protocol (AP2) gives your agent a machine-readable payment credential so it can pay and retry without a human in the loop.
Your MCP Tool Hit a 402. Here’s How to Make It Pay and Retry.
Your MCP server queries a paid on-chain data API. The API returns 402 Payment Required. Your agent stops. The task is dead. The human gets a notification asking for a credit card. At that point the agent is not an agent. It is a form with extra steps.
Every MCP server that wraps a paid endpoint hits this wall. The agent has no wallet and no payment credential. It either errors out or interrupts a human. Both paths break the autonomy that made the agent useful in the first place.
Google’s Agent Payments Protocol (AP2) fixes this. It gives your agent a cryptographically signed payment credential called an IntentMandate — a JWT that encodes a spend cap, an expiry, and a list of approved merchants. Your MCP tool wraps it in a catch-402-retry loop. The 402 becomes invisible. The agent recovers and settles USDC on-chain. The human never knows anything went wrong.
The pattern is five lines of Python. The reference implementation is at ap2-402-demo. Here is the whole thing.
The Core Pattern
Three steps. Every time.
- Call the API with an
intent_id - If 402 hits, grant a new payment credential
- Retry the call with the new credential
Paste this into your MCP handler:
from payment import PaymentGateway, PaymentRequired
gateway = PaymentGateway()
def mcp_paid_tool(label: str, intent_id: str, budget: float = 1.00) -> dict:
try:
return gateway.charge(intent_id, 0.50, label)
except PaymentRequired:
new = gateway.grant_intent(max_total_usd=budget)
return gateway.charge(new["intent_id"], 0.50, label)
Five lines. The agent never sees the 402. The tool absorbs it and recovers.
The Credential: IntentMandate
When grant_intent() runs, it creates an ES256-signed JWT that encodes four constraints. The server re-verifies this JWT on every charge() call against all four.
- Spend cap (
max_total_usd): maximum the agent can spend on this credential - Category: restricts what the credential pays for (e.g.
data-query) - Expiry: absolute TTL after which the credential is invalid
- Merchant: who the agent is paying
The intent_id is the agent’s payment ticket. It cannot be reused after expiry or after the budget runs out. The credential is checked on every request — signature, expiry, category, cap — so there is no way to slip a stale or tampered credential through.
intent = gateway.grant_intent(max_total_usd=1.50, ttl_s=3600)
# Returns:
# {
# "intent_id": "intent_a1b2c3d4e5f6",
# "cap_usd": 1.50,
# "category": "data-query",
# "rail": "x402",
# "status": "active"
# }
The Four 402 Gates
payment.py enforces four conditions before allowing a charge. When any check fails, it raises PaymentRequired. The recovery action depends on which gate failed.
| 402 Message | Meaning | Agent Action |
|---|---|---|
no intent |
No payment credential issued yet | Call grant_intent() |
signature invalid |
Credential tampered or forged | Discard, call grant_intent() |
intent expired |
Credential TTL elapsed | Call grant_intent() |
cap reached |
Budget fully spent | Top up with fresh intent or stop |
A blind retry-on-any-402 works for simple tools. Production MCPs should parse the 402 message and decide per case. When I wired this into a Bitquery MCP tool, the distinction between “cap reached” and “expired” was the difference between silently topping up a user’s budget vs. exposing the error to the orchestrator. You want the first. You do not want the second unless the user asked for it.
def recover_from_402(label: str, intent_id: str, error: PaymentRequired,
budget: float = 5.00) -> dict:
msg = str(error)
if "cap reached" in msg:
new = gateway.grant_intent(max_total_usd=budget)
return gateway.charge(new["intent_id"], 0.50, label)
if "expired" in msg or "no intent" in msg or "signature" in msg:
new = gateway.grant_intent(max_total_usd=budget)
return gateway.charge(new["intent_id"], 0.50, label)
raise RuntimeError(f"Unrecoverable 402: {msg}") from error
Shared Intent Across Multiple Tools
If your MCP exposes several paid tools, grant a single intent and share the intent_id. The cap is enforced globally. Every tool draws from the same budget.
intent_id = gateway.grant_intent(max_total_usd=3.00)["intent_id"]
gateway.charge(intent_id, 0.50, "pubmed: AI + healthcare")
gateway.charge(intent_id, 0.50, "arxiv: LLM + diagnosis")
gateway.charge(intent_id, 0.75, "sentiment: healthcare")
gateway.charge(intent_id, 0.75, "trends: AI + medicine")
gateway.charge(intent_id, 0.50, "summarize: findings")
This works for multi-step workflows where each step hits a different paid endpoint. The user authorizes one budget. Every step draws from it.
What Happens Under the Hood
When charge() passes all four gates, it mints the full AP2 mandate chain:
- IntentMandate: granted once, JWT signed by the human
- CartMandate: built per call, a W3C PaymentRequest signed by the merchant
- PaymentMandate: binds cart and payment hashes, signed by the network
- Settlement: USDC on Base (mock by default; set
X402_LIVE=1for real)
All three mandates are cryptographically chained. Each references the hash of its predecessor. The full payment trail is verifiable.
Adapting for Your MCP
The PaymentGateway class in the demo repo is a self-contained reference. Four things to change before production:
Swap the key material. Load real keys from environment variables or a KMS. The demo generates ephemeral JWKs that should never touch production.
Adjust the category. Change "category": "data-query" to match your API’s category.
Replace the settlement rail. x402.py uses mock USDC on Base. Swap for Stripe, Lightning, or your billing system. The only contract is that it returns { "settled": True, "tx_hash": "..." }.
Make it thread-safe. PaymentGateway uses threading.Lock(). For asyncio MCPs, switch to asyncio.Lock or wrap calls in run_in_executor.
Which Strategy to Pick
| Strategy | When | Lines |
|---|---|---|
| Catch-all retry | Simple tools, fixed-price calls | ~5 |
| Parse-and-decide | Multi-price tools, variable budgets | ~15 |
| Budget-aware | Expensive APIs, user-set limits | ~25 |
| Shared intent | Multi-tool workflows | ~10 |
All four are implemented in the demo repo. Pick the one that matches your MCP’s pricing model.
The full reference with an interactive walkthrough is at ap2-402-demo. Run python3 demo.py and pick scenario 6 for the full walkthrough. Each scenario shows what an agent experiences when it hits a specific 402 case and how AP2 resolves it.