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;
};| Field | Description |
|---|---|
amount | Fixed sats/cents amount, or a function that computes price from request context |
currency | Pricing unit: SAT (default) or USD |
apiKey | Optional override for ZBD_API_KEY |
tokenStorePath | File 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
}