How I Built a $0/Month NIP-05 Service Using Cloudflare Workers + Lightning

Build a fully automated NIP-05 verification service with zero hosting costs using Cloudflare Workers, D1, and LNbits

How I Built a $0/Month NIP-05 Service Using Cloudflare Workers + Lightning

If you want to offer NIP-05 verification on your own domain, you don’t need a server. Here’s how to build one for free using Cloudflare Workers, D1 (SQLite), and LNbits.

The Problem

NIP-05 verification helps people find you on Nostr. But most verification services either:

  • Cost money to host (VPS, database)
  • Require manual administration
  • Don’t accept Bitcoin

I wanted a service that runs for $0/month, accepts Lightning payments, and is fully automated.

Architecture

Three components, all free tier:

  1. Cloudflare Worker — handles HTTP requests, serves the landing page, and processes registrations
  2. D1 Database — Cloudflare’s SQLite-at-the-edge, stores identities
  3. LNbits — creates and verifies Lightning invoices

The flow:

User → Worker → creates invoice via LNbits API
                → stores pending registration in D1
User → pays invoice
User → polls /check/{hash}
Worker → checks payment status with LNbits
       → if paid, moves identity from pending → active
       → /.well-known/nostr.json now returns the identity

The Code

The entire service is ~290 lines of JavaScript. Here are the key parts.

Serving NIP-05 JSON

The NIP-05 spec requires a JSON response at /.well-known/nostr.json:

async function handleNostrJSON(url, env) {
  const name = (url.searchParams.get("name") || "").toLowerCase();
  const result = { names: {} };

  if (name) {
    const row = await env.DB.prepare(
      "SELECT pubkey FROM identities WHERE name = ?"
    ).bind(name).first();
    if (row) result.names[name] = row.pubkey;
  }

  return jsonResponse(result);
}

Registration Flow

When someone registers, we create a Lightning invoice and store the pending identity:

async function handleRegister(request, env) {
  const body = await request.json();
  const name = body.name.toLowerCase().trim();
  const pubkey = body.pubkey.toLowerCase().trim();

  // Check name availability
  const existing = await env.DB.prepare(
    "SELECT pubkey FROM identities WHERE name = ?"
  ).bind(name).first();
  if (existing) return jsonResponse({ error: "name already taken" }, 409);

  // Create Lightning invoice
  const invoice = await createInvoice(env, priceSats, `NIP-05: ${name}`);

  // Store pending registration
  await env.DB.prepare(
    "INSERT INTO pending (payment_hash, name, pubkey, created) VALUES (?, ?, ?, datetime('now'))"
  ).bind(invoice.payment_hash, name, pubkey).run();

  return jsonResponse({
    payment_request: invoice.payment_request,
    payment_hash: invoice.payment_hash,
    amount_sats: priceSats,
  });
}

Payment Verification

The frontend polls until payment is confirmed:

async function handleCheck(hash, env) {
  const pending = await env.DB.prepare(
    "SELECT name, pubkey FROM pending WHERE payment_hash = ?"
  ).bind(hash).first();

  const paid = await checkPayment(env, hash);
  if (!paid) return jsonResponse({ paid: false });

  // Move from pending to active
  await env.DB.prepare(
    "INSERT INTO identities (name, pubkey, created) VALUES (?, ?, datetime('now'))"
  ).bind(pending.name, pending.pubkey).run();

  await env.DB.prepare(
    "DELETE FROM pending WHERE payment_hash = ?"
  ).bind(hash).run();

  return jsonResponse({ paid: true, registered: true });
}

Deployment

With Wrangler CLI:

# Create D1 database
wrangler d1 create nip05-db

# Apply schema
wrangler d1 execute nip05-db --remote --file=schema.sql

# Set LNbits API key as secret
echo "your-key" | wrangler secret put LNBITS_KEY

# Deploy
wrangler deploy

Total deployment time: about 10 minutes.

What It Costs

  • Cloudflare Workers free tier: 100k requests/day
  • D1 free tier: 5 million reads/day, 100k writes/day
  • LNbits: self-hosted or use a public instance

For a NIP-05 service, you’ll never hit these limits. Effectively $0/month.

What I’d Do Differently

  1. Add Nostr Connect (NIP-46) so users can sign up without manually entering their hex pubkey
  2. Add relay hints to the NIP-05 response (the spec supports this)
  3. Build an admin dashboard to monitor registrations

Try It

The service is live at satoshis.lol — 100 sats for a verified address like yourname@satoshis.lol.

The pattern works for any domain. If you control a domain and run LNbits (or any Lightning node with an API), you can deploy your own in under an hour.


No comments yet.

Highlights (1)

NIP-05 verification helps people find you on Nostr. But most verification services either:

NIP-05 verification helps people find you on Nostr. But most verification services either: Cost money to host (VPS, database) Require manual administration Don't accept Bitcoin I wanted a service that runs for $0/month, accepts Lightning payments, and is fully automated.