ngx-l402

Accept Lightning payments in front of any HTTP API.

ngx-l402 is an open-source nginx module that puts a Lightning paywall in front of any upstream — no API rewrite, no SDK. It speaks L402 (bLIP-26), settles over the Lightning Network you already run, and verifies every payment locally — you keep custody of the sats.

MIT-licensed· Two lines of nginx· 8 payment backends· Non-custodial
/etc/nginx/conf.d/api.conf
# Put L402 in front of any upstream — no API rewrite.
server {
  listen 443 ssl;
  server_name api.example.com;

  location /v1/ {
    l402                      on;
    l402_amount_msat_default  10000;     # 10 sats / request
    l402_auto_detect_payment  on;        # client needn't echo the preimage

    proxy_pass                http://upstream;
  }
}
# Backend — LND · LNC · CLN · Eclair · LNURL · NWC · BOLT12 — is set via env vars.

↑ that block turns api.example.com/v1/ into a 10-sat-per-request Lightning endpoint. No application code touched.

How it works, in three requests.

L402 pairs two things: a macaroon (the access token) and a Lightning preimage (proof of payment), tied together by sha256(preimage) == paymentHash. The client — a human, a script, an AI agent — hits the endpoint, pays the returned invoice on Lightning, and retries with the macaroon and preimage. No accounts, no API keys, no chain confirmations.

$ first request 01 · challenge
# Hit the protected endpoint with no auth.
$ curl -i https://api.example.com/v1/chat

HTTP/1.1 402 Payment Required
WWW-Authenticate: L402 macaroon="AGIAJEemVQUTEyNCR0exk7ek...",
                  invoice="lnbc100n1p3xnhl2pp5..."

# The whole challenge lives in the WWW-Authenticate header:
# a macaroon (access token) + a BOLT-11 invoice to pay.
$ pay invoice 02 · settle
# Agent pays the invoice with any Lightning wallet.
# lncli (LND) shown here — Phoenix, Alby, NWC all work.
$ lncli payinvoice \
  --pay_req=lnbc100n1p3xnhl2pp5...

Payment hash: 9c3a2b1f8e7d6c5b4a39281706f5e4d3...
Payment preimage: 7a3c91b4e2d058...
Status: SUCCEEDED  ·  Fee: 0 sat  ·  Hops: 3

# sha256(preimage) == paymentHash → proof of payment.
# Verified locally at the edge — no callback to any third party.
$ retry with preimage 03 · 200 OK
# Repeat the request with macaroon + preimage.
$ curl -H "Authorization: L402 \
    $MACAROON:$PREIMAGE" \
  https://api.example.com/v1/chat

HTTP/1.1 200 OK
Content-Type: application/json

{
  "reply": "hello, agent.",
  "model": "gpt-4o-mini",
  "tokens": 42
}
// settles over Lightning · no chain confirmations, no accounts

⚡ Try it live

Run the real 402 → pay → unlock flow right here — the widget hits the live demo gateway by default, or point it at any ngx-l402 gateway you run.

Pay with an eCash token instead → · Watch an agent do it on its own →

Prefer privacy? Skip the invoice — pay with Cashu.

L402 is interactive: challenge, pay, retry. Cashu is bearer money — the client attaches an ecash token to the request itself and gets the resource in a single round trip. Tokens are blind-signed by the mint, so it can't link the token you spend to the wallet that bought it.

$ pay with ecash — one request
# No invoice round-trip. Attach a bearer token via X-Cashu.
$ curl -H "X-Cashu: cashuApGFtdWh0dHBzOi8vbWlu..." \
  https://api.example.com/v1/chat

HTTP/1.1 200 OK
Content-Type: application/json

# The gateway verified the token — offline in P2PK mode (NUT-12 DLEQ),
# or via a mint swap in standard mode — and proxied upstream.
 L402 · Lightning invoiceCashu · ecash token
Round trips2 — challenge → pay → retry1 — the token rides the request
Client holdsa Lightning walletbearer tokens from a mint
Proof of paymentmacaroon + preimagethe token itself — in P2PK mode, locked to your server's key so only you can redeem it
Privacyinvoice visible to the paying nodeblind-signed — the mint can't link a spent token to the wallet that bought it
Gateway verificationlocal hash checkoffline DLEQ check (P2PK) or mint swap (standard)
Replay protectionspent preimages in Redisspent tokens in Redis

// enable: CASHU_ECASH_SUPPORT=true · whitelist mints in production · P2PK mode for high traffic · back up the wallet's BIP39 phrase (NUT-13)

Three reasons it lives at the edge.

01 · drop-in

Two lines of nginx, no API rewrite.

Two lines in your existing nginx config wrap any upstream — no SDK to import, no application code to touch, no language lock-in. The diff fits in one commit, and reviews like any other reverse-proxy change.

02 · multi-backend

Eight payment backends, one config line.

Point ngx-l402 at the Lightning infrastructure you already run. Swap backends without touching your application — change one environment variable and restart nginx. No node? Receive into any wallet that speaks Nostr Wallet Connect. Your LND node doesn't even need a public IP: run it as a Tor onion service, set SOCKS5_PROXY, and ngx-l402 connects to it over Tor.

LND
CLN
Eclair
LNC
LNURL
NWC
BOLT12
Cashu
03 · non-custodial

Self-hosted, and you keep the sats.

Payments settle straight to the Lightning node or address you control — no middleman, no platform cut, no third party holding funds or metering your traffic. Every preimage is verified locally at the edge, so nothing about a request leaves your box.

L402 bLIP-26 · live Cashu private ecash

Also in the box

Dynamic pricing — change any route's price with one Redis SET; picked up on the next request, no nginx reload. docs

Dry-run (shadow) mode — evaluate the paywall on live production traffic, log what would happen, block nothing. docs

Prometheus metrics — an l402_metrics scrape endpoint with counters aggregated across all nginx workers. docs

Structured logs — l402_log_format json writes one JSON line per challenge, verification, or rate-limit event. docs

Multi-tenant payouts — per-route LNURL addresses (static or via Redis), so each tenant is paid to their own wallet. docs

Pay once, keep access — l402_indefinite_access turns one payment into a subscription-style credential. docs

Expiring access — l402_macaroon_timeout time-boxes credentials for metered plans. docs

Realms — l402_realm lets one payment unlock a whole group of paths: a day-pass or subscription instead of a per-URL charge. docs

Free methods — l402_exempt_methods HEAD; skips the paywall for the methods you name, so presence checks stay free while GET stays paid. docs

Auto-detect payment — the gateway confirms the invoice settled on its own Lightning node and unlocks automatically, so the client never has to return a preimage — even wallets that can't produce a usable one just work. Available on LND · CLN · BOLT12 · Eclair, and NWC wallets that support lookup_invoice. docs

HTTPS without a separate proxy — nginx in the Docker image serves HTTPS directly. Run certbot next to it and your free Let's Encrypt certificate renews on its own. docs

gRPC upstreams — the same paywall works in front of grpc_pass services over HTTP/2. docs

Self-describing APIs for autonomous agents.

ngx-l402 publishes a /.well-known/l402-services manifest — the agent-era equivalent of robots.txt. A client given only a hostname can discover which routes are paid, what they cost, and which payment backends are accepted, then pay and proceed — no API keys, no out-of-band integration, no human in the loop.

$ curl https://api.example.com/.well-known/l402-services
{
  "version": "1",
  "service": { "name": "Example API", "description": "Stock data, paid per request." },
  "payment_methods": [
    { "type": "lightning", "backend": "LNURL", "address": "api@getalby.com" },
    { "type": "cashu", "mints": ["https://mint.minibits.cash"], "p2pk_supported": true }
  ],
  "routes": [
    { "path": "/v1/chat", "price": { "type": "static", "amount_msat": 10000 } }
  ]
}

↑ one directive — l402_manifest; — makes the whole API surface discoverable: routes, prices, and payment backends, in one JSON document. Directories such as 402 Index already read it, and l402_payment_html off; gives agent-only routes a headers-only 402 — no HTML page to download.

Run the gateway in 60 seconds.

Pre-built container, one docker command. The simplest setup needs no node: receive at an LNURL address, or into your own wallet over NWC. Swap in LND, LNC, CLN, BOLT12, or Eclair by changing env vars.

$ run the gateway
$ docker run -d --name l402-nginx -p 8000:8000 \
  -e LN_CLIENT_TYPE=LNURL \
  -e LNURL_ADDRESS=you@your-lnurl-server.com \
  -e ROOT_KEY=$(openssl rand -hex 32) \
  ghcr.io/ngx-l402/ngx-l402:latest

# Gateway listens on :8000.
# curl http://localhost:8000/             → 200 OK
# curl -i http://localhost:8000/protected → 402 Payment Required
  1. 01

    Configure & run

    The image ships nginx, the module, and clients for all backends, with a default /protected route. Mount your own nginx.conf to define paid locations.

  2. 02

    Hit the endpoint

    Any request to a protected route comes back as 402 Payment Required with a Lightning invoice in the WWW-Authenticate header.

  3. 03

    Pay → preimage → 200

    Pay the invoice from any Lightning wallet. Retry with the preimage in the Authorization header — the gateway verifies locally and proxies through to your upstream.

How it compares.

L402 has a healthy ecosystem — but most tools live at a different layer: hosted platforms, or client libraries that pay endpoints. ngx-l402 is the self-hosted, non-custodial server side, as a module in the nginx you already run.

  ngx-l402 Aperture Lightning Enable
What it is nginx module standalone proxy hosted layer
Self-hosted yes — your nginx yes no (BYO key)
Non-custodial yes yes custodial provider
Backends LND · LNC · CLN · Eclair · LNURL · NWC · BOLT12 · Cashu LND (direct or via LNC) Strike / OpenNode
Node required no (LNURL / NWC) yes (LND) no
Cashu / privacy yes no no
Discovery manifest /.well-known/l402-services no hosted registry

// Alby and l402-requests are L402 clients — they pay ngx-l402 endpoints, so they complement it rather than compete.
// snapshot as of September 2026 — these projects evolve. Spot something stale or unfair? open an issue.

Built to sit in front of money.

non-custodial

Funds never touch the gateway.

Lightning payments settle straight to your node or address — the module only verifies proof of payment, sha256(preimage) == paymentHash, locally at the edge. Cashu tokens can auto-redeem to Lightning on an interval you set.

replay-safe

Spent proofs can't be reused.

Macaroons are bound to the request path and HTTP method, and settled preimages and Cashu tokens are recorded in Redis. If Redis is configured but unreachable, the gateway fails closed — it refuses payments rather than risk accepting a reused one.

abuse-resistant

Rate-limit invoice spam.

Per-route, per-IP invoice rate limiting (l402_invoice_rate_limit) caps challenge generation. It keys on the connection address, so forged X-Forwarded-For headers can't dodge it. Cashu P2PK proofs verify offline (NUT-12 DLEQ) — no mint round-trip on the hot path.

Questions.

Is it custodial? Where do the sats go?

Non-custodial. Payments go directly to the node or address you configure. ngx-l402 never holds funds — it only checks proof of payment.

Do I need to run a Lightning node?

No. Receive at a Lightning address with the LNURL backend, or straight into your own wallet with the NWC backend. NWC isn't only for paying: give ngx-l402 a Nostr Wallet Connect connection that may create invoices (make_invoice, plus lookup_invoice for auto-detect) and every payment lands in that wallet. It needs no permission to spend, so leave spending off. Run LND / CLN / Eclair only if you want to. docs

Which wallets work?

Any Lightning wallet can pay. To unlock, the payer pastes the preimage, the 64-character receipt most wallets show after paying. Some custodial wallets don't show it, especially when payer and payee use the same provider. For them, turn on auto-detect and the gateway checks the invoice itself (supported on the LND, CLN, BOLT12, and Eclair backends, and on NWC wallets that support lookup_invoice).

Which nginx versions are supported?

nginx 1.28.0 or later. Pre-built .so binaries ship for 1.28.0, 1.28.3, 1.29.8, 1.30.3, and 1.31.2; any other version builds in one command: docker build --build-arg NGX_VERSION=<version> .

Can I test it without spending real sats?

Yes — point it at a regtest LND or a Cashu mint for local testing. Mainnet and regtest use the same config.

What happens if a payment fails or is never made?

The client simply doesn't get access — it keeps receiving 402. No settlement, no access, and nothing to refund, because the gateway never takes custody.

How does an AI agent know what a route costs?

Enable l402_manifest to publish /.well-known/l402-services — a JSON document of routes, prices, and accepted backends. An agent with only your hostname can discover and pay.

Can someone spam my endpoint with invoice requests?

Set l402_invoice_rate_limit per route to cap invoice generation per IP. With Redis configured, replay and spend-tracking are shared across all nginx workers.

Can my Lightning node stay behind Tor?

Yes. Set SOCKS5_PROXY=socks5://127.0.0.1:9050 and the gateway reaches your LND node's .onion address over Tor, so the node never needs a public IP. docs