x402 Payment Protocol
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
receiveWithAuthorizationprevents transaction front-running. - Idempotent: Unique nonces ensure each payment can only be processed once.
Architecture
The x402 implementation consists of four components:
- MockUSDT0: ERC-20 token with EIP-3009 support (
transferWithAuthorization). - X402Facilitator: Contract that verifies and settles EIP-3009 authorizations.
- Server: Express backend implementing the x402 payment flow.
- 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:
- Client requests a protected resource.
- Server returns HTTP 402 with payment requirements.
- Client signs an EIP-712 authorization (off-chain, no gas required).
- Client resends request with
X-Paymentheader containing the signed authorization. - Server calls
settlePayment()on the Facilitator contract. - Facilitator executes
transferWithAuthorizationto transfer tokens. - Server returns the requested resource with payment confirmation.
Prerequisites
Before starting, ensure you have:
- Flare Hardhat Starter cloned.
- C2FLR tokens for gas on Coston2 testnet.
- Private key configured in
.env.
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
/**
* 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:
- Mints test tokens if needed.
- Creates an EIP-712 signed authorization.
- Executes
transferWithAuthorization. - 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:
| Endpoint | Price | Description |
|---|---|---|
GET /api/public | Free | Public data |
GET /api/premium-data | 0.1 USDT0 | Premium data |
GET /api/report | 0.5 USDT0 | Detailed report |
GET /health | Free | Health check |
View server implementation
/**
* 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
-
Configuration: Load environment variables for token, facilitator, and payee addresses.
-
Payment Requirements: Define resources that require payment with their prices.
-
x402 Middleware: Intercept requests to protected endpoints.
- If no
X-Paymentheader, return 402 with payment requirements. - If payment header present, verify and settle the payment.
- If no
-
Payment Verification: Use the Facilitator contract to verify the EIP-3009 authorization.
-
Payment Settlement: Call
settlePayment()to execute the token transfer. -
Response Headers: Include
X-Payment-Responsewith 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
/**
* 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
-
Front-running Protection: Use
receiveWithAuthorizationwhen the payee is a contract to prevent front-running attacks. -
Nonce Management: Always use cryptographically random 32-byte nonces. Never reuse nonces.
-
Time Bounds: Set reasonable
validAfterandvalidBeforewindows. The example uses a 5-minute window. -
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
- x402 Protocol Specification
- EIP-3009: Transfer With Authorization
- Coinbase x402 Implementation
- EIP-712: Typed Structured Data Hashing
To continue your development journey:
- Learn about FAssets minting.
- Explore swapping tokens to FXRP.
- Read about Flare's data protocols.