x402-fl 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of x402-fl might be problematic. Click here for more details.

@@ -0,0 +1,30 @@
1
+ import { createPublicClient as viemCreatePublicClient, createWalletClient as viemCreateWalletClient, defineChain, http, publicActions, } from "viem";
2
+ import { privateKeyToAccount } from "viem/accounts";
3
+ export async function fetchChainId(rpcUrl) {
4
+ const client = viemCreatePublicClient({ transport: http(rpcUrl) });
5
+ return client.getChainId();
6
+ }
7
+ export function createLocalChain(rpcUrl, chainId) {
8
+ return defineChain({
9
+ id: chainId,
10
+ name: "Local Anvil Fork",
11
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
12
+ rpcUrls: {
13
+ default: { http: [rpcUrl] },
14
+ },
15
+ });
16
+ }
17
+ export function createPublicClient(rpcUrl, chainId) {
18
+ return viemCreatePublicClient({
19
+ chain: createLocalChain(rpcUrl, chainId),
20
+ transport: http(rpcUrl),
21
+ });
22
+ }
23
+ export function createWalletClient(privateKey, rpcUrl, chainId) {
24
+ const account = privateKeyToAccount(privateKey);
25
+ return viemCreateWalletClient({
26
+ account,
27
+ chain: createLocalChain(rpcUrl, chainId),
28
+ transport: http(rpcUrl),
29
+ }).extend(publicActions);
30
+ }
@@ -0,0 +1,15 @@
1
+ export declare const accounts: {
2
+ facilitator: {
3
+ address: `0x${string}`;
4
+ privateKey: `0x${string}`;
5
+ };
6
+ };
7
+ export declare const defaults: {
8
+ readonly anvilPort: 8545;
9
+ readonly facilitatorPort: 4022;
10
+ };
11
+ export declare function networkId(chainId: number): `eip155:${number}`;
12
+ export declare const USDC_ADDRESS: `0x${string}`;
13
+ export declare const DEFAULT_DECIMALS = 6;
14
+ export declare function parseTokenAmount(amount: string, decimals?: number): bigint;
15
+ export declare function formatTokenAmount(raw: bigint, decimals?: number): string;
@@ -0,0 +1,31 @@
1
+ import { mnemonicToAccount } from "viem/accounts";
2
+ // Anvil default mnemonic — first account (index 0) is the facilitator
3
+ const ANVIL_MNEMONIC = "test test test test test test test test test test test junk";
4
+ const facilitatorAccount = mnemonicToAccount(ANVIL_MNEMONIC, { addressIndex: 0 });
5
+ export const accounts = {
6
+ facilitator: {
7
+ address: facilitatorAccount.address,
8
+ privateKey: `0x${Buffer.from(facilitatorAccount.getHdKey().privateKey).toString("hex")}`,
9
+ },
10
+ };
11
+ export const defaults = {
12
+ anvilPort: 8545,
13
+ facilitatorPort: 4022,
14
+ };
15
+ export function networkId(chainId) {
16
+ return `eip155:${chainId}`;
17
+ }
18
+ // Base mainnet USDC
19
+ export const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
20
+ export const DEFAULT_DECIMALS = 6;
21
+ export function parseTokenAmount(amount, decimals = DEFAULT_DECIMALS) {
22
+ const [whole, frac = ""] = amount.split(".");
23
+ const padded = frac.padEnd(decimals, "0").slice(0, decimals);
24
+ return BigInt(whole + padded);
25
+ }
26
+ export function formatTokenAmount(raw, decimals = DEFAULT_DECIMALS) {
27
+ const str = raw.toString().padStart(decimals + 1, "0");
28
+ const whole = str.slice(0, -decimals);
29
+ const frac = str.slice(-decimals).replace(/0+$/, "") || "0";
30
+ return `${whole}.${frac}`;
31
+ }
@@ -0,0 +1,7 @@
1
+ import type { Server } from "http";
2
+ export interface FacilitatorOptions {
3
+ port: number;
4
+ rpcUrl: string;
5
+ chainId: number;
6
+ }
7
+ export declare function startFacilitator(options: FacilitatorOptions): Promise<Server>;
@@ -0,0 +1,93 @@
1
+ import express from "express";
2
+ import { x402Facilitator } from "@x402/core/facilitator";
3
+ import { toFacilitatorEvmSigner } from "@x402/evm";
4
+ import { ExactEvmScheme } from "@x402/evm/exact/facilitator";
5
+ import { createWalletClient } from "./chain.js";
6
+ import { accounts, networkId } from "./config.js";
7
+ export function startFacilitator(options) {
8
+ const viemClient = createWalletClient(accounts.facilitator.privateKey, options.rpcUrl, options.chainId);
9
+ const evmSigner = toFacilitatorEvmSigner({
10
+ address: accounts.facilitator.address,
11
+ readContract: (args) => viemClient.readContract(args),
12
+ verifyTypedData: (args) => viemClient.verifyTypedData(args),
13
+ writeContract: (args) => viemClient.writeContract(args),
14
+ sendTransaction: (args) => viemClient.sendTransaction(args),
15
+ waitForTransactionReceipt: (args) => viemClient.waitForTransactionReceipt(args),
16
+ getCode: (args) => viemClient.getCode(args),
17
+ });
18
+ const facilitator = new x402Facilitator();
19
+ const network = networkId(options.chainId);
20
+ facilitator.register(network, new ExactEvmScheme(evmSigner));
21
+ const app = express();
22
+ app.use(express.json());
23
+ app.use((req, res, next) => {
24
+ const start = Date.now();
25
+ res.on("finish", () => {
26
+ console.log(`[facilitator] ${req.method} ${req.path} ${res.statusCode} (${Date.now() - start}ms)`);
27
+ });
28
+ next();
29
+ });
30
+ app.post("/verify", async (req, res) => {
31
+ try {
32
+ const { paymentPayload, paymentRequirements } = req.body;
33
+ if (!paymentPayload || !paymentRequirements) {
34
+ res
35
+ .status(400)
36
+ .json({ error: "Missing paymentPayload or paymentRequirements" });
37
+ return;
38
+ }
39
+ const response = await facilitator.verify(paymentPayload, paymentRequirements);
40
+ res.json(response);
41
+ }
42
+ catch (error) {
43
+ console.error("Verify error:", error);
44
+ res.status(500).json({
45
+ error: error instanceof Error ? error.message : "Unknown error",
46
+ });
47
+ }
48
+ });
49
+ app.post("/settle", async (req, res) => {
50
+ try {
51
+ const { paymentPayload, paymentRequirements } = req.body;
52
+ if (!paymentPayload || !paymentRequirements) {
53
+ res
54
+ .status(400)
55
+ .json({ error: "Missing paymentPayload or paymentRequirements" });
56
+ return;
57
+ }
58
+ const response = await facilitator.settle(paymentPayload, paymentRequirements);
59
+ res.json(response);
60
+ }
61
+ catch (error) {
62
+ console.error("Settle error:", error);
63
+ res.status(500).json({
64
+ error: error instanceof Error ? error.message : "Unknown error",
65
+ });
66
+ }
67
+ });
68
+ app.get("/supported", (_req, res) => {
69
+ try {
70
+ const response = facilitator.getSupported();
71
+ res.json(response);
72
+ }
73
+ catch (error) {
74
+ console.error("Supported error:", error);
75
+ res.status(500).json({
76
+ error: error instanceof Error ? error.message : "Unknown error",
77
+ });
78
+ }
79
+ });
80
+ app.get("/health", (_req, res) => {
81
+ res.json({
82
+ status: "ok",
83
+ network,
84
+ facilitator: accounts.facilitator.address,
85
+ });
86
+ });
87
+ return new Promise((resolve, reject) => {
88
+ const server = app.listen(options.port, () => {
89
+ resolve(server);
90
+ });
91
+ server.on("error", reject);
92
+ });
93
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "x402-fl",
3
+ "version": "0.0.0",
4
+ "description": "Local x402 facilitator dev tool, spin up Anvil + facilitator for local x402 development",
5
+ "type": "module",
6
+ "bin": {
7
+ "x402-fl": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/cli.ts dev"
12
+ },
13
+ "keywords": [
14
+ "x402",
15
+ "facilitator",
16
+ "local",
17
+ "development",
18
+ "anvil"
19
+ ],
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "@x402/core": "^2.5.0",
23
+ "@x402/evm": "^2.5.0",
24
+ "boxen": "^8.0.1",
25
+ "chalk": "^5.6.2",
26
+ "commander": "^12.1.0",
27
+ "dotenv": "^16.4.7",
28
+ "express": "^4.21.2",
29
+ "viem": "^2.39.3"
30
+ },
31
+ "devDependencies": {
32
+ "@types/express": "^5.0.0",
33
+ "@types/node": "^22.13.4",
34
+ "tsx": "^4.19.2",
35
+ "typescript": "^5.7.3"
36
+ },
37
+ "packageManager": "pnpm@10.28.2",
38
+ "engines": {
39
+ "node": ">=18"
40
+ }
41
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "fs";
4
+ import { Command, InvalidArgumentError } from "commander";
5
+ import dotenv from "dotenv";
6
+ import { defaults } from "./lib/config.js";
7
+
8
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
9
+
10
+ dotenv.config();
11
+
12
+ function parsePort(value: string): number {
13
+ const port = parseInt(value, 10);
14
+ if (isNaN(port) || port < 1 || port > 65535) {
15
+ throw new InvalidArgumentError("must be a number between 1 and 65535");
16
+ }
17
+ return port;
18
+ }
19
+
20
+ function parseAddress(value: string): `0x${string}` {
21
+ if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {
22
+ throw new InvalidArgumentError(
23
+ "must be a valid 0x-prefixed Ethereum address (42 characters)",
24
+ );
25
+ }
26
+ return value as `0x${string}`;
27
+ }
28
+
29
+ function parseAmount(value: string): string {
30
+ if (!/^\d+(\.\d+)?$/.test(value) || Number(value) <= 0) {
31
+ throw new InvalidArgumentError(
32
+ "must be a positive number (e.g. '100' or '1.5')",
33
+ );
34
+ }
35
+ return value;
36
+ }
37
+
38
+ const program = new Command();
39
+
40
+ program
41
+ .name("x402-fl")
42
+ .description(
43
+ "Local x402 facilitator dev tool\n\n" +
44
+ " Spin up a local Anvil fork and x402 facilitator server for development.\n" +
45
+ " Requires `anvil` (from Foundry) to be installed and available in PATH.\n\n" +
46
+ " Quick start:\n" +
47
+ " $ x402-fl dev --rpc-url https://mainnet.base.org\n" +
48
+ " $ x402-fl fund 0xYourAddress 100",
49
+ )
50
+ .version(pkg.version)
51
+ .showHelpAfterError(true)
52
+ .configureHelp({ showGlobalOptions: true });
53
+
54
+ program
55
+ .command("dev")
56
+ .description(
57
+ "Start Anvil fork + facilitator server for local x402 development",
58
+ )
59
+ .option(
60
+ "--port <number>",
61
+ `facilitator server port (default: ${defaults.facilitatorPort})`,
62
+ parsePort,
63
+ defaults.facilitatorPort,
64
+ )
65
+ .option(
66
+ "--anvil-port <number>",
67
+ `Anvil JSON-RPC port (default: ${defaults.anvilPort})`,
68
+ parsePort,
69
+ defaults.anvilPort,
70
+ )
71
+ .option(
72
+ "--rpc-url <url>",
73
+ "Base mainnet RPC URL to fork (or set BASE_RPC_URL in .env)",
74
+ )
75
+ .addHelpText(
76
+ "after",
77
+ `
78
+ Examples:
79
+ $ x402-fl dev --rpc-url https://mainnet.base.org
80
+ $ x402-fl dev --rpc-url $BASE_RPC_URL --port 5000 --anvil-port 9545
81
+
82
+ Environment variables:
83
+ BASE_RPC_URL Fallback RPC URL when --rpc-url is not provided`,
84
+ )
85
+ .action(async (opts) => {
86
+ const rpcUrl = opts.rpcUrl || process.env.BASE_RPC_URL;
87
+ if (!rpcUrl) {
88
+ program.error(
89
+ "missing required RPC URL\n\n" +
90
+ " Provide one of:\n" +
91
+ " --rpc-url <url> pass directly as a flag\n" +
92
+ " BASE_RPC_URL=<url> set in .env or environment\n\n" +
93
+ " Example:\n" +
94
+ " $ x402-fl dev --rpc-url https://mainnet.base.org",
95
+ );
96
+ }
97
+
98
+ const { devCommand } = await import("./commands/dev.js");
99
+ await devCommand({
100
+ port: opts.port,
101
+ anvilPort: opts.anvilPort,
102
+ rpcUrl,
103
+ });
104
+ });
105
+
106
+ program
107
+ .command("fund")
108
+ .description("Fund an address with USDC on local Anvil")
109
+ .argument("<address>", "0x-prefixed Ethereum address to fund", parseAddress)
110
+ .argument(
111
+ "<amount>",
112
+ "USDC amount (e.g. '100' or '1.5')",
113
+ parseAmount,
114
+ )
115
+ .option(
116
+ "--anvil-port <number>",
117
+ `Anvil JSON-RPC port`,
118
+ parsePort,
119
+ defaults.anvilPort,
120
+ )
121
+ .addHelpText(
122
+ "after",
123
+ `
124
+ Examples:
125
+ $ x402-fl fund 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 100
126
+ $ x402-fl fund 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 1.5 --anvil-port 9545`,
127
+ )
128
+ .action(async (address: `0x${string}`, amount: string, opts) => {
129
+ const { fundCommand } = await import("./commands/fund.js");
130
+ await fundCommand({
131
+ address,
132
+ amount,
133
+ anvilPort: opts.anvilPort,
134
+ });
135
+ });
136
+
137
+ program
138
+ .command("balance")
139
+ .description("Get USDC balance for an address on local Anvil")
140
+ .argument("<address>", "0x-prefixed Ethereum address to check", parseAddress)
141
+ .option(
142
+ "--anvil-port <number>",
143
+ `Anvil JSON-RPC port`,
144
+ parsePort,
145
+ defaults.anvilPort,
146
+ )
147
+ .addHelpText(
148
+ "after",
149
+ `
150
+ Examples:
151
+ $ x402-fl balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
152
+ $ x402-fl balance 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 --anvil-port 9545`,
153
+ )
154
+ .action(async (address: `0x${string}`, opts) => {
155
+ const { balanceCommand } = await import("./commands/balance.js");
156
+ await balanceCommand({
157
+ address,
158
+ anvilPort: opts.anvilPort,
159
+ });
160
+ });
161
+
162
+ program.parse();
@@ -0,0 +1,66 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import { createPublicClient, fetchChainId } from "../lib/chain.js";
4
+ import {
5
+ DEFAULT_DECIMALS,
6
+ formatTokenAmount,
7
+ USDC_ADDRESS,
8
+ } from "../lib/config.js";
9
+ import { ERC20_ABI } from "../lib/abi.js";
10
+
11
+ export interface BalanceOptions {
12
+ address: `0x${string}`;
13
+ anvilPort: number;
14
+ }
15
+
16
+ export async function balanceCommand(options: BalanceOptions): Promise<void> {
17
+ const { address, anvilPort } = options;
18
+ const rpcUrl = `http://localhost:${anvilPort}`;
19
+
20
+ let chainId: number;
21
+ try {
22
+ chainId = await fetchChainId(rpcUrl);
23
+ } catch {
24
+ console.error(
25
+ `Error: Could not connect to Anvil at ${rpcUrl}.\n` +
26
+ `Make sure Anvil is running (e.g. "x402-fl dev" or "anvil --fork-url ...").`,
27
+ );
28
+ process.exit(1);
29
+ }
30
+
31
+ const client = createPublicClient(rpcUrl, chainId);
32
+
33
+ let decimals = DEFAULT_DECIMALS;
34
+ try {
35
+ decimals = await client.readContract({
36
+ address: USDC_ADDRESS,
37
+ abi: ERC20_ABI,
38
+ functionName: "decimals",
39
+ });
40
+ } catch {
41
+ // Non-standard token without decimals(); fall back to default
42
+ }
43
+
44
+ const balance = await client.readContract({
45
+ address: USDC_ADDRESS,
46
+ abi: ERC20_ABI,
47
+ functionName: "balanceOf",
48
+ args: [address],
49
+ });
50
+
51
+ console.log(
52
+ boxen(
53
+ [
54
+ `${chalk.dim("Token")} ${USDC_ADDRESS}`,
55
+ `${chalk.dim("Address")} ${address}`,
56
+ `${chalk.dim("Balance")} ${chalk.green.bold(formatTokenAmount(balance, decimals))} USDC`,
57
+ ].join("\n"),
58
+ {
59
+ title: "USDC Balance",
60
+ padding: 1,
61
+ borderStyle: "round",
62
+ borderColor: "cyan",
63
+ },
64
+ ),
65
+ );
66
+ }
@@ -0,0 +1,112 @@
1
+ import chalk from "chalk";
2
+ import boxen from "boxen";
3
+ import type { ChildProcess } from "child_process";
4
+ import type { Server } from "http";
5
+ import { startAnvil, waitForAnvil } from "../lib/anvil.js";
6
+ import { startFacilitator } from "../lib/facilitator.js";
7
+ import { fetchChainId } from "../lib/chain.js";
8
+ import { accounts, defaults, networkId, USDC_ADDRESS } from "../lib/config.js";
9
+
10
+ export interface DevOptions {
11
+ port: number;
12
+ anvilPort: number;
13
+ rpcUrl: string;
14
+ }
15
+
16
+ export async function devCommand(options: DevOptions): Promise<void> {
17
+ const localRpcUrl = `http://localhost:${options.anvilPort}`;
18
+ let anvilProc: ChildProcess | null = null;
19
+ let server: Server | null = null;
20
+
21
+ function cleanup() {
22
+ console.log("\nShutting down...");
23
+ let pending = 0;
24
+ const done = () => { if (--pending <= 0) process.exit(0); };
25
+
26
+ if (server) {
27
+ pending++;
28
+ server.close(() => done());
29
+ server = null;
30
+ }
31
+ if (anvilProc) {
32
+ pending++;
33
+ anvilProc.on("exit", () => done());
34
+ anvilProc.kill("SIGTERM");
35
+ anvilProc = null;
36
+ }
37
+ if (pending === 0) process.exit(0);
38
+
39
+ // Force exit after 3s if graceful shutdown hangs
40
+ setTimeout(() => process.exit(0), 3000).unref();
41
+ }
42
+
43
+ process.on("SIGINT", cleanup);
44
+ process.on("SIGTERM", cleanup);
45
+
46
+ // 0. Detect chain ID from fork RPC
47
+ console.log(chalk.dim(`Detecting chain ID from ${options.rpcUrl}...`));
48
+ const chainId = await fetchChainId(options.rpcUrl);
49
+ console.log(chalk.dim(`Detected chain ID: ${chainId}`));
50
+
51
+ // 1. Start Anvil
52
+ console.log(
53
+ chalk.dim(
54
+ `Starting Anvil (forking chain ${chainId}, port ${options.anvilPort})...`,
55
+ ),
56
+ );
57
+ anvilProc = startAnvil({
58
+ forkUrl: options.rpcUrl,
59
+ port: options.anvilPort,
60
+ chainId,
61
+ });
62
+
63
+ anvilProc.on("exit", (code) => {
64
+ if (code !== null && code !== 0) {
65
+ console.error(chalk.red(`Anvil exited with code ${code}`));
66
+ process.exit(1);
67
+ }
68
+ });
69
+
70
+ // 2. Wait for Anvil to be ready
71
+ console.log(chalk.dim("Waiting for Anvil to be ready..."));
72
+ await waitForAnvil(localRpcUrl);
73
+ console.log(chalk.dim("Anvil is ready."));
74
+
75
+ // 3. Start facilitator
76
+ console.log(chalk.dim(`Starting facilitator on port ${options.port}...`));
77
+ server = await startFacilitator({
78
+ port: options.port,
79
+ rpcUrl: localRpcUrl,
80
+ chainId,
81
+ });
82
+
83
+ const network = networkId(chainId);
84
+
85
+ // 4. Print summary
86
+ console.log(
87
+ boxen(
88
+ [
89
+ `${chalk.dim("Anvil RPC")} ${chalk.cyan(localRpcUrl)}`,
90
+ `${chalk.dim("Facilitator")} ${chalk.cyan(`http://localhost:${options.port}`)}`,
91
+ `${chalk.dim("Network")} ${network}`,
92
+ `${chalk.dim("Chain ID")} ${chainId}`,
93
+ "",
94
+ chalk.dim("Accounts"),
95
+ ` ${chalk.dim("Facilitator")} ${accounts.facilitator.address}`,
96
+ "",
97
+ `${chalk.dim("USDC")} ${USDC_ADDRESS}`,
98
+ "",
99
+ `${chalk.dim("Fund account with:")}`,
100
+ `$ ${chalk.white("x402-fl fund <address> <amount>")}`,
101
+ "",
102
+ chalk.dim("Press Ctrl+C to stop."),
103
+ ].join("\n"),
104
+ {
105
+ title: "x402-fl dev",
106
+ padding: 1,
107
+ borderStyle: "round",
108
+ borderColor: "green",
109
+ },
110
+ ),
111
+ );
112
+ }