A multi-tenant x402 facilitator for Lightning Network payments, built on Nostr Wallet Connect (NWC).
┌─────────────────────────────────────────────────────────┐
│ Facilitator endpoints │
│ POST /register register a merchant NWC secret │
│ POST /invoice generate BOLT11 for a merchant │
│ POST /verify confirm invoice paid via NWC │
│ POST /settle confirm invoice settled via NWC │
│ GET /supported capability discovery │
│ GET /invoice/status?invoice=<bolt11> poll status │
├─────────────────────────────────────────────────────────┤
│ Demo (enabled when DEMO_NWC_SECRET is set) │
│ GET /demo/quote pay ~$0.01 → get a Satoshi quote │
└─────────────────────────────────────────────────────────┘
The facilitator holds no wallet credentials in its config. Each merchant calls POST /register with their NWC connection string and receives an opaque merchantId (UUID). The NWC secret is stored in Redis; clients only ever see the UUID. The facilitator uses it to:
- Create a BOLT11 invoice against the merchant's wallet (
POST /invoice) - Confirm settlement against the merchant's wallet (
POST /settle)
Multiple merchants can share one facilitator instance, each with their own independent wallet.
Unlike EVM-based x402 where the client signs a transaction, Lightning requires a BOLT11 invoice to be generated server-side before the client pays:
- Client
GET /resource→ server returns402with invoice inextra.invoice - Client pays the BOLT11 invoice out-of-band (Lightning wallet)
- Client retries with the paid invoice string in the
payment-signatureheader (payload.invoice) - Server calls
/verify(checks invoice is in Redis, amount matches, NWC confirmssettled_at) - Server calls
/settle(NWClookup_invoiceconfirmssettled_at, deletes invoice from Redis)
- Node.js 18+
- Redis (local or hosted — e.g. Upstash)
- At least one NWC-compatible wallet (e.g. Alby):
- Merchant wallet — receives payments
- Sender wallet — sends payments during e2e tests (optional)
npm installCopy and fill in .env:
# Required
REDIS_URL=redis://localhost:6379
# Optional — port defaults to 3000
PORT=3000
# Optional — public base URL (used in /.well-known/x402 response)
BASE_URL=https://your-domain.com
# Optional — enables the /demo/quote endpoint
DEMO_NWC_SECRET=nostr+walletconnect://<pubkey>?relay=<relay>&secret=<secret>For e2e tests, copy .env.sender.example to .env.sender and fill in a sender wallet NWC URL.
npm run devcurl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"nwcSecret":"nostr+walletconnect://..."}'
# → { "merchantId": "<uuid>" }Use the merchantId in your resource server's extra.merchantId field.
NWC clients are cached indefinitely by connection string. There is no reconnection logic or health-check. Long-lived connections may silently drop. Consider adding a ping/reconnect strategy.