Introduction
I built a NIP-05 verification service that charges 100 sats per registration. It runs on Cloudflare Workers for free and accepts Lightning payments through Nostr Wallet Connect. Here's the full walkthrough.
What you'll learn:
- How NWC simplifies Lightning payment integration
- Creating Lightning invoices with
@getalby/sdk - Polling for payment confirmation via NWC
- Deploying a Lightning-enabled service for free
Prerequisites: An Alby Hub instance (or any NWC-compatible wallet), basic JavaScript knowledge, a free Cloudflare account.
What is Nostr Wallet Connect?
NWC (defined in NIP-47) is a protocol that lets applications communicate with Lightning wallets over Nostr relays. Instead of running your own Lightning node or managing REST APIs, NWC gives you a single connection string that enables:
- Creating invoices (receive payments)
- Paying invoices (send payments)
- Checking balance
- Listing transactions
The connection string format:
nostr+walletconnect://<wallet-pubkey>?relay=<relay-url>&secret=<secret>
Your app signs NIP-47 request events, sends them to the relay, and the wallet responds with results. No REST endpoints, no API keys, no server infrastructure to maintain.
Why NWC Over Other Approaches?
I've used LND direct, LNbits API, and LNURL. Each has tradeoffs:
- LND/CLN direct — full control, but requires running a node and exposing gRPC/REST
- LNbits API — good middleware, but requires an LNbits instance and API key management
- LNURL — widely supported, but complex server-side callback setup
- NWC — one connection string, works from anywhere, wallet handles all the complexity
The tradeoff with NWC is relay availability. If the relay goes down, your wallet communication breaks. For a side project, that's fine. For critical infrastructure, you'd want fallback relays.
Step 1: Set Up Your Wallet
Using Alby Hub
- Install Alby Hub — it runs as a self-contained Lightning node
- Go to Settings → Wallet Connections
- Create a new app connection with permissions:
make_invoice,lookup_invoice - Copy the NWC connection string
For Testing: Developer Sandbox
Don't want to set up a real wallet yet? Use the NWC test faucet:
curl -X POST "https://faucet.nwc.dev?balance=10000"
This returns a connection string you can use immediately. Test wallets can pay each other but don't interact with the real Lightning network.
Step 2: Build the Payment Backend
Install the Alby SDK:
npm install @getalby/sdk
Here's the Cloudflare Worker. Two functions handle all wallet communication through NWC.
Creating an Invoice
import { NWCClient } from "@getalby/sdk/nwc";
async function createInvoice(env, amount, description) {
const client = new NWCClient({
nostrWalletConnectUrl: env.NWC_URL,
});
const response = await client.makeInvoice({
amount: amount * 1000, // NWC uses millisats
description,
});
return {
payment_request: response.invoice,
payment_hash: response.payment_hash,
};
}
makeInvoice sends a NIP-47 make_invoice request to your wallet via the relay. The wallet creates the BOLT11 invoice and returns it. Note that NWC amounts are in millisatoshis, so multiply sats by 1000.
Checking Payment Status
async function checkPayment(env, paymentHash) {
const client = new NWCClient({
nostrWalletConnectUrl: env.NWC_URL,
});
try {
const invoice = await client.lookupInvoice({
payment_hash: paymentHash,
});
return invoice.settled_at != null;
} catch (e) {
return false;
}
}
lookupInvoice queries your wallet for the invoice status. If settled_at is set, the payment has been received.
The Complete Worker
Wire it together into a registration flow: client submits a name and pubkey, server creates an invoice via NWC, client pays, server verifies via NWC and registers.
import { NWCClient } from "@getalby/sdk/nwc";
async function createInvoice(env, amount, description) {
const client = new NWCClient({
nostrWalletConnectUrl: env.NWC_URL,
});
const response = await client.makeInvoice({
amount: amount * 1000,
description,
});
return {
payment_request: response.invoice,
payment_hash: response.payment_hash,
};
}
async function checkPayment(env, paymentHash) {
const client = new NWCClient({
nostrWalletConnectUrl: env.NWC_URL,
});
try {
const invoice = await client.lookupInvoice({
payment_hash: paymentHash,
});
return invoice.settled_at != null;
} catch (e) {
return false;
}
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/api/register' && request.method === 'POST') {
const { name, pubkey } = await request.json();
if (!name || !/^[a-z0-9._-]+$/.test(name)) {
return Response.json({ error: 'Invalid name' }, { status: 400 });
}
const invoice = await createInvoice(env, 100, 'NIP-05: ' + name);
await env.DB.prepare(
'INSERT INTO pending (name, pubkey, payment_hash) VALUES (?, ?, ?)'
).bind(name, pubkey, invoice.payment_hash).run();
return Response.json({
payment_request: invoice.payment_request,
payment_hash: invoice.payment_hash
});
}
if (url.pathname.startsWith('/api/check/')) {
const hash = url.pathname.split('/').pop();
const paid = await checkPayment(env, hash);
if (paid) {
const p = await env.DB.prepare(
'SELECT name, pubkey FROM pending WHERE payment_hash = ?'
).bind(hash).first();
if (p) {
await env.DB.prepare(
'INSERT INTO verified (name, pubkey) VALUES (?, ?)'
).bind(p.name, p.pubkey).run();
await env.DB.prepare(
'DELETE FROM pending WHERE payment_hash = ?'
).bind(hash).run();
}
}
return Response.json({ paid });
}
return new Response('Not found', { status: 404 });
}
};
Step 3: Build the Frontend
The client-side flow: user submits the form, gets an invoice, pays it in their wallet, and the page polls until payment confirms.
async function register() {
const name = document.getElementById('name').value;
const pubkey = document.getElementById('pubkey').value;
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, pubkey })
});
const { payment_request, payment_hash } = await res.json();
// Display the BOLT11 invoice for the user to pay
document.getElementById('invoice').textContent = payment_request;
// Poll every 5 seconds for up to 5 minutes
let attempts = 0;
const poll = setInterval(async () => {
attempts++;
const check = await fetch('/api/check/' + payment_hash);
const { paid } = await check.json();
if (paid) {
clearInterval(poll);
showSuccess(name + '@yourdomain.com is verified!');
} else if (attempts >= 60) {
clearInterval(poll);
showError('Payment timeout');
}
}, 5000);
}
Five minutes of polling at 5-second intervals. If the payment doesn't come through, the user can always retry — the pending record stays in the database.
Step 4: Deploy for Free
The entire service runs on Cloudflare's free tier:
- Workers: 100,000 requests/day free
- D1: 5GB SQLite database free
- Total cost: $0/month
# Create D1 database
wrangler d1 create my-lightning-db
# Deploy schema
wrangler d1 execute my-lightning-db --remote --file=schema.sql
# Set your NWC connection string as a secret
echo "nostr+walletconnect://..." | wrangler secret put NWC_URL
# Deploy
wrangler deploy
The NWC connection string is stored as a Worker secret. It never appears in your source code or wrangler.toml.
Security Considerations
A few things I learned the hard way:
- Never expose your NWC connection string in client-side code. It contains the secret key that controls your wallet. All NWC communication goes through the Worker.
- Scope your NWC permissions — only grant
make_invoiceandlookup_invoice. Don't grantpay_invoiceunless the service needs to send payments. - Validate all inputs before creating invoices. The regex on the name field isn't optional.
- Set invoice expiry — 15 minutes is typical. Without it, unpaid invoices pile up.
- Handle idempotency — if payment confirms but the DB write fails, you need a way to recover. Check for existing verified records before inserting.
- Rate limit invoice creation to prevent someone from spamming thousands of invoices against your wallet.
Real-World Example
satoshis.lol is a live NIP-05 verification service built with this architecture. 100 sats per registration, Cloudflare Workers + D1, NWC for payments. Pay the invoice, get your Nostr identity verified, done.
Resources
- Alby JS SDK (@getalby/sdk) — NWCClient, WebLN provider, and LN client
- NWCClient Documentation — makeInvoice, lookupInvoice, payInvoice
- Alby Developer Guide
- NIP-47 Specification — Nostr Wallet Connect protocol
- Cloudflare Workers Docs
If you build something with this, let me know. I'm always interested to see what people do with Lightning payments in their projects.