This guide shows you how to manually integrate X402 payments into your frontend application without using middleware. This approach gives you complete control over the payment flow and is ideal for custom implementations.

Prerequisites

Before starting, ensure you have:
  • A React/Next.js application
  • Wallet connection functionality (MetaMask, WalletConnect, etc.)
  • Access to a Meridian facilitator service
  • Required dependencies installed

Installation

Install the necessary packages:
npm install viem axios x402/client x402/types
# or
yarn add viem axios x402/client x402/types
# or
pnpm add viem axios x402/client x402/types

Environment Setup

Configure your environment variables:
# .env.local
NEXT_PUBLIC_MERIDIAN_PK=pk_...
NEXT_PUBLIC_FACILITATOR_URL=https://api.mrdn.finance/v1

Core Implementation

1. Wallet Connection Setup

First, set up wallet connection functionality:
import { createWalletClient, custom, WalletClient } from "viem";
import { baseSepolia } from "viem/chains";

// Extend Window interface for wallet access
interface ExtendedWindow extends Window {
  ethereum?: {
    request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
    on: (event: string, callback: (...args: unknown[]) => void) => void;
    removeListener: (
      event: string,
      callback: (...args: unknown[]) => void
    ) => void;
  };
}

const getExtendedWindow = (): ExtendedWindow | null => {
  if (typeof window !== "undefined") {
    return window as ExtendedWindow;
  }
  return null;
};

const connectWallet = async () => {
  const extendedWindow = getExtendedWindow();
  if (extendedWindow?.ethereum) {
    try {
      // Request account access
      const accounts = (await extendedWindow.ethereum.request({
        method: "eth_requestAccounts",
      })) as string[];

      if (accounts.length > 0) {
        const account = accounts[0] as `0x${string}`;

        // Create wallet client with browser wallet
        const walletClient = createWalletClient({
          account,
          transport: custom(extendedWindow.ethereum),
          chain: baseSepolia,
        });

        return { account, walletClient };
      }
    } catch (error) {
      console.error("Error connecting to wallet:", error);
      throw error;
    }
  } else {
    throw new Error("Please install MetaMask or another wallet extension");
  }
};

2. Authentication Headers

Create functions to generate authentication headers:
// Function to create Bearer Auth header with API key
const createAuthHeader = () => {
  const apiKey = process.env.NEXT_PUBLIC_MERIDIAN_PK;

  if (!apiKey) {
    console.warn("API key not configured. Requests may fail authentication.");
    return {};
  }

  return {
    Authorization: `Bearer ${apiKey}`,
  };
};

// Function to create headers for forwarding to x402 middleware
const createForwardingHeaders = () => {
  const apiKey = process.env.NEXT_PUBLIC_MERIDIAN_PK;

  if (!apiKey) {
    console.warn("API key not configured for forwarding");
    return {};
  }

  return {
    Authorization: `Bearer ${apiKey}`,
  };
};

// Function to create Meridian-specific forwarding headers
const createMeridianForwardingHeaders = () => {
  const meridianPk = process.env.NEXT_PUBLIC_MERIDIAN_PK;

  if (!meridianPk) {
    console.warn("Meridian public key not configured, using default API key");
    return createForwardingHeaders();
  }

  return {
    Authorization: `Bearer ${meridianPk}`,
  };
};

3. Making API Requests

Implement the core API request logic with payment handling:
import axios from "axios";
import { createPaymentHeader, selectPaymentRequirements } from "x402/client";
import { ChainIdToNetwork, PaymentRequirementsSchema } from "x402/types";

interface PaymentRequirements {
  amount: string;
  recipient: string;
  network: string;
  scheme: string;
  maxAmountRequired: string;
  resource: string;
  mimeType: string;
  payTo: string;
  maxTimeoutSeconds: number;
  asset: string;
}

const requestSample = async (client: WalletClient, account: string) => {
  try {
    // Make a regular API request to the facilitator with auth headers
    const response = await axios.get("http://localhost:4021/v1/samples", {
      headers: {
        Authorization: `Bearer ${process.env.NEXT_PUBLIC_MERIDIAN_PK}`,
      },
    });

    console.log("Sample response:", response.data);

    // Check if payment is required
    if (response.status === 402) {
      // Handle payment manually
      console.log("Payment required:", response.data.accepts[0]);
      await handlePayment(
        response.data.accepts[0],
        response.data.x402Version,
        response.data.accepts,
        client,
        account
      );
    }

    return response.data;
  } catch (error) {
    console.error("Error requesting sample:", error);
    if (axios.isAxiosError(error) && error.response?.status === 402) {
      // Handle payment requirement
      console.log("Payment required:", error.response.data.accepts[0]);
      await handlePayment(
        error.response.data.accepts[0],
        error.response.data.x402Version,
        error.response.data.accepts,
        client,
        account
      );
    } else {
      throw error;
    }
  }
};

4. Payment Handling

Implement the payment processing logic:
const handlePayment = async (
  paymentDetails: PaymentRequirements,
  x402Version: number,
  accepts: PaymentRequirements[],
  client: WalletClient,
  account: string
) => {
  try {
    const parsed = accepts.map((x) => PaymentRequirementsSchema.parse(x));

    // Get chain ID from the client's chain property
    const chainId = client.chain?.id;

    const selectedPaymentRequirements = selectPaymentRequirements(
      parsed,
      chainId ? ChainIdToNetwork[chainId] : undefined,
      "exact"
    );

    console.log("selectedPaymentRequirements", selectedPaymentRequirements);

    const paymentHeader = await createPaymentHeader(
      client as unknown as Parameters<typeof createPaymentHeader>[0],
      x402Version,
      selectedPaymentRequirements
    );

    console.log("Payment header:", paymentHeader);

    // Send the payment header to our facilitator
    const facilitatorResponse = await axios.get(
      "http://localhost:4021/v1/settle",
      {
        headers: {
          Authorization: `Bearer ${process.env.NEXT_PUBLIC_MERIDIAN_PK}`,
          "X-PAYMENT": paymentHeader,
          "Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE",
        },
      }
    );

    console.log("Facilitator response:", facilitatorResponse.data);

    // Request the sample again with the payment header
    const response = await axios.get("http://localhost:4021/v1/samples", {
      headers: {
        Authorization: `Bearer ${process.env.NEXT_PUBLIC_MERIDIAN_PK}`,
        "X-PAYMENT": paymentHeader,
        "Access-Control-Expose-Headers": "X-PAYMENT-RESPONSE",
      },
    });

    console.log("Sample response:", response.data);
    return response.data;
  } catch (error: unknown) {
    console.error("Payment error:", error);
    const errorMessage =
      error instanceof Error ? error.message : "Unknown error";
    throw new Error("Payment failed: " + errorMessage);
  }
};