Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.0xmeta.ai/llms.txt

Use this file to discover all available pages before exploring further.

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

x402 Integration

Complete integration guide

Architecture

Understand payment flow

API Reference

Explore endpoints

FAQ

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