LOOMAL
For Sellers

Accept payments

Charge AI agents per call in USDC for any HTTP endpoint or MCP tool

You ship a paid HTTP endpoint or MCP tool. Loomal handles the x402 dance: serves a 402 challenge, verifies the buyer's signed USDC authorization, settles on Base, and signs an Ed25519 receipt. This page is the end-to-end seller flow.

Before you start

  • A project at console.loomal.ai with Pay → Accept turned on.
  • Your loid-... API key from the project dashboard.
  • The SDK: npm install @loomal/sdk (Node/Bun/Deno) or pip install "loomal-sdk[fastapi]" (Python).

The five-line paywall

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);

const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });

app.get(
  "/search",
  requirePayment({ amount: "0.05" }),
  (req, res) => res.json({ results: [/* ... */] }),
);

app.listen(3030);

That's the whole integration. Set LOOMAL_API_KEY=loid-... and run. The middleware reads the env var automatically; requirePayment does the 402 challenge and the verify-and-settle step.

Other frameworks

Same shape, different import:

Hono (Node, Bun, Deno, Cloudflare Workers)
import { Hono } from "hono";
import { requirePayment } from "@loomal/sdk/paywall/hono";

const app = new Hono();
app.get("/search", requirePayment({ amount: "0.05" }), (c) => c.json({ results: [] }));
MCP tool
import { requirePayment } from "@loomal/sdk/paywall/mcp";

server.tool(
  "search",
  { description: "Paid search" },
  requirePayment({ amount: "0.05" }, async (args) => ({ results: [/* ... */] })),
);
FastAPI
from fastapi import FastAPI, Depends
from loomal.paywall import require_payment

app = FastAPI()

@app.get("/search", dependencies=[Depends(require_payment(amount="0.05"))])
def search():
    return {"results": [...]}

Without middleware (custom framework)

For Fastify, NestJS, Next.js, or any framework Loomal doesn't ship middleware for, wire the two SDK calls into your handler:

server.ts
app.get("/search", async (req, res) => {
  const resource = `${req.protocol}://${req.get("host")}${req.path}`;
  const xPayment = req.get("x-payment");

  if (!xPayment) {
    const challenge = await loomal.payments.challenge({ amount: "0.05", resource });
    return res.status(402).json(challenge);
  }

  const result = await loomal.payments.redeem({
    paymentHeader: xPayment, resource, amount: "0.05",
  });
  if (!result.ok) return res.status(402).json(result.requirement);

  res.setHeader("X-Payment-Response", result.paymentResponse);
  res.json({ results: [/* ... */] });
});

This is exactly what requirePayment does internally. Same SDK client, no extra dependencies, works on any HTTP server.

Test locally

api.loomal.ai fetches your URL from the public internet to drive the buyer side, so localhost won't work. Expose your server with a tunnel:

ngrok http 3030
# Forwarding   https://abcd-1234.ngrok-free.app -> http://localhost:3030

Two things keep the URL the buyer signs in sync:

  • app.set("trust proxy", true) — Express reports https:// (not http://) behind a tunnel.
  • In console.loomal.ai → your project → Pay → Add endpoint, register the ngrok URL (exact match: host + path).

Sanity check:

curl -i https://abcd-1234.ngrok-free.app/search
# HTTP/1.1 402 Payment Required
# { "x402Version": 1, "accepts": [{ ... }] }

See payments programmatically

const recent = await loomal.payments.list({ limit: 20 });
const one = await loomal.payments.get("cmoh...");
console.log(one.signedReceipt); // Ed25519-signed proof of payment

Same data the Pay tab shows. Use for in-product receipts, reconciliation, or your own admin views.

Webhooks

In console.loomal.ai → your project → Pay → Endpoints, edit the endpoint and add a webhookUrl. Loomal generates a whsec_… signing secret and shows it once — copy it then.

Loomal POSTs payment.received to your URL on every successful settle. Body is the Ed25519-signed receipt; X-Loomal-Signature: sha256=<hex> is HMAC-SHA256 over the raw body. Companion headers: X-Loomal-Event, X-Loomal-Idempotency-Key (de-dupe on this).

import express from "express";
import { verifyWebhook } from "@loomal/sdk/webhook";

app.post(
  "/webhooks/loomal",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const ok = await verifyWebhook(
      req.body.toString(),
      req.header("x-loomal-signature"),
      process.env.LOOMAL_WEBHOOK_SECRET!,
    );
    if (!ok) return res.status(400).send("invalid signature");
    // de-dupe on x-loomal-idempotency-key, then handle the receipt
    res.sendStatus(200);
  },
);
from loomal.webhook import verify_webhook

@app.post("/webhooks/loomal")
async def webhook(request: Request):
    raw = await request.body()
    ok = verify_webhook(
        raw, request.headers.get("x-loomal-signature"),
        os.environ["LOOMAL_WEBHOOK_SECRET"],
    )
    if not ok:
        raise HTTPException(400, "invalid signature")
    # handle event ...

Retry policy on delivery: 5xx and network errors back off (1s · 4s · 16s · 64s · 300s). 4xx aborts retries.

On this page