What We’re Building

In Part 1, we explored why the web needs a native payment protocol. In Part 2, we dissected how the protocol works under the hood. Now we build.

By the end of this tutorial you will have:

  • A running Express.js API that charges USDT for access
  • A client that discovers prices, signs payments, and receives data
  • Full sandbox test coverage using magic addresses
  • A clear path to production deployment

Every code block is copy-pasteable. No hand-waving.

graph LR
    A[npm install] --> B[Add Middleware]
    B --> C[Test with Sandbox]
    C --> D[Get API Key]
    D --> E[Deploy to Production]

Prerequisites

You need three things:

  1. Node.js 18+ — check with node -v
  2. Any EVM wallet — MetaMask, Rainbow, or a raw private key (for testing, a throwaway key is fine)
  3. Basic HTTP knowledge — you know what a GET request is

No blockchain node. No RPC endpoint. No API key (yet).

Project Setup

mkdir my-paid-api && cd my-paid-api
npm init -y
npm install express @t402/express @t402/core

That gives you:

Package Purpose
express Web framework
@t402/express Server-side middleware (returns 402, verifies payments)
@t402/core Shared types, constants, payment construction utilities

For the client side (in a separate directory or the same project):

npm install @t402/fetch

Server Side: Express.js Middleware

Minimal Server

Create server.js:

const express = require("express");
const { paymentMiddleware } = require("@t402/express");

const app = express();

// Configure the payment middleware
const payment = paymentMiddleware({
  // Your wallet address — where payments are sent
  payTo: "0xYourWalletAddressHere",

  // Use the public sandbox facilitator (no API key needed)
  facilitator: "https://sandbox.t402.io",

  // Which networks and assets you accept
  accepts: [
    {
      network: "eip155:421614", // Arbitrum Sepolia testnet
      asset: "0x...USDTContractAddress", // USDT on this testnet
      scheme: "exact",
      amount: "100000", // ₮0.10 (6 decimals)
    },
  ],
});

// Protected endpoint — payment required
app.get("/api/data", payment, (req, res) => {
  res.json({
    message: "You paid for this data!",
    timestamp: Date.now(),
    settlement: req.t402?.settlement,
  });
});

// Free endpoint — no middleware
app.get("/api/health", (req, res) => {
  res.json({ status: "ok" });
});

app.listen(3000, () => {
  console.log("Paid API running on http://localhost:3000");
});

Run it:

node server.js

Hit the protected endpoint without payment:

curl -i http://localhost:3000/api/data

You get back:

HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: eyJ0NDAyVmVyc2lvbiI6Mn0=...

The PAYMENT-REQUIRED header contains a Base64-encoded JSON object describing what the server accepts. That is the entire server-side integration. One middleware function.

Let’s Test It

Here is exactly what you see when you hit a protected endpoint without payment:

$ curl -i http://localhost:3000/api/premium
HTTP/1.1 402 Payment Required
PAYMENT-REQUIRED: eyJ0NDAy...
Content-Type: application/json

{"error":"Payment required","amount":"₮0.01","network":"eip155:42161"}

And here is what a successful paid request looks like:

$ curl -i http://localhost:3000/api/premium -H "PAYMENT-SIGNATURE: eyJhY2..."
HTTP/1.1 200 OK
PAYMENT-RESPONSE: eyJzdWNj...
Content-Type: application/json

{"data":"premium content here","settled":true}

The 402 tells the client what to pay. The client signs and resends. The 200 confirms settlement. Three HTTP messages, one on-chain transaction, done.

Multiple Endpoints with Different Prices

Real APIs have different prices for different resources. Apply the middleware per route with different configurations:

const cheapData = paymentMiddleware({
  payTo: "0xYourWallet",
  facilitator: "https://sandbox.t402.io",
  accepts: [
    {
      network: "eip155:421614",
      asset: "0x...USDT",
      scheme: "exact",
      amount: "10000", // ₮0.01
    },
  ],
  description: "Basic market data snapshot",
});

const expensiveData = paymentMiddleware({
  payTo: "0xYourWallet",
  facilitator: "https://sandbox.t402.io",
  accepts: [
    {
      network: "eip155:421614",
      asset: "0x...USDT",
      scheme: "exact",
      amount: "1000000", // ₮1.00
    },
  ],
  description: "Full historical dataset with analytics",
});

app.get("/api/ticker", cheapData, (req, res) => {
  res.json({ price: 67432.1, pair: "BTC/USD" });
});

app.get("/api/history", expensiveData, (req, res) => {
  res.json({ dataPoints: 1000, range: "30d" });
});

Dynamic Pricing

For pricing that depends on the request (query params, user tier, time of day), use a function:

const dynamicPayment = paymentMiddleware({
  payTo: "0xYourWallet",
  facilitator: "https://sandbox.t402.io",
  accepts: (req) => {
    const rows = parseInt(req.query.rows) || 10;
    const pricePerRow = 1000; // ₮0.001 per row

    return [
      {
        network: "eip155:421614",
        asset: "0x...USDT",
        scheme: "exact",
        amount: String(rows * pricePerRow),
      },
    ];
  },
});

app.get("/api/query", dynamicPayment, (req, res) => {
  const rows = parseInt(req.query.rows) || 10;
  // Return the requested number of rows
  const data = generateData(rows);
  res.json({ rows: data });
});

Now GET /api/query?rows=100 costs ₮0.10 and GET /api/query?rows=5 costs ₮0.005.

Error Handling

The middleware handles most errors automatically. For custom behavior, wrap it:

app.use((err, req, res, next) => {
  if (err.code === "PAYMENT_VERIFICATION_FAILED") {
    return res.status(402).json({
      error: "Payment verification failed",
      detail: err.message,
      hint: "Check that your signature is valid and not expired",
    });
  }

  if (err.code === "SETTLEMENT_FAILED") {
    return res.status(502).json({
      error: "Settlement failed on-chain",
      detail: err.message,
      hint: "The facilitator could not settle. Try a different network.",
    });
  }

  next(err);
});

Client Side: @t402/fetch

Basic Client Setup

Create client.js:

const { T402Client } = require("@t402/fetch");

const client = new T402Client({
  // Your private key (for testing only — use a secure signer in production)
  privateKey: "0xYourTestPrivateKeyHere",

  // Optional: preferred network
  preferredNetwork: "eip155:421614",
});

async function main() {
  try {
    // This single call handles the entire 402 flow:
    // 1. Sends GET to the URL
    // 2. Receives 402 with payment requirements
    // 3. Signs the payment off-chain
    // 4. Resends the request with the PAYMENT header
    // 5. Server verifies, facilitator settles, server returns 200
    const response = await client.fetch("http://localhost:3000/api/data");

    if (response.ok) {
      const data = await response.json();
      console.log("Received:", data);
    }
  } catch (err) {
    console.error("Payment failed:", err.message);
  }
}

main();

Run it:

node client.js

Output:

Received: {
  message: 'You paid for this data!',
  timestamp: 1735307200000,
  settlement: { txHash: '0x...', network: 'eip155:421614' }
}

One function call. The entire 402 handshake happens inside client.fetch().

Handling the 402 Response Manually

If you want fine-grained control over the flow:

const { T402Client, parse402Response } = require("@t402/fetch");

const client = new T402Client({
  privateKey: "0xYourTestPrivateKeyHere",
});

async function manualFlow() {
  // Step 1: Make the initial request
  const initialResponse = await fetch("http://localhost:3000/api/data");

  if (initialResponse.status !== 402) {
    console.log("No payment required!");
    return;
  }

  // Step 2: Parse the 402 response
  const paymentDetails = parse402Response(initialResponse);
  console.log("Server wants:", paymentDetails);
  // {
  //   resource: { url: '/api/data', description: '...' },
  //   accepts: [{ scheme: 'exact', network: '...', amount: '100000', ... }]
  // }

  // Step 3: Choose an offer and sign the payment
  const offer = paymentDetails.accepts[0];
  const signedPayment = await client.signPayment(offer);

  // Step 4: Resend with the payment header
  const paidResponse = await fetch("http://localhost:3000/api/data", {
    headers: {
      PAYMENT: signedPayment.toHeader(),
    },
  });

  const data = await paidResponse.json();
  console.log("Received:", data);
}

manualFlow();

Checking Balance Before Paying

Before committing to a payment, check if you can afford it:

const { T402Client, parse402Response } = require("@t402/fetch");

const client = new T402Client({
  privateKey: "0xYourTestPrivateKeyHere",
});

async function checkAndPay(url) {
  const initialResponse = await fetch(url);

  if (initialResponse.status !== 402) {
    return await initialResponse.json();
  }

  const paymentDetails = parse402Response(initialResponse);
  const offer = paymentDetails.accepts[0];

  // Check on-chain balance for the required asset and network
  const balance = await client.getBalance(offer.network, offer.asset);
  const required = BigInt(offer.amount);

  if (balance < required) {
    throw new Error(`Insufficient balance: have ${balance}, need ${required} ` + `on ${offer.network}`);
  }

  console.log(`Balance sufficient. Paying ${offer.amount} ` + `(have ${balance.toString()})`);

  // Proceed with payment
  return await client.fetch(url);
}

Testing with the Sandbox

The t402 sandbox at https://sandbox.t402.io is a public facilitator that requires no API key, no registration, and no real funds. It simulates the full verify-and-settle cycle.

Magic Test Addresses

The sandbox recognizes special payTo addresses that trigger deterministic behaviors. Use these to test every edge case:

Magic Address Suffix Verify Result Settle Result Use Case
0x...CAFE01 Valid signature Settlement success Happy path
0x...CAFE02 Bad signature N/A Invalid payment testing
0x...CAFE03 Expired authorization N/A Expiry handling
0x...CAFE12 Valid signature Insufficient funds Post-verify failure
0x...CAFE99 Valid (2s delay) Success (2s delay) Timeout / latency testing

Example: testing the happy path:

const payment = paymentMiddleware({
  payTo: "0x0000000000000000000000000000000000CAFE01",
  facilitator: "https://sandbox.t402.io",
  accepts: [
    {
      network: "eip155:421614",
      asset: "0x0000000000000000000000000000000000000001",
      scheme: "exact",
      amount: "100000",
    },
  ],
});

Example: testing settlement failure:

const paymentFail = paymentMiddleware({
  payTo: "0x0000000000000000000000000000000000CAFE12",
  facilitator: "https://sandbox.t402.io",
  accepts: [
    {
      network: "eip155:421614",
      asset: "0x0000000000000000000000000000000000000001",
      scheme: "exact",
      amount: "100000",
    },
  ],
});

// This will verify OK but fail during on-chain settlement
app.get("/api/test-settle-fail", paymentFail, (req, res) => {
  // This handler never runs — settlement fails before reaching here
  res.json({ data: "unreachable" });
});
graph TD
    REQ[Test Request] --> ADDR{Which Address?}
    ADDR -->|"0x...CAFE01"| V1[Verify: Valid]
    ADDR -->|"0x...CAFE02"| V2[Verify: Bad Signature]
    ADDR -->|"0x...CAFE03"| V3[Verify: Expired]
    V1 --> S1[Settle: Success ✓]
    V2 --> S2[Settle: Success ✓]
    V3 --> S3[Settle: Success ✓]
    ADDR -->|"0x...CAFE12"| V4[Verify: Valid]
    V4 --> S4[Settle: Insufficient Funds ✗]
    ADDR -->|"0x...CAFE99"| V5[2s Delay]
    V5 --> S5[Both Succeed ✓]

Supported Testnets

The sandbox supports 7 testnets so you can test against any chain you plan to deploy on:

Network Chain ID (CAIP-2)
Ethereum Sepolia eip155:11155111
Arbitrum Sepolia eip155:421614
Optimism Sepolia eip155:11155420
Base Sepolia eip155:84532
Polygon Amoy eip155:80002
BSC Testnet eip155:97
Avalanche Fuji eip155:43113

Writing Automated Tests

Combine magic addresses with your test framework:

const request = require("supertest");
const express = require("express");
const { paymentMiddleware } = require("@t402/express");
const { T402Client } = require("@t402/fetch");

describe("Paid API", () => {
  let app;
  let client;

  beforeAll(() => {
    app = express();

    const payment = paymentMiddleware({
      payTo: "0x0000000000000000000000000000000000CAFE01",
      facilitator: "https://sandbox.t402.io",
      accepts: [
        {
          network: "eip155:421614",
          asset: "0x0000000000000000000000000000000000000001",
          scheme: "exact",
          amount: "100000",
        },
      ],
    });

    app.get("/api/data", payment, (req, res) => {
      res.json({ value: 42 });
    });

    client = new T402Client({
      privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
    });
  });

  test("returns 402 without payment", async () => {
    const res = await request(app).get("/api/data");
    expect(res.status).toBe(402);
    expect(res.headers["payment-required"]).toBeDefined();
  });

  test("returns 200 with valid payment", async () => {
    const response = await client.fetch("http://localhost:3000/api/data");
    expect(response.status).toBe(200);
    const data = await response.json();
    expect(data.value).toBe(42);
  });

  test("handles expired authorization", async () => {
    // Use the CAFE03 magic address for expiry testing
    const expiredApp = express();
    const expiredPayment = paymentMiddleware({
      payTo: "0x0000000000000000000000000000000000CAFE03",
      facilitator: "https://sandbox.t402.io",
      accepts: [
        {
          network: "eip155:421614",
          asset: "0x0000000000000000000000000000000000000001",
          scheme: "exact",
          amount: "100000",
        },
      ],
    });

    expiredApp.get("/api/data", expiredPayment, (req, res) => {
      res.json({ value: 42 });
    });

    const res = await request(expiredApp).get("/api/data");
    expect(res.status).toBe(402);
  });
});

Multi-Framework Support

t402 is not Express-only. The pattern is identical across frameworks: a middleware/plugin intercepts requests, returns 402 when no payment header is present, and verifies+settles when one is.

Hono

import { Hono } from "hono";
import { paymentMiddleware } from "@t402/hono";

const app = new Hono();

app.use(
  "/api/premium/*",
  paymentMiddleware({
    payTo: "0xYourWallet",
    facilitator: "https://sandbox.t402.io",
    accepts: [
      {
        network: "eip155:421614",
        asset: "0x...USDT",
        scheme: "exact",
        amount: "100000",
      },
    ],
  })
);

app.get("/api/premium/data", (c) => {
  return c.json({ data: "paid content" });
});

export default app;

Fastify

import Fastify from "fastify";
import { paymentPlugin } from "@t402/fastify";

const app = Fastify();

app.register(paymentPlugin, {
  payTo: "0xYourWallet",
  facilitator: "https://sandbox.t402.io",
  accepts: [
    {
      network: "eip155:421614",
      asset: "0x...USDT",
      scheme: "exact",
      amount: "100000",
    },
  ],
  // Apply to specific routes by prefix
  prefix: "/api/premium",
});

app.get("/api/premium/data", async (request, reply) => {
  return { data: "paid content" };
});

app.listen({ port: 3000 });

Next.js (Route Handler)

// app/api/premium/route.ts
import { withPayment } from "@t402/next";

const handler = async (req: Request) => {
  return Response.json({ data: "paid content" });
};

export const GET = withPayment(handler, {
  payTo: "0xYourWallet",
  facilitator: "https://sandbox.t402.io",
  accepts: [
    {
      network: "eip155:421614",
      asset: "0x...USDT",
      scheme: "exact",
      amount: "100000",
    },
  ],
});

Python, Go, and Java SDKs

The t402 protocol is language-agnostic. The same pattern applies everywhere: middleware returns 402, client signs, facilitator settles.

Python (Flask)

from flask import Flask
from t402_flask import payment_required

app = Flask(__name__)

@app.route("/api/data")
@payment_required(
    pay_to="0xYourWallet",
    facilitator="https://sandbox.t402.io",
    accepts=[{
        "network": "eip155:421614",
        "asset": "0x...USDT",
        "scheme": "exact",
        "amount": "100000",
    }],
)
def get_data():
    return {"data": "paid content"}

Python client:

from t402 import T402Client

client = T402Client(private_key="0xYourTestKey")
response = client.fetch("http://localhost:5000/api/data")
print(response.json())

Go (net/http)

package main

import (
    "encoding/json"
    "net/http"

    "github.com/t402-io/t402-go/middleware"
)

func main() {
    payment := middleware.PaymentRequired(middleware.Config{
        PayTo:       "0xYourWallet",
        Facilitator: "https://sandbox.t402.io",
        Accepts: []middleware.Offer,
    })

    http.Handle("/api/data", payment(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(map[string]string{"data": "paid content"})
    })))

    http.ListenAndServe(":3000", nil)
}

Java (Spring Boot)

@RestController
@RequestMapping("/api")
public class PaidApiController {

    @GetMapping("/data")
    @PaymentRequired(
        payTo = "0xYourWallet",
        facilitator = "https://sandbox.t402.io",
        network = "eip155:421614",
        asset = "0x...USDT",
        amount = "100000"
    )
    public Map<String, Object> getData() {
        return Map.of("data", "paid content");
    }
}

The annotation approach. Spring Boot auto-configuration handles the middleware registration.

Going to Production

Three changes take you from sandbox to real money.

1. Get a Facilitator API Key

Register at t402.io and create a facilitator API key. This key authenticates your server with the facilitator for settlement.

2. Switch Facilitator URL and Add the API Key

const payment = paymentMiddleware({
  payTo: "0xYourProductionWallet",

  // Production facilitator
  facilitator: "https://facilitator.t402.io",
  facilitatorApiKey: process.env.T402_API_KEY,

  accepts: [
    {
      network: "eip155:42161", // Arbitrum mainnet (not Sepolia)
      asset: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", // Real USDT
      scheme: "exact",
      amount: "100000", // ₮0.10
    },
  ],
});

3. Configure Mainnet Networks

Replace testnet chain IDs with mainnet equivalents:

Testnet Mainnet Chain ID
Ethereum Sepolia (11155111) Ethereum Mainnet eip155:1
Arbitrum Sepolia (421614) Arbitrum One eip155:42161
Optimism Sepolia (11155420) Optimism eip155:10
Base Sepolia (84532) Base eip155:8453
Polygon Amoy (80002) Polygon eip155:137
BSC Testnet (97) BNB Smart Chain eip155:56
Avalanche Fuji (43113) Avalanche C-Chain eip155:43114

That is it. No code changes beyond configuration. The protocol, the signing flow, and the middleware logic are identical.

Common Patterns

Per-Route Pricing

The cleanest approach for APIs with many endpoints at different price points:

function priced(amount, description) {
  return paymentMiddleware({
    payTo: process.env.WALLET_ADDRESS,
    facilitator: process.env.T402_FACILITATOR,
    facilitatorApiKey: process.env.T402_API_KEY,
    accepts: [
      {
        network: "eip155:42161",
        asset: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
        scheme: "exact",
        amount,
      },
    ],
    description,
  });
}

// ₮0.001 per call
app.get("/api/ticker", priced("1000", "Current price ticker"));
// ₮0.01 per call
app.get("/api/ohlcv", priced("10000", "OHLCV candle data"));
// ₮0.10 per call
app.get("/api/orderbook", priced("100000", "Full order book snapshot"));
// ₮1.00 per call
app.get("/api/backtest", priced("1000000", "Strategy backtesting engine"));

Usage-Based Billing (upto Scheme Preview)

The exact scheme charges a fixed price per request. The upto scheme reserves a maximum and charges the actual amount after the resource is served. This enables pay-per-row, pay-per-token, or pay-per-byte models:

const usageBased = paymentMiddleware({
  payTo: "0xYourWallet",
  facilitator: "https://sandbox.t402.io",
  accepts: [
    {
      network: "eip155:42161",
      asset: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
      scheme: "upto",
      maxAmount: "1000000", // Reserve up to ₮1.00
    },
  ],
});

app.get("/api/search", usageBased, (req, res) => {
  const results = performSearch(req.query.q);

  // Charge based on actual results returned
  const actualCost = results.length * 100; // ₮0.0001 per result
  req.t402.setFinalAmount(String(actualCost));

  res.json({ results });
});

The client signs an authorization for up to ₮1.00, but only the actual cost is settled on-chain.

Multi-Chain Acceptance

Accept payment on any of N networks. The client picks whichever chain they have funds on:

const multiChain = paymentMiddleware({
  payTo: "0xYourWallet",
  facilitator: process.env.T402_FACILITATOR,
  facilitatorApiKey: process.env.T402_API_KEY,
  accepts: [
    {
      network: "eip155:42161", // Arbitrum
      asset: "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
      scheme: "exact",
      amount: "100000",
    },
    {
      network: "eip155:10", // Optimism
      asset: "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58",
      scheme: "exact",
      amount: "100000",
    },
    {
      network: "eip155:8453", // Base
      asset: "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2",
      scheme: "exact",
      amount: "100000",
    },
    {
      network: "eip155:137", // Polygon
      asset: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F",
      scheme: "exact",
      amount: "100000",
    },
  ],
});

The 402 response includes all four offers. The client SDK automatically selects the first network where the wallet has sufficient balance:

const client = new T402Client({
  privateKey: "0xYourKey",
  // The client will check balance on each offered network
  // and pick the first one with sufficient funds
});

const response = await client.fetch("http://api.example.com/api/data");

No client-side configuration for chain selection. It just works.

The Complete Server

Putting it all together, here is a complete, runnable server with multiple pricing tiers, health checks, and error handling:

// server.js
const express = require("express");
const { paymentMiddleware } = require("@t402/express");

const app = express();

// --- Configuration ---
const FACILITATOR = process.env.T402_FACILITATOR || "https://sandbox.t402.io";
const API_KEY = process.env.T402_API_KEY || undefined;
const PAY_TO = process.env.WALLET_ADDRESS || "0x0000000000000000000000000000000000CAFE01"; // sandbox magic address
const NETWORK = process.env.T402_NETWORK || "eip155:421614";
const ASSET = process.env.T402_ASSET || "0x0000000000000000000000000000000000000001";

// --- Helper ---
function priced(amount, description) {
  return paymentMiddleware({
    payTo: PAY_TO,
    facilitator: FACILITATOR,
    ...(API_KEY && { facilitatorApiKey: API_KEY }),
    accepts: [{ network: NETWORK, asset: ASSET, scheme: "exact", amount }],
    description,
  });
}

// --- Routes ---

// Free
app.get("/", (req, res) => {
  res.json({
    name: "My Paid API",
    version: "1.0.0",
    endpoints: {
      "/api/ticker": "₮0.001",
      "/api/ohlcv": "₮0.01",
      "/api/orderbook": "₮0.10",
    },
  });
});

app.get("/health", (req, res) => {
  res.json({ status: "ok", uptime: process.uptime() });
});

// Paid
app.get("/api/ticker", priced("1000", "Price ticker"), (req, res) => {
  res.json({
    pair: "BTC/USD",
    price: 67432.1,
    timestamp: Date.now(),
  });
});

app.get("/api/ohlcv", priced("10000", "OHLCV candles"), (req, res) => {
  res.json({
    pair: "BTC/USD",
    interval: "1h",
    candles: [
      { o: 67400, h: 67500, l: 67300, c: 67432, v: 1234.5 },
      { o: 67432, h: 67600, l: 67400, c: 67580, v: 987.3 },
    ],
  });
});

app.get("/api/orderbook", priced("100000", "Order book snapshot"), (req, res) => {
  res.json({
    pair: "BTC/USD",
    bids: [
      [67430, 1.5],
      [67425, 2.3],
      [67420, 0.8],
    ],
    asks: [
      [67435, 1.2],
      [67440, 3.1],
      [67445, 0.5],
    ],
  });
});

// --- Error handling ---
app.use((err, req, res, next) => {
  if (err.code === "PAYMENT_VERIFICATION_FAILED") {
    return res.status(402).json({
      error: "Payment verification failed",
      detail: err.message,
    });
  }
  if (err.code === "SETTLEMENT_FAILED") {
    return res.status(502).json({
      error: "Settlement failed",
      detail: err.message,
    });
  }
  console.error(err);
  res.status(500).json({ error: "Internal server error" });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Paid API running on http://localhost:${PORT}`);
  console.log(`Facilitator: ${FACILITATOR}`);
  console.log(`Pay to: ${PAY_TO}`);
});

And the complete client:

// client.js
const { T402Client } = require("@t402/fetch");

const client = new T402Client({
  privateKey: process.env.PRIVATE_KEY || "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
});

async function main() {
  const base = process.env.API_URL || "http://localhost:3000";

  // Free endpoint
  console.log("--- Health Check ---");
  const health = await fetch(`${base}/health`);
  console.log(await health.json());

  // Paid endpoints
  const endpoints = ["/api/ticker", "/api/ohlcv", "/api/orderbook"];

  for (const endpoint of endpoints) {
    console.log(`\n--- ${endpoint} ---`);
    try {
      const response = await client.fetch(`${base}${endpoint}`);
      if (response.ok) {
        console.log(await response.json());
      } else {
        console.log(`Status: ${response.status}`);
      }
    } catch (err) {
      console.error(`Error: ${err.message}`);
    }
  }
}

main();

Run both:

# Terminal 1
node server.js

# Terminal 2
node client.js

What You Built

You now have a production-ready paid API. Any HTTP client — browser, mobile app, CLI tool, or AI agent — can discover your prices, pay in USDT, and access your data. The entire payment settles on-chain in under 3 seconds.

What’s Next

This is Part 3 of the t402 series:

  1. t402: The Internet Finally Gets a Payment Protocol — Why HTTP 402 matters after 29 years
  2. t402 Protocol Deep Dive: How HTTP 402 Actually Works — The complete protocol specification, three-actor architecture, and formal security model
  3. Building with t402: From Zero to Paid API in 10 Minutes <– You are here
  4. t402 and the AI Economy: Machine-to-Machine Payments — MCP integration, A2A transport, ERC-8004 agent identity
  5. t402 Multi-Chain Architecture: 52 Networks, One Protocol — Chain mechanisms, ERC-4337 gasless, USDT0 cross-chain bridging

t402 is open source under the Apache 2.0 license.