Skip to main content

Overview

This guide walks you through setting up your merchant account to accept x402 payments via 0xmeta facilitator with pre-settlement fee collection.
Trust-Minimized: Customer payments go directly to your address. The facilitator collects a $0.01 fee from your pre-approved USDC balance after each settlement.

Prerequisites

1

Merchant Wallet

An Ethereum wallet address where you’ll receive customer payments
2

Private Key Access

Access to the wallet’s private key (for one-time USDC approval)
3

Base Network Assets

  • ETH: ~0.001 ETH for approval transaction gas
  • USDC: ~100 USDC for fees (covers ~10,000 settlements)
4

Network Selection

Choose Base Mainnet (production) or Base Sepolia (testing)

Step 1: Get Testnet Assets (For Testing)

If testing on Base Sepolia first (recommended):

Get Base Sepolia ETH

Visit the Coinbase faucet:
https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet
  • Connect your merchant wallet
  • Request 0.05 ETH (free, once per 24 hours)
  • Wait ~30 seconds for confirmation

Get Base Sepolia USDC

Visit the Circle faucet:
https://faucet.circle.com/
  • Select “Base Sepolia”
  • Enter your merchant address
  • Request 10 USDC
  • Wait ~1 minute for confirmation
Verify you received the tokens on BaseScan Sepolia

Step 2: Approve USDC Spending

This is the only required setup step. You must approve 0xmeta’s treasury to collect the $0.01 fee per settlement from your USDC balance.

Why Approval Is Required

The facilitator uses standard ERC-20 transferFrom to collect fees:
// You approve facilitator (one-time)
USDC.approve(treasury, 100 * 10^6); // 100 USDC

// Later, facilitator collects fee per settlement
USDC.transferFrom(merchant, treasury, 0.01 * 10^6); // $0.01
This is how the facilitator maintains x402 trust-minimization:
  • ✅ Customers pay YOU directly
  • ✅ Facilitator collects fee from YOUR approved balance
  • ✅ No facilitator custody of customer funds
1. Install dependencies:
npm install ethers dotenv
2. Configure environment:
# Create .env file
cat > .env << EOF
EVM_PRIVATE_KEY=0x...  # Your merchant private key
NETWORK=sepolia        # or mainnet
EOF
3. Run approval:
node approve-facilitator.mjs
Expected output:
📍 Base Sepolia
   Merchant: 0xA821f428Ef8cC9f54A9915336A82220853059090
   Treasury: 0x5D791e3554D0e83f171126905Bda1640Bf6f9A8B

💰 Approving 100 USDC for fee collection...
   Transaction: 0x123...
✅ Approval successful!
   Allowance: 100.0 USDC
   Settlements: ~10000
Script source: See approve-facilitator.mjs

Option B: Manual Approval (via Etherscan)

1. Visit Base USDC contract: Sepolia:
https://sepolia.basescan.org/address/0x036CbD53842c5426634e7929541eC2318f3dCF7e#writeContract
Mainnet:
https://basescan.org/address/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913#writeContract
2. Connect your wallet 3. Call approve function:
  • spender: 0x5D791e3554D0e83f171126905Bda1640Bf6f9A8B (Treasury)
  • amount: 100000000 (100 USDC with 6 decimals)
4. Confirm transaction
Approval amount = settlements × $0.01Example: 100 USDC approval = 10,000 settlements

Step 3: Verify Setup

Check that approval was successful:
node check-allowance.mjs
Expected output:
============================================================
0xmeta Facilitator Fee Allowance Check
============================================================

👤 Merchant: 0xA821f428Ef8cC9f54A9915336A82220853059090
🏦 Treasury: 0x5D791e3554D0e83f171126905Bda1640Bf6f9A8B

📍 Base Sepolia
   USDC Balance: 500.00 USDC
   Fee Allowance: 100.00 USDC
   Settlements Remaining: 10000
   ✅ Good

============================================================
Script source: See check-allowance.mjs

Step 4: Integrate x402 Middleware

Now you’re ready to accept payments! Choose your x402 version:
import { config } from "dotenv";
import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";

config();

const evmAddress = process.env.EVM_ADDRESS as `0x${string}`;
const facilitatorUrl = "https://facilitator.0xmeta.ai/v1";

const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl });
const app = express();

app.use(
  paymentMiddleware(
    {
      "GET /premium-content": {
        accepts: [{
          scheme: "exact",
          price: "$0.02",           // Your price + fee
          network: "eip155:84532",  // Base Sepolia
          payTo: evmAddress,        // YOUR address
        }],
        description: "Premium content access"
      }
    },
    new x402ResourceServer(facilitatorClient)
      .register("eip155:84532", new ExactEvmScheme())
  )
);

app.get("/premium-content", (req, res) => {
  res.json({ content: "This is premium content" });
});

app.listen(4021, () => {
  console.log("Server ready at http://localhost:4021");
});

Option B: x402 v1 (Legacy)

import { config } from "dotenv";
import express from "express";
import { paymentMiddleware } from "x402-express";

config();

const evmAddress = process.env.EVM_ADDRESS as `0x${string}`;
const facilitatorUrl = "https://facilitator.0xmeta.ai/v1";

const app = express();

app.use(
  paymentMiddleware(
    evmAddress,  // YOUR address
    {
      "GET /premium-content": {
        price: "$0.02",
        network: "base-sepolia",
      }
    },
    { url: facilitatorUrl }
  )
);

app.get("/premium-content", (req, res) => {
  res.json({ content: "This is premium content" });
});

app.listen(4021);

Step 5: Test Payment Flow

1. Start Your Server

npm start

2. Test with x402 Client

Create a test client (or use our example):
import { x402Client, wrapAxiosWithPayment } from "@x402/axios";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { privateKeyToAccount } from "viem/accounts";
import axios from "axios";

const clientPrivateKey = process.env.CLIENT_PRIVATE_KEY as `0x${string}`;
const evmSigner = privateKeyToAccount(clientPrivateKey);

const client = new x402Client();
registerExactEvmScheme(client, { signer: evmSigner });

const api = wrapAxiosWithPayment(axios.create(), client);

// Make request
const response = await api.get("http://localhost:4021/premium-content");
console.log(response.data); // { content: "This is premium content" }

3. Expected Flow

1. ✅ Client requests /premium-content
2. ✅ Server returns 402 Payment Required
   Headers: payTo = YOUR_MERCHANT_ADDRESS
3. ✅ Client creates authorization to YOUR address
4. ✅ Client sends to facilitator
5. ✅ Facilitator verifies authorization
6. ✅ Facilitator collects $0.01 from YOUR approved balance
7. ✅ Facilitator executes: Customer → YOU ($0.02)
8. ✅ Server returns 200 OK + premium content
Success! If you see the premium content, your integration is working.

Step 6: Monitor Allowance

Set up regular monitoring to ensure you don’t run out of approved funds:

Manual Check

node check-allowance.mjs

Automated Monitoring

Option 1: Cron job
# Check allowance daily at 9 AM
0 9 * * * cd /path/to/project && node check-allowance.mjs
Option 2: Application monitoring
import { ethers } from "ethers";

async function checkAllowance() {
  const provider = new ethers.JsonRpcProvider("https://mainnet.base.org");
  const usdc = new ethers.Contract(USDC_ADDRESS, USDC_ABI, provider);
  
  const allowance = await usdc.allowance(merchantAddress, treasuryAddress);
  const settlements = Math.floor(Number(ethers.formatUnits(allowance, 6)) / 0.01);
  
  if (settlements < 100) {
    alert("Low allowance! Top up soon.");
  }
  
  return settlements;
}

// Check every hour
setInterval(checkAllowance, 3600000);

Step 7: Production Deployment

When ready for production:

Switch to Base Mainnet

1. Update configuration:
// v2
network: "eip155:8453"  // Base Mainnet

// v1
network: "base"
2. Run approval on mainnet:
# Get real USDC and ETH first!
NETWORK=mainnet node approve-facilitator.mjs
3. Update environment:
EVM_ADDRESS=0x...  # Your mainnet address
NETWORK=mainnet

Production Checklist

1

Approval Confirmed

  • Approved ≥ 100 USDC on mainnet
  • Verified via check-allowance.mjs
2

Testing Complete

  • Tested full payment flow on Sepolia
  • Verified customer payments received
  • Confirmed fee collection working
3

Monitoring Setup

  • Automated allowance checks configured
  • Alerts for low balance set up
  • Settlement logging in place
4

Security

  • Private keys secured (not in code)
  • .env file in .gitignore
  • Reasonable approval amount (not infinite)

Network Information

Base Sepolia (Testnet)

ResourceValue
Chain ID84532
USDC0x036CbD53842c5426634e7929541eC2318f3dCF7e
Treasury0x5D791e3554D0e83f171126905Bda1640Bf6f9A8B
RPChttps://sepolia.base.org
Explorerhttps://sepolia.basescan.org
Faucethttps://www.coinbase.com/faucets/base-ethereum-sepolia-faucet

Base Mainnet (Production)

ResourceValue
Chain ID8453
USDC0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
Treasury0x5D791e3554D0e83f171126905Bda1640Bf6f9A8B
RPChttps://mainnet.base.org
Explorerhttps://basescan.org

Troubleshooting

Causes:
  • Insufficient ETH for gas
  • Wrong network selected
  • Invalid treasury address
Solution:
  • Check ETH balance: Must have ~0.001 ETH
  • Verify you’re on correct network
  • Double-check treasury address: 0x5D791e3554D0e83f171126905Bda1640Bf6f9A8B
Cause: Approval wasn’t successful or depletedSolution:
# Check current allowance
node check-allowance.mjs

# If zero, run approval again
node approve-facilitator.mjs
Cause: Network issues or RPC rate limitingSolution:
  • Wait a few minutes and try again
  • Use custom RPC (Alchemy, Infura, QuickNode)
  • Check Base network status
Cause: Wrong address in payToSolution: Verify payTo uses YOUR merchant address (not treasury):
// ✅ Correct
payTo: process.env.EVM_ADDRESS  // Your address

// ❌ Wrong
payTo: "0x5D791e3554D0e83f171126905Bda1640Bf6f9A8B"  // Treasury

Script References

Approval Script Reference

Save as approve-facilitator.mjs:
import { ethers } from "ethers";
import dotenv from "dotenv";
dotenv.config();

const NETWORKS = {
  sepolia: {
    name: "Base Sepolia",
    rpc: "https://sepolia.base.org",
    chainId: 84532,
    usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  },
  mainnet: {
    name: "Base Mainnet",
    rpc: "https://mainnet.base.org",
    chainId: 8453,
    usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  },
};

const OXMETA_TREASURY = "0x5D791e3554D0e83f171126905Bda1640Bf6f9A8B";
const USDC_ABI = [
  "function approve(address spender, uint256 amount) external returns (bool)",
  "function allowance(address owner, address spender) external view returns (uint256)",
];

async function main() {
  const networkKey = process.env.NETWORK || "sepolia";
  const network = NETWORKS[networkKey];
  
  const privateKey = process.env.EVM_PRIVATE_KEY;
  if (!privateKey) {
    console.error("❌ EVM_PRIVATE_KEY required");
    process.exit(1);
  }
  
  const provider = new ethers.JsonRpcProvider(network.rpc);
  const merchant = new ethers.Wallet(privateKey, provider);
  const usdc = new ethers.Contract(network.usdc, USDC_ABI, merchant);
  
  console.log(`\n📍 ${network.name}`);
  console.log(`   Merchant: ${merchant.address}`);
  console.log(`   Treasury: ${OXMETA_TREASURY}`);
  
  const approvalAmount = ethers.parseUnits("100", 6); // 100 USDC
  
  console.log(`\n💰 Approving 100 USDC...`);
  const tx = await usdc.approve(OXMETA_TREASURY, approvalAmount);
  console.log(`   Transaction: ${tx.hash}`);
  
  await tx.wait();
  console.log(`✅ Approval successful!`);
  
  const allowance = await usdc.allowance(merchant.address, OXMETA_TREASURY);
  console.log(`   Allowance: ${ethers.formatUnits(allowance, 6)} USDC`);
  console.log(`   Settlements: ~${Math.floor(Number(ethers.formatUnits(allowance, 6)) / 0.01)}`);
}

main().catch(console.error);

Monitoring Script Reference

Save as check-allowance.mjs:
import { ethers } from "ethers";
import dotenv from "dotenv";
dotenv.config();

const NETWORKS = {
  sepolia: {
    name: "Base Sepolia",
    rpc: "https://sepolia.base.org",
    usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
  },
  mainnet: {
    name: "Base Mainnet",
    rpc: "https://mainnet.base.org",
    usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  },
};

const OXMETA_TREASURY = "0x5D791e3554D0e83f171126905Bda1640Bf6f9A8B";
const USDC_ABI = [
  "function allowance(address owner, address spender) external view returns (uint256)",
  "function balanceOf(address account) external view returns (uint256)",
];

async function checkNetwork(networkKey, merchantAddress) {
  const network = NETWORKS[networkKey];
  const provider = new ethers.JsonRpcProvider(network.rpc);
  const usdc = new ethers.Contract(network.usdc, USDC_ABI, provider);
  
  const [allowance, balance] = await Promise.all([
    usdc.allowance(merchantAddress, OXMETA_TREASURY),
    usdc.balanceOf(merchantAddress),
  ]);
  
  const allowanceFormatted = ethers.formatUnits(allowance, 6);
  const balanceFormatted = ethers.formatUnits(balance, 6);
  const settlements = Math.floor(Number(allowanceFormatted) / 0.01);
  
  console.log(`\n📍 ${network.name}`);
  console.log(`   USDC Balance: ${balanceFormatted} USDC`);
  console.log(`   Fee Allowance: ${allowanceFormatted} USDC`);
  console.log(`   Settlements Remaining: ${settlements}`);
  
  if (settlements < 10) {
    console.log(`   ⚠️  Low - run: NETWORK=${networkKey} node approve-facilitator.mjs`);
  } else if (settlements < 50) {
    console.log(`   ⚠️  Getting low`);
  } else {
    console.log(`   ✅ Good`);
  }
}

async function main() {
  const merchantAddress = process.env.EVM_ADDRESS;
  if (!merchantAddress) {
    console.error("❌ Set EVM_ADDRESS in .env");
    process.exit(1);
  }
  
  console.log("============================================================");
  console.log("0xmeta Facilitator Fee Allowance Check");
  console.log("============================================================");
  console.log(`\n👤 Merchant: ${merchantAddress}`);
  console.log(`🏦 Treasury: ${OXMETA_TREASURY}`);
  
  const networkArg = process.argv[2] || "both";
  
  if (networkArg === "both") {
    await checkNetwork("sepolia", merchantAddress);
    await checkNetwork("mainnet", merchantAddress);
  } else {
    await checkNetwork(networkArg, merchantAddress);
  }
  
  console.log("\n============================================================");
}

main().catch(console.error);

Next Steps

You’re all set! Start accepting x402 payments with pre-settlement fee collection.