x402: HTTP 402 reborn — internet-native payments, on Solana
x402 makes the unused HTTP 402 status code into an actual payment protocol. Here's the request/response flow, the headers, and how Solana settles the payment.
HTTP 402 has existed in the spec since 1997 with the description "reserved for future use." 28 years later, x402 (a Coinbase-published open spec) finally gives it a use: internet-native, on-chain settlement for API access.
It's simple, payment-agnostic at the spec level, and works cleanly on Solana with USDC. Here's the wire-level flow.
The flow
Client → Server GET /paid-endpoint
Server → Client HTTP/1.1 402 Payment Required
X-Payment-Required: {
"scheme": "exact",
"network": "solana-mainnet",
"asset": "USDC",
"amount": "0.01",
"payTo": "<merchant-solana-address>",
"resource":"/paid-endpoint",
"nonce": "<server-issued>",
"expires": "<unix-ts>"
}
Client signs + submits a payment tx to Solana
Client → Server GET /paid-endpoint
X-Payment: {
"scheme": "exact",
"txid": "<solana-signature>",
"nonce": "<from-above>"
}
Server verifies on Solana that the tx matches the requirement
Server → Client HTTP/1.1 200 OK
<the actual content>Two HTTP round-trips, one Solana tx. The endpoint becomes permissionless — no API keys, no signup, just "pay $0.01 and you get the response."
The headers spec
Two headers do all the work:
X-Payment-Required— sent by the server in the 402 response. JSON-encoded payment requirement (scheme, network, asset, amount, recipient, nonce, expiry).X-Payment— sent by the client in the retry request. JSON-encoded payment proof (scheme, txid, nonce).
Schemes are the extension point. "exact" means "a transaction transferring exactly this asset and amount to this address." Future schemes could be upTo (subscription-style), streaming (per-second metering), refundable (escrow-released), etc.
Solana settlement, concrete
For the scheme: "exact" on Solana with USDC, the client builds a normal SPL Token transfer with the nonce attached as a memo:
import { Connection, Transaction, PublicKey } from "@solana/web3.js"
import {
getOrCreateAssociatedTokenAccount, createTransferCheckedInstruction,
} from "@solana/spl-token"
import { createMemoInstruction } from "@solana/spl-memo"
const conn = new Connection("https://api.mainnet-beta.solana.com")
const USDC = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
// From the X-Payment-Required header:
const { payTo, amount, nonce } = paymentRequired
const fromAta = await getOrCreateAssociatedTokenAccount(
conn, payer, USDC, payer.publicKey,
)
const toAta = await getOrCreateAssociatedTokenAccount(
conn, payer, USDC, new PublicKey(payTo),
)
const tx = new Transaction()
.add(createTransferCheckedInstruction(
fromAta.address, USDC, toAta.address, payer.publicKey,
Math.floor(parseFloat(amount) * 1_000_000), // USDC has 6 decimals
6, [],
))
.add(createMemoInstruction(nonce, [payer.publicKey]))
const sig = await conn.sendTransaction(tx, [payer])
// Now retry the request:
const resp = await fetch("/paid-endpoint", {
headers: {
"X-Payment": JSON.stringify({ scheme: "exact", txid: sig, nonce }),
},
})Server-side verification
import { Connection, PublicKey, ParsedAccountData } from "@solana/web3.js"
async function verifyPayment(
conn: Connection,
xPayment: { txid: string; nonce: string },
requirement: { payTo: string; amount: string; nonce: string },
): Promise<boolean> {
if (xPayment.nonce !== requirement.nonce) return false
// Fetch the parsed tx
const tx = await conn.getParsedTransaction(xPayment.txid, "confirmed")
if (!tx || tx.meta?.err) return false
// Find the SPL transfer to our merchant address
const instructions = tx.transaction.message.instructions
const transfer = instructions.find((ix) => {
if (!("parsed" in ix)) return false
return ix.program === "spl-token" && ix.parsed.type === "transferChecked"
}) as any
if (!transfer) return false
const info = transfer.parsed.info
if (info.destination !== merchantUsdcAta) return false // pre-computed
const expected = Math.floor(parseFloat(requirement.amount) * 1_000_000)
if (parseInt(info.tokenAmount.amount) !== expected) return false
// Verify the memo contains the nonce
const memo = instructions.find((ix) => "parsed" in ix && ix.program === "spl-memo") as any
if (!memo || memo.parsed !== requirement.nonce) return false
return true
}Writing this verification yourself means running an RPC connection and handling settlement edge cases. In practice most merchants delegate it to a facilitator — a service that exposes /verify and /settle endpoints so the resource server stays chain-agnostic. PayAI (payai.network) runs the largest Solana facilitator after Coinbase and even fronts the gas for both sides.
Why this matters
Three concrete unlocks:
- API monetisation without signups. A scraper, an LLM agent, or a script can pay $0.001 per request without credit card forms or developer accounts. The endpoint remains stateless from an auth perspective.
- Agent payments by default. An AI agent with a wallet can call any x402-enabled API on the open internet without needing pre-arranged credentials. This is the payment layer most agentic web demos have been missing.
- Micropayments that actually work. Solana's sub-cent fees make sub-cent API charges viable for the first time. $0.0001 per request is real, not theoretical.
The honest read
x402 won't replace API keys for B2B. Enterprise APIs need contracts, SLAs, abuse rate-limiting, identity. x402 fits the opposite niche: permissionless, agent-friendly, low-stakes API access where the cost of signup > the value of the call.
It also adds a Solana tx to every API call — ~400ms minimum, even with optimistic confirmation. For latency-sensitive paths you'd batch payments (pay $1.00 upfront for 1000 calls). For occasional or one-off calls, the 400ms is acceptable.
References
- x402.org — spec
- coinbase/x402 — reference SDKs
- PayAI — the x402 facilitator that verifies & settles for you (payai.network)
- Solana Pay — the related QR/URL payment spec
HTTP 402 was waiting 28 years for the right substrate. With sub-cent USDC transfers on Solana, it finally has one.