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
Merchant Wallet
An Ethereum wallet address where you’ll receive customer payments
Private Key Access
Access to the wallet’s private key (for one-time USDC approval)
Base Network Assets
ETH : ~0.001 ETH for approval transaction gas
USDC : ~100 USDC for fees (covers ~10,000 settlements)
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
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
Option A: Using Our Setup Script (Recommended)
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.01 Example: 100 USDC approval = 10,000 settlements
Step 3: Verify Setup
Check that approval was successful:
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:
Option A: x402 v2 (Recommended)
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
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
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
Base Sepolia (Testnet)
Resource Value Chain ID 84532USDC 0x036CbD53842c5426634e7929541eC2318f3dCF7eTreasury 0x5D791e3554D0e83f171126905Bda1640Bf6f9A8BRPC https://sepolia.base.orgExplorer https://sepolia.basescan.orgFaucet https://www.coinbase.com/faucets/base-ethereum-sepolia-faucet
Base Mainnet (Production)
Resource Value Chain ID 8453USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913Treasury 0x5D791e3554D0e83f171126905Bda1640Bf6f9A8BRPC https://mainnet.base.orgExplorer https://basescan.org
Troubleshooting
Approval transaction fails
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
Settlement fails with 'insufficient_allowance'
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
Customer payments not arriving
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.