Building with t402: From Zero to Paid API in 10 Minutes
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:
- Node.js 18+ — check with
node -v - Any EVM wallet — MetaMask, Rainbow, or a raw private key (for testing, a throwaway key is fine)
- 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:
- t402: The Internet Finally Gets a Payment Protocol — Why HTTP 402 matters after 29 years
- t402 Protocol Deep Dive: How HTTP 402 Actually Works — The complete protocol specification, three-actor architecture, and formal security model
- Building with t402: From Zero to Paid API in 10 Minutes <– You are here
- t402 and the AI Economy: Machine-to-Machine Payments — MCP integration, A2A transport, ERC-8004 agent identity
- t402 Multi-Chain Architecture: 52 Networks, One Protocol — Chain mechanisms, ERC-4337 gasless, USDT0 cross-chain bridging
Links
- t402.io — Protocol overview
- Documentation — Full technical docs
- GitHub — Open source (Apache 2.0)
- Sandbox — Public testnet for developers
- Explorer — Real-time settlement browser
t402 is open source under the Apache 2.0 license.