x402 Payment Flow¶
This document provides a comprehensive technical explanation of how the x402 protocol is implemented in WasiAI for pay-per-inference payments.
Overview¶
The x402 protocol enables HTTP 402 (Payment Required) responses with cryptocurrency payments. It allows micropayments for API calls without requiring users to pay gas fees.
Key Benefits¶
- Gasless for users: Facilitator pays blockchain gas
- Micropayments: As low as $0.001 per call
- Standard HTTP: Works with existing web infrastructure
- Instant settlement: ~1 second on Avalanche
Protocol Flow¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ x402 PAYMENT FLOW │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ USER FRONTEND BACKEND │
│ ──── ──────── ─────── │
│ │
│ 1. Click "Run Model" │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ X402InferencePanel │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ 2. POST /api/inference/{modelId} ──────────────────► 3. Check payment │
│ (no X-PAYMENT header) │ │
│ ▼ │
│ No X-PAYMENT header │
│ │ │
│ ▼ │
│ 4. Receive HTTP 402 ◄──────────────────────────── Return 402 + │
│ + X402PaymentRequirement PaymentRequirement │
│ │ │
│ ▼ │
│ 5. Build authorization object │
│ { from, to, value, validAfter, validBefore, nonce } │
│ │ │
│ ▼ │
│ 6. Sign with wallet (EIP-712) │
│ signTypedDataAsync() │
│ │ │
│ ▼ │
│ 7. Build X-PAYMENT header (base64) │
│ │ │
│ ▼ │
│ 8. POST /api/inference/{modelId} ──────────────────► 9. Verify payment │
│ + X-PAYMENT header │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Decode payload │ │
│ │ Validate fields │ │
│ │ Check nonce │ │
│ │ Verify amount │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ FACILITATOR │ │
│ │ .settle() │ │
│ │ │ │
│ │ transferWith │ │
│ │ Authorization() │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ 10. Run inference │
│ │ │
│ ▼ │
│ 11. Receive result ◄───────────────────────────── Return 200 + │
│ + X-PAYMENT-RESPONSE result │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Step-by-Step Breakdown¶
Step 1-2: Initial Request¶
The user clicks "Run Model" and the frontend makes an initial POST request without a payment header.
// Frontend: X402InferencePanel.tsx
const checkRes = await fetch(`/api/inference/${modelId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input })
})
Step 3-4: HTTP 402 Response¶
The backend checks for the X-PAYMENT header. If missing, it returns HTTP 402 with payment requirements.
// Backend: /api/inference/[modelId]/route.ts
if (!paymentHeader) {
return NextResponse.json({
x402Version: 1,
accepts: [paymentRequirement],
error: 'X-PAYMENT header is required'
}, { status: 402 })
}
Payment Requirement Structure:
interface X402PaymentRequirement {
scheme: 'exact' // Payment scheme
network: 'avalanche-fuji' // Blockchain network
maxAmountRequired: '10000' // Amount in USDC base units ($0.01)
resource: string // API endpoint URL
description: string // Human-readable description
payTo: string // Recipient address (splitter)
asset: string // USDC contract address
maxTimeoutSeconds: 60 // Payment validity window
}
Step 5: Build Authorization¶
The frontend constructs an EIP-3009 TransferWithAuthorization message.
// Frontend: X402InferencePanel.tsx
const now = Math.floor(Date.now() / 1000)
const authorization = {
from: address, // User's wallet
to: requirement.payTo, // Splitter address
value: BigInt(requirement.maxAmountRequired), // Payment amount
validAfter: BigInt(now - 60), // Valid from 1 min ago
validBefore: BigInt(now + 60), // Expires in 1 min
nonce: generateNonce() // Random 32 bytes
}
Step 6: EIP-712 Signature¶
The user signs the authorization using their wallet. This is a gasless operation.
// Frontend: X402InferencePanel.tsx
const signature = await signTypedDataAsync({
domain: {
name: 'USD Coin',
version: '2',
chainId: 43113,
verifyingContract: USDC_ADDRESS
},
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' }
]
},
primaryType: 'TransferWithAuthorization',
message: authorization
})
Step 7: Build X-PAYMENT Header¶
The frontend packages the signature and authorization into a base64-encoded header.
// Frontend: X402InferencePanel.tsx
const paymentPayload = {
x402Version: 1,
scheme: 'exact',
network: 'avalanche-fuji',
payload: {
signature,
authorization: {
from: address,
to: requirement.payTo,
value: requirement.maxAmountRequired,
validAfter: validAfter.toString(),
validBefore: validBefore.toString(),
nonce
}
}
}
const xPaymentHeader = btoa(JSON.stringify(paymentPayload))
Step 8: Retry with Payment¶
The frontend retries the request with the X-PAYMENT header.
// Frontend: X402InferencePanel.tsx
const inferRes = await fetch(`/api/inference/${modelId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-PAYMENT': xPaymentHeader
},
body: JSON.stringify({ input, agentId })
})
Step 9: Payment Verification¶
The backend performs multiple validation checks:
// Backend: /api/inference/[modelId]/route.ts
async function verifyPaymentWithFacilitator(paymentHeader, requirement) {
// 1. Decode base64 payload
const payload = JSON.parse(Buffer.from(paymentHeader, 'base64').toString())
// 2. Validate x402 version
if (payload.x402Version !== 1) return { valid: false, error: 'Unsupported version' }
// 3. Validate scheme
if (payload.scheme !== 'exact') return { valid: false, error: 'Unsupported scheme' }
// 4. Validate network
if (payload.network !== 'avalanche-fuji') return { valid: false, error: 'Wrong network' }
// 5. Check nonce (replay protection)
if (usedNonces.has(payload.payload.authorization.nonce)) {
return { valid: false, error: 'Nonce already used' }
}
// 6. Validate amount
if (BigInt(payload.payload.authorization.value) < BigInt(requirement.maxAmountRequired)) {
return { valid: false, error: 'Insufficient payment' }
}
// 7. Validate recipient
if (payload.payload.authorization.to !== requirement.payTo) {
return { valid: false, error: 'Wrong recipient' }
}
// 8. Validate time window
const now = Math.floor(Date.now() / 1000)
if (now < payload.payload.authorization.validAfter) {
return { valid: false, error: 'Payment not yet valid' }
}
if (now > payload.payload.authorization.validBefore) {
return { valid: false, error: 'Payment expired' }
}
// 9. Execute settlement via facilitator
const result = await facilitator.settle(payload, requirement)
// 10. Mark nonce as used
usedNonces.add(payload.payload.authorization.nonce)
return { valid: true, txHash: result.txHash, payer: result.payer }
}
Step 10: Facilitator Settlement¶
The facilitator executes the USDC transfer on-chain.
Ultravioleta DAO (Free)¶
// POST to facilitator endpoint
const response = await fetch('https://facilitator.ultravioletadao.xyz/settle', {
method: 'POST',
body: JSON.stringify({
x402Version: 1,
paymentPayload: payload,
paymentRequirements: requirement
})
})
Thirdweb (Multi-chain)¶
// Direct contract call via Thirdweb SDK
const transaction = prepareContractCall({
contract: usdcContract,
method: 'transferWithAuthorization',
params: [from, to, value, validAfter, validBefore, nonce, v, r, s]
})
await sendTransaction({ transaction, account: backendWallet })
Step 11: Success Response¶
After successful payment and inference, the backend returns the result.
// Backend: /api/inference/[modelId]/route.ts
const response = NextResponse.json({
ok: true,
result: inferenceOutput,
latencyMs: 150,
payment: {
txHash: '0x...',
payer: '0x...',
amount: '10000',
amountFormatted: '$0.0100',
currency: 'USDC',
verified: true
}
})
response.headers.set('X-PAYMENT-RESPONSE',
btoa(JSON.stringify({
success: true,
transaction: txHash,
network: 'avalanche-fuji',
payer: payerAddress
}))
)
Key Files¶
| File | Purpose |
|---|---|
src/components/X402InferencePanel.tsx | Frontend UI and payment signing |
src/app/api/inference/[modelId]/route.ts | Backend payment verification and inference |
src/lib/x402-constants.ts | Shared types, constants, helpers |
src/lib/x402-facilitators.ts | Facilitator abstraction layer |
Facilitator Configuration¶
WasiAI supports two facilitator providers:
Ultravioleta DAO (Default)¶
- Cost: Free
- Gas: Covered by Ultravioleta
- Networks: Avalanche only
- Endpoint:
https://facilitator.ultravioletadao.xyz
Thirdweb¶
- Cost: $99/month
- Gas: Backend wallet pays
- Networks: Multi-chain
- Requires:
THIRDWEB_SECRET_KEY,PRIVATE_KEY
Configuration:
# .env.local
X402_FACILITATOR_PROVIDER=ultravioleta # or 'thirdweb'
THIRDWEB_SECRET_KEY=your_key # if using Thirdweb
PRIVATE_KEY=backend_wallet_key # if using Thirdweb
Security Considerations¶
Replay Protection¶
- Each payment has a unique 32-byte nonce
- Used nonces are tracked in memory
- Risk: Nonces reset on server restart
- Mitigation: Persist nonces to database for production
Time Windows¶
- Payments are valid for 60 seconds
validAfterandvalidBeforeare checked server-side- Prevents stale authorizations from being used
Amount Validation¶
- Server verifies payment amount matches requirement
- Exact match required (no overpayment accepted)
Recipient Validation¶
- Payment must go to the correct splitter address
- Prevents payment redirection attacks
Error Handling¶
| Error | Cause | Solution |
|---|---|---|
Unsupported x402 version | Wrong protocol version | Update client |
Wrong network | Network mismatch | Switch to correct network |
Nonce already used | Replay attempt | Generate new nonce |
Insufficient payment | Amount too low | Check pricing |
Payment expired | Time window passed | Sign new authorization |
Settlement failed | Facilitator error | Check USDC balance |
Next Steps¶
- Pay-Per-Inference Guide: User-facing documentation
- Revenue Splits: How payments are distributed
- Smart Contracts: On-chain components