TUTORIALx402 ProtocolUSDC MicropaymentsCloudflare Workers
22 min read

Build a Paid API with x402
USDC Micropayments: Complete Tutorial

Stripe charges $0.30 + 2.9% per transaction, killing any API priced under a dollar. The x402 protocol settles USDC payments on Base for under $0.01 in fees, making true per-request micropayments viable for the first time. This tutorial walks you through building, testing, and deploying a production-ready paid API using Hono, x402, and Cloudflare Workers — complete with a real SERP scraper example powered by Proxies.sx mobile proxies.

100M+
x402 Payments Processed
<$0.01
Network Fee (Base)
$72B+
USDC Market Cap
~2s
Base Settlement

What You Will Build

1Why x402 Beats Stripe for API Micropayments
2Scaffold a Hono Project
3Install x402 SDK Packages
4Implement x402 Middleware on Endpoints
5Verify Solana Payments
6Add Base Network Support
7Test with curl (402 Response)
8Deploy to Cloudflare Workers / Node
9Full SERP Scraper with Proxies.sx + x402
10FAQ

Prerequisites: Node.js 20+, a USDC wallet on Base or Solana, and a Cloudflare account (free tier works). Familiarity with TypeScript and REST APIs is assumed.

1. Why x402 Beats Stripe for API Micropayments

Every developer has hit the same wall: you build an API that delivers real value per request — a search result, a proxy session, an AI inference — but charging for individual requests is economically impossible with traditional payment processors. Stripe's minimum effective cost is $0.30 + 2.9% per transaction. PayPal is similar. Lemon Squeezy and Paddle take even larger cuts on small amounts. If your API call is worth $0.01, you lose money on every single transaction.

This is why APIs have relied on subscription tiers, prepaid credit bundles, and monthly invoicing for decades. These models work, but they create friction: customers must commit before they use, overages are confusing, and AI agents cannot sign up for monthly plans autonomously.

x402 changes the economics. Launched by Coinbase on May 6, 2025, the protocol uses HTTP status code 402 (Payment Required) to enable per-request payments with USDC stablecoins. On Base (Coinbase's L2 chain), a payment transaction costs under $0.01 in network fees with approximately 2-second finality. On Solana, fees drop to fractions of a cent with approximately 400ms settlement. By December 2025, the protocol had processed over 100 million payments. In September 2025, Coinbase and Cloudflare announced the x402 Foundation to formalize the protocol as an open internet standard.

Featurex402 + USDCStripePayPal
Minimum viable charge$0.001~$0.35~$0.35
Fee on $0.01 tx<$0.01 (gas)$0.30 + 2.9%$0.30 + 3.49%
Fee on $1.00 tx<$0.01$0.33$0.33
Settlement time2s (Base)2 business daysInstant to PayPal
Merchant signupNot requiredKYC requiredKYC required
Buyer signupNot requiredCard requiredAccount required
AI agent compatibleNativeRequires integrationLimited
Global availabilityPermissionless46 countries200+ countries

The bottom line: If your API charges less than $1 per request, traditional payment processors take 30-100% of your revenue. x402 takes under 1%. For high-volume, low-value API calls — the exact pattern AI agents need — x402 is the only economically viable payment rail.

2. Scaffold a Hono Project

We are using Hono because it is lightweight (14KB), fully TypeScript, runs on every JavaScript runtime (Cloudflare Workers, Deno, Bun, Node.js), and has native support for middleware chains — exactly what we need for payment gating. Hono's routing is Web Standards-based, meaning the same code deploys to Cloudflare Workers with zero modifications.

Create a new Hono project targeting Cloudflare Workers:

terminal
# Create a new Hono project for Cloudflare Workers
npm create hono@latest my-paid-api

# When prompted:
#   Which template? -> cloudflare-workers
#   Do you want to install dependencies? -> yes

cd my-paid-api

Your project structure will look like this:

project structure
my-paid-api/
  src/
    index.ts          # Main application entry
  wrangler.toml       # Cloudflare Workers config
  package.json
  tsconfig.json

Open src/index.ts and verify the default Hono app works:

src/index.ts
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.json({
    name: 'my-paid-api',
    version: '1.0.0',
    status: 'running',
  })
})

export default app
terminal
# Start the development server
npm run dev

# Test it
curl http://localhost:8787/
# {"name":"my-paid-api","version":"1.0.0","status":"running"}

3. Install x402 SDK Packages

The x402 ecosystem provides several npm packages. For our paid API, we need the core types and utilities, plus chain-specific verification logic. Here is what each package does:

x402

Core protocol types, schemas, and utilities. Defines PaymentRequired, PaymentPayload, and verification interfaces.

@coinbase/x402

Official Coinbase facilitator client. Handles payment verification through the hosted facilitator service.

ethers

Ethereum library for Base (EVM) on-chain verification. Parses transaction receipts and USDC transfer events.

@solana/web3.js

Solana SDK for on-chain payment verification. Confirms transactions and parses SPL token transfers.

terminal
# Install core x402 packages
npm install x402 @coinbase/x402

# Install chain-specific verification libraries
npm install ethers @solana/web3.js @solana/spl-token

# Install type definitions (dev)
npm install -D @types/node

Your package.json dependencies should now include:

package.json (excerpt)
{
  "dependencies": {
    "hono": "^4.7.0",
    "x402": "^0.3.0",
    "@coinbase/x402": "^0.2.0",
    "ethers": "^6.13.0",
    "@solana/web3.js": "^1.98.0",
    "@solana/spl-token": "^0.4.0"
  }
}

4. Implement x402 Middleware on Endpoints

The core of x402 is a middleware function that intercepts requests to protected endpoints. If the request contains no payment proof, the middleware returns HTTP 402 with machine-readable payment terms. If a payment proof is present, it verifies the on-chain transaction before allowing the request through.

Create the middleware file:

src/middleware/x402.ts
// src/middleware/x402.ts
import { Context, Next } from 'hono'
import { ethers } from 'ethers'

// Configuration interface
interface X402Config {
  walletAddress: string        // Your USDC receiving address
  network: 'base' | 'solana'   // Primary network
  rpcUrl: string               // RPC endpoint for verification
  usdcContract: string         // USDC contract address on the chain
}

// Payment requirement returned in 402 response
interface PaymentRequirement {
  scheme: 'exact'
  network: string
  maxAmountRequired: string
  resource: string
  description: string
  mimeType: string
  payTo: string
  maxTimeoutSeconds: number
  asset: string
  extra?: Record<string, unknown>
}

// Base mainnet USDC configuration
const BASE_CONFIG: X402Config = {
  walletAddress: '', // Set from environment
  network: 'base',
  rpcUrl: 'https://mainnet.base.org',
  usdcContract: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
}

// ERC-20 Transfer event ABI for parsing
const TRANSFER_EVENT_ABI = [
  'event Transfer(address indexed from, address indexed to, uint256 value)',
]

/**
 * Verify a USDC payment on Base (EVM) by checking the transaction receipt.
 * Returns the verified payment amount or null if verification fails.
 */
async function verifyBasePayment(
  txHash: string,
  expectedRecipient: string,
  expectedAmount: string,
  rpcUrl: string,
  usdcContract: string
): Promise<{ amount: string; from: string } | null> {
  try {
    const provider = new ethers.JsonRpcProvider(rpcUrl)
    const receipt = await provider.getTransactionReceipt(txHash)

    if (!receipt || receipt.status !== 1) {
      return null // Transaction failed or not found
    }

    // Parse USDC Transfer events from the receipt logs
    const iface = new ethers.Interface(TRANSFER_EVENT_ABI)

    for (const log of receipt.logs) {
      // Only check logs from the USDC contract
      if (log.address.toLowerCase() !== usdcContract.toLowerCase()) {
        continue
      }

      try {
        const parsed = iface.parseLog({
          topics: [...log.topics],
          data: log.data,
        })

        if (
          parsed?.name === 'Transfer' &&
          parsed.args.to.toLowerCase() === expectedRecipient.toLowerCase()
        ) {
          const paidAmount = ethers.formatUnits(parsed.args.value, 6) // USDC has 6 decimals
          if (parseFloat(paidAmount) >= parseFloat(expectedAmount)) {
            return { amount: paidAmount, from: parsed.args.from }
          }
        }
      } catch {
        continue // Skip non-Transfer logs
      }
    }

    return null // No matching transfer found
  } catch {
    return null // RPC error
  }
}

/**
 * x402 middleware factory for Hono.
 * Wraps any route with USDC payment gating.
 *
 * Usage:
 *   app.get('/api/data', x402Middleware('0.05', 'Premium data endpoint'), handler)
 */
export function x402Middleware(
  amountUSDC: string,
  description: string,
  config?: Partial<X402Config>
) {
  return async (c: Context, next: Next) => {
    const walletAddress = config?.walletAddress
      || c.env?.WALLET_ADDRESS
      || BASE_CONFIG.walletAddress

    const rpcUrl = config?.rpcUrl || c.env?.BASE_RPC_URL || BASE_CONFIG.rpcUrl
    const usdcContract = config?.usdcContract || BASE_CONFIG.usdcContract
    const network = config?.network || 'base'

    // Check for payment signature header (x402 V2 uses X-PAYMENT)
    const paymentHeader = c.req.header('X-PAYMENT')
      || c.req.header('X-PAYMENT-PROOF')
      || c.req.header('Payment')

    if (!paymentHeader) {
      // No payment proof: return 402 Payment Required
      const amountInSmallestUnit = (
        parseFloat(amountUSDC) * 1_000_000
      ).toString()

      const paymentRequirements: PaymentRequirement[] = [
        {
          scheme: 'exact',
          network: network,
          maxAmountRequired: amountInSmallestUnit,
          resource: c.req.url,
          description: description,
          mimeType: 'application/json',
          payTo: walletAddress,
          maxTimeoutSeconds: 300,
          asset: usdcContract,
        },
      ]

      // Set x402 response headers
      c.header('X-PAYMENT-NETWORK', network)
      c.header('X-PAYMENT-TOKEN', 'USDC')
      c.header('X-PAYMENT-ADDRESS', walletAddress)
      c.header('X-PAYMENT-AMOUNT', amountInSmallestUnit)
      c.header('X-PAYMENT-DECIMALS', '6')

      return c.json(
        {
          error: 'payment_required',
          amount: amountUSDC,
          currency: 'USDC',
          network: network,
          address: walletAddress,
          description: description,
          expires_in: 300,
          paymentRequirements,
          x402Version: 2,
        },
        402
      )
    }

    // Payment proof present: verify on-chain
    let paymentData: string
    try {
      // x402 V2 sends base64-encoded payment payload
      const decoded = atob(paymentHeader)
      const payload = JSON.parse(decoded)
      paymentData = payload.transaction || payload.txHash || paymentHeader
    } catch {
      // V1 format or raw transaction hash
      paymentData = paymentHeader
    }

    // Verify the payment on Base
    const verification = await verifyBasePayment(
      paymentData,
      walletAddress,
      amountUSDC,
      rpcUrl,
      usdcContract
    )

    if (!verification) {
      return c.json(
        {
          error: 'payment_verification_failed',
          message: 'Could not verify USDC payment on-chain.',
          hint: 'Ensure the transaction is confirmed, sent the correct amount, and targets the correct address.',
        },
        400
      )
    }

    // Attach payment info to context for downstream handlers
    c.set('payment', {
      txHash: paymentData,
      amount: verification.amount,
      from: verification.from,
      network: network,
      currency: 'USDC',
    })

    await next()
  }
}

Now wire the middleware into your Hono routes. Each protected endpoint gets its own price and description:

src/index.ts
// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { x402Middleware } from './middleware/x402'

type Bindings = {
  WALLET_ADDRESS: string
  BASE_RPC_URL: string
}

const app = new Hono<{ Bindings: Bindings }>()

// Enable CORS for agent clients
app.use('*', cors())

// Public endpoint: health check
app.get('/', (c) => {
  return c.json({
    name: 'my-paid-api',
    version: '1.0.0',
    status: 'running',
    x402: true,
    pricing: {
      '/api/search': '$0.003/query',
      '/api/premium-data': '$0.05/request',
      '/api/bulk-export': '$0.50/export',
    },
  })
})

// x402-protected endpoint: $0.003 per search query
app.get(
  '/api/search',
  x402Middleware('0.003', 'SERP search query'),
  async (c) => {
    const query = c.req.query('q') || 'default'
    const payment = c.get('payment')

    // Your actual API logic here
    return c.json({
      query,
      results: [
        { title: 'Result 1', url: 'https://example.com/1' },
        { title: 'Result 2', url: 'https://example.com/2' },
      ],
      payment: {
        tx_hash: payment.txHash,
        amount: payment.amount,
        network: payment.network,
      },
    })
  }
)

// x402-protected endpoint: $0.05 per premium data request
app.get(
  '/api/premium-data',
  x402Middleware('0.05', 'Premium data access'),
  async (c) => {
    const payment = c.get('payment')

    return c.json({
      data: { /* your premium data */ },
      payment: {
        tx_hash: payment.txHash,
        amount: payment.amount,
      },
    })
  }
)

// x402-protected endpoint: $0.50 per bulk export
app.post(
  '/api/bulk-export',
  x402Middleware('0.50', 'Bulk data export'),
  async (c) => {
    const body = await c.req.json()
    const payment = c.get('payment')

    return c.json({
      export_id: crypto.randomUUID(),
      status: 'processing',
      records: body.ids?.length || 0,
      payment: {
        tx_hash: payment.txHash,
        amount: payment.amount,
      },
    })
  }
)

export default app

Key design decision: Each endpoint gets its own price. A search query costs $0.003, premium data costs $0.05, and a bulk export costs $0.50. This mirrors real-world API pricing where different operations have different computational costs. The x402 middleware makes this trivial to implement.

5. Verify Solana Payments

Supporting Solana gives your API access to the fastest settlement times in the x402 ecosystem: approximately 400ms finality with fees under $0.001. Solana uses a different transaction model than EVM chains, so verification requires parsing SPL token transfer instructions rather than ERC-20 events.

Add the Solana verification function to your middleware:

src/middleware/solana-verify.ts
// src/middleware/solana-verify.ts
import {
  Connection,
  PublicKey,
  ParsedTransactionWithMeta,
} from '@solana/web3.js'

// USDC on Solana mainnet
const SOLANA_USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'

interface SolanaPaymentResult {
  amount: string
  from: string
  signature: string
}

/**
 * Verify a USDC payment on Solana by parsing the transaction.
 * Returns verified payment details or null if verification fails.
 */
export async function verifySolanaPayment(
  signature: string,
  expectedRecipient: string,
  expectedAmount: string,
  rpcUrl: string = 'https://api.mainnet-beta.solana.com'
): Promise<SolanaPaymentResult | null> {
  try {
    const connection = new Connection(rpcUrl, 'confirmed')

    // Fetch the parsed transaction
    const tx: ParsedTransactionWithMeta | null =
      await connection.getParsedTransaction(signature, {
        maxSupportedTransactionVersion: 0,
      })

    if (!tx || tx.meta?.err) {
      return null // Transaction not found or failed
    }

    // Look through inner instructions for SPL token transfers
    const instructions = [
      ...(tx.transaction.message.instructions || []),
      ...(tx.meta?.innerInstructions?.flatMap((ix) => ix.instructions) || []),
    ]

    for (const ix of instructions) {
      // Check for parsed SPL token transfer instructions
      if ('parsed' in ix && ix.parsed?.type === 'transferChecked') {
        const info = ix.parsed.info

        // Verify it is USDC (correct mint)
        if (info.mint !== SOLANA_USDC_MINT) {
          continue
        }

        // Verify the recipient matches (token account owner)
        // Note: the destination is a token account, not the wallet directly
        const destOwner = await getTokenAccountOwner(
          connection,
          new PublicKey(info.destination)
        )

        if (destOwner?.toLowerCase() !== expectedRecipient.toLowerCase()) {
          continue
        }

        // Verify the amount (USDC has 6 decimals on Solana)
        const paidAmount = parseFloat(info.tokenAmount.uiAmountString)
        if (paidAmount >= parseFloat(expectedAmount)) {
          return {
            amount: info.tokenAmount.uiAmountString,
            from: info.authority || info.source,
            signature,
          }
        }
      }

      // Also check simple 'transfer' type (without checked)
      if ('parsed' in ix && ix.parsed?.type === 'transfer') {
        const info = ix.parsed.info
        if (info.destination?.toLowerCase() === expectedRecipient.toLowerCase()) {
          const paidLamports = parseInt(info.amount || '0')
          const paidAmount = paidLamports / 1_000_000 // 6 decimals
          if (paidAmount >= parseFloat(expectedAmount)) {
            return {
              amount: paidAmount.toString(),
              from: info.authority || info.source,
              signature,
            }
          }
        }
      }
    }

    return null // No matching USDC transfer found
  } catch (error) {
    console.error('Solana verification error:', error)
    return null
  }
}

/**
 * Get the owner of an SPL token account.
 */
async function getTokenAccountOwner(
  connection: Connection,
  tokenAccount: PublicKey
): Promise<string | null> {
  try {
    const accountInfo = await connection.getParsedAccountInfo(tokenAccount)
    if (
      accountInfo.value?.data &&
      'parsed' in accountInfo.value.data
    ) {
      return accountInfo.value.data.parsed.info.owner || null
    }
    return null
  } catch {
    return null
  }
}

Solana nuance: On Solana, USDC transfers go to token accounts (associated token accounts, or ATAs), not directly to wallet addresses. The verification code resolves the token account owner to match against your wallet address. Make sure your receiving wallet has an initialized USDC token account before accepting payments.

6. Add Base Network Support (Dual-Network)

Production APIs should support both Base and Solana to maximize the number of paying clients. The client indicates their preferred network via the X-PAYMENT-NETWORK header. Here is the updated middleware that routes verification to the correct chain:

src/middleware/x402-dual.ts
// src/middleware/x402-dual.ts
import { Context, Next } from 'hono'
import { ethers } from 'ethers'
import { verifySolanaPayment } from './solana-verify'

interface DualNetworkConfig {
  base: {
    walletAddress: string
    rpcUrl: string
    usdcContract: string
  }
  solana: {
    walletAddress: string  // Solana pubkey
    rpcUrl: string
  }
}

const DEFAULT_CONFIG: DualNetworkConfig = {
  base: {
    walletAddress: '',
    rpcUrl: 'https://mainnet.base.org',
    usdcContract: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
  },
  solana: {
    walletAddress: '',
    rpcUrl: 'https://api.mainnet-beta.solana.com',
  },
}

const TRANSFER_EVENT_ABI = [
  'event Transfer(address indexed from, address indexed to, uint256 value)',
]

/**
 * Dual-network x402 middleware.
 * Supports both Base (EVM) and Solana for USDC payments.
 */
export function x402DualMiddleware(
  amountUSDC: string,
  description: string,
  config?: Partial<DualNetworkConfig>
) {
  return async (c: Context, next: Next) => {
    // Resolve config from environment or defaults
    const baseWallet = config?.base?.walletAddress
      || c.env?.BASE_WALLET_ADDRESS || ''
    const solanaWallet = config?.solana?.walletAddress
      || c.env?.SOLANA_WALLET_ADDRESS || ''
    const baseRpc = config?.base?.rpcUrl
      || c.env?.BASE_RPC_URL || DEFAULT_CONFIG.base.rpcUrl
    const solanaRpc = config?.solana?.rpcUrl
      || c.env?.SOLANA_RPC_URL || DEFAULT_CONFIG.solana.rpcUrl
    const usdcContract = config?.base?.usdcContract
      || DEFAULT_CONFIG.base.usdcContract

    // Check for payment header
    const paymentHeader = c.req.header('X-PAYMENT')
      || c.req.header('X-PAYMENT-PROOF')
      || c.req.header('Payment')

    if (!paymentHeader) {
      // Return 402 with payment options for BOTH networks
      const amountRaw = (parseFloat(amountUSDC) * 1_000_000).toString()

      return c.json(
        {
          error: 'payment_required',
          amount: amountUSDC,
          currency: 'USDC',
          description,
          expires_in: 300,
          x402Version: 2,
          networks: {
            base: {
              address: baseWallet,
              usdcContract,
              chainId: 8453,
              estimatedFee: '<$0.01',
              estimatedFinality: '~2s',
            },
            solana: {
              address: solanaWallet,
              usdcMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
              estimatedFee: '<$0.001',
              estimatedFinality: '~400ms',
            },
          },
          paymentRequirements: [
            {
              scheme: 'exact',
              network: 'base',
              maxAmountRequired: amountRaw,
              resource: c.req.url,
              description,
              payTo: baseWallet,
              maxTimeoutSeconds: 300,
              asset: usdcContract,
            },
            {
              scheme: 'exact',
              network: 'solana',
              maxAmountRequired: amountRaw,
              resource: c.req.url,
              description,
              payTo: solanaWallet,
              maxTimeoutSeconds: 300,
              asset: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
            },
          ],
        },
        402
      )
    }

    // Determine which network the payment was made on
    const network = (
      c.req.header('X-PAYMENT-NETWORK') || 'base'
    ).toLowerCase()

    // Decode payment proof
    let txData: string
    try {
      const decoded = atob(paymentHeader)
      const payload = JSON.parse(decoded)
      txData = payload.transaction || payload.txHash || paymentHeader
    } catch {
      txData = paymentHeader
    }

    // Verify on the correct chain
    if (network === 'solana') {
      const result = await verifySolanaPayment(
        txData,
        solanaWallet,
        amountUSDC,
        solanaRpc
      )

      if (!result) {
        return c.json(
          { error: 'payment_verification_failed', network: 'solana' },
          400
        )
      }

      c.set('payment', {
        txHash: result.signature,
        amount: result.amount,
        from: result.from,
        network: 'solana',
        currency: 'USDC',
      })
    } else {
      // Default: verify on Base
      const result = await verifyBasePayment(
        txData,
        baseWallet,
        amountUSDC,
        baseRpc,
        usdcContract
      )

      if (!result) {
        return c.json(
          { error: 'payment_verification_failed', network: 'base' },
          400
        )
      }

      c.set('payment', {
        txHash: txData,
        amount: result.amount,
        from: result.from,
        network: 'base',
        currency: 'USDC',
      })
    }

    await next()
  }
}

// Base verification (same as before, inlined for self-contained module)
async function verifyBasePayment(
  txHash: string,
  expectedRecipient: string,
  expectedAmount: string,
  rpcUrl: string,
  usdcContract: string
): Promise<{ amount: string; from: string } | null> {
  try {
    const provider = new ethers.JsonRpcProvider(rpcUrl)
    const receipt = await provider.getTransactionReceipt(txHash)
    if (!receipt || receipt.status !== 1) return null

    const iface = new ethers.Interface(TRANSFER_EVENT_ABI)
    for (const log of receipt.logs) {
      if (log.address.toLowerCase() !== usdcContract.toLowerCase()) continue
      try {
        const parsed = iface.parseLog({
          topics: [...log.topics],
          data: log.data,
        })
        if (
          parsed?.name === 'Transfer' &&
          parsed.args.to.toLowerCase() === expectedRecipient.toLowerCase()
        ) {
          const paidAmount = ethers.formatUnits(parsed.args.value, 6)
          if (parseFloat(paidAmount) >= parseFloat(expectedAmount)) {
            return { amount: paidAmount, from: parsed.args.from }
          }
        }
      } catch { continue }
    }
    return null
  } catch { return null }
}

Base (Coinbase L2)

  • Chain ID: 8453
  • Finality: ~2 seconds
  • Tx fee: <$0.01
  • USDC: Native Circle deployment
  • Best for: EVM developers, Coinbase users

Solana

  • Cluster: mainnet-beta
  • Finality: ~400ms
  • Tx fee: <$0.001
  • USDC: $14.75B issued (55.7% market share)
  • Best for: Speed-critical, high-frequency

7. Test with curl (402 Response)

Before writing any client-side payment code, test the 402 flow manually with curl. This verifies that your middleware is correctly returning payment terms, headers, and the proper HTTP status code.

Start the development server and hit a protected endpoint:

terminal
# Start the dev server
npm run dev

# Hit a protected endpoint without payment
curl -i http://localhost:8787/api/search?q=test

# Expected response:
# HTTP/1.1 402 Payment Required
# X-PAYMENT-NETWORK: base
# X-PAYMENT-TOKEN: USDC
# X-PAYMENT-ADDRESS: 0xYourWalletAddress
# X-PAYMENT-AMOUNT: 3000
# X-PAYMENT-DECIMALS: 6
# Content-Type: application/json
#
# {
#   "error": "payment_required",
#   "amount": "0.003",
#   "currency": "USDC",
#   "description": "SERP search query",
#   "expires_in": 300,
#   "x402Version": 2,
#   "networks": {
#     "base": {
#       "address": "0xYourBaseAddress",
#       "usdcContract": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
#       "chainId": 8453,
#       "estimatedFee": "<$0.01",
#       "estimatedFinality": "~2s"
#     },
#     "solana": {
#       "address": "YourSolanaPublicKey",
#       "usdcMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
#       "estimatedFee": "<$0.001",
#       "estimatedFinality": "~400ms"
#     }
#   },
#   "paymentRequirements": [...]
# }

Now test with a payment proof header. For local development, you can temporarily add a bypass in your middleware or use a testnet transaction:

terminal
# Simulate a request WITH payment proof (using a real Base tx hash)
curl -i http://localhost:8787/api/search?q=test \
  -H "X-PAYMENT-PROOF: 0xabc123...your_tx_hash" \
  -H "X-PAYMENT-NETWORK: base"

# If the tx verifies successfully:
# HTTP/1.1 200 OK
# Content-Type: application/json
#
# {
#   "query": "test",
#   "results": [...],
#   "payment": {
#     "tx_hash": "0xabc123...",
#     "amount": "0.003000",
#     "network": "base"
#   }
# }

# If verification fails:
# HTTP/1.1 400 Bad Request
# {
#   "error": "payment_verification_failed",
#   "network": "base"
# }

Testing tip: Use Base Sepolia (testnet) for development. Set your RPC URL to https://sepolia.base.org and get test USDC from the Circle faucet. The USDC contract on Base Sepolia is 0x036CbD53842c5426634e7929541eC2318f3dCF7e.

8. Deploy to Cloudflare Workers / Node

Hono runs identically on Cloudflare Workers and Node.js. The deployment path depends on your infrastructure preference. Cloudflare Workers gives you global edge distribution with zero cold starts; Node.js gives you more control and a familiar hosting model.

Option A: Cloudflare Workers (Recommended)

Configure your wrangler.toml with environment secrets:

wrangler.toml
# wrangler.toml
name = "my-paid-api"
main = "src/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]

[vars]
# Non-secret configuration
BASE_RPC_URL = "https://mainnet.base.org"
SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com"

# Secrets (set via wrangler secret put)
# WALLET_ADDRESS
# BASE_WALLET_ADDRESS
# SOLANA_WALLET_ADDRESS
terminal
# Set wallet addresses as secrets (not checked into source)
wrangler secret put BASE_WALLET_ADDRESS
# Enter: 0xYourBaseWalletAddress

wrangler secret put SOLANA_WALLET_ADDRESS
# Enter: YourSolanaPublicKey

# Deploy to production
wrangler deploy

# Output:
# Uploaded my-paid-api (2.34 sec)
# Published my-paid-api (0.47 sec)
#   https://my-paid-api.your-subdomain.workers.dev

Option B: Node.js Server

To run on Node.js instead, install the Node.js adapter and create an entry point:

terminal
npm install @hono/node-server
src/node-server.ts
// src/node-server.ts
import { serve } from '@hono/node-server'
import app from './index'

// Load environment variables
process.env.BASE_WALLET_ADDRESS = process.env.BASE_WALLET_ADDRESS || ''
process.env.SOLANA_WALLET_ADDRESS = process.env.SOLANA_WALLET_ADDRESS || ''

const port = parseInt(process.env.PORT || '3000')

serve({
  fetch: app.fetch,
  port,
})

console.log(`x402-enabled API running on http://localhost:${port}`)
console.log('Protected endpoints:')
console.log('  GET /api/search?q=...     ($0.003/query)')
console.log('  GET /api/premium-data     ($0.05/request)')
console.log('  POST /api/bulk-export     ($0.50/export)')
terminal
# Run with Node.js
npx tsx src/node-server.ts

# Or add to package.json scripts:
# "start:node": "tsx src/node-server.ts"

# Deploy to any Node.js host: Railway, Render, Fly.io, VPS, etc.

Cloudflare Workers

  • + Global edge (300+ data centers)
  • + Zero cold starts
  • + Free tier: 100K requests/day
  • + KV storage for tx hash caching
  • - 128MB memory limit per invocation

Node.js

  • + Full Node.js API access
  • + No memory/CPU limits
  • + Use any NPM package
  • + Familiar deployment model
  • - You manage scaling/hosting

9. Full SERP Scraper with Proxies.sx + x402

Here is the payoff: a complete, production-ready SERP (Search Engine Results Page) scraping API monetized with x402 micropayments. Each search query costs $0.003 in USDC. The API uses Proxies.sx 4G/5G mobile proxies to scrape search results through real mobile IPs, avoiding detection and rate limiting.

This example ties everything together: x402 payment gating, on-chain verification, proxy-based scraping, and structured JSON responses.

src/routes/serp.ts
// src/routes/serp.ts
// Full SERP scraping API with x402 payment gating + Proxies.sx mobile proxies
import { Hono } from 'hono'
import { x402DualMiddleware } from '../middleware/x402-dual'

type Bindings = {
  BASE_WALLET_ADDRESS: string
  SOLANA_WALLET_ADDRESS: string
  BASE_RPC_URL: string
  SOLANA_RPC_URL: string
  PROXY_HOST: string      // e.g., "us-4g.proxies.sx"
  PROXY_PORT: string      // e.g., "5057"
  PROXY_USER: string      // Proxies.sx username
  PROXY_PASS: string      // Proxies.sx password
}

const serp = new Hono<{ Bindings: Bindings }>()

// Discovery endpoint: describe the API and pricing
serp.get('/serp', (c) => {
  return c.json({
    service: 'SERP Scraping API',
    version: '1.0.0',
    pricing: {
      '/serp/search': {
        method: 'GET',
        cost: '$0.003 USDC per query',
        params: {
          q: 'Search query (required)',
          num: 'Number of results (1-20, default 10)',
          gl: 'Country code (us, uk, de, etc.)',
        },
      },
    },
    networks: ['base', 'solana'],
    protocol: 'x402',
    docs: 'https://proxies.sx/blog/build-paid-api-x402-usdc-tutorial',
  })
})

// x402-protected search endpoint: $0.003 per query
serp.get(
  '/serp/search',
  x402DualMiddleware('0.003', 'SERP search query - real-time results via mobile proxy'),
  async (c) => {
    const query = c.req.query('q')
    if (!query) {
      return c.json({ error: 'Missing required parameter: q' }, 400)
    }

    const num = Math.min(parseInt(c.req.query('num') || '10'), 20)
    const gl = c.req.query('gl') || 'us'
    const payment = c.get('payment')

    try {
      // Build the search URL
      const searchUrl = new URL('https://www.google.com/search')
      searchUrl.searchParams.set('q', query)
      searchUrl.searchParams.set('num', num.toString())
      searchUrl.searchParams.set('gl', gl)
      searchUrl.searchParams.set('hl', 'en')

      // Fetch through Proxies.sx mobile proxy (SOCKS5)
      // In Cloudflare Workers, use the fetch API with proxy config
      // For Node.js, use undici or node-fetch with proxy-agent
      const proxyUrl = `http://${c.env.PROXY_USER}:${c.env.PROXY_PASS}@${c.env.PROXY_HOST}:${c.env.PROXY_PORT}`

      const response = await fetch(searchUrl.toString(), {
        headers: {
          'User-Agent':
            'Mozilla/5.0 (Linux; Android 14; Pixel 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36',
          'Accept-Language': 'en-US,en;q=0.9',
          Accept:
            'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        },
        // @ts-expect-error - Proxy support varies by runtime
        agent: createProxyAgent(proxyUrl),
      })

      if (!response.ok) {
        return c.json(
          {
            error: 'scrape_failed',
            status: response.status,
            message: 'Search engine returned non-200 status',
          },
          502
        )
      }

      const html = await response.text()

      // Parse search results from HTML
      const results = parseSearchResults(html, num)

      return c.json({
        query,
        country: gl,
        results_count: results.length,
        results,
        scraped_at: new Date().toISOString(),
        proxy: {
          type: '4g_mobile',
          country: gl.toUpperCase(),
          provider: 'proxies.sx',
        },
        payment: {
          tx_hash: payment.txHash,
          amount: payment.amount,
          network: payment.network,
          currency: payment.currency,
        },
      })
    } catch (error) {
      return c.json(
        {
          error: 'internal_error',
          message: error instanceof Error ? error.message : 'Unknown error',
        },
        500
      )
    }
  }
)

/**
 * Parse Google search results from raw HTML.
 * In production, use a proper HTML parser like cheerio.
 */
function parseSearchResults(
  html: string,
  maxResults: number
): Array<{ position: number; title: string; url: string; snippet: string }> {
  const results: Array<{
    position: number
    title: string
    url: string
    snippet: string
  }> = []

  // Simple regex-based extraction (use cheerio in production)
  const linkPattern = /<a href="\/url\?q=([^"&]+)[^"]*"[^>]*>.*?<h3[^>]*>(.*?)<\/h3>/gs
  let match
  let position = 1

  while (
    (match = linkPattern.exec(html)) !== null &&
    results.length < maxResults
  ) {
    const url = decodeURIComponent(match[1])
    const title = match[2].replace(/<[^>]*>/g, '') // Strip HTML tags

    if (url.startsWith('http')) {
      results.push({
        position,
        title,
        url,
        snippet: '', // Extract from adjacent div in production
      })
      position++
    }
  }

  return results
}

export default serp

Wire the SERP router into your main application:

src/index.ts (final)
// src/index.ts (updated)
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import serp from './routes/serp'

const app = new Hono()

app.use('*', cors())

// Health check
app.get('/', (c) => c.json({ status: 'running', x402: true }))

// Mount the SERP scraping API
app.route('/api', serp)

// x402 discovery endpoint (well-known)
app.get('/.well-known/x402.json', (c) => {
  return c.json({
    x402Version: 2,
    seller: {
      name: 'My Paid API',
      description: 'SERP scraping API with mobile proxy support',
      url: 'https://my-paid-api.workers.dev',
    },
    endpoints: [
      {
        path: '/api/serp/search',
        method: 'GET',
        price: '0.003',
        currency: 'USDC',
        networks: ['base', 'solana'],
        description: 'Search engine results via mobile proxy',
      },
    ],
    marketplace: 'https://agents.proxies.sx/marketplace/',
  })
})

export default app

Test the complete flow end-to-end:

terminal
# 1. Discover pricing
curl http://localhost:8787/api/serp
# {
#   "service": "SERP Scraping API",
#   "pricing": {
#     "/serp/search": { "cost": "$0.003 USDC per query" }
#   },
#   "networks": ["base", "solana"],
#   "protocol": "x402"
# }

# 2. Try to search without payment
curl -i "http://localhost:8787/api/serp/search?q=best+mobile+proxies"
# HTTP/1.1 402 Payment Required
# {"error":"payment_required","amount":"0.003","currency":"USDC",...}

# 3. Check the well-known x402 discovery endpoint
curl http://localhost:8787/.well-known/x402.json
# { "x402Version": 2, "endpoints": [...] }

# 4. An AI agent discovers this API, pays $0.003 USDC on Base,
#    and retries with the payment proof header.
#    The server verifies, scrapes via Proxies.sx mobile proxy,
#    and returns structured SERP data.

Why Proxies.sx mobile proxies? Search engines aggressively block datacenter IPs. Proxies.sx provides real 4G/5G mobile IPs from 15+ countries with a 92% success rate on major platforms. At $4-6/GB, the proxy cost per search query is approximately $0.0005 (assuming ~100KB per SERP page), leaving healthy margins on the $0.003 per-query price.

Production Hardening Checklist

Before going live with your x402 API, address these production concerns:

Replay Attack Prevention

Store verified transaction hashes in a KV store or database. Reject any payment proof that has been used before. Cloudflare KV or Redis are ideal for this.

Rate Limiting

Even with payment gating, rate-limit by wallet address to prevent abuse. A wallet making 1,000 requests/second may be attempting to exploit race conditions in verification.

RPC Reliability

Use a dedicated RPC provider (Alchemy, QuickNode, Helius for Solana) instead of public endpoints. Public RPCs have aggressive rate limits and no SLA.

Amount Precision

Always compare amounts using integer arithmetic (multiply by 10^6 for USDC) rather than floating point. Floating point rounding errors can cause valid payments to be rejected.

Transaction Logging

Log every payment verification attempt (success and failure) with the tx hash, claimed amount, verified amount, and wallet address. This is essential for debugging and dispute resolution.

Confirmation Depth

For high-value requests ($1+), wait for multiple block confirmations before granting access. On Base, 2-3 blocks (6-9 seconds) provides strong finality. For micropayments, 1 confirmation is sufficient.

Complete Project Structure

final project structure
my-paid-api/
  src/
    index.ts                    # Main app + route mounting
    node-server.ts              # Node.js entry (optional)
    middleware/
      x402.ts                   # Single-network x402 middleware
      x402-dual.ts              # Dual-network (Base + Solana)
      solana-verify.ts          # Solana payment verification
    routes/
      serp.ts                   # SERP scraping API
  wrangler.toml                 # Cloudflare Workers config
  package.json
  tsconfig.json

Frequently Asked Questions

Start Building with x402 Today

The x402 protocol has processed over 100 million payments since launch. USDC has a $72B+ market cap. Base settles in 2 seconds for under a penny. The infrastructure is ready. If your API has value, you can monetize every single request — starting now.

Related Articles