# L402 Setup Guide — WebberSites x402 Data API **Goal:** Accept Bitcoin Lightning payments (L402) alongside x402 USDC on the same 402 responses. Tier 1, self-wired: Rizful custodial wallet + NWC, no managed-mode fee, $0 setup. > **Why Rizful, not Coinos:** we originally ran on Coinos, but its relay/API sit behind a Cloudflare zone that 403's datacenter egress IPs — Render's outbound IP got blocked and the rail went dark. Rizful's relay accepts datacenter IPs, so the rail stays up in production. Any NWC wallet works; this guide uses Rizful because it's free, no-KYC, and unblocked. --- ## Part 1 — Rizful account + receive-only NWC connection string (~10 min) 1. Go to **rizful.com** → sign up. Free, custodial, no KYC. 2. From the main menu, tap **NWC** to open your Nostr Wallet Connect settings. 3. In the **Get NWC Codes** window, tap **Copy receive-only code**. This copies the connection string to your clipboard. It looks like: ``` nostr+walletconnect://?relay=wss://relay-nwc.rizful.com/v1&secret= ``` 4. **Use the receive-only code, not a full one.** A receive-only NWC code can `make_invoice`/`lookup_invoice` but physically cannot `pay_invoice` — so even if the string leaks, nobody can spend from your vault. That's exactly the scope this server needs; it never sends. (No manual permission-scoping step required — receive-only enforces it.) 5. Save it as an env var on your server: ``` NWC_URL=nostr+walletconnect://... L402_SECRET= # for signing macaroons ``` Same pattern as your CDP facilitator vars. > **Custody note:** Rizful holds the sats. Sweep to a wallet you control (even just Alby Go on your phone) whenever the balance is worth caring about. --- ## Part 2 — Install packages ```bash npm install @getalby/sdk ``` That's the only new dependency. `@getalby/sdk` gives you an NWC client (works with Rizful, Alby Hub, or any NWC wallet — this is what makes the later Tier 2 migration a config change). We hand-roll the L402 middleware (~80 lines) instead of using l402-kit, so you control verification and there's no third party in the flow. L402 clients treat the macaroon as an opaque base64 blob they echo back, so an HMAC-signed token is protocol-compatible. --- ## Part 3 — The L402 module Create `payments/l402.js`: ```js // payments/l402.js const crypto = require("crypto"); const { nwc } = require("@getalby/sdk"); const client = new nwc.NWCClient({ nostrWalletConnectUrl: process.env.NWC_URL, }); const SECRET = process.env.L402_SECRET; // ---- BTC/USD conversion, cached 60s ---- let rateCache = { rate: 0, ts: 0 }; async function usdToSats(usd) { if (Date.now() - rateCache.ts > 60_000) { const r = await fetch("https://api.coinbase.com/v2/prices/BTC-USD/spot"); const j = await r.json(); rateCache = { rate: parseFloat(j.data.amount), ts: Date.now() }; } return Math.max(1, Math.ceil((usd / rateCache.rate) * 100_000_000)); } // ---- macaroon = HMAC-signed token binding the payment hash ---- function mintMacaroon(paymentHash, path) { const payload = Buffer.from( JSON.stringify({ ph: paymentHash, path, exp: Date.now() + 3600_000 }) ).toString("base64url"); const sig = crypto.createHmac("sha256", SECRET).update(payload).digest("base64url"); return `${payload}.${sig}`; } function verifyMacaroon(mac, path) { const [payload, sig] = (mac || "").split("."); if (!payload || !sig) return null; const expected = crypto.createHmac("sha256", SECRET).update(payload).digest("base64url"); if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null; const data = JSON.parse(Buffer.from(payload, "base64url").toString()); if (data.exp < Date.now()) return null; if (data.path !== path) return null; return data; // { ph, path, exp } } // ---- Express middleware factory ---- // usage: app.get("/api/seo-audit", l402({ priceUsd: 0.01 }), handler) function l402({ priceUsd }) { return async (req, res, next) => { const auth = req.get("Authorization") || ""; // 1) Client presenting proof? Verify statelessly, no network call. if (auth.startsWith("L402 ")) { const [mac, preimage] = auth.slice(5).split(":"); const data = verifyMacaroon(mac, req.path); if (data && preimage) { const hash = crypto .createHash("sha256") .update(Buffer.from(preimage, "hex")) .digest("hex"); if (hash === data.ph) { req.l402 = { paymentHash: data.ph, sats: true }; // for your hit logging return next(); } } // fall through to a fresh challenge on bad proof } // 2) No/invalid proof → issue challenge. Coexists with x402: // L402 lives in WWW-Authenticate; x402 keeps the body/headers. try { const sats = await usdToSats(priceUsd); const inv = await client.makeInvoice({ amount: sats * 1000, // NWC amounts are millisats description: `WebberSites API ${req.path}`, }); const mac = mintMacaroon(inv.payment_hash, req.path); res.set( "WWW-Authenticate", `L402 macaroon="${mac}", invoice="${inv.invoice}"` ); } catch (e) { console.error("L402 invoice creation failed:", e.message); // Don't block the request path — x402 challenge still goes out. } return next(); // hand off so your x402 middleware builds the 402 body }; } module.exports = { l402 }; ``` **How it works:** - No proof → creates a Lightning invoice via NWC, mints a macaroon binding the invoice's payment hash to the route, puts both in `WWW-Authenticate`, then lets your existing x402 middleware produce the 402 body. One response, two rails. - Proof presented (`Authorization: L402 :`) → verifies HMAC + `sha256(preimage) === payment_hash`. Only someone who paid the invoice learns the preimage, so this is cryptographic proof of payment with **zero database and zero network call**. - If NWC/Rizful is down, x402 still works — L402 header just gets skipped. --- ## Part 4 — Mounting alongside x402 Order matters: **L402 middleware first, then x402.** ```js const { l402 } = require("./payments/l402"); // before (x402 only): // app.get("/api/seo-audit", x402Middleware(...), handler); // after (dual): app.get("/api/seo-audit", l402({ priceUsd: 0.01 }), x402Middleware(...), handler); ``` But your x402 middleware currently gates unconditionally — it needs one change: **skip the x402 check when `req.l402` is set** (payment already proven via Lightning). If you can't modify it, wrap it: ```js const skipIfL402 = (mw) => (req, res, next) => req.l402 ? next() : mw(req, res, next); app.get("/api/seo-audit", l402({ priceUsd: 0.01 }), skipIfL402(x402Middleware(...)), handler); ``` Since you have 40+ endpoints driven by a manifest, add a helper that builds the middleware pair from each manifest entry's price so you wire it once. --- ## Part 5 — Hit logging In your existing hit logger, alongside payer-wallet capture from X-PAYMENT: ```js if (req.l402) { log.rail = "l402"; log.payment_hash = req.l402.paymentHash; } ``` Heads-up: payment hashes are per-invoice, so unlike Base wallets they give you **no cross-call buyer identity**. Loyalty pricing stays an x402-only feature. --- ## Part 6 — Discovery 1. **`/.well-known/l402.json`** — advertise your endpoints/prices (mirror your manifest): ```js app.get("/.well-known/l402.json", (req, res) => { res.json({ name: "WebberSites x402 Data API", endpoints: manifest.map((e) => ({ path: e.path, method: e.method, price_usd: e.priceUsd, protocols: ["l402", "x402"], })), }); }); ``` 2. Update your landing page + existing x402 directory listings (x402scan, Bazaar, x402-list) to mention dual-rail support. 3. Submit to the Lightning-side ecosystem: Alby's agent tools directory (getalby.com/ai) and lncurl.lol list agentic L402 services. --- ## Part 7 — Testing 1. **Challenge shape:** ```bash curl -i https://api.webbersites.com/api/seo-audit ``` Expect `402`, a `WWW-Authenticate: L402 macaroon="...", invoice="lnbc..."` header, AND your normal x402 body. Confirm x402 clients still work (regression!). 2. **End-to-end payment:** install a Lightning wallet with a few hundred sats (a separate Rizful account, or Alby Go). Pay the `lnbc...` invoice, grab the preimage from the wallet's payment detail, then: ```bash curl -H 'Authorization: L402 :' https://api.webbersites.com/api/seo-audit ``` Expect `200`. 3. **Agent-side test:** Lightning Labs' `lnget` CLI or Lightning Wallet MCP — both auto-detect the L402 challenge and pay. This is what real agent traffic will look like. 4. **Failure mode:** kill `NWC_URL` and confirm endpoints still serve x402 challenges normally. --- ## Part 8 — Ops checklist - [ ] Restore server.js to working state **before any of this** (icon engine + helpers) - [ ] `NWC_URL` + `L402_SECRET` in env (never in repo — you deploy via GitHub web UI, so double-check nothing lands in a commit) - [ ] Sweep Rizful balance periodically - [ ] Watch logs for `rail: "l402"` hits — this is your demand signal - [ ] If real volume shows up → Tier 2: install Alby Hub, open the LSP channel (~$10–15 one-time), swap `NWC_URL` to the Hub's connection string. **No code changes.** ## Things to verify against live docs (UI details drift) - Rizful NWC generation (confirmed 2026-07-12 via docs.megalithic.me): main menu → **NWC** → **Get NWC Codes** → **Copy receive-only code**. Relay `wss://relay-nwc.rizful.com/v1`. - `@getalby/sdk` NWC method names (`makeInvoice` / `lookupInvoice`) against current docs at github.com/getAlby/js-sdk - Permission scoping: Rizful's **receive-only** code is inherently invoice-only (cannot `pay_invoice`), so no separate scoping step is needed.