Skip to main content

Manual x402 client flow (Python)

This page shows how to perform the entire x402 flow by hand in Python: no starter kit, no x402.http.clients wrappers. You send raw HTTP requests, decode the 402 response, build the payment payload, and resend with the payment signature. For production youโ€™ll usually use the httpx or requests quickstarts; this tutorial is for agents or environments that need a minimal, dependency-light implementation or want to understand the protocol step by step.

1. Request the resource

Send a normal GET (or other method) to the protected URL. No special headers yet.
import requests

url = "http://localhost:4021/weather"
response = requests.get(url)

2. Handle 402 and decode PAYMENT-REQUIRED

If the server requires payment, it returns 402 Payment Required and puts the payment options in the PAYMENT-REQUIRED header (base64-encoded JSON).
import base64
import json

if response.status_code != 402:
    # 200: already paid or free; 4xx/5xx: handle as needed
    print(response.json())

payment_required_b64 = response.headers.get("PAYMENT-REQUIRED")
if not payment_required_b64:
    raise ValueError("402 without PAYMENT-REQUIRED header")

payment_required = json.loads(
    base64.b64decode(payment_required_b64).decode("utf-8")
)
# payment_required has: x402Version, error, resource, accepts, extensions
# accepts is a list of payment options (scheme, network, amount, asset, payTo, etc.)
Exact field names and shapes are in the x402 Reference. You must use x402Version: 2 and the accepts array.

3. Choose an accepted option

Pick one entry from payment_required["accepts"] by network or scheme. For EVM (steps below), take an option whose network starts with eip155:. For Solana, take one whose network starts with solana: (see Solana (exact scheme) in step 4).
accepts = payment_required["accepts"]
# EVM path:
accepted = next((a for a in accepts if a["network"].startswith("eip155:")), None)
if not accepted:
    raise ValueError("No EVM accept option")

4. Build the payment payload

For the exact scheme on EVM, the client must produce an EIP-3009-style authorization and sign it with EIP-712 (see x402 Reference). The payload is sent in the PAYMENT-SIGNATURE header as base64-encoded JSON. You need a signer (e.g. eth_account or web3) to produce the signature and authorization fields. Example shape (simplified; real code must use correct domain, types, and signing):
import os
import time

# You need: payer private key, accepted requirement, token contract (asset),
# valid_after, valid_before, nonce, and EIP-712 signing.
payload = {
    "x402Version": 2,
    "scheme": "exact",
    "network": accepted["network"],
    "accepted": {
        "scheme": accepted["scheme"],
        "network": accepted["network"],
        "amount": accepted["amount"],
        "asset": accepted["asset"],
        "payTo": accepted["payTo"],
        "maxTimeoutSeconds": accepted["maxTimeoutSeconds"],
        **({"extra": accepted["extra"]} if accepted.get("extra") else {}),
    },
    "payload": {
        "signature": "0x...",  # EIP-712 signature
        "authorization": {
            "from": payer_address,  # Replace with your wallet address
            "to": accepted["payTo"],
            "value": accepted["amount"],
            "validAfter": "0",
            "validBefore": str(int(time.time()) + 300),
            "nonce": "0x" + os.urandom(32).hex(),
        },
    },
    "extensions": {},
}
payment_signature_b64 = base64.b64encode(json.dumps(payload).encode()).decode()

Solana (exact scheme)

On Solana, you choose a Solana accept option (e.g. network starting with solana:), then build a partially-signed transaction and send it in the payload as base64. The transaction must contain these instructions in this order:
  1. SetComputeUnitLimit โ€” max 40,000 compute units
  2. SetComputeUnitPrice โ€” max 5 microlamports per compute unit
  3. TransferChecked โ€” token transfer (amount, mint, decimals, source, destination)
  4. Optional: Lighthouse (wallets like Phantom/Solflare may add up to two; merchants do not add these)
You need a Solana SDK (e.g. solders or solana-py) to build and sign the transaction. The payment payload is JSON with the transaction in payload.transaction as base64:
# 3. Choose Solana option instead of EVM
accepted = next((a for a in accepts if a["network"].startswith("solana:")), None)
if not accepted:
    raise ValueError("No Solana accept option")

# 4. Build Solana transaction (pseudocode โ€” use solders/solana-py in practice)
# - Add SetComputeUnitLimit(40_000)
# - Add SetComputeUnitPrice(5)   # microlamports
# - Add TransferChecked: amount=accepted["amount"], mint=accepted["asset"],
#   decimals from token metadata, source=payer_token_account, destination=payee_token_account
# - Sign with payer keypair (partial sign; facilitator may add Lighthouse instructions)
# - Serialize transaction to bytes, then base64

# transaction_bytes = serialized_signed_tx  # from your Solana library
transaction_b64 = base64.b64encode(transaction_bytes).decode()

payload = {
    "x402Version": 2,
    "scheme": "exact",
    "network": accepted["network"],
    "accepted": {
        "scheme": accepted["scheme"],
        "network": accepted["network"],
        "amount": accepted["amount"],
        "asset": accepted["asset"],
        "payTo": accepted["payTo"],
        "maxTimeoutSeconds": accepted["maxTimeoutSeconds"],
        **({"extra": accepted["extra"]} if accepted.get("extra") else {}),
    },
    "payload": {
        "transaction": transaction_b64,
    },
    "extensions": {},
}
payment_signature_b64 = base64.b64encode(json.dumps(payload).encode()).decode()
Exact instruction formats, token-account derivation, and limits are in the x402 Reference (ยง6.2 Solana).

5. Resend the request with PAYMENT-SIGNATURE

Send the same request again (same URL and method), this time adding the PAYMENT-SIGNATURE header.
retry_response = requests.get(
    url,
    headers={"PAYMENT-SIGNATURE": payment_signature_b64},
)

6. Parse the response and PAYMENT-RESPONSE

On success the server returns 200 and may include PAYMENT-RESPONSE (base64-encoded settlement details). Decode the header and parse the JSON to get settlement info.
retry_response.raise_for_status()
body = retry_response.json()

payment_response_b64 = retry_response.headers.get("PAYMENT-RESPONSE")
if payment_response_b64:
    payment_response = json.loads(
        base64.b64decode(payment_response_b64).decode("utf-8")
    )
    # success, transaction, network, payer
    print("Settlement:", payment_response)

print(body)
Decoded PAYMENT-RESPONSE examples On success (EVM):
{
  "success": true,
  "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
  "network": "eip155:84532",
  "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66"
}
On success (Solana / SVM):
{
  "success": true,
  "transaction": "AXGcLa7sqSjt7pXV4mpVRP5a77tVjokZbgx8gkQ16X8Wgg3vDkWKMh9BrPTr1f2KrDuf9nSX7FrZEAQTkJ3y5UN",
  "network": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d",
  "payer": "6KPYDyuRnpuKcm1TerUmwLd2BcaihvhF4Ccrr8beruu2"
}
On failure (e.g. insufficient funds), the server may return a non-2xx status and/or a PAYMENT-RESPONSE header with an error. Example decoded payload:
{
  "x402Version": 2,
  "error": "Payment failed: insufficient funds",
  "accepts": [...]
}

Summary

StepAction
1GET (or other method) the resource URL
2If status is 402, decode PAYMENT-REQUIRED (base64 JSON)
3Choose one entry from accepts
4Build payment payload (EIP-712 sign for EVM exact scheme; or Solana tx) and base64-encode it
5Resend the same request with PAYMENT-SIGNATURE header
6On 200, use response body and optionally decode PAYMENT-RESPONSE
For exact field names, types, and facilitator usage, see the x402 Reference. For a ready-made client, use the httpx or requests quickstarts.

Need help?

Join our Community

Have questions or want to connect with other developers? Join our Discord server.