Axo

Protect Routes with Axo Pay

Require Lightning payment on HTTP routes using Express, Hono, or Next.js adapters.

@axobot/pay is the server side of the L402 flow. It returns a 402 challenge when proof is missing, then verifies the retry proof before allowing your handler.

Basic Usage

import express from "express";
import { createExpressPaymentMiddleware } from "@axobot/pay";

const app = express();

app.get(
  "/premium",
  createExpressPaymentMiddleware({
    amount: 100,
    apiKey: process.env.ZBD_API_KEY,
  }),
  (_req, res) => {
    res.json({ content: "Premium data" });
  },
);
import { Hono } from "hono";
import { createHonoPaymentMiddleware } from "@axobot/pay";

const app = new Hono();

app.use(
  "/premium",
  createHonoPaymentMiddleware({
    amount: 100,
    apiKey: process.env.ZBD_API_KEY,
  }),
);
import { withPaymentRequired } from "@axobot/pay/next";

export const GET = withPaymentRequired(
  {
    amount: 100,
    apiKey: process.env.ZBD_API_KEY,
  },
  async () => Response.json({ content: "Premium data" }),
);

PaymentConfig

type PaymentConfig<RequestLike = unknown> = {
  amount: number | ((request: RequestLike) => number | Promise<number>);
  currency?: "SAT" | "USD";
  apiKey?: string;
  tokenStorePath?: string;
};
FieldDescription
amountFixed sats/cents amount, or a function that computes price from request context
currencyPricing unit: SAT (default) or USD
apiKeyOptional override for ZBD_API_KEY
tokenStorePathFile path for settled-token cache on server

Dynamic Pricing

Use a function for request-based pricing:

app.get(
  "/premium",
  createExpressPaymentMiddleware({
    amount: (req) => {
      const url = new URL(req.url, `http://${req.headers.host}`);
      const tier = url.searchParams.get("tier");
      return tier === "pro" ? 500 : 100;
    },
    currency: "SAT",
    apiKey: process.env.ZBD_API_KEY,
  }),
  (_req, res) => res.json({ premium: true }),
);

Axo Pay evaluates pricing at challenge time and verification time. If the token amount no longer matches current route price, it returns amount_mismatch.

Fiat Pricing

If you prefer cent-based pricing:

createExpressPaymentMiddleware({
  amount: 50,
  currency: "USD",
  apiKey: process.env.ZBD_API_KEY,
});

50 with USD means $0.50 (50 cents). The upstream charge is still settled over Lightning.

Token Lifetime

Challenge responses include expiresAt. This value is tied to charge expiry from the payment provider.

Note

Unlike some SDKs, Axo Pay does not currently expose an explicit expirySeconds field in PaymentConfig.

402 Challenge Shape

WWW-Authenticate: L402 macaroon="<token>", invoice="<bolt11>"
{
  "error": {
    "code": "payment_required",
    "message": "Payment required"
  },
  "macaroon": "<token>",
  "invoice": "<bolt11>",
  "paymentHash": "<hash>",
  "amountSats": 100,
  "expiresAt": 1735766400
}

On this page