Paid Search API
Charge AI agents $0.05 in USDC per call on an HTTP search endpoint — Express, Hono, or Next.js
A complete HTTP endpoint that returns search results gated by a $0.05 USDC payment. This is the Seller flow: install the SDK, wrap a handler, ship. Pick the framework closest to your stack — same logic, only the middleware import changes.
Before you start
- A project at console.loomal.ai with Pay → Accept turned on.
- Your
loid-...API key from the project dashboard. - A buyer to call your endpoint. The reference buyer lives in
loomal-ai/loomal-pay-examples.
npm install @loomal/sdk express hono @hono/node-server nextExpress
requirePayment is the whole integration. It runs the 402 challenge on the first hit, then verifies and settles the buyer's signed authorization on the retry. Set LOOMAL_API_KEY=loid-... in the environment.
import express from "express";
import { Loomal } from "@loomal/sdk";
import { requirePayment } from "@loomal/sdk/paywall/express";
const app = express();
app.set("trust proxy", true); // so resource URL is https:// behind a tunnel
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
app.get(
"/search",
requirePayment({ amount: "0.05", description: "Search API" }),
(req, res) => {
res.json({
query: req.query.q,
results: [{ title: "Result A", url: "https://example.com/a" }],
});
},
);
app.listen(3030, () => console.log("listening on :3030"));Hono
Works on Node, Bun, Deno, and Cloudflare Workers. The middleware signature is identical to Express — only the import path changes.
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { Loomal } from "@loomal/sdk";
import { requirePayment } from "@loomal/sdk/paywall/hono";
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
const app = new Hono();
app.get(
"/search",
requirePayment({ amount: "0.05", description: "Search API" }),
(c) =>
c.json({
query: c.req.query("q"),
results: [{ title: "Result A", url: "https://example.com/a" }],
}),
);
serve({ fetch: app.fetch, port: 3030 });Next.js (App Router)
Next.js doesn't have shipped middleware, so call loomal.payments.challenge and loomal.payments.redeem from the route handler. This is exactly what requirePayment does internally.
import { NextResponse } from "next/server";
import { Loomal } from "@loomal/sdk";
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
export async function GET(req: Request) {
const url = new URL(req.url);
const resource = `${url.origin}${url.pathname}`;
const xPayment = req.headers.get("x-payment");
// No payment yet — return a 402 challenge describing what to pay.
if (!xPayment) {
const challenge = await loomal.payments.challenge({
amount: "0.05",
resource,
description: "Search API",
});
return NextResponse.json(challenge, { status: 402 });
}
// Buyer retried with X-Payment — verify the signed authorization and settle.
const result = await loomal.payments.redeem({
paymentHeader: xPayment,
resource,
amount: "0.05",
});
if (!result.ok) {
return NextResponse.json(result.requirement, { status: 402 });
}
return NextResponse.json(
{
query: url.searchParams.get("q"),
results: [{ title: "Result A", url: "https://example.com/a" }],
},
{ status: 200, headers: { "X-Payment-Response": result.paymentResponse } },
);
}What the buyer sees
# First request — no payment header. Server returns 402 with what to pay.
curl -i http://localhost:3030/search?q=test
# HTTP/1.1 402 Payment Required
# {"x402Version":1,"accepts":[{"scheme":"exact","network":"base", ...}]}
# Buyer signs an EIP-3009 authorization and retries with X-Payment.
# On success, server returns 200 with the result and an Ed25519 receipt.
# HTTP/1.1 200 OK
# X-Payment-Response: <base64 receipt>
# {"query":"test","results":[...]}The on-chain settle shows up in console.loomal.ai → Pay → Recent payments within a second.