ZapAPI Docs
Get StartedConceptsAI GatewayGuidesPayment GatewaySDKAPI ReferenceResources
Guides

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 wallet

Key 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.com for 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_sats is 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_sats at reservation differs from the search quote by >5%, pause checkout and re-confirm with the user.

Edge cases found in source (services.py):

  • rank == 0 name is unbuyable, not just expensive (get_identifier_price_data returns None). Search returns available: false with no price_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_identifier activates 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)

  1. 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.py creates it with wallet_id=domain.wallet).
  2. Pay the invoice (any Lightning wallet; from our side we pay from the buyer's ZapAPI wallet).
  3. Auto-activation: LNbits watches the payment (check_address_payment matches payment.extra.address_id) and flips the address active + sets expires_at = now + 365 × years.
  4. Optional: provider can wire the name to a real LNURL-pay link (update_ln_address) the sold name becomes a working lightning address too.
  5. 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 throwaway owner_id and returns a rotation_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 persist rotation_secret on 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 use get_active_address_by_local_part (WHERE active = true), and create_address only asserts not 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 old deleteNip5AddressPublic assumption 404s — verified live). Consequences:
  1. Abandoned checkouts do NOT brick names — no provider-side cleanup needed or possible. Our sweeper only flips stale local orders to expired.
  2. 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)

  1. An LNbits instance with the nostrnip5 extension enabled.
  2. A domain they control, with /.well-known/nostr.json proxied to their LNbits (nginx/Caddy snippet from the extension docs — included in §8).
  3. One domain entry (wallet + currency + price + tiers).
  4. 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), createdAt

Onboarding a provider (admin or self-serve):

  1. Provider gives their LNbits base URL + one invoice key (read-only, required for the domain catalog — verified: GET /domains uses require_invoice_key; full packet in §8).
  2. 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).
  3. We import their domains + prices as a cached catalog (refresh on demand + TTL 15 min).
  4. 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-addresses capability, 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, and cost_extra.max_years caps it per domain.
  • Display both priceFiat (canonical, for sorting/comparison) and priceSats (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 commissionPct and 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_address asserts not 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_part ignores expires_at, so expired-but-active names keep blocking availability until the provider deletes them manually. Design consequence:
  1. We track addressExpiresAt on every order and badge orders expiring within 30 days ("renew soon").
  2. 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

ConcernDecision
CustodyProvider's domain wallet receives 100% of the price. ZapAPI never routes their sats.
Provider keysPublic-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 racesExtension 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 checkouts30-min order TTL + sweeper cron cancels unpaid reservations via public DELETE — otherwise names brick permanently (§2.3, §3.3).
Rotation secretsEvery 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 driftFiat price is canonical for comparison; sats re-quoted at reservation; >5% drift pauses checkout for re-confirmation (§2.2).
Fake providersAdmin approval for self-serve providers + health-check + domain ownership proof (their nostr.json must resolve on the claimed domain).
Dead NIP-05 proxyHealth-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 trustSpec: clients follow pubkeys, not names — a malicious provider can re-point a name, so "first-party zapapi.app" + "verified provider" badges matter in UI.
WebhooksPurchase 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)

  1. DB: address_providers, marketplace_domains (cached catalog).
  2. 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)
  1. 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

  1. POST /marketplace/orders reserve at provider, re-quote check, return invoice + QR; persist rotation_secret (encrypted) + expiresAt (now+30min).
  2. "Pay from my wallet" (uses the buyer's currently-selected wallet — reuses /my-wallet/pay) or external QR.
  3. Poll activation order flips to activated; show in "My Addresses" (all providers). Show rotation_secret once at activation ("your management key at the provider").
  4. Orders page: reserved/paid/activated/expired/refund-owed states.
  5. Sweeper job (cron every 5 min): flip reserved orders past expiresAt to expired — local bookkeeping only (reservations don't block names provider-side and there's no public DELETE, §2.3).
  6. Pay-time availability re-check: before paying from the buyer's wallet, re-search the name; abort to failed if taken since reservation.

Phase 3 — Provider self-serve + commission

  1. Provider onboarding form (base URL + invoice key health-check pending admin approve active). Admin approval is the launch trust model (§9.1).
  2. 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.
  3. 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

  1. Renewal reminders: addressExpiresAt tracked per order + "renew soon" badge within 30 days. One-click renew is BLOCKED UPSTREAM (§3.5) — expiring orders surface the provider contact instead.
  2. Premium-name auctions later (out of scope v1).
  3. 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 domains

8. Provider onboarding packet (what we hand to a provider)

  1. Run LNbits + enable nostrnip5 (registry or community manifest).
  2. Create a domain: wallet + currency + base cost + optional tiers (char-length brackets, Cloudflare-rank brackets, promo codes).
  3. 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)

  1. 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.
  2. 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.
  3. 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).
  4. 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 typeExamplesCan we broker?
Custodial walletsWallet of Satoshi, Alby, Strike, ZEBEDEE, Coinos, PrimalIn-app purchases, no public register API
LNbits + nostrnip5nostrfeed.com + a long tail of self-hostersThe entire target market — public register, domain wallets, tiered pricing
Federated bridgessatdress, BridgeAddr, Ligess, LnMe, Electrum pluginSelf-host tooling, not sellers
NIP-05-only sellersnostrcheck.me, nostrplebsOnly 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)

#RiskMitigationWhere
1Abandoned reservations brick names — FALSE ALARM (verified in crud.py): pending rows don't affect availability; no public DELETE existsSweeper flips stale orders to expired locally; pay-time availability re-check guards the race§2.3, §3.3, Phase 2
2Every brokered purchase is anonymous to the providerPersist rotation_secret encrypted; show once at activation§2.3, §3.4
3Auto-refund impossible for cross-instance buyers"Refund owed" UI = provider contact flow, not a claim button§2.3, §3.4, §8
4Sats price drifts between search and checkoutFiat canonical + re-quote at reservation, >5% re-confirm§2.2, §3.3
5Domain catalog import needs a read-only key (not fully public)Onboarding collects one invoice key; documented as spend-incapable§4, §8
6Provider's domain may not proxy nostr.json sold names don't verifyHealth-check probes the live well-known endpoint (CORS + no-redirect)§4, §8
7rank == 0 names are unbuyable, shown as plain "taken"Distinguish blockedReason: taken | unbuyable in search rows§2.2, §3.2
8Free-tier suffix names (alice.482910) hidden in APISurface free_identifier_number as a suggestion row§2.2, §3.2
9Renewal 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
10Domain catalog is scoped to the key-owner's LNbits user — multi-seller instances blocked by baseUrl uniquenessUniqueness (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.

On this page