Payment Flow
This guide provides a detailed walkthrough of the payment flow in x402test, from initial request to final response.
Overview
Section titled “Overview”The payment flow consists of six main steps:
- Initial request without payment
- Server returns 402 with payment requirements
- Client creates and signs transaction
- Client submits transaction to blockchain
- Client retries request with payment proof
- Server verifies and returns protected content
Detailed Flow
Section titled “Detailed Flow”Step 1: Initial Request
Section titled “Step 1: Initial Request”The client makes a GET request to a payment-protected endpoint:
const response = await fetch("http://localhost:4402/api/premium", { method: "GET", headers: { "Content-Type": "application/json", },});The request does not include any payment information.
Step 2: Payment Required Response
Section titled “Step 2: Payment Required Response”The server responds with status code 402 and payment requirements:
// Response status: 402// Response body:{ x402Version: 1, accepts: [{ scheme: "solanaTransferChecked", network: "solana-devnet", maxAmountRequired: "100000", // 0.10 USDC resource: "http://localhost:4402/api/premium", description: "Premium content access", mimeType: "application/json", payTo: "FcxKSp7YxqYXdq...", // Recipient wallet asset: "EPjFWdd5AufqSSqeM2...", // USDC mint maxTimeoutSeconds: 30 }], error: null}Step 3: Parse Requirements
Section titled “Step 3: Parse Requirements”The client parses the payment requirements:
import { parse402Response } from "x402test";
const requirements = parse402Response(responseBody);
console.log("Amount required:", requirements.maxAmountRequired);console.log("Pay to:", requirements.payTo);console.log("Asset:", requirements.asset);Step 4: Create Payment
Section titled “Step 4: Create Payment”The client creates a Solana SPL token transfer:
import { createPayment } from "x402test";
// Get test walletconst wallet = await getWallet();
// Create and sign transactionconst signature = await createPayment(wallet, requirements);
console.log("Transaction signature:", signature);// Output: "5XzT4qW3..."This process:
- Gets or creates token accounts for sender and recipient
- Creates a
transferCheckedinstruction - Signs the transaction with the wallet keypair
- Submits to the Solana blockchain
- Waits for confirmation
Step 5: Create Payment Header
Section titled “Step 5: Create Payment Header”The client creates the X-PAYMENT header:
import { createXPaymentHeader } from "x402test";
const paymentHeader = createXPaymentHeader( signature, requirements, wallet.publicKey.toBase58());
// Header value is base64-encoded JSON:// eyJ4NDAyVmVyc2lvbiI6MSwic2NoZW1lIjoi...Header structure:
{ "x402Version": 1, "scheme": "solanaTransferChecked", "network": "solana-devnet", "payload": { "signature": "5XzT4qW3...", "from": "FcxKSp7YxqYX...", "amount": "100000", "mint": "EPjFWdd5Aufq...", "timestamp": 1699564800000 }}Step 6: Retry with Payment
Section titled “Step 6: Retry with Payment”The client retries the request with the payment header:
const response = await fetch("http://localhost:4402/api/premium", { method: "GET", headers: { "Content-Type": "application/json", "X-PAYMENT": paymentHeader, },});Step 7: Server Verification
Section titled “Step 7: Server Verification”The server verifies the payment:
// 1. Decode X-PAYMENT headerconst payment = parse402PaymentHeader(req.headers["x-payment"]);
// 2. Verify on blockchainconst verification = await verifyPayment( payment.payload.signature, new PublicKey(recipientAddress), BigInt(expectedAmount), usdcMintAddress);
if (!verification.isValid) { return res.status(402).json({ error: verification.invalidReason, });}
// 3. Mark signature as usedmarkSignatureUsed(payment.payload.signature, req.path, payment.payload.amount);
// 4. Return protected contentres.status(200).json({ data: "This is premium content!", timestamp: Date.now(),});Step 8: Success Response
Section titled “Step 8: Success Response”If verification succeeds, the server returns the protected content:
// Response status: 200// Response headers:{ "Content-Type": "application/json", "X-PAYMENT-RESPONSE": "eyJzdWNjZXNzIjp0cnVlLCJ0eEhhc2giOi..."}
// Response body:{ "data": "This is premium content!", "timestamp": 1699564800000}The X-PAYMENT-RESPONSE header contains:
{ "success": true, "error": null, "txHash": "5XzT4qW3...", "networkId": "solana-devnet"}Automated Flow with x402test
Section titled “Automated Flow with x402test”The x402test client automates this entire flow:
import { x402 } from "x402test";
// All steps happen automaticallyconst response = await x402("http://localhost:4402/api/premium") .withPayment({ amount: "0.10" }) .expectStatus(200) .execute();
// Response includes payment detailsconsole.log("Payment signature:", response.payment?.signature);console.log("Amount paid:", response.payment?.amount);console.log("From:", response.payment?.from);console.log("To:", response.payment?.to);Verification Process
Section titled “Verification Process”Transaction Lookup
Section titled “Transaction Lookup”The server fetches the transaction from Solana:
const connection = getConnection();const tx = await connection.getTransaction(signature, { commitment: "confirmed", maxSupportedTransactionVersion: 0,});
if (!tx) { return { isValid: false, invalidReason: "Transaction not found" };}Amount Verification
Section titled “Amount Verification”Check the transferred amount:
const transferAmount = BigInt(transfer.amount);const expectedAmount = BigInt(requirements.maxAmountRequired);
if (transferAmount < expectedAmount) { return { isValid: false, invalidReason: `Insufficient amount: expected ${expectedAmount}, got ${transferAmount}`, };}Recipient Verification
Section titled “Recipient Verification”Verify the recipient address:
if (transfer.destinationOwner !== expectedRecipient.toBase58()) { return { isValid: false, invalidReason: `Wrong recipient: expected ${expectedRecipient}, got ${transfer.destinationOwner}`, };}Token Verification
Section titled “Token Verification”Confirm the correct token was used:
if (transfer.mint !== expectedMint.toBase58()) { return { isValid: false, invalidReason: `Wrong token: expected ${expectedMint}, got ${transfer.mint}`, };}Replay Check
Section titled “Replay Check”Ensure the signature hasn’t been used:
if (isSignatureUsed(signature)) { return { isValid: false, invalidReason: "Payment already processed", };}Error Scenarios
Section titled “Error Scenarios”Insufficient Payment
Section titled “Insufficient Payment”// Server requires 0.10 USDC// Client pays 0.05 USDC
const response = await x402(url).withPayment("0.05").execute();
// Error: "Client max amount 50000 is less than server required amount 100000"Transaction Not Confirmed
Section titled “Transaction Not Confirmed”// Transaction hasn't been confirmed yet
// Error: "Transaction not found or not confirmed"Replay Attack
Section titled “Replay Attack”// Attempting to reuse a signature
// Error: "Payment already processed"Network Mismatch
Section titled “Network Mismatch”// Server expects devnet, transaction on mainnet
// Error: "Network mismatch"Best Practices
Section titled “Best Practices”Client-Side
Section titled “Client-Side”- Check Requirements: Always parse and validate requirements before paying
- Validate Amount: Ensure you’re willing to pay the required amount
- Handle Errors: Implement proper error handling for failed payments
- Retry Logic: Implement exponential backoff for transaction confirmation
- Log Transactions: Keep records of all payment transactions
Server-Side
Section titled “Server-Side”- Clear Requirements: Provide detailed payment requirements
- Verify Completely: Don’t skip any verification steps
- Track Signatures: Always check for replay attacks
- Error Messages: Return clear, actionable error messages
- Timeout Handling: Implement reasonable timeout for payment confirmation
Next Steps
Section titled “Next Steps”- Testing Client - Learn about the testing client
- Mock Server - Set up the mock server
- API Reference - Verification API details
- Examples - See complete examples