Skip to main content

x402 Payment Protocol

MockUSDT0 Token

This guide currently uses a MockUSDT0 token for demonstration purposes. FXRP will be supported once it implements the required EIP-3009 standard (transferWithAuthorization). This guide will be updated when FXRP gains EIP-3009 support.

Overview

The x402 protocol enables HTTP-native payments using the long-reserved HTTP 402 "Payment Required" status code. This guide demonstrates how to implement x402 payments on Flare using EIP-3009 for gasless payment settlements.

Key benefits of the x402 protocol:

  • HTTP-native: Payments are integrated directly into HTTP request/response flow.
  • Gasless for payers: Users sign authorizations off-chain; servers settle on-chain.
  • Front-running protection: EIP-3009's receiveWithAuthorization prevents transaction front-running.
  • Idempotent: Unique nonces ensure each payment can only be processed once.

Architecture

The x402 implementation consists of four components:

  1. MockUSDT0: ERC-20 token with EIP-3009 support (transferWithAuthorization).
  2. X402Facilitator: Contract that verifies and settles EIP-3009 authorizations.
  3. Server: Express backend implementing the x402 payment flow.
  4. Agent: CLI tool for making automated payments.

EIP-3009 Payment Flow

┌─────────┐     1. Request Resource      ┌─────────────┐
│ Agent │ ─────────────────────────────▶│ Server │
│ │ ◀───────────────────────────── │ │
│ │ 2. 402 + Payment Req │ │
│ │ │ │
│ │ 3. Sign EIP-712 Auth │ │
│ │ (off-chain signature) │ │
│ │ │ │
│ │ 4. Request + X-Payment │ │
│ │ ─────────────────────────────▶│ │
│ │ ┌───────────┴─────────────┤
│ │ 5. settlePayment() │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────┐ │
│ │ │ Facilitator │ │
│ │ └──────┬──────┘ │
│ │ │ │
│ │ 6. transferWithAuthorization │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────┐ │
│ │ │ MockUSDT0 │ │
│ │ └─────────────┘ │
│ │ │ │
│ │ ◀───────────────────────────── │ │
└─────────┘ 7. 200 OK + Resource └─────────────┘

The flow works as follows:

  1. Client requests a protected resource.
  2. Server returns HTTP 402 with payment requirements.
  3. Client signs an EIP-712 authorization (off-chain, no gas required).
  4. Client resends request with X-Payment header containing the signed authorization.
  5. Server calls settlePayment() on the Facilitator contract.
  6. Facilitator executes transferWithAuthorization to transfer tokens.
  7. Server returns the requested resource with payment confirmation.

Prerequisites

Before starting, ensure you have:

Step 1: Deploy Contracts

Deploy the MockUSDT0 token and X402Facilitator contracts.

yarn hardhat run scripts/x402/deploy.ts --network coston2

The deployment script outputs contract addresses. Add them to your .env file:

X402_TOKEN_ADDRESS=0x...
X402_FACILITATOR_ADDRESS=0x...
X402_PAYEE_ADDRESS=0x...
View deployment script
scripts/x402/deploy.ts
/**
* x402 Demo Deployment Script
*
* Deploys MockUSDT0 and X402Facilitator contracts.
*
* Usage:
* yarn hardhat run scripts/x402/deploy.ts --network coston2
*/

import { run, web3, artifacts } from "hardhat";

const MockUSDT0 = artifacts.require("MockUSDT0");
const X402Facilitator = artifacts.require("X402Facilitator");

async function deployAndVerify() {
const [deployer] = await web3.eth.getAccounts();

console.log("═".repeat(60));
console.log("x402 Demo Deployment");
console.log("═".repeat(60));
console.log(`Deployer: ${deployer}`);
const balance = await web3.eth.getBalance(deployer);
console.log(`Balance: ${web3.utils.fromWei(balance, "ether")} C2FLR`);
console.log("─".repeat(60));

// Deploy MockUSDT0
console.log("\n📦 Deploying MockUSDT0...");
const mockUSDT0 = await MockUSDT0.new();
console.log(` MockUSDT0 deployed to: ${mockUSDT0.address}`);

// Get token info
const name = await mockUSDT0.name();
const symbol = await mockUSDT0.symbol();
const decimals = await mockUSDT0.decimals();
const totalSupply = await mockUSDT0.totalSupply();
console.log(` Name: ${name}`);
console.log(` Symbol: ${symbol}`);
console.log(` Decimals: ${decimals}`);
console.log(
` Initial Supply: ${web3.utils.fromWei(totalSupply.toString(), "mwei")}`,
);

// Deploy X402Facilitator
console.log("\n📦 Deploying X402Facilitator...");
const feeBps = 0; // No fees for demo
const facilitatorArgs = [deployer, feeBps];
const facilitator = await X402Facilitator.new(deployer, feeBps);
console.log(` X402Facilitator deployed to: ${facilitator.address}`);

// Add token as supported
console.log("\n⚙️ Configuring facilitator...");
await facilitator.addSupportedToken(mockUSDT0.address);
console.log(` Added MockUSDT0 as supported token`);

// Summary
console.log("\n" + "═".repeat(60));
console.log("Deployment Summary");
console.log("═".repeat(60));
console.log(`MockUSDT0: ${mockUSDT0.address}`);
console.log(`X402Facilitator: ${facilitator.address}`);
console.log(`Payee Address: ${deployer}`);
console.log("─".repeat(60));
console.log("\n📝 Add these to your .env file:");
console.log(`X402_TOKEN_ADDRESS=${mockUSDT0.address}`);
console.log(`X402_FACILITATOR_ADDRESS=${facilitator.address}`);
console.log(`X402_PAYEE_ADDRESS=${deployer}`);
console.log("─".repeat(60));

// Verify contracts
console.log("\n🔍 Verifying contracts on explorer...");

try {
await run("verify:verify", {
address: mockUSDT0.address,
constructorArguments: [],
});
console.log(" MockUSDT0 verified");
} catch (e: unknown) {
const msg = e instanceof Error ? e.message.slice(0, 100) : String(e);
console.log(` MockUSDT0 verification: ${msg}`);
}

try {
await run("verify:verify", {
address: facilitator.address,
constructorArguments: facilitatorArgs,
});
console.log(" X402Facilitator verified");
} catch (e: unknown) {
const msg = e instanceof Error ? e.message.slice(0, 100) : String(e);
console.log(` X402Facilitator verification: ${msg}`);
}

console.log("\n✅ Deployment complete!");
}

void deployAndVerify().then(() => {
process.exit(0);
});

Deployment Output

═══════════════════════════════════════════════════════════
x402 Demo Deployment
═══════════════════════════════════════════════════════════
Deployer: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Balance: 100.0 C2FLR
──────────────────────────────────────────────────────────

📦 Deploying MockUSDT0...
MockUSDT0 deployed to: 0x1234...
Name: Mock USDT0
Symbol: USDT0
Decimals: 6

📦 Deploying X402Facilitator...
X402Facilitator deployed to: 0x5678...

⚙️ Configuring facilitator...
Added MockUSDT0 as supported token

═══════════════════════════════════════════════════════════
Deployment Summary
═══════════════════════════════════════════════════════════
MockUSDT0: 0x1234...
X402Facilitator: 0x5678...
Payee Address: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e

Step 2: Test EIP-3009 Directly

Before running the full x402 flow, verify EIP-3009 works correctly:

yarn hardhat run scripts/x402/testEip3009.ts --network coston2

This script:

  1. Mints test tokens if needed.
  2. Creates an EIP-712 signed authorization.
  3. Executes transferWithAuthorization.
  4. Verifies nonce replay protection.

Step 3: Start the Server

The server implements the x402 payment middleware.

npx ts-node scripts/x402/server.ts

The server runs on http://localhost:3402 and exposes:

EndpointPriceDescription
GET /api/publicFreePublic data
GET /api/premium-data0.1 USDT0Premium data
GET /api/report0.5 USDT0Detailed report
GET /healthFreeHealth check
View server implementation
scripts/x402/server.ts
/**
* x402 Payment Demo Server
*
* Run: npx ts-node scripts/x402/server.ts
*
* This implements the x402 HTTP payment protocol with EIP-3009 support.
*/

import express, {
type Request,
type Response,
type NextFunction,
} from "express";
import cors from "cors";
import { ethers } from "ethers";
import * as fs from "fs";
import * as path from "path";
import "dotenv/config";
import rateLimit from "express-rate-limit";

const app = express();
app.use(cors());

const rootRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});
app.use(express.json());

// Configuration
const PORT = process.env.X402_PORT || 3402;
const COSTON2_RPC =
process.env.COSTON2_RPC_URL || "https://coston2-api.flare.network/ext/C/rpc";
const TOKEN_ADDRESS = process.env.X402_TOKEN_ADDRESS || "";
const FACILITATOR_ADDRESS = process.env.X402_FACILITATOR_ADDRESS || "";
const PAYEE_ADDRESS = process.env.X402_PAYEE_ADDRESS || "";
const PRIVATE_KEY = process.env.PRIVATE_KEY || "";

// ABIs (minimal)
const FACILITATOR_ABI = [
"function verifyPayment((address from, address to, address token, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)) view returns (bytes32 paymentId, bool valid)",
"function settlePayment((address from, address to, address token, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)) returns (bytes32)",
];

// Provider setup
const provider = new ethers.JsonRpcProvider(COSTON2_RPC);

// x402 Payment requirement structure
interface PaymentRequirement {
scheme: string;
network: string;
maxAmountRequired: string;
resource: string;
description: string;
mimeType: string;
payTo: string;
maxTimeoutSeconds: number;
asset: string;
extra: {
tokenAddress: string;
facilitatorAddress: string;
chainId: number;
};
}

// x402 Payment payload from client
interface PaymentPayload {
from: string;
to: string;
token: string;
value: string;
validAfter: string;
validBefore: string;
nonce: string;
v: number;
r: string;
s: string;
}

// Resources that require payment
const PAID_RESOURCES: Record<string, { price: bigint; content: () => object }> =
{
"/api/premium-data": {
price: BigInt(100000), // 0.1 USDT0 (6 decimals)
content: () => ({
message: "Premium data accessed successfully!",
data: {
flarePrice: 0.0234,
timestamp: Date.now(),
secret: "This is premium content only available after payment",
},
}),
},
"/api/report": {
price: BigInt(500000), // 0.5 USDT0
content: () => ({
message: "Detailed report generated",
report: {
title: "Market Analysis Report",
sections: ["Overview", "Technical Analysis", "Predictions"],
generatedAt: new Date().toISOString(),
},
}),
},
};

// Middleware to check x402 payment
async function x402Middleware(req: Request, res: Response, next: NextFunction) {
const resource = req.path;
const paidResource = PAID_RESOURCES[resource];

if (!paidResource) {
return next();
}

// Check for PAYMENT header
const paymentHeader = req.headers["x-payment"] as string;

if (!paymentHeader) {
// Return 402 Payment Required
const paymentRequirement: PaymentRequirement = {
scheme: "exact",
network: "flare-coston2",
maxAmountRequired: paidResource.price.toString(),
resource: resource,
description: `Payment required to access ${resource}`,
mimeType: "application/json",
payTo: PAYEE_ADDRESS,
maxTimeoutSeconds: 300,
asset: "USDT0",
extra: {
tokenAddress: TOKEN_ADDRESS,
facilitatorAddress: FACILITATOR_ADDRESS,
chainId: 114,
},
};

res.status(402).json({
error: "Payment Required",
x402Version: "1",
accepts: [paymentRequirement],
});
return;
}

// Parse and verify payment
try {
const paymentPayload: PaymentPayload = JSON.parse(
Buffer.from(paymentHeader, "base64").toString("utf-8"),
);

// Verify payment amount
if (BigInt(paymentPayload.value) < paidResource.price) {
res.status(402).json({
error: "Insufficient payment",
required: paidResource.price.toString(),
received: paymentPayload.value,
});
return;
}

// Verify with facilitator contract
const facilitator = new ethers.Contract(
FACILITATOR_ADDRESS,
FACILITATOR_ABI,
provider,
);

const [paymentId, isValid] = await facilitator.verifyPayment({
from: paymentPayload.from,
to: paymentPayload.to,
token: paymentPayload.token,
value: paymentPayload.value,
validAfter: paymentPayload.validAfter,
validBefore: paymentPayload.validBefore,
nonce: paymentPayload.nonce,
v: paymentPayload.v,
r: paymentPayload.r,
s: paymentPayload.s,
});

if (!isValid) {
res.status(402).json({
error: "Invalid payment authorization",
paymentId: paymentId,
});
return;
}

// Settle the payment
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
const facilitatorWithSigner = new ethers.Contract(
FACILITATOR_ADDRESS,
FACILITATOR_ABI,
wallet,
);

const settlePayment = facilitatorWithSigner.getFunction("settlePayment");
const tx = await settlePayment({
from: paymentPayload.from,
to: paymentPayload.to,
token: paymentPayload.token,
value: paymentPayload.value,
validAfter: paymentPayload.validAfter,
validBefore: paymentPayload.validBefore,
nonce: paymentPayload.nonce,
v: paymentPayload.v,
r: paymentPayload.r,
s: paymentPayload.s,
});

const receipt = await tx.wait();

// Add payment response header
res.setHeader(
"X-Payment-Response",
Buffer.from(
JSON.stringify({
paymentId: paymentId,
transactionHash: receipt.hash,
settled: true,
}),
).toString("base64"),
);

// Store payment info for the request
(req as unknown as { paymentInfo: object }).paymentInfo = {
paymentId,
transactionHash: receipt.hash,
amount: paymentPayload.value,
};

next();
} catch (error: unknown) {
console.error("Payment verification error:", error);
res.status(402).json({
error: "Payment verification failed",
message: error instanceof Error ? error.message : String(error),
});
}
}

// Apply rate limiting and x402 middleware to all routes
app.use(rootRateLimiter, (req, res, next) => {
void x402Middleware(req, res, next);
});

// Public endpoint (no payment required)
app.get("/api/public", (req: Request, res: Response) => {
res.json({
message: "This is free public data",
timestamp: Date.now(),
});
});

// Protected endpoints (payment required)
app.get("/api/premium-data", (req: Request, res: Response) => {
const content = PAID_RESOURCES["/api/premium-data"].content();
(content as unknown as { paymentInfo: object }).paymentInfo = (
req as unknown as { paymentInfo: object }
).paymentInfo;
res.json(content);
});

app.get("/api/report", (req: Request, res: Response) => {
const content = PAID_RESOURCES["/api/report"].content();
(content as unknown as { paymentInfo: object }).paymentInfo = (
req as unknown as { paymentInfo: object }
).paymentInfo;
res.json(content);
});

// Get payment requirements without triggering 402
app.get("/api/payment-info/:resource", (req: Request, res: Response) => {
const resource = "/api/" + req.params.resource;
const paidResource = PAID_RESOURCES[resource];

if (!paidResource) {
res.status(404).json({ error: "Resource not found" });
return;
}

res.json({
resource,
price: paidResource.price.toString(),
priceFormatted: `${Number(paidResource.price) / 1e6} USDT0`,
tokenAddress: TOKEN_ADDRESS,
facilitatorAddress: FACILITATOR_ADDRESS,
payeeAddress: PAYEE_ADDRESS,
chainId: 114,
});
});

// Health check
app.get("/health", (req: Request, res: Response) => {
res.json({
status: "ok",
config: {
token: TOKEN_ADDRESS,
facilitator: FACILITATOR_ADDRESS,
payee: PAYEE_ADDRESS,
},
});
});

// Serve frontend - inject config into HTML
app.get("/", rootRateLimiter, (req: Request, res: Response) => {
const frontendPath = path.join(__dirname, "frontend.html");
let html = fs.readFileSync(frontendPath, "utf-8");

// Inject token address into the HTML
html = html.replace(
'placeholder="MockUSDT0 address"',
`placeholder="MockUSDT0 address" value="${TOKEN_ADDRESS}"`,
);

res.setHeader("Content-Type", "text/html");
res.send(html);
});

// Start server
function startServer() {
if (!TOKEN_ADDRESS || !FACILITATOR_ADDRESS || !PAYEE_ADDRESS) {
console.error("❌ Missing required environment variables:");
console.error(
" X402_TOKEN_ADDRESS, X402_FACILITATOR_ADDRESS, X402_PAYEE_ADDRESS",
);
console.error(
"\nRun deployment first: yarn hardhat run scripts/x402/deploy.ts --network coston2",
);
process.exit(1);
}

app.listen(PORT, () => {
console.log("═".repeat(60));
console.log("x402 Demo Server");
console.log("═".repeat(60));
console.log(`Frontend: http://localhost:${PORT}/`);
console.log(`Token: ${TOKEN_ADDRESS}`);
console.log(`Facilitator: ${FACILITATOR_ADDRESS}`);
console.log(`Payee: ${PAYEE_ADDRESS}`);
console.log("─".repeat(60));
console.log("Endpoints:");
console.log(" GET / - Frontend UI");
console.log(" GET /api/public - Free");
console.log(" GET /api/premium-data - 0.1 USDT0");
console.log(" GET /api/report - 0.5 USDT0");
console.log(" GET /health - Health check");
console.log("═".repeat(60));
});
}

startServer();

Server Code Breakdown

  1. Configuration: Load environment variables for token, facilitator, and payee addresses.

  2. Payment Requirements: Define resources that require payment with their prices.

  3. x402 Middleware: Intercept requests to protected endpoints.

    • If no X-Payment header, return 402 with payment requirements.
    • If payment header present, verify and settle the payment.
  4. Payment Verification: Use the Facilitator contract to verify the EIP-3009 authorization.

  5. Payment Settlement: Call settlePayment() to execute the token transfer.

  6. Response Headers: Include X-Payment-Response with transaction details.

Step 4: Run the Agent

The agent is an interactive CLI for making x402 payments.

npx ts-node scripts/x402/agent.ts
View agent implementation
scripts/x402/agent.ts
/**
* x402 Payment Agent
*
* Interactive CLI agent for making x402 payments with EIP-3009.
*
* Run: npx ts-node scripts/x402/agent.ts
*/

// full walkthrough of x402 payment process including authorization and payment.

import { ethers } from "ethers";
import * as readline from "readline";
import "dotenv/config";

// Configuration
const COSTON2_RPC =
process.env.COSTON2_RPC_URL || "https://coston2-api.flare.network/ext/C/rpc";
const PRIVATE_KEY = process.env.PRIVATE_KEY || "";
const TOKEN_ADDRESS = process.env.X402_TOKEN_ADDRESS || "";
const FACILITATOR_ADDRESS = process.env.X402_FACILITATOR_ADDRESS || "";
const BACKEND_URL = process.env.X402_BACKEND_URL || "http://localhost:3402";

// ABIs
const TOKEN_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function balanceOf(address) view returns (uint256)",
"function DOMAIN_SEPARATOR() view returns (bytes32)",
"function authorizationState(address, bytes32) view returns (bool)",
"function mint(address to, uint256 amount)",
];

const FACILITATOR_ABI = [
"function verifyPayment((address from, address to, address token, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)) view returns (bytes32 paymentId, bool valid)",
];

// EIP-712 Types for transferWithAuthorization
const EIP712_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" },
],
};

interface PaymentRequirement {
scheme: string;
network: string;
maxAmountRequired: string;
payTo: string;
extra: {
tokenAddress: string;
facilitatorAddress: string;
chainId: number;
};
}

interface AuthorizationParams {
from: string;
to: string;
value: bigint;
validAfter: number;
validBefore: number;
nonce: string;
}

interface SignedAuthorization extends AuthorizationParams {
v: number;
r: string;
s: string;
signature: string;
}

class X402Agent {
private provider: ethers.JsonRpcProvider;
private wallet: ethers.Wallet;
private tokenContract: ethers.Contract;
private facilitatorContract: ethers.Contract;

constructor() {
this.provider = new ethers.JsonRpcProvider(COSTON2_RPC);
this.wallet = new ethers.Wallet(PRIVATE_KEY, this.provider);
this.tokenContract = new ethers.Contract(
TOKEN_ADDRESS,
TOKEN_ABI,
this.wallet,
);
this.facilitatorContract = new ethers.Contract(
FACILITATOR_ADDRESS,
FACILITATOR_ABI,
this.wallet,
);
}

async getBalance(): Promise<string> {
const balance = await this.tokenContract.balanceOf(this.wallet.address);
const decimals = await this.tokenContract.decimals();
const symbol = await this.tokenContract.symbol();
return `${ethers.formatUnits(balance, decimals)} ${symbol}`;
}

async fetchPaymentRequirements(
resourcePath: string,
): Promise<PaymentRequirement | null> {
const response = await fetch(`${BACKEND_URL}${resourcePath}`);

if (response.status !== 402) {
console.log("Resource is free or already accessible");
const data = await response.json();
console.log("Response:", JSON.stringify(data, null, 2));
return null;
}

const paymentData = await response.json();
return paymentData.accepts[0];
}

async createAuthorization(
params: AuthorizationParams,
): Promise<SignedAuthorization> {
const tokenName = await this.tokenContract.name();

const domain = {
name: tokenName,
version: "1",
chainId: 114,
verifyingContract: TOKEN_ADDRESS,
};

const message = {
from: params.from,
to: params.to,
value: params.value,
validAfter: params.validAfter,
validBefore: params.validBefore,
nonce: params.nonce,
};

console.log("\n📝 EIP-712 Authorization Details:");
console.log("─".repeat(50));
console.log(`From: ${params.from}`);
console.log(`To: ${params.to}`);
console.log(`Value: ${ethers.formatUnits(params.value, 6)} USDT0`);
console.log(
`Valid After: ${new Date(params.validAfter * 1000).toISOString()}`,
);
console.log(
`Valid Before: ${new Date(params.validBefore * 1000).toISOString()}`,
);
console.log(`Nonce: ${params.nonce}`);
console.log("─".repeat(50));

const signature = await this.wallet.signTypedData(
domain,
EIP712_TYPES,
message,
);
const sig = ethers.Signature.from(signature);

return {
...params,
v: sig.v,
r: sig.r,
s: sig.s,
signature,
};
}

async verifyAuthorization(
auth: SignedAuthorization,
): Promise<{ paymentId: string; valid: boolean }> {
const payload = {
from: auth.from,
to: auth.to,
token: TOKEN_ADDRESS,
value: auth.value,
validAfter: auth.validAfter,
validBefore: auth.validBefore,
nonce: auth.nonce,
v: auth.v,
r: auth.r,
s: auth.s,
};

const [paymentId, valid] =
await this.facilitatorContract.verifyPayment(payload);
return { paymentId, valid };
}

async executePaymentAndFetch(
resourcePath: string,
auth: SignedAuthorization,
): Promise<{ data: unknown; paymentResponse: unknown }> {
const paymentPayload = {
from: auth.from,
to: auth.to,
token: TOKEN_ADDRESS,
value: auth.value.toString(),
validAfter: auth.validAfter.toString(),
validBefore: auth.validBefore.toString(),
nonce: auth.nonce,
v: auth.v,
r: auth.r,
s: auth.s,
};

const paymentHeader = Buffer.from(JSON.stringify(paymentPayload)).toString(
"base64",
);

const response = await fetch(`${BACKEND_URL}${resourcePath}`, {
headers: {
"X-Payment": paymentHeader,
},
});

const data = await response.json();

if (response.status === 200) {
const paymentResponseHeader = response.headers.get("X-Payment-Response");
if (paymentResponseHeader) {
data.x402PaymentResponse = JSON.parse(
Buffer.from(paymentResponseHeader, "base64").toString(),
);
}
}

return { status: response.status, data };
}

async processPayment(resourcePath: string): Promise<void> {
console.log(`\n🔍 Checking payment requirements for ${resourcePath}...`);

// Step 1: Fetch payment requirements
const requirement = await this.fetchPaymentRequirements(resourcePath);
if (!requirement) {
return;
}

console.log("\n💰 Payment Required:");
console.log(
` Amount: ${ethers.formatUnits(requirement.maxAmountRequired, 6)} USDT0`,
);
console.log(` Payee: ${requirement.payTo}`);

// Step 2: Check balance
const balance = await this.getBalance();
console.log(`\n💳 Your Balance: ${balance}`);

// Step 3: Prompt for confirmation
const confirmed = await this.promptConfirmation(
`Do you want to authorize payment of ${ethers.formatUnits(requirement.maxAmountRequired, 6)} USDT0?`,
);

if (!confirmed) {
console.log("❌ Payment cancelled");
return;
}

// Step 4: Create authorization
console.log("\n✍️ Creating EIP-3009 authorization...");

const nonce = ethers.hexlify(ethers.randomBytes(32));
const validAfter = Math.floor(Date.now() / 1000) - 60;
const validBefore = Math.floor(Date.now() / 1000) + 300;

const authParams: AuthorizationParams = {
from: this.wallet.address,
to: requirement.payTo,
value: BigInt(requirement.maxAmountRequired),
validAfter,
validBefore,
nonce,
};

const signedAuth = await this.createAuthorization(authParams);
console.log("✅ Authorization signed");

// Step 5: Verify authorization
console.log("\n🔐 Verifying authorization with facilitator...");
const { paymentId, valid } = await this.verifyAuthorization(signedAuth);
console.log(` Payment ID: ${paymentId}`);
console.log(` Valid: ${valid}`);

if (!valid) {
console.log("❌ Authorization verification failed");
return;
}

// Step 6: Execute payment and fetch resource
console.log("\n📤 Submitting payment and fetching resource...");
const result = await this.executePaymentAndFetch(resourcePath, signedAuth);

if (result.status === 200) {
console.log("\n✅ Payment successful!");
console.log("─".repeat(50));
console.log("📦 Resource Data:");
console.log(JSON.stringify(result.data, null, 2));

if (result.data.x402PaymentResponse) {
console.log("\n🧾 Payment Receipt:");
console.log(
` Transaction: ${result.data.x402PaymentResponse.transactionHash}`,
);
console.log(
` Payment ID: ${result.data.x402PaymentResponse.paymentId}`,
);
}
} else {
console.log(`\n❌ Payment failed (${result.status}):`);
console.log(JSON.stringify(result.data, null, 2));
}
}

private promptConfirmation(question: string): Promise<boolean> {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

rl.question(`\n${question} (y/n): `, (answer) => {
rl.close();
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
});
});
}

private prompt(question: string): Promise<string> {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

rl.question(question, (answer) => {
rl.close();
resolve(answer);
});
});
}

async interactiveMode(): Promise<void> {
console.log("\n🤖 x402 Payment Agent");
console.log("═".repeat(50));
console.log(`Wallet: ${this.wallet.address}`);
console.log(`Token: ${TOKEN_ADDRESS}`);
console.log(`Facilitator: ${FACILITATOR_ADDRESS}`);
console.log(`Backend: ${BACKEND_URL}`);
console.log("═".repeat(50));

const balance = await this.getBalance();
console.log(`Balance: ${balance}`);

// eslint-disable-next-line no-constant-condition
while (true) {
console.log("\n📋 Available Commands:");
console.log(" 1. Fetch /api/public (free)");
console.log(" 2. Fetch /api/premium-data (0.1 USDT0)");
console.log(" 3. Fetch /api/report (0.5 USDT0)");
console.log(" 4. Check balance");
console.log(" 5. Mint test tokens");
console.log(" 6. Exit");

const choice = await this.prompt("\nSelect option: ");

switch (choice) {
case "1":
await this.fetchPaymentRequirements("/api/public");
break;
case "2":
await this.processPayment("/api/premium-data");
break;
case "3":
await this.processPayment("/api/report");
break;
case "4": {
const bal = await this.getBalance();
console.log(`\n💳 Balance: ${bal}`);
break;
}
case "5": {
console.log("\n🪙 Minting test tokens...");
const tx = await this.tokenContract.mint(
this.wallet.address,
ethers.parseUnits("1000", 6),
);
await tx.wait();
console.log("✅ Minted 1000 test tokens");
break;
}
case "6":
console.log("\n👋 Goodbye!");
process.exit(0);
break;
default:
console.log("Invalid option");
}
}
}
}

// Run agent
async function main() {
if (!PRIVATE_KEY) {
console.error("❌ PRIVATE_KEY not set in environment");
process.exit(1);
}

if (!TOKEN_ADDRESS) {
console.error("❌ X402_TOKEN_ADDRESS not set in environment");
console.error(
"\nRun deployment first: yarn hardhat run scripts/x402/deploy.ts --network coston2",
);
process.exit(1);
}

if (!FACILITATOR_ADDRESS) {
console.error("❌ X402_FACILITATOR_ADDRESS not set in environment");
console.error(
"\nRun deployment first: yarn hardhat run scripts/x402/deploy.ts --network coston2",
);
process.exit(1);
}

const agent = new X402Agent();
await agent.interactiveMode();
}

main().catch(console.error);

Agent Commands

📋 Available Commands:
1. Fetch /api/public (free)
2. Fetch /api/premium-data (0.1 USDT0)
3. Fetch /api/report (0.5 USDT0)
4. Check balance
5. Mint test tokens
6. Exit

Example Payment Flow

🔍 Checking payment requirements for /api/premium-data...

💰 Payment Required:
Amount: 0.1 USDT0
Payee: 0x742d35Cc6634C0532925a3b844Bc454e4438f44e

💳 Your Balance: 1000.0 USDT0

Do you want to authorize payment of 0.1 USDT0? (y/n): y

✍️ Creating EIP-3009 authorization...

📝 EIP-712 Authorization Details:
──────────────────────────────────────────────────
From: 0xYourAddress...
To: 0xPayeeAddress...
Value: 0.1 USDT0
Valid After: 2024-01-15T10:00:00.000Z
Valid Before: 2024-01-15T10:05:00.000Z
Nonce: 0x1234...
──────────────────────────────────────────────────
✅ Authorization signed

🔐 Verifying authorization with facilitator...
Payment ID: 0xabcd...
Valid: true

📤 Submitting payment and fetching resource...

✅ Payment successful!
──────────────────────────────────────────────────
📦 Resource Data:
{
"message": "Premium data accessed successfully!",
"data": {
"flarePrice": 0.0234,
"timestamp": 1705312800000,
"secret": "This is premium content only available after payment"
}
}

🧾 Payment Receipt:
Transaction: 0xdef456...
Payment ID: 0xabcd...

x402 Protocol Details

402 Response Format

When a resource requires payment, the server returns:

{
"error": "Payment Required",
"x402Version": "1",
"accepts": [
{
"scheme": "exact",
"network": "flare-coston2",
"maxAmountRequired": "100000",
"payTo": "0x...",
"asset": "USDT0",
"extra": {
"tokenAddress": "0x...",
"facilitatorAddress": "0x...",
"chainId": 114
}
}
]
}

X-Payment Header

The client sends payment authorization as a Base64-encoded JSON:

{
"from": "0x...",
"to": "0x...",
"token": "0x...",
"value": "100000",
"validAfter": "1704067200",
"validBefore": "1704070800",
"nonce": "0x...",
"v": 28,
"r": "0x...",
"s": "0x..."
}

X-Payment-Response Header

The server confirms settlement with:

{
"paymentId": "0x...",
"transactionHash": "0x...",
"settled": true
}

EIP-712 Signature

The agent signs authorizations using EIP-712 typed data:

const domain = {
name: "Mock USDT0",
version: "1",
chainId: 114,
verifyingContract: tokenAddress,
};

const 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" },
],
};

const message = {
from: userAddress,
to: payeeAddress,
value: amount,
validAfter: Math.floor(Date.now() / 1000) - 60,
validBefore: Math.floor(Date.now() / 1000) + 300,
nonce: ethers.hexlify(ethers.randomBytes(32)),
};

const signature = await wallet.signTypedData(domain, types, message);

Security Considerations

  1. Front-running Protection: Use receiveWithAuthorization when the payee is a contract to prevent front-running attacks.

  2. Nonce Management: Always use cryptographically random 32-byte nonces. Never reuse nonces.

  3. Time Bounds: Set reasonable validAfter and validBefore windows. The example uses a 5-minute window.

  4. Domain Verification: Always verify the contract address in the EIP-712 domain matches the expected token.

Troubleshooting

Error: X402_TOKEN_ADDRESS not set

  • Solution: Deploy contracts first and add addresses to .env.

Error: Payment verification failed

  • Solution: Ensure the authorization hasn't expired (validBefore).
  • Check that the nonce hasn't been used before.

Error: Insufficient balance

  • Solution: Mint test tokens using the agent's option 5.

References

Next Steps

To continue your development journey: