LOOMAL
Examples

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

npm install @loomal/sdk express hono @hono/node-server next

Express

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.

express/server.ts
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.

hono/server.ts
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.

app/api/search/route.ts
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.

On this page