Skip to main content

Architecture Overview

Faremeter separates concerns into two main components:
  1. Wallet packages - Handle wallet creation, key management, and transaction signing
  2. Payment packages - Handle payment logic, x402 protocol, and transaction construction
This separation means you can integrate any wallet by implementing a simple interface, without modifying the payment logic.

Understanding Wallet Interfaces

EVM Wallet Interface

For EVM chains (Ethereum, Base, etc.), wallets must implement this interface:
interface WalletForPayment {
  chain: {
    id: number;                       // Chain ID (e.g., 8453 for Base)
    name: string;                     // Chain name
  };
  address: Hex;                       // Wallet's Ethereum address
  account: {
    signTypedData: (params: {
      domain: Record<string, unknown>;
      types: Record<string, unknown>;
      primaryType: string;
      message: Record<string, unknown>;
    }) => Promise<Hex>;
  };
}
The key requirement is implementing EIP-712 typed data signing for gasless USDC transfers using EIP-3009.
When your wallet’s signTypedData is called, it will receive this structure:Domain:
{
  name: "USD Coin",              // Token contract name (Base mainnet uses "USD Coin", testnet may use "USDC")
  version: "2",                  // EIP-3009 version
  chainId: 8453,                 // e.g., Base mainnet
  verifyingContract: "0x..."    // USDC contract address
}
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" }
  ]
}
Message:
{
  from: "0x...",                // Your wallet address
  to: "0x...",                  // Payment recipient
  value: 1000000n,              // Amount (BigInt, e.g., 1 USDC = 1000000)
  validAfter: 1234567890n,      // Unix timestamp (BigInt)
  validBefore: 1234567990n,     // Unix timestamp (BigInt)
  nonce: "0x..."                // 32-byte hex string
}
The signature must follow EIP-712 exactly - the USDC contract will verify that the signature recovers to the from address. This is why smart wallets cannot use this scheme (they don’t have an EOA private key for signature recovery).
Note on Chain Types: If you’re using viem’s Chain type (from viem/chains), it already includes id and name fields along with many others. The payment handler only uses id and name, so you can pass a full Chain object directly - the extra fields will be ignored.
import { base } from "viem/chains";
// base has: id, name, rpcUrls, blockExplorers, contracts, etc.
// Payment handler only uses: id and name
const wallet = { chain: base, ... };

Solana Wallet Interface

For Solana, wallets must implement this interface:
type Wallet = {
  network: string;                    // e.g., "mainnet-beta", "devnet", "testnet"
  publicKey: PublicKey;               // Wallet's public key
  buildTransaction?: (
    instructions: TransactionInstruction[],
    recentBlockHash: string
  ) => Promise<VersionedTransaction>;
  updateTransaction?: (
    tx: VersionedTransaction
  ) => Promise<VersionedTransaction>;
  sendTransaction?: (
    tx: VersionedTransaction
  ) => Promise<string>;
};
All three methods are optional. Choose which to implement based on your wallet’s capabilities:updateTransaction only (most common)
  • Use when: Your wallet just needs to sign transactions
  • The payment handler builds the transaction automatically
  • Examples: Phantom, local keypair wallets, hardware wallets
buildTransaction only
  • Use when: Your wallet needs complete control over transaction construction AND signing happens inside buildTransaction
  • The payment handler lets you build everything, then sends the serialized transaction
  • Example: Squads multisig (builds with vault PDA as payer, coordinates proposals/approvals, signs within buildTransaction)
buildTransaction + updateTransaction
  • Use when: You need custom transaction construction but signing is separate
  • buildTransaction creates the tx structure, updateTransaction adds signatures
  • Example: Custom fee payer pattern (see Solana Example 3 below)
sendTransaction only
  • Use when: The wallet SDK doesn’t expose signed transactions or RPC access - it handles everything internally and only returns a hash
  • Why: Custodial/smart wallet SDKs (like Crossmint) manage their own RPC endpoints and transaction broadcasting
  • The payment handler just gets back a transaction hash
  • Only works with: Settlement scheme (exact scheme doesn’t support this)
  • Example: Crossmint smart wallets (see Solana Example 4 below)
Why sendTransaction? Some wallet SDKs (like Crossmint, custodial wallets) don’t give you direct RPC access or expose signed transaction objects. They handle the entire transaction lifecycle through their API and only return a transaction hash. Use sendTransaction for these wallets.
Important: Use raw cluster names (“mainnet-beta”, “devnet”, “testnet”) for the network field. The payment handler automatically converts these to x402 format (“solana-mainnet-beta”) internally.

EVM Wallet Integration

Example 1: Browser Wallet (Phantom for EVM)

Here’s how to integrate a browser-based wallet like Phantom’s EVM support:
import { createPaymentHandler } from "@faremeter/payment-evm/exact";
import { wrap as wrapFetch } from "@faremeter/fetch";
import type { Hex, Chain } from "viem";
import { base } from "viem/chains";

async function createPhantomEvmWallet(chain: Chain) {
  if (!window.phantom?.ethereum) {
    throw new Error("Phantom wallet not installed");
  }

  const provider = window.phantom.ethereum;

  const accounts = await provider.request({
    method: "eth_requestAccounts"
  });

  const address = accounts[0] as Hex;

  return {
    chain,
    address,
    account: {
      signTypedData: async (params) => {
        const signature = await provider.request({
          method: "eth_signTypedData_v4",
          params: [
            address,
            JSON.stringify({
              domain: params.domain,
              types: params.types,
              primaryType: params.primaryType,
              message: params.message,
            }),
          ],
        });
        return signature as Hex;
      },
    },
  };
}

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

const response = await fetchWithPayer("https://triton.api.corbits.dev", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "getBlockHeight",
  }),
});

Example 3: Custom EVM Wallet

If you’re building your own wallet or wrapping another provider:
import { createPaymentHandler } from "@faremeter/payment-evm/exact";
import type { Hex, Chain } from "viem";

async function createCustomEvmWallet(
  chain: Chain,
  signingFunction: (message: any) => Promise<Hex>
) {
  const address = await getWalletAddress();

  return {
    chain,
    address,
    account: {
      signTypedData: async (params) => {
        const signature = await signingFunction({
          domain: params.domain,
          types: params.types,
          primaryType: params.primaryType,
          message: params.message,
        });
        return signature;
      },
    },
  };
}

Solana Wallet Integration

Example 1: Browser Wallet (Phantom for Solana)

Here’s how to integrate Phantom wallet for Solana:
import { PublicKey, VersionedTransaction, Connection } from "@solana/web3.js";
import { createPaymentHandler } from "@faremeter/payment-solana/exact";
import { wrap as wrapFetch } from "@faremeter/fetch";
import { lookupKnownSPLToken } from "@faremeter/info/solana";

async function createPhantomSolanaWallet(
  network: string,
  connection: Connection
) {
  if (!window.phantom?.solana) {
    throw new Error("Phantom wallet not installed");
  }

  const phantom = window.phantom.solana;
  await phantom.connect();

  const usdcInfo = lookupKnownSPLToken(network, "USDC");
  if (!usdcInfo) {
    throw new Error(`Couldn't look up USDC on ${network}`);
  }
  const mint = new PublicKey(usdcInfo.address);

  const wallet = {
    network,
    publicKey: phantom.publicKey,
    updateTransaction: async (tx: VersionedTransaction) => {
      const signedTx = await phantom.signTransaction(tx);
      return signedTx;
    },
  };

  return {
    wallet,
    handler: createPaymentHandler(wallet, mint, connection),
  };
}

const network = "mainnet-beta";
const connection = new Connection("https://api.mainnet-beta.solana.com");

const { wallet, handler } = await createPhantomSolanaWallet(
  network,
  connection
);

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

const response = await fetchWithPayer("https://triton.api.corbits.dev", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "getBlockHeight",
  }),
});

Example 3: Advanced Solana Wallet with Custom Transaction Building

If your wallet needs to customize transaction building with a custom fee payer:
import {
  PublicKey,
  VersionedTransaction,
  TransactionMessage,
  TransactionInstruction,
  Connection,
  Keypair,
} from "@solana/web3.js";
import { createPaymentHandler } from "@faremeter/payment-solana/exact";
import { lookupKnownSPLToken } from "@faremeter/info/solana";

async function createAdvancedSolanaWallet(
  network: string,
  connection: Connection,
  signingKeypair: Keypair,
  feePayerKeypair: Keypair
) {
  const usdcInfo = lookupKnownSPLToken(network, "USDC");
  if (!usdcInfo) {
    throw new Error(`Couldn't look up USDC on ${network}`);
  }
  const mint = new PublicKey(usdcInfo.address);

  const wallet = {
    network,
    publicKey: signingKeypair.publicKey,

    buildTransaction: async (
      instructions: TransactionInstruction[],
      recentBlockHash: string
    ) => {
      // Build transaction with custom fee payer
      const message = new TransactionMessage({
        instructions,
        payerKey: feePayerKeypair.publicKey,
        recentBlockhash: recentBlockHash,
      }).compileToV0Message();

      const tx = new VersionedTransaction(message);
      // Sign with fee payer first
      tx.sign([feePayerKeypair]);

      return tx;
    },

    updateTransaction: async (tx: VersionedTransaction) => {
      // Add wallet's signature after transaction is built
      tx.sign([signingKeypair]);
      return tx;
    },
  };

  return {
    wallet,
    handler: createPaymentHandler(wallet, mint, connection),
  };
}
Why both methods? This example uses buildTransaction to construct the transaction with a custom fee payer (not the wallet’s publicKey), then updateTransaction to add the wallet’s signature. The payment handler calls buildTransaction first, then updateTransaction if it exists, allowing you to separate transaction construction from signing.Use case: This pattern enables gas sponsorship where a service pays transaction fees for users, improving UX by eliminating the need for users to hold SOL for fees.

Example 4: Smart Wallet with Settlement Scheme (Crossmint)

For Solana smart wallets (account abstraction), use the settlement scheme. The key difference is importing @faremeter/x-solana-settlement instead of @faremeter/payment-solana/exact:
import { PublicKey, VersionedTransaction } from "@solana/web3.js";
import { createPaymentHandler } from "@faremeter/x-solana-settlement"; // Settlement scheme for smart wallets
import { wrap as wrapFetch } from "@faremeter/fetch";
import {
  createCrossmint,
  CrossmintWallets,
  SolanaWallet,
} from "@crossmint/wallets-sdk";

async function createCrossmintWallet(
  network: string,
  crossmintApiKey: string,
  crossmintWalletAddress: string
) {
  const crossmint = createCrossmint({
    apiKey: crossmintApiKey,
  });

  const crossmintWallets = CrossmintWallets.from(crossmint);
  const wallet = await crossmintWallets.getWallet(crossmintWalletAddress, {
    chain: "solana",
    signer: { type: "api-key" },
  });

  const solanaWallet = SolanaWallet.from(wallet);
  const publicKey = new PublicKey(solanaWallet.address);

  return {
    network,
    publicKey,
    sendTransaction: async (tx: VersionedTransaction) => {
      const solTx = await solanaWallet.sendTransaction({
        transaction: tx,
      });
      return solTx.hash;
    },
  };
}

const wallet = await createCrossmintWallet(
  "mainnet-beta",
  process.env.CROSSMINT_API_KEY,
  process.env.CROSSMINT_WALLET
);

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

const response = await fetchWithPayer("https://triton.api.corbits.dev", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: 1,
    method: "getBlockHeight",
  }),
});
When to use settlement vs exact scheme? Use the settlement scheme (@faremeter/x-solana-settlement) for smart wallets like Crossmint and Squads that use account abstraction. Use the exact scheme (@faremeter/payment-solana/exact) for standard EOA wallets like Phantom. The settlement scheme uses escrow PDAs - the client creates an escrow payment, and the server settles it after verification.

Complete Working Examples

Full EVM Example: Next.js App with Phantom

// app/payment/page.tsx
"use client";

import { useState } from "react";
import { createPaymentHandler } from "@faremeter/payment-evm/exact";
import { wrap as wrapFetch } from "@faremeter/fetch";
import type { Hex } from "viem";
import { base } from "viem/chains";

export default function PaymentPage() {
  const [result, setResult] = useState<string>("");
  const [loading, setLoading] = useState(false);

  async function connectAndPay() {
    setLoading(true);
    try {
      if (!window.phantom?.ethereum) {
        throw new Error("Phantom wallet not installed");
      }

      const provider = window.phantom.ethereum;
      const accounts = await provider.request({
        method: "eth_requestAccounts",
      });

      const wallet = {
        chain: base,
        address: accounts[0] as Hex,
        account: {
          signTypedData: async (params: any) => {
            const signature = await provider.request({
              method: "eth_signTypedData_v4",
              params: [
                accounts[0],
                JSON.stringify({
                  domain: params.domain,
                  types: params.types,
                  primaryType: params.primaryType,
                  message: params.message,
                }),
              ],
            });
            return signature as Hex;
          },
        },
      };

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

      const response = await fetchWithPayer("https://triton.api.corbits.dev", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          jsonrpc: "2.0",
          id: 1,
          method: "getBlockHeight",
        }),
      });
      const data = await response.json();

      setResult(JSON.stringify(data, null, 2));
    } catch (error) {
      setResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div>
      <h1>EVM Payment with Phantom</h1>
      <button onClick={connectAndPay} disabled={loading}>
        {loading ? "Processing..." : "Connect & Pay"}
      </button>
      {result && <pre>{result}</pre>}
    </div>
  );
}

Full Solana Example: React App with Phantom

// components/SolanaPayment.tsx
import { useState } from "react";
import { PublicKey, VersionedTransaction, Connection } from "@solana/web3.js";
import { createPaymentHandler } from "@faremeter/payment-solana/exact";
import { wrap as wrapFetch } from "@faremeter/fetch";
import { lookupKnownSPLToken } from "@faremeter/info/solana";

export default function SolanaPayment() {
  const [result, setResult] = useState<string>("");
  const [loading, setLoading] = useState(false);

  async function connectAndPay() {
    setLoading(true);
    try {
      if (!window.phantom?.solana) {
        throw new Error("Phantom wallet not installed");
      }

      const phantom = window.phantom.solana;
      await phantom.connect();

      const network = "mainnet-beta";
      const connection = new Connection("https://api.mainnet-beta.solana.com");

      const usdcInfo = lookupKnownSPLToken(network, "USDC");
      if (!usdcInfo) {
        throw new Error(`Couldn't look up USDC on ${network}`);
      }
      const usdcMint = new PublicKey(usdcInfo.address);

      const wallet = {
        network,
        publicKey: phantom.publicKey,
        updateTransaction: async (tx: VersionedTransaction) => {
          const signedTx = await phantom.signTransaction(tx);
          return signedTx;
        },
      };

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

      const response = await fetchWithPayer("https://triton.api.corbits.dev", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          jsonrpc: "2.0",
          id: 1,
          method: "getBlockHeight",
        }),
      });
      const data = await response.json();

      setResult(JSON.stringify(data, null, 2));
    } catch (error) {
      setResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div>
      <h1>Solana Payment with Phantom</h1>
      <button onClick={connectAndPay} disabled={loading}>
        {loading ? "Processing..." : "Connect & Pay"}
      </button>
      {result && <pre>{result}</pre>}
    </div>
  );
}

Reference Implementations

Faremeter includes several reference wallet implementations you can study:

EVM Wallets

PackageDescriptionGitHub
@faremeter/wallet-evmLocal private key wallet for EVM chainsView Source
@faremeter/wallet-ledgerLedger hardware wallet with EVM supportView Source

Solana Wallets

PackageDescriptionGitHub
@faremeter/wallet-solanaLocal keypair wallet for SolanaView Source
@faremeter/wallet-crossmintCrossmint smart wallet - uses x-solana-settlement schemeView Source
@faremeter/wallet-ledgerLedger hardware wallet with Solana supportView Source
@faremeter/wallet-solana-squadsSquads multisig wallet implementationView Source
Implementation Notes: The Ledger wallet uses signEIP712HashedMessage (pre-hashing domain and message separately), while viem’s privateKeyToAccount uses standard EIP-712 signing. Both approaches produce compatible signatures when implemented correctly according to the EIP-712 specification.

Payment Schemes

Faremeter supports multiple payment schemes to accommodate different wallet types and use cases. This guide focuses on the “exact” scheme, which is the most widely compatible:

Exact Scheme (This Guide)

  • EVM: Uses EIP-3009 (gasless USDC transfers with meta-transactions)
  • Solana: Uses direct SPL token transfers
  • Compatible with: EOA wallets (Phantom, MetaMask), hardware wallets (Ledger)
  • Packages: @faremeter/payment-evm/exact, @faremeter/payment-solana/exact

Settlement Scheme (Advanced)

For smart wallets and use cases requiring on-chain settlement:
  • Solana: Uses @faremeter/x-solana-settlement with escrow PDAs
    • Two-step flow: client creates escrow, server settles and closes it
    • Race-condition safe via unique settle nonce
    • Rent (~0.002 SOL) returned to payer on settlement
  • Compatible with: Crossmint smart wallets, Squads multisig
  • EVM: Under development - contact the team for smart wallet support
Why different schemes? EIP-3009 requires signing with an EOA private key for on-chain verification. Smart wallets use contract accounts without traditional private keys, so they need alternative payment schemes like settlement.

Troubleshooting

EVM: Signature Issues

EIP-712 Implementation
  • Ensure your signTypedData follows the EIP-712 specification exactly
  • Test with a known-working reference like viem’s privateKeyToAccount
  • Different wallet SDKs may have different EIP-712 implementations - verify yours produces valid signatures
Verify Your Signature
import { verifyTypedData } from 'viem';

// Test that your wallet's signature is valid
const isValid = await verifyTypedData({
  address: wallet.address,
  domain,
  types,
  primaryType,
  message,
  signature
});
Common Issues
  • Signature doesn’t recover to the wallet address
  • SDK doesn’t support EIP-712 typed data signing
  • Wallet is a smart contract (use settlement scheme instead)

Solana: Transaction Failures

1. Missing Token Account
# Ensure your wallet has a USDC token account
spl-token accounts --owner <YOUR_WALLET_ADDRESS>
2. Insufficient Balance
  • Check both SOL balance (for fees) and USDC balance (for payment)
  • Need ~0.01 SOL for transaction fees
3. Network Mismatch
// Make sure network string matches everywhere
const network = "mainnet-beta";  // Correct: Consistent
// NOT "mainnet" or "solana-mainnet-beta"

Next Steps

Additional Resources