What Is L402?
HTTP has a status code for payments: 402 Payment Required. It sat unused for decades. L402 gives it a purpose. When a client hits your API without paying, you return a 402 with a Lightning invoice. The client pays, retries with proof, and gets the response.
No API keys to manage. No subscriptions. No billing dashboards. Every request is self-contained: pay and get data.
What We're Building
A Cloudflare Worker that serves an API endpoint. First request returns a 402 with a Lightning invoice. After payment, the same endpoint returns data. The whole thing runs on the free tier.
Stack:
- Cloudflare Workers — serverless runtime, free tier covers ~100k requests/day
- LNbits — Lightning wallet with REST API for invoices. Runs on Alby Hub, your own node, or lnbits.com
Step 1: Set Up Your Lightning Wallet
You need a wallet that can create and check invoices via API. Two options:
Option A: Alby Hub (recommended)
Alby Hub gives you a self-hosted Lightning node with NWC (Nostr Wallet Connect) and LNbits built in. Install it, fund your node, and enable the LNbits extension. Your API key is on the LNbits dashboard.
Option B: Standalone LNbits
If you already run a Lightning node (LND, CLN), install LNbits and connect it. The API key is on the wallet page under "API info."
Either way, you need two things:
LNBITS_URL— e.g.,https://your-lnbits.example.comLNBITS_KEY— your Invoice/Read key (NOT the Admin key)
Step 2: Create the Worker
Initialize a new Cloudflare Worker project:
npm create cloudflare@latest l402-api -- --type=hello-world
cd l402-api
Replace src/index.js with this:
const PRICE_SATS = 21;
export default {
async fetch(request, env) {
if (request.method !== "POST") {
return new Response(JSON.stringify({
service: "My L402 API",
usage: 'POST with {"prompt": "your question"}',
price: PRICE_SATS + " sats per request"
}), { headers: { "Content-Type": "application/json" } });
}
const body = await request.json().catch(() => null);
if (!body?.prompt) {
return new Response(
JSON.stringify({ error: "prompt is required" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Check for payment proof
const paymentHash = body.payment_hash || "";
if (paymentHash) {
const paid = await checkPayment(env, paymentHash);
if (paid) {
// Payment verified — return the goods
return new Response(JSON.stringify({
result: "This is your paid API response for: " + body.prompt
}), { headers: { "Content-Type": "application/json" } });
}
return new Response(
JSON.stringify({ error: "payment not confirmed yet" }),
{ status: 402, headers: { "Content-Type": "application/json" } }
);
}
// No payment — create invoice and return 402
const invoice = await createInvoice(env, PRICE_SATS, "API query");
if (!invoice) {
return new Response(
JSON.stringify({ error: "failed to create invoice" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
return new Response(JSON.stringify({
status: "payment_required",
price_sats: PRICE_SATS,
payment_request: invoice.payment_request,
payment_hash: invoice.payment_hash,
instructions: "Pay the invoice, then POST again with payment_hash in the body."
}), {
status: 402,
headers: {
"Content-Type": "application/json",
"WWW-Authenticate": `L402 invoice="${invoice.payment_request}", payment_hash="${invoice.payment_hash}"`
}
});
}
};
async function createInvoice(env, amount, memo) {
const resp = await fetch(`${env.LNBITS_URL}/api/v1/payments`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": env.LNBITS_KEY
},
body: JSON.stringify({ out: false, amount, memo })
});
if (resp.status !== 201) return null;
return resp.json();
}
async function checkPayment(env, hash) {
const resp = await fetch(`${env.LNBITS_URL}/api/v1/payments/${hash}`, {
headers: { "X-Api-Key": env.LNBITS_KEY }
});
const data = await resp.json();
return data.paid === true;
}
Step 3: Configure Secrets
Add your LNbits credentials as Worker secrets:
npx wrangler secret put LNBITS_URL
# Enter: https://your-lnbits.example.com
npx wrangler secret put LNBITS_KEY
# Enter: your-invoice-read-key
Step 4: Deploy
npx wrangler deploy
Your L402 API is live. Test it:
# Step 1: Get the invoice
curl -X POST https://l402-api.your-account.workers.dev \
-H "Content-Type: application/json" \
-d '{"prompt": "Hello"}'
# Response: 402 with payment_request and payment_hash
# Step 2: Pay the invoice with any Lightning wallet
# Step 3: Retry with proof
curl -X POST https://l402-api.your-account.workers.dev \
-H "Content-Type: application/json" \
-d '{"prompt": "Hello", "payment_hash": "abc123..."}'
# Response: 200 with your data
How the Flow Works
Client Worker LNbits
| | |
|-- POST {prompt} ----------->| |
| |-- Create invoice ------>|
| |<-- payment_request -----|
|<-- 402 + invoice ---------- | |
| | |
| (user pays invoice) | |
| | |
|-- POST {prompt, hash} ----->| |
| |-- Check payment ------->|
| |<-- paid: true ----------|
|<-- 200 + data --------------| |
Adding the WWW-Authenticate Header
The L402 spec puts the invoice in the WWW-Authenticate header so clients can auto-detect paywalled endpoints:
WWW-Authenticate: L402 invoice="lnbc210n1...", payment_hash="abc123..."
Smart clients (like Fewsats or MCP tools) can parse this header and pay automatically. The JSON body is for humans and simpler clients.
Production Tips
Add a free tier
Give each IP one free request per day. Store the IP hash in KV:
// In wrangler.toml:
// [[kv_namespaces]]
// binding = "RATE_KV"
// id = "your-kv-namespace-id"
const ipHash = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(clientIp + new Date().toDateString())
);
const key = "free:" + btoa(String.fromCharCode(...new Uint8Array(ipHash)));
const used = await env.RATE_KV.get(key);
if (!used) {
await env.RATE_KV.put(key, "1", { expirationTtl: 86400 });
// Serve free response
}
Support the Authorization header
Some L402 clients send proof via header instead of body:
const authHeader = request.headers.get("Authorization") || "";
const match = authHeader.match(/^L402\s+(\S+)/i);
if (match) {
const paymentHash = match[1].split(":")[0];
// Verify payment...
}
Add CORS for browser clients
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization"
};
Real Example
This tutorial is based on a live API. Try it:
curl -X POST https://maximumsats.com/api/dvm \
-H "Content-Type: application/json" \
-d '{"prompt": "What is the Lightning Network?"}'
First query is free. After that, you'll get a 402 with a 21-sat invoice.
Consuming an L402 API (Client Side)
Building the server is half the story. Here's how to consume an L402 API using the Alby SDK to pay invoices programmatically via NWC:
import { LN } from "@getalby/sdk/lnclient";
async function fetchL402(url, body, nwcCredentials) {
// Step 1: Make the initial request
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body)
});
// Step 2: If not 402, return as-is
if (res.status !== 402) return res.json();
// Step 3: Parse the invoice from the 402 response
const data = await res.json();
const { payment_request, payment_hash } = data;
// Step 4: Pay the invoice via NWC
const ln = new LN(nwcCredentials);
await ln.pay(payment_request);
ln.close();
// Step 5: Retry with proof of payment
const paid = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...body, payment_hash })
});
return paid.json();
}
// Usage:
const result = await fetchL402(
"https://maximumsats.com/api/dvm",
{ prompt: "What is Lightning?" },
"nostr+walletconnect://..."
);
console.log(result);
This pattern works with any L402 API. The NWC connection handles the payment automatically — no manual wallet interaction needed. Use a dedicated NWC connection with limited permissions (pay_invoice only) and a spending cap.
Why This Matters
L402 turns every API into a vending machine. No signup, no billing, no chargebacks. The payment IS the authentication. This is how APIs should work in a world with programmable money.
What to build with this:
- AI inference endpoints (text, image, code)
- Data feeds (market data, analytics, weather)
- Content APIs (premium articles, research, media)
- Developer tools (linting, testing, CI services)
The entire deployment costs $0. Revenue is 100% margin.