Skip to main content

▶ Watch the 402 flow first

Interactive terminal demo — see request → 402 → Lightning pay → 200 OK, live in your browser.

Option A — Scaffold a full server in one command (fastest)

npx create-l402-app my-api
This creates a complete Express + l402-kit project: server.ts, .env.example, tsconfig.json, and a /premium endpoint ready to accept Lightning payments.
my-api/
  src/server.ts      ← your API with l402 middleware
  .env.example       ← Blink/OpenNode credentials template
  package.json       ← npm install l402-kit + tsx
  tsconfig.json
  README.md
Then:
cd my-api
cp .env.example .env   # add your Blink API key
npm install
npm run dev
# ⚡ l402-kit server running on http://localhost:3000
# curl http://localhost:3000/premium  →  402 Payment Required

Option B — Add to an existing project

1. Choose your mode

ManagedSoberano
Setup time~2 min~5 min
Monthly cost$0$0
Per-transaction fee0.3%0%
What you needA Lightning addressA Blink / Alby / BTCPay account
Processing 10,000 sats30 sat fee$0 fee
Best forGetting started fastVolume / production
Not sure? Start with Managed — no node, no account, just a Lightning address. Switch to Soberano in one line of code whenever you want 0% fees. Already-paid tokens keep working after the switch. Get a Lightning address (free, 2 min): Sign up at dashboard.blink.sv — you’ll receive yourname@blink.sv. Or use Alby, Phoenix, or Wallet of Satoshi. Soberano setup: Sign up at dashboard.blink.svAPI Keys → create key → copy your BTC Wallet ID from the wallet page. Set BLINK_API_KEY and BLINK_WALLET_ID in your .env.

2. Install

npm install l402-kit

3. Add to your API

4. Test it

curl http://localhost:3000/premium
Response:
{
  "error": "Payment Required",
  "price_sats": 100,
  "invoice": "lnbc1u1p...",
  "macaroon": "eyJoYXNo..."
}
Pay the invoice with any Lightning wallet, then:
curl http://localhost:3000/premium \
  -H "Authorization: L402 <macaroon>:<preimage>"
Response:
{ "data": "You paid 100 sats. Here is your data." }
Your API now accepts Bitcoin payments.

Test without real sats

You don’t need a Lightning wallet to test your integration. Use a mock provider — it generates valid cryptographic token pairs locally, zero network calls:
import { createHash, randomBytes } from "crypto";
import { l402 } from "l402-kit";
import type { LightningProvider, Invoice } from "l402-kit";

// Drop-in mock — generates real SHA256 hash/preimage pairs
function makeMockProvider(): LightningProvider & { preimage: string } {
  const preimage = randomBytes(32).toString("hex");
  const paymentHash = createHash("sha256").update(Buffer.from(preimage, "hex")).digest("hex");
  return {
    preimage, // use this in your test Authorization header
    async createInvoice(amountSats: number): Promise<Invoice> {
      const macaroon = Buffer.from(
        JSON.stringify({ hash: paymentHash, exp: Date.now() + 3_600_000 })
      ).toString("base64");
      return { paymentRequest: "lnbc_mock", paymentHash, macaroon, amountSats };
    },
    async checkPayment(): Promise<boolean> { return true; },
  };
}

// Usage in tests:
const mock = makeMockProvider();
app.get("/premium", l402({ priceSats: 10, lightning: mock }), handler);

// Step 1 — unauthenticated → 402
const res402 = await request(app).get("/premium");
// res402.body.macaroon  ← use this

// Step 2 — pay with mock preimage → 200
const res200 = await request(app)
  .get("/premium")
  .set("Authorization", `L402 ${res402.body.macaroon}:${mock.preimage}`);
// res200.status === 200 ✓
For real-money testing with the sandbox, use OpenNode’s testMode:
const lightning = new OpenNodeProvider(process.env.OPENNODE_KEY!, true); // testMode: no real sats
Full testing guide → Testing