Lightning Address Marketplace
One search across every Lightning address provider — real prices, paid directly to providers over Lightning.
One search box across every lightning address provider on ZapAPI. Type a name once, see availability and real prices across all domains (zapapi.app plus every registered provider), pick one, pay, and the address activates at that provider — paid directly to the provider over Lightning. ZapAPI brokers the flow and never holds the name or the funds.
1. What a "lightning address" actually is (protocol mechanics)
A lightning address alice@example.com is two protocols sharing one identifier:
1.1 LNURL-pay (the payment side — LUD-06, LUD-12, LUD-16)
Payer's wallet Address host (provider)
│ │
│ 1. GET https://example.com/.well-known/lnurlp/alice
│ ─────────────────────────────────────► │
│ │
│ 2. { callback, minSendable, maxSendable, metadata, tag:"payRequest",
│ commentAllowed, allowsNostr?, nostrPubkey? }
│ ◄───────────────────────────────────── │
│ │
│ 3. GET {callback}?amount=21000&comment=hi[&nostr={zap request}]
│ ─────────────────────────────────────► │
│ │
│ 4. { pr: "lnbc210n1…" } fresh BOLT11 on recipient's wallet
│ ◄───────────────────────────────────── │
│ │
│ 5. wallet pays the invoice directly over Lightning ────────► recipient's walletKey facts (verified against the LUD specs + our live implementation):
- Resolution is plain HTTPS on the domain — any host can serve it.
- The invoice is minted on the recipient's wallet at payment time (not pre-generated). So "who owns the wallet that receives" is decided by the callback, not by the name.
commentAllowed(LUD-12),allowsNostr+nostrPubkey(NIP-57 zaps) are optional enrichments.- Success metadata can carry
successAction(message / url / AES secret).
1.2 NIP-05 (the identity side)
GET https://example.com/.well-known/nostr.json?name=alice
{ "names": { "alice": "<hex pubkey>" },
"relays": { "<hex pubkey>": ["wss://relay1", "wss://relay2"] } }- It's identification, not verification — the domain attests "this pubkey is known here as alice". Followers should key on the pubkey, not the name.
- Must be served with
Access-Control-Allow-Origin: *and no redirects (spec). - Same name can serve both protocols: most wallets resolve
alice@example.comfor payments via LNURL AND for nostr via NIP-05.
1.3 Who can sell these?
Anyone who controls a domain + a Lightning wallet. The ecosystem splits into:
- Custodial wallets (Wallet of Satoshi, Alby, Strike) — free random address, paid custom name (WoS: unlock via proof-of-work; Alby: paid plan).
- Federated/bridge servers (satdress, BridgeAddr, lnme, PHP scripts) — self-host, point DNS at the bridge, receive to your own node.
- LNbits + nostrnip5 extension — the provider sells identifiers on a domain they control, per-year pricing, and it can ALSO wire the sold name to a real Lightning Address on the same LNbits (via a generated LNURL-pay link).
- LNbits + lnaddress extension — rent addresses per-day on a domain.
2. The provider side — LNbits nostrnip5 in depth (our primary engine)
Verified against lnbits/nostrnip5 source (services.py) + the OpenAPI spec:
2.1 Domain model
Each domain row: { id, domain, wallet, currency, cost, cost_extra } where
cost_extra = { max_years, transfer_secret, char_count_cost[], rank_cost[], promotions[] }.
2.2 Pricing engine (all provider-side, real)
GET /nostrnip5/api/v1/domain/{id}/search?q={name}&years={n} AddressStatus:
{ identifier, available, price, price_in_sats, price_reason, currency,
free_identifier_number? }Price = highest of (base cost × years, char-length bracket, Cloudflare-rank bracket) − promo discount.
price_reason says which bracket won (base / char-length / rank) — we surface it in the UI.
We already consume this live (our /my-wallet/nip5/search proxies it).
Fiat sats conversion (verified against LNbits core):
- Providers price in fiat (
domain.currency, e.g. USD/EUR);price_in_satsis computed at query time via the instance's configured rate provider. - Rate sources: CoinGecko (default), Yadio, ExchangeRate.host — set via
LNBITS_EXCHANGE_RATE_PROVIDER. Some instances use the LNbits Price Aggregator (price.lnbits.com) which medians 7 exchanges every 60s. - Consequence: the same fiat price shows different sats across providers (different rate sources + cache TTLs), and the sats quote drifts between search and checkout as BTC moves. Design rule: display both fiat and sats, treat fiat as canonical for price comparison, and treat the reservation response (§3.3) — never the search response — as the authoritative charge.
- Re-quote rule: if
price_in_satsat reservation differs from the search quote by >5%, pause checkout and re-confirm with the user.
Edge cases found in source (services.py):
rank == 0name is unbuyable, not just expensive (get_identifier_price_datareturnsNone). Search returnsavailable: falsewith noprice_reason. UI must distinguish "taken" from "blocked/blacklisted" copy.free_identifier_number— when the exact name is taken, search returns a random free suffix (alice.482910). Providers can offer a free tier this way (is_free_identifieractivates instantly without payment). Good UX: "alice is taken — alice.482910 is free on this domain."
2.3 Purchase lifecycle (verified against nostrnip5 v1.1.0 source + live ln.nostrfeed.com)
- Reserve + invoice:
POST /nostrnip5/api/v1/public/domain/{id}/address{ local_part, years, pubkey?, relays?, promo_code?, create_invoice: true }pending address +{ payment_hash, payment_request, rotation_secret? }(invoice minted on the domain wallet —services.pycreates it withwallet_id=domain.wallet). - Pay the invoice (any Lightning wallet; from our side we pay from the buyer's ZapAPI wallet).
- Auto-activation: LNbits watches the payment (
check_address_paymentmatchespayment.extra.address_id) and flips the addressactive+ setsexpires_at = now + 365 × years. - Optional: provider can wire the name to a real LNURL-pay link
(
update_ln_address) the sold name becomes a working lightning address too. - Refunds: if two buyers race a name, the loser is marked "refund owed".
Three source-verified facts that shape the marketplace design:
- Every brokered purchase is anonymous to the provider. The public endpoint
uses
optional_user_id— an LNbits user, which we can't authenticate as on an arbitrary provider's instance. So the extension generates a throwawayowner_idand returns arotation_secret(nostr_nip_5_rotation_secret_<uuid>) — the buyer's only credential to later rotate their pubkey or manage the address on the provider's site. We must persistrotation_secreton the order and show it to the user once ("save this — it's your management key at the provider"). - Reservations are soft — they do NOT block the name (verified in
crud.py). Availability checks useget_active_address_by_local_part(WHERE active = true), andcreate_addressonly assertsnot address.active— so a pending (unpaid) row neither shows the name as taken nor blocks a new reservation. The name is only locked once an invoice is PAID. There is no reservation TTL and no public DELETE route in the extension (our olddeleteNip5AddressPublicassumption 404s — verified live). Consequences:
- Abandoned checkouts do NOT brick names — no provider-side cleanup needed
or possible. Our sweeper only flips stale local orders to
expired. - Two buyers can hold reservations for the same name simultaneously —
whoever's invoice settles first activates; the loser's payment activates
nothing and is marked "refund owed" provider-side (§3.4). This makes the
re-check-at-payment step load-bearing: before paying from the buyer's
wallet we re-search the name; if it became taken since reservation, we
abort the pay step and mark the order
failed(nothing to refund — the buyer never paid).
- Refunds cannot be automatic for our buyers. Per the official extension docs, automatic refunds only work when the buyer paid from a wallet on the same LNbits instance — there's no wallet to credit back otherwise. Our buyers pay from ZapAPI-platform wallets (a different instance than the provider's), so "refund owed" is a manual provider-side flow: surface the state + provider contact in the order UI; the provider's remedy is either a manual reimburse (admin endpoint exists) or "activate without payment". Design the orders page around this, not around an automatic claim button.
2.4 Why this is the ideal provider engine
- Public, unauthenticated register flow (anyone's server can call it).
- Per-domain wallet the provider gets paid directly.
- Deterministic pricing including premium tiers (we just display it).
- Activation is automatic on payment — no manual step.
- NIP-05 served by the extension:
GET /nostrnip5/api/v1/domain/{id}/nostr.json.
2.5 What a provider must run (onboarding checklist for THEM)
- An LNbits instance with the
nostrnip5extension enabled. - A domain they control, with
/.well-known/nostr.jsonproxied to their LNbits (nginx/Caddy snippet from the extension docs — included in §8). - One domain entry (wallet + currency + price + tiers).
- For real lightning addresses on the names: LNURL-pay enabled (core in LNbits).
For ZapAPI-hosted domains we do this ourselves (already live on zapapi.app): our own LNURL module serves params/callback/NIP-05 and mints the invoice on the owner's ZapAPI wallet — no LNbits domain needed for the first-party free tier.
3. The marketplace model (how domain registrars do it — GoDaddy/Namecheap pattern)
What the user asked for is the domain search aggregator:
┌──────────────────────────────────────────────────────────────┐
│ ZapAPI — "Find your lightning address" │
│ │
│ [ alice_____________________] Search │
│ │
│ alice@zapapi.app FREE · first-party Claim │
│ alice@ln.nostrfeed.com 2,100 sats/yr · NostrFeed Buy │
│ alice@premium.io taken │
│ alice@getzap.me 3,000 sats · NostrFeed Buy │
│ │
│ Filters: □ available only □ cheapest first ⏱ 1–3 years │
└──────────────────────────────────────────────────────────────┘3.1 Provider registry (new ZapAPI primitive)
AddressProvider entity (admin-managed):
id, name, logoUrl?, baseUrl (LNbits), type: 'lnbits-nip5' | 'zapapi-first-party',
access: 'public-register' (keyless) | 'admin-key',
domains: cached from /nostrnip5/api/v1/domains,
status: 'active' | 'paused', commissionPct (optional, default 0),
webhookSecret (for purchase callbacks), createdAtOnboarding a provider (admin or self-serve):
- Provider gives their LNbits base URL + one invoice key (read-only, required
for the domain catalog — verified:
GET /domainsusesrequire_invoice_key; full packet in §8). - We health-check:
GET {base}/nostrnip5/api/v1/domains?all_wallets=true+ a live probe of their domain's/.well-known/nostr.json(CORS + no redirects). - We import their domains + prices as a cached catalog (refresh on demand + TTL 15 min).
- Provider sets payout expectation: purchases go to THEIR domain wallet by design — ZapAPI never holds their sats.
Catalog scoping (verified in extension source): GET /domains returns
only the domains owned by the key-owner's LNbits user account (all of
their wallets via all_wallets=true) — there is NO endpoint to enumerate
every domain on an instance (deliberate privacy boundary). Consequences:
- A provider selling on their own instance sees all of THEIR domains — correct.
- A shared/hosted instance (many sellers) needs one application per seller:
uniqueness is
(baseUrl, ownerUserId), and search + catalog dedupe by DNS domain so overlapping visibility never yields duplicate rows. - Auto-onboarding: when a user connects their own LNbits provider
(Projects Providers) and the connection test finds the
nip5-addressescapability, we auto-register a pending marketplace application with their invoice key and import every domain their LNbits user can see — zero duplicate data entry for sellers who are already ZapAPI users.
3.2 Search aggregation (the core UX)
GET /api/v1/marketplace/search?q=alice&years=1&availableOnly=true&sort=price
Fan-out to every ACTIVE provider in parallel:
- zapapi first-party our own
lnurlService.isUsernameAvailable(free). - each LNbits provider
GET {base}/nostrnip5/api/v1/domain/{id}/search?q=…(per domain), normalize into one result row.
Each row:
{ providerId, providerName, domain, fullAddress, available,
priceSats|null (null = free), priceFiat?, currency, priceReason?,
freeIdentifierNumber?, // "alice.482910" fallback when taken (§2.2)
blockedReason?: 'taken' | 'unbuyable', // rank==0 unbuyable (§2.2)
termYears }Timeout per provider ~2.5s, Promise.allSettled — a dead provider degrades to a
grey "unreachable" row, never blocks the search. Cache the per-provider domain
list 15 min; the name-check itself is always live (availability changes fast).
Notes:
- Fan-out is per-domain, not per-provider — a provider with 5 domains costs 5 parallel calls. Cheap (DB lookup + cached fiat rate), but mind provider rate limits as the registry grows.
- Pass the user's selected term (
years) into every search call — price scales with years, andcost_extra.max_yearscaps it per domain. - Display both
priceFiat(canonical, for sorting/comparison) andpriceSats(what will actually be charged, subject to the §3.3 re-quote rule).
3.3 Purchase brokering ("user pays direct provider, we forward")
User (buyer) ZapAPI (broker) Provider (LNbits)
│ │ │
│ pick alice@prov.io │ │
│ ─────────────────────────► │ POST {base}/nostrnip5/api/v1/public/domain/{id}/address
│ │ ─────────────────────────────► │ reserve + invoice
│ │ │ (create_invoice: true)
│ │ ◄─ payment_hash + bolt11 ─────│
│ │ + rotation_secret + price_in_sats
│ │ │
│ │ RE-QUOTE: price_in_sats vs search quote
│ │ differs >5%? ask user to re-confirm
│ │ │
│ │ save MarketplaceOrder { reserved,
│ │ invoice, rotationSecret, │
│ │ externalAddressId, │
│ │ expiresAt: now+30min } │
│ │ │
│ show QR + "Pay" │ │
│ ◄───────────────────────── │ │
│ │ │
│ pay from MY ZapAPI wallet (or any external wallet) │
│ ─────────────────────────► │ pay bolt11 ─────────────────► │ invoice settles
│ │ │ on DOMAIN wallet
│ │ │
│ │ poll GET /nostrnip5/api/v1/domain/{id}/payments/{hash}
│ │ ─────────────────────────────► │
│ │ ◄────────── paid: true ───────│ auto-activate
│ alice@prov.io live │ │
│ ◄───────────────────────── │ order activated; │
│ + show rotation_secret once ("save this — manage your │
│ address at the provider with it") │
│ │ │
│ ── no payment within 30 min ── │
│ │ SWEEPER (cron, 5 min): │
│ │ order expired (LOCAL only — │
│ │ no provider call: reservations │
│ │ don't block names, §2.3) │Why this satisfies "users only pay direct provider and we send to provider":
- The invoice is minted on the provider's domain wallet — sats never touch ZapAPI's wallet.
- When the buyer pays "from my ZapAPI wallet", that's just the payment rail (like a card network); the destination is the provider's wallet.
- Optional commission: if a provider wants revenue share, they set
commissionPctand WE charge the buyer a separate tiny ZapAPI-fee invoice — never mixed with the provider's price. Default 0 (free market).
Two race guards (§2.3): (1) the sweeper flips stale unpaid orders to
expired locally — reservations don't block names provider-side, so that's
bookkeeping only; (2) before paying we re-check availability and abort if
the name was taken since reservation — the buyer never pays into the void.
3.4 Order records (receipts without custody)
MarketplaceOrder entity:
id, userId, providerId, domain, localPart, fullAddress,
priceSats, priceFiat, currency, years, // priceSats from the RESERVATION response
invoice: { paymentHash, bolt11, amountSats, status },
status: 'reserved' | 'paid' | 'activated' | 'expired' | 'failed' | 'refund_owed' | 'refunded',
externalAddressId (provider-side address id),
rotationSecret (encrypted; shown to the user once — §2.3),
expiresAt (reservation TTL, now+30min — drives the sweeper),
createdAt, paidAt?, activatedAt?We poll check_address_payment until activated. If the provider reports
"refund owed" we surface it in the user's orders with the provider's contact
info — refunds are a manual provider-side flow for cross-instance buyers
(§2.3), not an automatic claim button.
3.5 Renewals & management
GET /nostrnip5/api/v1/user/addresses(usr-token) per provider user's addresses across all providers in one "My Addresses" table (we already have this API surface in our adapter).- Renewal does not exist in nostrnip5 v1.1.0 (verified in source):
create_addressassertsnot address.active, so re-registering an active name always fails — even for the original buyer. There is no renew endpoint, and no expiry sweep:get_active_address_by_local_partignoresexpires_at, so expired-but-active names keep blocking availability until the provider deletes them manually. Design consequence:
- We track
addressExpiresAton every order and badge orders expiring within 30 days ("renew soon"). - One-click renew is blocked upstream — the renewal path today is the provider's own UI/contact. We surface provider contact info on expiring orders. (If the extension ships a renew endpoint later, it plugs straight into the order flow.)
- Provider-side deletes stay with the provider; we sync on read.
4. Money, security, and trust boundaries
| Concern | Decision |
|---|---|
| Custody | Provider's domain wallet receives 100% of the price. ZapAPI never routes their sats. |
| Provider keys | Public-register needs NO keys. Domain catalog import needs one invoice key (read-only — can list domains/addresses, cannot spend; verified: GET /domains uses require_invoice_key). Admin APIs (list user addresses) use usr-token per ZapAPI-user, never provider admin keys on our server. |
| Availability races | Extension marks loser "refund owed" — surfaced in orders with provider contact; refund is manual provider-side (auto-refund only works for same-instance wallets, §2.3). |
| Abandoned checkouts | 30-min order TTL + sweeper cron cancels unpaid reservations via public DELETE — otherwise names brick permanently (§2.3, §3.3). |
| Rotation secrets | Every brokered purchase is anonymous to the provider we store rotation_secret encrypted on the order and show it once. Losing it = user can't rotate their pubkey at the provider. |
| Price drift | Fiat price is canonical for comparison; sats re-quoted at reservation; >5% drift pauses checkout for re-confirmation (§2.2). |
| Fake providers | Admin approval for self-serve providers + health-check + domain ownership proof (their nostr.json must resolve on the claimed domain). |
| Dead NIP-05 proxy | Health-check must verify https://{domain}/.well-known/nostr.json?name= returns valid JSON with Access-Control-Allow-Origin: * and no redirects (NIP-05 spec hard-requirements). If unproxied, sold names don't verify — badge "NIP-05 unverified" until fixed. |
| NIP-05 trust | Spec: clients follow pubkeys, not names — a malicious provider can re-point a name, so "first-party zapapi.app" + "verified provider" badges matter in UI. |
| Webhooks | Purchase completion via polling (spec-safe). Optional providerZapAPI webhook for instant activation UI. |
5. What already exists in ZapAPI (foundation we build on)
- First-party free address engine (
/lnurl/*) — params, callback, NIP-05, claiming, bio tips. Live on zapapi.app incl. localhost dev. - Full LNbits adapter: NIP-5 domains, search, public register (with
create_invoice), check-payment, usr-token address mgmt, cancel/delete — all verified live. /my-wallet/nip5/*— the current single-provider buy flow (UI exists:/dashboard/lightning-address).- Multi-wallet settlement per user (pay from any selected wallet).
- Response envelope + JWT + per-user encrypted keys.
The marketplace is a layer on top of existing primitives — provider registry + aggregated search + brokered orders — not a rewrite.
6. Implementation plan (phased)
Phase 1 — Provider registry + aggregated search (read-only marketplace)
- DB:
address_providers,marketplace_domains(cached catalog). - Server:
MarketplaceModule
POST /admin/marketplace/providers(register + health-check + import domains)GET /marketplace/providers(public catalog)GET /marketplace/search?q=&years=&availableOnly=&sort=(fan-out, normalized rows)
- Client: Address Marketplace page (
/dashboard/addresses):
- Big search box (debounced) grouped results (Free first-party first, then by price)
- Result rows: full address, provider chip, price + term selector (1/2/3 yrs), taken state
- Filters: available-only, price sort, term years
Phase 2 — Brokered purchase
POST /marketplace/ordersreserve at provider, re-quote check, return invoice + QR; persistrotation_secret(encrypted) +expiresAt(now+30min).- "Pay from my wallet" (uses the buyer's currently-selected wallet — reuses
/my-wallet/pay) or external QR. - Poll activation order flips to
activated; show in "My Addresses" (all providers). Showrotation_secretonce at activation ("your management key at the provider"). - Orders page: reserved/paid/activated/expired/refund-owed states.
- Sweeper job (cron every 5 min): flip
reservedorders pastexpiresAttoexpired— local bookkeeping only (reservations don't block names provider-side and there's no public DELETE, §2.3). - Pay-time availability re-check: before paying from the buyer's wallet,
re-search the name; abort to
failedif taken since reservation.
Phase 3 — Provider self-serve + commission
- Provider onboarding form (base URL + invoice key health-check pending admin approve active). Admin approval is the launch trust model (§9.1).
- Optional commission (default 0%): when
commissionPct > 0, a separate ZapAPI-fee invoice is minted on the platform wallet at pay time and paid from the buyer's wallet right after the provider's invoice — never mixed. - Provider dashboard on ZapAPI: their domains, sales (our order receipts — payouts happen at purchase time straight into their domain wallet, which we cannot and do not read).
Phase 4 — Polish
- Renewal reminders:
addressExpiresAttracked per order + "renew soon" badge within 30 days. One-click renew is BLOCKED UPSTREAM (§3.5) — expiring orders surface the provider contact instead. - Premium-name auctions later (out of scope v1).
- Nostr verification deep-link (publish kind 0 with the new nip05 for generated identities).
7. API surface (ZapAPI additions)
GET /marketplace/providers public catalog
GET /marketplace/search?q=&years=&availableOnly=&sort=
POST /marketplace/orders { order, invoice }
GET /marketplace/orders my orders (all providers)
GET /marketplace/orders/:id order detail
POST /marketplace/orders/:id/check poll activation
DELETE /marketplace/orders/:id cancel reserved (best-effort)
# Admin
POST /admin/marketplace/providers register provider (+ health-check)
GET /admin/marketplace/providers all providers
PATCH /admin/marketplace/providers/:id pause/resume, commission
POST /admin/marketplace/providers/:id/refresh re-import domains8. Provider onboarding packet (what we hand to a provider)
- Run LNbits + enable
nostrnip5(registry or community manifest). - Create a domain: wallet + currency + base cost + optional tiers (char-length brackets, Cloudflare-rank brackets, promo codes).
- Proxy your domain's well-known to LNbits (nginx):
location /.well-known/nostr.json {
proxy_pass https://{your-lnbits}/nostrnip5/api/v1/domain/{domain_id}/nostr.json;
proxy_set_header Host {your-lnbits};
proxy_ssl_server_name on;
add_header Cache-Control "public, no-transform";
add_header Access-Control-Allow-Origin *;
}(Extension caches nostr.json responses for 10 min internally — that's fine,
keep our proxy cache at or below that.)
4. Give ZapAPI: (a) your LNbits base URL, (b) one invoice key from any wallet
on the instance. The invoice key is read-only — it can list your domains
(GET /nostrnip5/api/v1/domains?all_wallets=true requires it — verified) but
can never spend. Purchases themselves need no key at all (public register).
5. We health-check: (a) domain catalog imports, (b) a probe purchase search
returns sane pricing, (c) https://{your-domain}/.well-known/nostr.json
responds with valid JSON, Access-Control-Allow-Origin: *, and no
redirects (NIP-05 hard requirement — clients must ignore redirects).
6. Done. Your domain appears in the marketplace; buyers pay straight into your domain wallet.
What we tell providers about refunds: if two buyers race a name, the loser is marked "refund owed" — but auto-refund only reaches buyers who paid from a wallet on your instance. Marketplace buyers pay cross-instance, so the remedy is manual: either the admin reimburse endpoint or "activate without payment". We surface your contact info on refund-owed orders.
9. Open questions (decided, with research backing)
- Self-serve provider signup — Admin-approved at launch. Decided. The refund limitation (§2.3) means a malicious provider could take payment and never activate. Admin vetting + health-check + domain-ownership proof (nostr.json resolves on the claimed domain) is the minimum viable trust. Later: provider stake or sales-based reputation.
- Commission — 0% at launch. Decided. "Providers keep 100%" is the entire value prop vs. custodial wallets (WoS/Alby sell names in-app with no public register API — we can't and won't broker those). A fee, even a transparent one, kills provider onboarding. Monetize later via promoted placement, never via a cut of the sale.
- Free first-party tier limits — Keep 1 free address per user. It's the top-of-funnel that makes the marketplace a destination (search always shows the free zapapi.app option first).
- Cross-provider username bundles — v2. Technically trivial (N parallel orders — every provider purchase is anonymous anyway, §2.3). The hard part is UX: N separate invoices in one checkout screen. Do NOT merge into one ZapAPI invoice we split — that breaks the custody-free principle.
10. Competitive landscape (why this is first-mover)
| Provider type | Examples | Can we broker? |
|---|---|---|
| Custodial wallets | Wallet of Satoshi, Alby, Strike, ZEBEDEE, Coinos, Primal | In-app purchases, no public register API |
| LNbits + nostrnip5 | nostrfeed.com + a long tail of self-hosters | The entire target market — public register, domain wallets, tiered pricing |
| Federated bridges | satdress, BridgeAddr, Ligess, LnMe, Electrum plugin | Self-host tooling, not sellers |
| NIP-05-only sellers | nostrcheck.me, nostrplebs | Only if they run nostrnip5 under the hood |
There is no existing lightning-address aggregator — no EPP equivalent, no GoDaddy for lightning addresses. The de facto sell-side standard is the nostrnip5 REST API, which is exactly what we aggregate. Provider sourcing: nostr.how's verification-provider directory + direct outreach to known nostrnip5 operators.
Tailwind: ICANN's 2026 new-gTLD round includes a .bitcoin application
(endorsed by Lightning Labs). If delegated, expect name.bitcoin lightning
addresses as a new category — the registry must make onboarding a new provider
an under-5-minute operation.
11. Risk register (source-verified gaps, all addressed above)
| # | Risk | Mitigation | Where |
|---|---|---|---|
| 1 | crud.py): pending rows don't affect availability; no public DELETE exists | Sweeper flips stale orders to expired locally; pay-time availability re-check guards the race | §2.3, §3.3, Phase 2 |
| 2 | Every brokered purchase is anonymous to the provider | Persist rotation_secret encrypted; show once at activation | §2.3, §3.4 |
| 3 | Auto-refund impossible for cross-instance buyers | "Refund owed" UI = provider contact flow, not a claim button | §2.3, §3.4, §8 |
| 4 | Sats price drifts between search and checkout | Fiat canonical + re-quote at reservation, >5% re-confirm | §2.2, §3.3 |
| 5 | Domain catalog import needs a read-only key (not fully public) | Onboarding collects one invoice key; documented as spend-incapable | §4, §8 |
| 6 | Provider's domain may not proxy nostr.json sold names don't verify | Health-check probes the live well-known endpoint (CORS + no-redirect) | §4, §8 |
| 7 | rank == 0 names are unbuyable, shown as plain "taken" | Distinguish blockedReason: taken | unbuyable in search rows | §2.2, §3.2 |
| 8 | Free-tier suffix names (alice.482910) hidden in API | Surface free_identifier_number as a suggestion row | §2.2, §3.2 |
| 9 | Renewal missing upstream (no endpoint, no expiry sweep — names stay taken forever) | Track addressExpiresAt + 30-day badge; renew = provider contact until extension ships it | §3.5, Phase 4 |
| 10 | Domain catalog is scoped to the key-owner's LNbits user — multi-seller instances blocked by baseUrl uniqueness | Uniqueness (baseUrl, ownerUserId); search/catalog dedupe by DNS domain; auto-onboard from user provider connections | §3.1, Phase 3 |
12. Summary (the one-paragraph answer to "how is this possible")
A lightning address is just two HTTPS endpoints (LNURL-pay + NIP-05) that any
domain owner can serve, and LNbits' nostrnip5 extension already implements
the full sell-side (domains, tiered pricing, invoice-on-domain-wallet, automatic
activation, refunds). So ZapAPI becomes the aggregator: providers register
their LNbits instances with us, we health-check and import their domain catalogs,
and one search box fans the query out to all of them (plus our own free
first-party zapapi.app engine). When a user buys, we call the provider's public
register endpoint (invoice is minted on the provider's wallet — ZapAPI never
holds their sats), the buyer pays from their ZapAPI wallet, LNbits auto-activates,
and we record the receipt. That's exactly the domain-registrar model
(GoDaddy-style search provider-direct payment activation), protocol-clean
and custody-free.