@lnbot/l402
v1.0.0
Published
L402 Lightning payment middleware for Express.js — paywall any API in one line
Downloads
23
Maintainers
Readme
@lnbot/l402
L402 payment middleware for Express.js — paywall any API in one line. Built on ln.bot.
Add Lightning-powered pay-per-request to any Express API. Protect premium routes with a paywall, or build clients that auto-pay L402-protected services — all without touching any cryptography.
import express from "express";
import { l402, LnBot } from "@lnbot/l402";
const app = express();
const ln = new LnBot({ apiKey: "key_..." });
app.use("/api/premium", l402.paywall(ln, { price: 10 }));
app.get("/api/premium/data", (req, res) => {
res.json({ data: "premium content" });
});This package is a thin glue layer. All L402 logic — macaroon creation, signature verification, preimage checking — lives in the ln.bot API via
@lnbot/sdk. Zero crypto dependencies.
What is L402?
L402 is a protocol built on HTTP 402 Payment Required. It enables machine-to-machine micropayments over the Lightning Network:
- Client requests a protected resource
- Server returns
402with a Lightning invoice and a macaroon token - Client pays the invoice, obtains the preimage as proof of payment
- Client retries the request with
Authorization: L402 <macaroon>:<preimage> - Server verifies the token and grants access
L402 is ideal for API monetization, AI agent tool access, pay-per-request data feeds, and any scenario where you want instant, permissionless, per-request payments without subscriptions or API key provisioning.
Install
npm install @lnbot/l402pnpm add @lnbot/l402yarn add @lnbot/l402@lnbot/sdk and express are peer dependencies and will be resolved automatically.
Server — Protect Routes with L402
The l402.paywall() middleware intercepts requests, verifies L402 tokens via the SDK, and issues new challenges when payment is needed. Two SDK calls, ~40 lines of glue code, zero crypto.
import express from "express";
import { l402, LnBot } from "@lnbot/l402";
const app = express();
const ln = new LnBot({ apiKey: "key_..." });
// Paywall a route group — 10 sats per request
app.use("/api/premium", l402.paywall(ln, {
price: 10,
description: "API access",
}));
app.get("/api/premium/data", (req, res) => {
// req.l402 is populated after successful payment verification
res.json({
data: "premium content",
paymentHash: req.l402?.paymentHash,
});
});
// Free routes still work normally
app.get("/api/free/health", (req, res) => {
res.json({ status: "ok" });
});
app.listen(3000);How the middleware works
- Checks for an
Authorization: L402 ...header - If present, calls
ln.l402.verify()— the SDK checks signature, preimage, and caveats server-side - If valid, populates
req.l402and callsnext() - If missing or invalid, calls
ln.l402.createChallenge()and returns a402response with the invoice and macaroon
Dynamic pricing
// Fixed price per route
app.use("/api/cheap", l402.paywall(ln, { price: 1 }));
app.use("/api/expensive", l402.paywall(ln, { price: 100 }));
// Custom pricing function — receives the request, returns price in sats
app.use("/api/dynamic", l402.paywall(ln, {
price: (req) => {
if (req.path.includes("/bulk")) return 50;
return 5;
},
}));Paywall options
| Option | Type | Description |
| --- | --- | --- |
| price | number \| (req) => number | Price in satoshis — fixed or per-request |
| description | string | Invoice memo shown in wallets |
| expirySeconds | number | Challenge expiry in seconds |
| caveats | string[] | Macaroon caveats to attach |
Client — Auto-Pay L402 APIs
The l402.client() wrapper makes L402 payment transparent. It detects 402 responses, pays the Lightning invoice via the SDK, caches the token, and retries — all in one fetch call.
import { l402, LnBot } from "@lnbot/l402";
const ln = new LnBot({ apiKey: "key_..." });
const client = l402.client(ln, {
maxPrice: 100, // refuse to pay more than 100 sats per request
budgetSats: 50000, // spending limit for the period
budgetPeriod: "day", // reset period: "hour" | "day" | "week" | "month"
store: "memory", // token cache: "memory" (default) | "none" | custom TokenStore
});
// Use like fetch — L402 payment is transparent
const response = await client.fetch("https://api.example.com/premium/data");
const data = await response.json();
// Convenience methods — all HTTP verbs
const json = await client.get("https://api.example.com/premium/data");
const result = await client.post("https://api.example.com/premium/submit", {
body: JSON.stringify({ query: "test" }),
});
await client.put("https://api.example.com/premium/item/1", {
body: JSON.stringify({ name: "updated" }),
});
await client.patch("https://api.example.com/premium/item/1", {
body: JSON.stringify({ status: "active" }),
});
await client.delete("https://api.example.com/premium/item/1");How the client works
- Checks the token cache for a valid credential
- If cached, sends the request with the
Authorizationheader - If no cache (or server rejects), makes a plain request
- On
402, parses the challenge and checks budget limits - Calls
ln.l402.pay()— the SDK pays the invoice and returns a ready-to-use token - Caches the token and retries the request with authorization
Client options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| maxPrice | number | 1000 | Max sats to pay for a single request |
| budgetSats | number | unlimited | Total budget in sats for the period |
| budgetPeriod | string | — | Reset period: "hour", "day", "week", "month" |
| store | string \| TokenStore | "memory" | Token cache: "memory", "none", or custom |
Custom token store
Implement the TokenStore interface for Redis, file system, or any persistence layer:
import { l402, LnBot } from "@lnbot/l402";
import type { TokenStore } from "@lnbot/l402";
const ln = new LnBot({ apiKey: "key_..." });
const redisStore: TokenStore = {
async get(url) { /* read from Redis */ },
async set(url, token) { /* write to Redis */ },
async delete(url) { /* delete from Redis */ },
};
const client = l402.client(ln, { store: redisStore });Header Utilities
Parse and format L402 headers for custom integrations:
import { l402 } from "@lnbot/l402";
// Parse Authorization: L402 <macaroon>:<preimage>
l402.parseAuthorization("L402 mac_base64:preimage_hex");
// → { macaroon: "mac_base64", preimage: "preimage_hex" }
// Parse WWW-Authenticate: L402 macaroon="...", invoice="..."
l402.parseChallenge('L402 macaroon="abc", invoice="lnbc1..."');
// → { macaroon: "abc", invoice: "lnbc1..." }
// Format headers
l402.formatAuthorization("mac_base64", "preimage_hex");
// → "L402 mac_base64:preimage_hex"
l402.formatChallenge("abc", "lnbc1...");
// → 'L402 macaroon="abc", invoice="lnbc1..."'Error Handling
import { L402Error, L402BudgetExceededError, L402PaymentFailedError } from "@lnbot/l402";
try {
const data = await client.get("https://api.example.com/expensive");
} catch (err) {
if (err instanceof L402BudgetExceededError) {
// Price exceeds maxPrice or total budget exhausted
} else if (err instanceof L402PaymentFailedError) {
// Lightning payment failed or didn't settle
} else if (err instanceof L402Error) {
// Other L402 protocol error (missing header, parse failure)
}
}API Reference
Server
| Export | Description |
| --- | --- |
| l402.paywall(ln, options) | Express middleware factory — protects routes behind an L402 paywall |
Client
| Export | Description |
| --- | --- |
| l402.client(ln, options?) | Creates an L402-aware HTTP client with automatic payment |
| client.fetch(url, init?) | L402-aware fetch — pays 402 challenges automatically |
| client.get(url, init?) | GET + JSON parse with automatic L402 payment |
| client.post(url, init?) | POST + JSON parse with automatic L402 payment |
| client.put(url, init?) | PUT + JSON parse with automatic L402 payment |
| client.patch(url, init?) | PATCH + JSON parse with automatic L402 payment |
| client.delete(url, init?) | DELETE + JSON parse with automatic L402 payment |
Header Utilities
| Export | Description |
| --- | --- |
| l402.parseAuthorization(header) | Parse Authorization: L402 ... into { macaroon, preimage } |
| l402.parseChallenge(header) | Parse WWW-Authenticate: L402 ... into { macaroon, invoice } |
| l402.formatAuthorization(macaroon, preimage) | Format an Authorization header value |
| l402.formatChallenge(macaroon, invoice) | Format a WWW-Authenticate header value |
Types
| Type | Description |
| --- | --- |
| L402PaywallOptions | Options for l402.paywall() |
| L402ClientOptions | Options for l402.client() |
| L402Token | Cached L402 credential (macaroon + preimage + metadata) |
| TokenStore | Interface for custom token caches |
| L402RequestData | Data attached to req.l402 after verification |
Errors
| Class | Description |
| --- | --- |
| L402Error | Base error for all L402 protocol errors |
| L402BudgetExceededError | Price or cumulative spend exceeds configured limits |
| L402PaymentFailedError | Lightning payment failed or didn't return authorization |
Requirements
- Node.js 18+, Bun, or Deno
- Express 4+ (server middleware)
- An ln.bot API key — create a wallet to get one
Related packages
@lnbot/sdk— The TypeScript SDK this package is built on- Python SDK · pypi
- Go SDK · pkg.go.dev
- Rust SDK · crates.io
Links
License
MIT
