Skip to content

How x402 Works

The x402 protocol is an extension of the HTTP 402 Payment Required status code, enabling micropayments for API access using blockchain transactions.

The HTTP 402 status code was reserved for future payment systems. The x402 protocol uses this status code to indicate that payment is required before accessing a resource.

sequenceDiagram
Client->>Server: GET /api/premium
Server->>Client: 402 Payment Required + Requirements
Client->>Blockchain: Create & Sign Transaction
Blockchain->>Client: Transaction Signature
Client->>Server: GET /api/premium + X-PAYMENT Header
Server->>Blockchain: Verify Transaction
Blockchain->>Server: Transaction Confirmed
Server->>Client: 200 OK + Protected Content

The client makes a request to a payment-protected endpoint:

const response = await fetch("http://localhost:4402/api/premium");

The server responds with a 402 status code and payment requirements:

{
"x402Version": 1,
"accepts": [
{
"scheme": "solanaTransferChecked",
"network": "solana-devnet",
"maxAmountRequired": "100000",
"resource": "http://localhost:4402/api/premium",
"description": "Premium content access",
"payTo": "FcxKSp...",
"asset": "EPjFWdd...",
"maxTimeoutSeconds": 30
}
],
"error": null
}

The client creates a Solana SPL token transfer:

import { x402 } from "x402test";
const response = await x402("http://localhost:4402/api/premium")
.withPayment({ amount: "0.10" })
.execute();

Behind the scenes:

  • Creates a token transfer instruction
  • Signs the transaction with the test wallet
  • Submits to the Solana blockchain
  • Waits for confirmation

The client retries the request with the X-PAYMENT header:

// Header format (base64 encoded JSON):
{
"x402Version": 1,
"scheme": "solanaTransferChecked",
"network": "solana-devnet",
"payload": {
"signature": "5Xz...",
"from": "FcxK...",
"amount": "100000",
"mint": "EPjF...",
"timestamp": 1699564800000
}
}

The server verifies the payment:

  1. Decode Header: Extract payment information
  2. Fetch Transaction: Get transaction from blockchain
  3. Verify Amount: Check payment amount matches requirement
  4. Verify Recipient: Ensure payment went to correct address
  5. Verify Asset: Confirm correct token (USDC) was used
  6. Check Replay: Ensure signature hasn’t been used before
  7. Mark Used: Store signature to prevent replay attacks

If verification succeeds, the server returns the protected content:

{
"data": "This is premium content!",
"timestamp": 1699564800000
}
FieldTypeDescription
schemestringPayment scheme (e.g., “solanaTransferChecked”)
networkstringBlockchain network (e.g., “solana-devnet”)
maxAmountRequiredstringMaximum amount in atomic units
resourcestringURL of the protected resource
payTostringRecipient wallet address
assetstringToken mint address (USDC)
FieldTypeDescription
descriptionstringHuman-readable description
mimeTypestringResponse content type
maxTimeoutSecondsnumberPayment timeout in seconds
outputSchemaobjectExpected response schema

The X-PAYMENT header contains a base64-encoded JSON payload:

interface PaymentPayload {
x402Version: number;
scheme: string;
network: string;
payload: {
signature: string; // Transaction signature
from: string; // Payer address
amount: string; // Amount in atomic units
mint: string; // Token mint address
timestamp: number; // Unix timestamp
};
}

x402test tracks used transaction signatures:

.x402test-signatures.json
[
{
"signature": "5Xz...",
"usedAt": 1699564800000,
"endpoint": "/api/premium",
"amount": "100000"
}
]

Once a signature is used, it cannot be reused:

// First request succeeds
await x402("http://localhost:4402/api/data").withPayment("0.01").execute();
// Attempting to reuse the same transaction fails
// Server returns 402 with "Payment already processed" error

The server ensures the paid amount meets requirements:

// Server requires 0.10 USDC
const requirements = {
maxAmountRequired: "100000", // 0.10 USDC in atomic units
};
// Client pays 0.05 USDC - REJECTED
await x402(url).withPayment("0.05").execute(); // Error!
// Client pays 0.10 USDC or more - ACCEPTED
await x402(url).withPayment("0.10").execute(); // Success!

Payments must go to the specified recipient:

// Transaction must transfer to correct address
const verification = await verifyPayment(
signature,
expectedRecipient, // Must match payment requirement
expectedAmount,
expectedMint
);

USDC uses 6 decimal places. Amounts are specified in atomic units:

USDCAtomic Units
0.0110,000
0.10100,000
1.001,000,000
10.0010,000,000
// Helper function
const toAtomicUnits = (usdc: string): string => {
return (parseFloat(usdc) * 1e6).toString();
};
toAtomicUnits("0.10"); // "100000"
{
"x402Version": 1,
"accepts": [...],
"error": "Payment amount 50000 is less than required 100000"
}
{
"error": "Transaction not found or not confirmed"
}
{
"error": "Payment already processed"
}
{
"error": "Wrong recipient: expected FcxK..., got EPjF..."
}