Skip to content

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
  • validAfter and validBefore are 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