Skip to main content
The quickest path to a working h402 integration is a thin gateway that sits in front of your HTTP service. This guide shows how to implement both client and server components using the @bit-gpt/h402 package.

Client and Server quickstart

1

Installation

npm install @bit-gpt/h402
2

Server implementation (to h402 gate your resources)

import { config } from "dotenv";
import express from "express";
import { paymentMiddleware, Resource, createRouteConfigFromPrice, Network } from "h402-express";

config();

const facilitatorUrl = process.env.FACILITATOR_URL as Resource;
const payTo = process.env.ADDRESS as `0x${string}`;
const network = process.env.NETWORK as Network;

if (!facilitatorUrl || !payTo || !network) {
  console.error("Missing required environment variables");
  process.exit(1);
}

const app = express();

console.log("Server is running");

app.use(
  paymentMiddleware(
    payTo,
    {
      // Use createRouteConfigFromPrice to construct the RouteConfig
      "/weather": createRouteConfigFromPrice("$0.001", network),
      // Example of advanced configuration with multiple payment options
      "/premium/*": {
        paymentRequirements: [
          {
            scheme: "exact",
            namespace: "evm",
            tokenAddress: "0x55d398326f99059ff775485246999027b3197955", // USDT on BSC
            amountRequired: 0.01,
            amountRequiredFormat: "humanReadable",
            networkId: "56",
            payToAddress: payTo,
            description: "Premium content access with USDT on BSC",
            tokenDecimals: 18,
          },
          {
            scheme: "exact",
            namespace: "solana",
            tokenAddress: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC on Solana
            amountRequired: 0.01,
            amountRequiredFormat: "humanReadable",
            networkId: "mainnet",
            payToAddress: "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", // Example Solana address
            description: "Premium content access with USDC on Solana",
            tokenDecimals: 6,
          },
        ],
      },
    },
    {
      url: facilitatorUrl,
    },
  ),
);

app.get("/weather", (req, res) => {
  res.send({
    report: {
      weather: "sunny",
      temperature: 70,
    },
  });
});

app.get("/premium/content", (req, res) => {
  res.send({
    content: "This is premium content accessible via multiple payment methods",
    supportedPayments: ["USDT on BSC", "USDC on Solana"],
  });
});

app.listen(4021, () => {
  console.log(`Server listening at http://localhost:${4021}`);
});
3

Client implementation (to interact with h402-gated servers)

Client.ts
import axios from "axios";
import { config } from "dotenv";
import { createWalletClient, http, publicActions, type Hex } from "viem";
import { withPaymentInterceptor, decodeXPaymentResponse } from "h402-axios";
import { Keypair } from "@solana/web3.js";
import bs58 from "bs58";
import { createKeyPairSignerFromBytes } from "@solana/signers";
import type { TransactionModifyingSigner } from "@solana/signers";
import type { Transaction } from "@solana/transactions";
import { privateKeyToAccount } from "viem/accounts";
import { bsc } from "viem/chains";

config();

const evmPrivateKey = process.env.EVM_PRIVATE_KEY as Hex;
const solanaPrivateKey = process.env.SOLANA_PRIVATE_KEY as Hex;
const baseURL = process.env.RESOURCE_SERVER_URL as string; // e.g. http://localhost:3000
const endpointPath = process.env.ENDPOINT_PATH as string; // e.g. /image

if (!baseURL || !evmPrivateKey || !endpointPath) {
  console.error("Missing required environment variables");
  process.exit(1);
}

// EVM client
const evmAccount = privateKeyToAccount(evmPrivateKey);
const evmClient = createWalletClient({
  account: evmAccount,
  transport: http(),
  chain: bsc,
}).extend(publicActions);

// Solana client
const solanaKeypair = Keypair.fromSecretKey(bs58.decode(solanaPrivateKey));
const solanaClient = {
  publicKey: solanaKeypair.publicKey.toBase58(),
  signTransaction: async <T extends Transaction>(
    transactions: readonly T[],
  ): Promise<readonly T[]> => {
    const signer = await createKeyPairSignerFromBytes(solanaKeypair.secretKey);
    const signatures = await signer.signTransactions(transactions);
    const modifiedTransactions = transactions.map((transaction, index) => {
      const signature = signatures[index];
      if (!signature || Object.keys(signature).length === 0) {
        throw new Error(`Failed to sign transaction at index ${index}`);
      }
      return {
        ...transaction,
        signatures: {
          ...transaction.signatures,
          ...signature,
        },
      } as T;
    });
    return modifiedTransactions;
  },
} satisfies {
  publicKey: string;
  signTransaction: TransactionModifyingSigner["modifyAndSignTransactions"];
};

// Create the API client with payment interceptor
// If multiple clients are provided, the payment interceptor will use the first one that is available according to payment requirements
// You can comment out the evmClient to test the solana client
const api = withPaymentInterceptor(
  axios.create({
    baseURL,
  }),
  {
    evmClient,
    solanaClient,
  },
);

api
  .get(endpointPath)
  .then(response => {
    console.log(response.data);

    const paymentResponse = decodeXPaymentResponse(response.headers["x-payment-response"]);
    console.log(paymentResponse);
  })
  .catch(error => {
    console.error("example axios error", error);
  });

Key Points

  • The resource server remains completely stateless - all necessary information is encoded in the payment header
  • No hidden session state or database required
  • Payment verification is deterministic and based on cryptographic proofs
  • The same pattern works for any HTTP service, from API endpoints to static files
  • Support for multiple payment networks through the same interface
For production deployments, consider:
  • Adding proper error handling
  • Implementing retry logic
  • Setting up monitoring and logging
  • Using a production-grade facilitator (or use ours https://facilitator.bitgpt.xyz/)