x402 Protocol¶
The x402 protocol enables HTTP 402 (Payment Required) responses with cryptocurrency payments. WasiAI uses x402 for pay-per-inference micropayments.
What is x402?¶
x402 is a payment protocol that:
- Extends HTTP: Uses the standard 402 status code
- Enables Micropayments: Payments as low as $0.001
- Is Gasless for Users: Facilitator pays blockchain gas
- Settles Instantly: ~1 second on Avalanche
Why x402?¶
Traditional payment systems have minimum transaction sizes ($0.50+) due to fees. x402 enables true micropayments by:
- Using EIP-3009 (transferWithAuthorization) for gasless USDC transfers
- Leveraging facilitators that batch and pay gas
- Settling on Avalanche with sub-cent transaction costs
How It Works¶
High-Level Flow¶
1. Client requests protected resource
2. Server returns HTTP 402 + payment requirements
3. Client signs USDC authorization (no gas)
4. Client retries with X-PAYMENT header
5. Server verifies payment via facilitator
6. Facilitator executes USDC transfer on-chain
7. Server returns resource + X-PAYMENT-RESPONSE
Detailed Sequence¶
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Client │ │ Server │ │Facilitator│ │ USDC │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ POST /api/inference │ │ │
│────────────────────>│ │ │
│ │ │ │
│ HTTP 402 + Req │ │ │
│<────────────────────│ │ │
│ │ │ │
│ Sign authorization │ │ │
│ (wallet popup) │ │ │
│ │ │ │
│ POST + X-PAYMENT │ │ │
│────────────────────>│ │ │
│ │ │ │
│ │ POST /settle │ │
│ │────────────────────>│ │
│ │ │ │
│ │ │ transferWithAuth() │
│ │ │────────────────────>│
│ │ │ │
│ │ │ txHash │
│ │ │<────────────────────│
│ │ │ │
│ │ { success, tx } │ │
│ │<────────────────────│ │
│ │ │ │
│ 200 OK + Response │ │ │
│<────────────────────│ │ │
│ │ │ │
Payment Requirement¶
When a server requires payment, it returns HTTP 402 with a JSON body:
{
"x402Version": 1,
"accepts": [
{
"scheme": "exact",
"network": "avalanche-fuji",
"maxAmountRequired": "10000",
"resource": "https://wasiai.io/api/inference/1",
"description": "AI inference on Crypto Sentiment Analyzer",
"payTo": "0x1234...5678",
"asset": "0x5425890298aed601595a70AB815c96711a31Bc65",
"maxTimeoutSeconds": 60
}
],
"error": "X-PAYMENT header is required"
}
Fields¶
| Field | Type | Description |
|---|---|---|
scheme | string | Payment scheme ("exact" = exact amount) |
network | string | Blockchain network identifier |
maxAmountRequired | string | Amount in token base units |
resource | string | URL of the protected resource |
description | string | Human-readable description |
payTo | string | Recipient address |
asset | string | Token contract address (USDC) |
maxTimeoutSeconds | number | Payment validity window |
Payment Payload¶
The client constructs a payment payload and sends it as a base64-encoded X-PAYMENT header:
{
"x402Version": 1,
"scheme": "exact",
"network": "avalanche-fuji",
"payload": {
"signature": "0x...",
"authorization": {
"from": "0xUserWallet...",
"to": "0xRecipient...",
"value": "10000",
"validAfter": "1702300000",
"validBefore": "1702300060",
"nonce": "0x..."
}
}
}
Authorization Fields¶
| Field | Type | Description |
|---|---|---|
from | address | Payer's wallet address |
to | address | Recipient address (splitter) |
value | string | Amount in base units |
validAfter | string | Unix timestamp (payment valid after) |
validBefore | string | Unix timestamp (payment expires) |
nonce | bytes32 | Unique 32-byte nonce |
EIP-3009: TransferWithAuthorization¶
x402 uses EIP-3009, which allows gasless USDC transfers via signed messages.
How It Works¶
- User signs an EIP-712 typed message authorizing a transfer
- Anyone (facilitator) can submit this signature to the USDC contract
- USDC contract verifies signature and executes transfer
- User pays no gas; facilitator pays gas
EIP-712 Domain¶
const domain = {
name: 'USD Coin',
version: '2',
chainId: 43113,
verifyingContract: '0x5425890298aed601595a70AB815c96711a31Bc65'
}
EIP-712 Types¶
const types = {
TransferWithAuthorization: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'validAfter', type: 'uint256' },
{ name: 'validBefore', type: 'uint256' },
{ name: 'nonce', type: 'bytes32' }
]
}
Facilitators¶
A facilitator is a service that executes USDC transfers on behalf of users.
Ultravioleta DAO (Default)¶
- Cost: Free
- Gas: Covered by Ultravioleta
- Networks: Avalanche only
- Endpoint:
https://facilitator.ultravioletadao.xyz
// Settlement request
POST https://facilitator.ultravioletadao.xyz/settle
{
"x402Version": 1,
"paymentPayload": { ... },
"paymentRequirements": { ... }
}
Thirdweb¶
- Cost: $99/month
- Gas: Backend wallet pays
- Networks: Multi-chain
// Direct contract call
const tx = await usdcContract.transferWithAuthorization(
from, to, value, validAfter, validBefore, nonce, v, r, s
)
Configuration¶
# .env.local
X402_FACILITATOR_PROVIDER=ultravioleta # or 'thirdweb'
# For Thirdweb
THIRDWEB_SECRET_KEY=your_key
PRIVATE_KEY=backend_wallet_key
Payment Response¶
After successful payment, the server returns the resource with an X-PAYMENT-RESPONSE header:
{
"success": true,
"transaction": "0xabc123...",
"network": "avalanche-fuji",
"payer": "0xUserWallet...",
"errorReason": null
}
Security¶
Replay Protection¶
Each payment has a unique 32-byte nonce. Used nonces are tracked to prevent replay attacks.
Time Windows¶
Payments are only valid within a time window: - validAfter: Payment cannot be used before this time - validBefore: Payment expires after this time
Typical window: 60 seconds.
Amount Validation¶
Server verifies the payment amount matches the requirement:
Recipient Validation¶
Payment must go to the correct address:
Error Handling¶
| Error | Cause | Solution |
|---|---|---|
Unsupported x402 version | Wrong protocol version | Update client |
Wrong network | Network mismatch | Switch networks |
Nonce already used | Replay attempt | Generate new nonce |
Insufficient payment | Amount too low | Check pricing |
Payment expired | Time window passed | Sign new authorization |
Wrong recipient | Address mismatch | Check payTo field |
Integration Example¶
Client-Side (TypeScript)¶
import { useSignTypedData } from 'wagmi'
async function payForInference(modelId: number, input: string) {
// 1. Get payment requirements
const res = await fetch(`/api/inference/${modelId}`, {
method: 'POST',
body: JSON.stringify({ input })
})
if (res.status !== 402) {
return res.json() // No payment required
}
const { accepts } = await res.json()
const requirement = accepts[0]
// 2. Build authorization
const now = Math.floor(Date.now() / 1000)
const authorization = {
from: userAddress,
to: requirement.payTo,
value: BigInt(requirement.maxAmountRequired),
validAfter: BigInt(now - 60),
validBefore: BigInt(now + 60),
nonce: generateNonce()
}
// 3. Sign with wallet
const signature = await signTypedDataAsync({
domain: USDC_DOMAIN,
types: TRANSFER_WITH_AUTHORIZATION_TYPES,
primaryType: 'TransferWithAuthorization',
message: authorization
})
// 4. Build X-PAYMENT header
const payload = {
x402Version: 1,
scheme: 'exact',
network: 'avalanche-fuji',
payload: { signature, authorization }
}
const xPayment = btoa(JSON.stringify(payload))
// 5. Retry with payment
const paidRes = await fetch(`/api/inference/${modelId}`, {
method: 'POST',
headers: { 'X-PAYMENT': xPayment },
body: JSON.stringify({ input })
})
return paidRes.json()
}