Axo

Axo Pay

Protect HTTP routes with L402 or MPP payment challenges in Express, Hono, and Next.js.

@axobot/pay is the server half of Axo's paid-route stack. It can emit classic L402 challenges or Lightning MPP charge/session challenges, then verify the resulting proof before allowing access.

Install

npm install @axobot/pay

Environment

VariableRequiredDefault
ZBD_API_KEYYes (unless passed in config)none
ZBD_API_BASE_URLNohttps://api.zbdpay.com

Quickstarts

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

const app = express();

app.get(
  "/protected",
  createExpressPaymentMiddleware({
    amount: 21,
    apiKey: process.env.ZBD_API_KEY,
  }),
  (_req, res) => {
    res.json({ ok: true });
  },
);
import { Hono } from "hono";
import { createHonoPaymentMiddleware } from "@axobot/pay";

const app = new Hono();

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

export const GET = withPaymentRequired(
  {
    amount: 21,
    apiKey: process.env.ZBD_API_KEY,
  },
  async () => Response.json({ ok: true }),
);

PaymentConfig

type PaymentConfig<RequestLike = unknown> = {
  amount: number | ((request: RequestLike) => number | Promise<number>);
  currency?: "SAT" | "USD";
  apiKey?: string;
  tokenStorePath?: string;
  protocol?: "L402" | "MPP";
  mppIntent?: "charge" | "session";
};

Note

SAT pricing is the default and the most direct production path.

MPP Session Mode

Use protocol: "MPP" and mppIntent: "session" when you want session-based charging:

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

const middleware = createExpressPaymentMiddleware({
  amount: 5,
  apiKey: process.env.ZBD_API_KEY,
  protocol: "MPP",
  mppIntent: "session",
  mppDepositMultiplier: 2,
});

That flow issues Lightning MPP session challenges, verifies open / bearer / topUp / close, and can refund to a return invoice or return Lightning address.

402 Response Shape

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

Error Codes

HTTPCodeMeaning
402payment_requiredNo valid payment proof provided
401invalid_credentialMacaroon signature invalid
401invalid_payment_proofPreimage mismatch
403resource_mismatchProof for different route
403amount_mismatchProof for different price
403token_expiredProof expired
500configuration_errorMissing API key/config
500pricing_errorDynamic pricing function failed
502invoice_creation_failedUpstream invoice creation failed

Runnable Example

The repository includes examples/http-server.mjs for a minimal paid route.

From the agents workspace, run:

npm --prefix axobot-pay run build
ZBD_API_KEY=<your_api_key> npm --prefix axobot-pay run example:http-server

Optional verbose logs:

ZBD_PAY_DEBUG=1 ZBD_API_KEY=<your_api_key> npm --prefix axobot-pay run example:http-server

Then hit the route with your wallet CLI:

axo fetch "https://api.zbdpay.com/protected" --max-sats 100

Storage

By default, settled token metadata is persisted at:

~/.zbd-wallet/server-tokens.json

Override via tokenStorePath when needed for your deployment model.

On this page