Skip to main content
This example demonstrates how to make x402 payments in a browser application using Phantom wallet. It includes a complete setup with a Hono middleware server and a React/Vite frontend application. Source: GitHub › examples-browser

Architecture

The example consists of two parts:
  1. Server (apps/server): Hono middleware server that issues x402 payment requirements
  2. Browser App (apps/browser-app): React/Vite frontend that handles wallet connection and payments

Quick Start

Prerequisites

  • pnpm installed
  • Phantom wallet installed in your browser
  • USDC on Solana devnet in your Phantom wallet

Step 1: Install Dependencies

pnpm install -r

Step 2: Run the Server

cd apps/server && SOLANA_PAYTO_ADDRESS=91CR7uzzSPyPTKahKQkTeK4ZQneMg1TYSf9UumSEQcGD pnpm tsx src
Set SOLANA_PAYTO_ADDRESS to the Solana address that will receive payments.

Step 3: Run the Browser App

cd apps/browser-app && pnpm vite

Step 4: Open in Browser

Open http://localhost:5173 in a browser with Phantom wallet installed.

Step 5: Make a Payment

  1. Click the “Make Request” button
  2. Phantom wallet will prompt you to connect (if not already connected)
  3. Phantom will prompt you to authorize the payment transaction
  4. After approval, you should see:
    status: OK (200)
    {"msg":"success"}
    

Full Example Code

Server (Hono Middleware)

The server uses @faremeter/middleware to protect routes with x402 payments:
import { Hono } from "hono";
import { createMiddleware } from "@faremeter/middleware/hono";
import { solana } from "@faremeter/info";

const app = new Hono();

const middleware = await createMiddleware({
  facilitatorURL: "http://localhost:3000/facilitator",
  accepts: [
    solana.x402Exact({
      network: "devnet",
      asset: "USDC",
      amount: "0.01",
      payTo: process.env.SOLANA_PAYTO_ADDRESS!,
    }),
  ],
});

app.use("/protected", middleware);

app.get("/protected", (c) => {
  return c.json({ msg: "success" });
});

export default app;

Facilitator Setup

The facilitator service handles payment requirements and settlement. It needs to be running separately:
import { Hono } from "hono";
import { createFacilitatorRoutes } from "@faremeter/facilitator";
import { createFacilitatorHandler } from "@faremeter/payment-solana/exact";
import { Connection, Keypair, PublicKey } from "@solana/web3.js";
import { clusterApiUrl } from "@solana/web3.js";
import { lookupKnownSPLToken } from "@faremeter/info/solana";

const app = new Hono();

// Setup Solana connection
const network = "devnet";
const connection = new Connection(clusterApiUrl(network));

// Get USDC mint
const usdcInfo = lookupKnownSPLToken(network, "USDC");
if (!usdcInfo) {
  throw new Error("Could not find USDC mint");
}
const usdcMint = new PublicKey(usdcInfo.address);

// Create fee payer keypair (this pays for transaction fees)
const feePayerKeypair = Keypair.fromSecretKey(/* your fee payer keypair */);

// Create facilitator handler
const solanaHandler = await createFacilitatorHandler(
  network,
  connection,
  feePayerKeypair,
  usdcMint
);

// Mount facilitator routes
const facilitatorRoutes = createFacilitatorRoutes({
  handlers: [solanaHandler],
});

app.route("/facilitator", facilitatorRoutes);

export default app;

Browser App (React/Vite)

The browser app connects to Phantom and handles payments:
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 PaymentApp() {
  const [result, setResult] = useState<string>("");
  const [loading, setLoading] = useState(false);

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

      const phantom = window.phantom.solana;

      // Connect to Phantom
      if (!phantom.isConnected) {
        await phantom.connect();
      }

      // Setup Solana connection
      const network = "devnet";
      const connection = new Connection(
        "https://api.devnet.solana.com",
        "confirmed"
      );

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

      // Create wallet interface from Phantom
      const wallet = {
        network,
        publicKey: phantom.publicKey!,
        updateTransaction: async (tx: VersionedTransaction) => {
          const signedTx = await phantom.signTransaction(tx);
          return signedTx;
        },
      };

      // Create payment handler
      const handler = createPaymentHandler(wallet, usdcMint, connection);

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

      // Make the API call (payment happens automatically)
      const response = await fetchWithPayer(
        "http://localhost:3000/protected",
        {
          method: "GET",
        }
      );

      const data = await response.json();
      setResult(`status: ${response.status === 200 ? "OK" : "ERROR"} (${response.status})\n${JSON.stringify(data, null, 2)}`);
    } catch (error) {
      setResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div>
      <h1>Faremeter Browser Payment Example</h1>
      <button onClick={makePayment} disabled={loading}>
        {loading ? "Processing..." : "Make Request"}
      </button>
      {result && <pre>{result}</pre>}
    </div>
  );
}

Facilitator Interactions

The facilitator is a separate service that orchestrates payment flows. The middleware communicates with it at two key points:

1. Getting Payment Requirements (POST /accepts)

When a client first requests a protected resource:
  1. Middleware calls facilitator: The middleware sends the desired payment requirements to POST /facilitator/accepts
    {
      "accepts": [{
        "scheme": "exact",
        "network": "solana-devnet",
        "maxAmountRequired": "0.01",
        "asset": "USDC",
        "payTo": "91CR7uzzSPyPTKahKQkTeK4ZQneMg1TYSf9UumSEQcGD"
      }]
    }
    
  2. Facilitator enriches requirements: The facilitator handler adds network-specific details:
    {
      "accepts": [{
        "scheme": "exact",
        "network": "solana-devnet",
        "maxAmountRequired": "0.01",
        "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
        "payTo": "91CR7uzzSPyPTKahKQkTeK4ZQneMg1TYSf9UumSEQcGD",
        "extra": {
          "feePayer": "...",
          "recentBlockhash": "...",
          "decimals": 6
        }
      }]
    }
    
  3. Middleware returns 402: The middleware sends this enriched response to the client as a 402 Payment Required response.

2. Settling Payments (POST /settle)

After the client creates and submits a payment:
  1. Client retries with payment proof: The browser app includes the payment transaction in the X-PAYMENT header:
    {
      "transaction": "base64EncodedWireTransaction..."
    }
    
  2. Middleware forwards to facilitator: The middleware sends the payment payload to POST /facilitator/settle:
    {
      "scheme": "exact",
      "network": "solana-devnet",
      "payload": {
        "transaction": "base64EncodedWireTransaction..."
      }
    }
    
  3. Facilitator validates and executes: The facilitator handler:
    • Validates the transaction structure
    • Verifies the payment amount and destination
    • Partially signs with the fee payer keypair
    • Submits the transaction to the Solana network
    • Polls for confirmation
  4. Settlement response: If successful, the facilitator returns:
    {
      "success": true
    }
    
  5. Request proceeds: The middleware allows the original request to continue, returning the protected resource.

Step-by-Step Breakdown

1. Check Phantom Installation

if (!window.phantom?.solana) {
  throw new Error("Phantom wallet not installed");
}
Check if Phantom wallet is installed. If not, prompt the user to install it.

2. Connect to Phantom

const phantom = window.phantom.solana;

if (!phantom.isConnected) {
  await phantom.connect();
}
Connect to Phantom wallet. This will prompt the user to approve the connection.

3. Create Wallet Interface

const wallet = {
  network,
  publicKey: phantom.publicKey!,
  updateTransaction: async (tx: VersionedTransaction) => {
    const signedTx = await phantom.signTransaction(tx);
    return signedTx;
  },
};
Create a wallet interface that:
  • Exposes the connected Phantom wallet’s public key
  • Signs transactions through Phantom’s signTransaction method
  • Matches the interface expected by @faremeter/payment-solana

4. Create Payment Handler

const handler = createPaymentHandler(wallet, usdcMint, connection);
Create a payment handler that will:
  • Intercept 402 Payment Required responses
  • Create Solana SPL token transfer transactions
  • Sign them using Phantom
  • Submit them to the network

5. Make Payment

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

const response = await fetchWithPayer("http://localhost:3000/protected");
When the server returns a 402:
  1. The payment handler creates a transaction
  2. Phantom prompts the user to sign
  3. The transaction is submitted
  4. The request is retried with payment proof
  5. The server responds with 200 OK

Phantom Wallet Integration

Required Methods

Phantom wallet must implement:
  • connect(): Connect to the wallet
  • signTransaction(tx): Sign a Solana transaction
  • publicKey: The wallet’s public key

Transaction Signing

When a payment is required, Phantom will prompt the user to:
  1. Review the transaction details
  2. Approve the USDC transfer
  3. Confirm the transaction
The user must have:
  • Phantom wallet installed and unlocked
  • USDC in their wallet on the correct network (devnet/mainnet)
  • Sufficient balance for the payment amount

Environment Variables

Server (Middleware)

  • SOLANA_PAYTO_ADDRESS: Solana address that receives payments (required)

Facilitator Service

  • FEE_PAYER_KEYPAIR: Secret key for the fee payer account (required)
  • SOLANA_RPC_URL: Optional custom RPC endpoint (defaults to public devnet)

Browser App

  • No environment variables needed (uses browser’s Phantom wallet)

Running the Complete Stack

To run the full example, you need three services:

1. Start the Facilitator

cd facilitator-service
FEE_PAYER_KEYPAIR="[your keypair array]" pnpm tsx src/index.ts
The facilitator will run on http://localhost:3000/facilitator.

2. Start the Middleware Server

cd apps/server
SOLANA_PAYTO_ADDRESS=91CR7uzzSPyPTKahKQkTeK4ZQneMg1TYSf9UumSEQcGD pnpm tsx src
The server will run on http://localhost:3000 and call the facilitator at http://localhost:3000/facilitator.

3. Start the Browser App

cd apps/browser-app
pnpm vite
The browser app will run on http://localhost:5173.

Common Issues

”Phantom wallet not installed”

”Insufficient USDC balance”

  • Get devnet USDC from a faucet
  • Make sure you’re connected to the correct network (devnet/mainnet)

“Transaction rejected”

  • Check that you approved the transaction in Phantom
  • Verify the payment amount matches what you expect

”Connection refused”

  • Make sure the server is running on port 3000
  • Check that the browser app is pointing to the correct server URL

Debugging

Open browser developer tools (F12) to see:
  • Console errors
  • Network requests (including 402 responses)
  • Transaction signatures
  • Payment flow logs

Comparison: Browser vs Node.js

import { createPaymentHandler } from "@faremeter/payment-solana/exact";

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

const handler = createPaymentHandler(wallet, usdcMint, connection);
Tradeoffs:
  • Browser: User-friendly, no server-side key management, requires wallet extension
  • Node.js: Automated, full control, but requires private key management