Skip to main content
This example demonstrates how to make x402 payments on SKALE Europa Testnet using an EIP-3009 forwarder contract. Forwarders enable gasless transactions by allowing a third party to submit signed authorizations on behalf of the payer. Source: GitHub › scripts/evm-example/skale-europa-testnet-payment.ts

What is EIP-3009 Forwarding?

EIP-3009 forwarders enable gasless payments by:
  1. The payer signs an authorization message (EIP-712)
  2. The facilitator submits the transaction using the forwarder contract
  3. The forwarder contract pays for gas and executes the transfer
This is especially useful for micro-payments where gas costs would exceed the payment amount.

Full Example

import "dotenv/config";
import { logger, logResponse } from "../logger";
import { createLocalWallet } from "@faremeter/wallet-evm";
import { createPaymentHandler } from "@faremeter/payment-evm/exact";
import { wrap as wrapFetch } from "@faremeter/fetch";
import {
  erc20Abi,
  createPublicClient,
  createWalletClient,
  http,
  getContract,
  getAddress,
  isHex,
  parseUnits,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { skaleEuropaTestnet } from "viem/chains";
import { lookupKnownAsset } from "@faremeter/info/evm";

const EIP3009_FORWARDER = getAddress(
  "0x7779B0d1766e6305E5f8081E3C0CDF58FcA24330",
); // SKALE Europa Testnet, USDC

const { EVM_PRIVATE_KEY } = process.env;

if (!EVM_PRIVATE_KEY) {
  throw new Error("EVM_PRIVATE_KEY must be set in your environment");
}

if (!isHex(EVM_PRIVATE_KEY)) {
  throw new Error("Private Key is Not hex value. Must start with 0x");
}

// Setup clients for approval transaction
const publicClient = createPublicClient({
  chain: skaleEuropaTestnet,
  transport: http(),
});

const walletClient = createWalletClient({
  chain: skaleEuropaTestnet,
  transport: http(),
  account: privateKeyToAccount(EVM_PRIVATE_KEY),
});

// Lookup USDC token info
const token = lookupKnownAsset("skale-europa-testnet", "USDC");

if (!token) {
  throw new Error("Invalid or Missing Token Address in @faremeter/info/evm");
}

const contract = getContract({
  abi: erc20Abi,
  address: getAddress(token?.address),
  client: {
    account: walletClient,
    public: publicClient,
  },
});

// Check and approve forwarder if needed
const allowance = await contract.read.allowance([
  getAddress(walletClient.account.address),
  EIP3009_FORWARDER,
]);
if (allowance < parseUnits("0.01", 6)) {
  // Approve 1 USDC to enable 100 micro txs without re-up
  const simulateApproval = await contract.simulate.approve(
    [EIP3009_FORWARDER, parseUnits("1", 6)],
    {
      account: walletClient.account,
    },
  );
  const txHash = await walletClient.writeContract(simulateApproval.request);
  await publicClient.waitForTransactionReceipt({ hash: txHash });
  logger.info(`Approval for 1 ${token.name} Completed`);
}

// Parse command line arguments
const args = process.argv.slice(2);
const port = args[0] ?? "4021";
const endpoint = args[1] ?? "weather";
const url = `http://localhost:${port}/${endpoint}`;

logger.info("Creating wallet for SKALE Europa Testnet USDC payments...");
const wallet = await createLocalWallet(skaleEuropaTestnet, EVM_PRIVATE_KEY);
logger.info(`Wallet address: ${wallet.address}`);

const fetchWithPayer = wrapFetch(fetch, {
  handlers: [createPaymentHandler(wallet)],
});

logger.info(`Making payment request to ${url}...`);
const req = await fetchWithPayer(url);
await logResponse(req);

Step-by-Step Breakdown

1. Forwarder Approval Setup

const EIP3009_FORWARDER = getAddress(
  "0x7779B0d1766e6305E5f8081E3C0CDF58FcA24330",
);
The forwarder contract address for SKALE Europa Testnet USDC. This contract will execute transfers on your behalf, paying gas fees.

2. Setup Clients

const publicClient = createPublicClient({
  chain: skaleEuropaTestnet,
  transport: http(),
});

const walletClient = createWalletClient({
  chain: skaleEuropaTestnet,
  transport: http(),
  account: privateKeyToAccount(EVM_PRIVATE_KEY),
});
Create viem clients for:
  • publicClient: Read blockchain state (check allowances, wait for transactions)
  • walletClient: Write transactions (approve forwarder, send transactions)

3. Lookup Token and Setup Contract

const token = lookupKnownAsset("skale-europa-testnet", "USDC");

if (!token) {
  throw new Error("Invalid or Missing Token Address in @faremeter/info/evm");
}

const contract = getContract({
  abi: erc20Abi,
  address: getAddress(token?.address),
  client: {
    account: walletClient,
    public: publicClient,
  },
});
Look up the USDC token address and create a contract instance for interacting with the ERC-20 token (checking allowance, approving).

4. Check Allowance

const allowance = await contract.read.allowance([
  getAddress(walletClient.account.address),
  EIP3009_FORWARDER,
]);
Before the forwarder can transfer tokens, you must approve it. Check if you have sufficient allowance for multiple transactions.

5. Approve Forwarder

if (allowance < parseUnits("0.01", 6)) {
  const simulateApproval = await contract.simulate.approve(
    [EIP3009_FORWARDER, parseUnits("1", 6)],
    {
      account: walletClient.account,
    },
  );
  const txHash = await walletClient.writeContract(simulateApproval.request);
  await publicClient.waitForTransactionReceipt({ hash: txHash });
}
Approve 1 USDC to the forwarder. This allows the forwarder to execute up to 100 micro-payments (0.01 USDC each) without requiring another approval transaction.

6. Create Payment Handler

const wallet = await createLocalWallet(skaleEuropaTestnet, EVM_PRIVATE_KEY);

const fetchWithPayer = wrapFetch(fetch, {
  handlers: [createPaymentHandler(wallet)],
});
The payment handler automatically detects the forwarder from @faremeter/info/evm and uses it for gasless transactions.

7. Make Payment

const req = await fetchWithPayer(url);
await logResponse(req);
Make the payment request. The payment handler will use the approved forwarder to execute gasless transactions.

Why Use Forwarders?

  • Gasless for users: The facilitator pays gas fees
  • Micro-payment friendly: Transaction costs don’t exceed payment amounts
  • Better UX: Users don’t need native tokens (ETH/MATIC) for payments
  • Batch efficiency: Forwarders can batch multiple payments

Forwarder vs Direct Transfer

// User signs authorization
const authorization = {
  from: userAddress,
  to: merchantAddress,
  value: "1000000", // 1 USDC
  validAfter: now - 60,
  validBefore: now + 300,
  nonce: randomNonce,
};

// Facilitator submits via forwarder (pays gas)
// User pays nothing for gas!

Environment Variables

  • EVM_PRIVATE_KEY: Your EVM private key (0x-prefixed hex string)

Command Line Arguments

  • First argument: Server port (default: 4021)
  • Second argument: Endpoint path (default: weather)
Example:
tsx skale-europa-testnet-payment.ts 4021 weather