Nostr Integration
NIP-05 identity, NIP-57 zaps (full spec) and NIP-47/NWC subscriptions.
Nostr is our identity + social payment layer. Three protocols matter: NIP-05 (identity), NIP-57 (zaps), NIP-47/NWC (wallet connect for subscriptions).
Library: nostr-tools (JS/TS, reference impl).
1. NIP-05 — Verifiable Identity (user@zapapi.com)
Endpoint
GET /.well-known/nostr.json?name=alice
{
"names": { "alice": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d" },
"relays": {
"3bf0c6…459d": ["wss://relay.damus.io", "wss://nos.lol"]
}
}Rules
- Username on NIP-05 must equal Lightning Address username — one identity everywhere
- User sets their own pubkey (npub or hex); we verify by challenge:
user signs a NIP-98 event or a kind-0 note mentioning
zapapi_verification:<code> - Server stores pubkey only, never nsec
- Optional: offer to generate a keypair client-side for Nostr-newcomers (nsec shown once, never sent to server)
Value
- Instant "verified" badge in every Nostr client
- Free trust signal for bio pages
2. NIP-57 — Zaps (the core creator primitive)
How zaps work (research recap)
- Sender's client fetches our LNURL-pay endpoint, sees
allowsNostr: true+nostrPubkey - Sender creates kind 9734 (zap request): signed event with amount, relays, target pubkey/note
- Client calls our callback with
nostr=<urlencoded 9734 json> - We validate, create invoice with
description_hash = sha256(9734 json) - Sender pays. We publish kind 9735 (zap receipt) to the relays from the request
- Every Nostr client renders the zap under the note/profile — public social proof
Validation checklist (from NIP-57 spec)
- [] Valid schnorr signature on 9734
- []
amounttag ==amountquery param - []
relaystag present (non-empty) - []
ptag == recipient pubkey registered on ZapAPI - [] Invoice amount within min/max sendable
- [] Reject expired/future
created_at(±15 min window)
Building the invoice
import { sha256 } from '@noble/hashes/sha256';
const zapRequestJson = JSON.stringify(zapRequestEvent); // exact raw string from query!
const descriptionHash = Buffer.from(sha256(new TextEncoder().encode(zapRequestJson)))
.toString('hex');
// LNbits: POST /api/v1/payments with { out:false, amount: sats, description_hash, unhashed_description? }
// LNbits supports description_hash directly on invoice creation.Publishing the receipt (kind 9735)
const receipt = {
kind: 9735,
pubkey: recipientPubkey, // MUST equal the p tag
created_at: Math.floor(Date.now()/1000),
tags: [
...zapRequest.tags.filter(t => ['p','e','a'].includes(t[0])),
['bolt11', bolt11Invoice],
['preimage', paymentPreimage],
['description', zapRequestJson],
],
content: '',
};
// SIGNING: see §4 key strategy
await pool.publish(relaysFromZapRequest, signedReceipt);Receipt signing strategy — IMPLEMENTED (A + B):
| Strategy | How | Status |
|---|---|---|
| A. Platform zap key | Receipts signed server-side by NOSTR_ZAP_SIGNING_KEY; its pubkey is advertised as nostrPubkey in LNURL params (NIP-57 requires signer == advertised). Same model as Wallet of Satoshi. | ** Live (fallback)** |
| B. NIP-46 remote signer (nsecBunker) | User connects a bunker:// signer (nsecBunker/Amber/Alby) from Dashboard Identity Remote Signer; each receipt is signed by the USER's key via kind 24133. URI stored AES-256 encrypted; key never touches our servers. | ** Live (preferred)** |
| C. NIP-26 delegation | Delegation token for kind 9735 only | Skip (rarely supported) |
Resolution order in ZapsService.publishReceipt: user signer (B) platform
key (A) skip when neither is configured. Signer errors (unreachable bunker)
are recorded on the connection and fall back to A automatically.
Receipts render fine in Damus/Primal because the description tag carries the
original zap request. Implementation in server/src/nostr/: NostrService
(NIP-01 sign/publish — verified against relay.damus.io), ZapsService
(record + build 9735), ZapReceiptProcessor (30s cron settlepublish with
retry ladder, 24h give-up). The zaps table tracks the full lifecycle:
pending paid receipt_published / failed.
Zap analytics (creator dashboard)
- Total zaps, sats received, top zappers, zaps per note
- Feed from our DB (we see every zap request) + receipt confirmations
3. NIP-47 — Nostr Wallet Connect (subscriptions backbone)
Two directions:
A. Follower's wallet connects TO us (auto-pay their subscription)
- Follower pastes
nostr+walletconnect://<wallet-pubkey>?relay=…&secret=…from Alby Hub / other NWC wallet, with a monthly budget set on their side - Each cycle we send NWC request event (kind 23194, encrypted w/ NIP-04/NIP-44):
{ method: "pay_invoice", params: { invoice: <our renewal bolt11> } } - Wallet auto-pays within budget we get response (kind 23195) cycle marked paid
B. We act as NWC wallet service for OUR custodial wallets (Phase 4)
- Give each ZapAPI wallet a NWC connection string users plug it into any NWC app
- Server handles NWC requests over our relay, enforced by per-connection budgets
- This is how we later enable "AI agents pay via ZapAPI wallet"
Relays for NWC
- Run our own relay (e.g.
nrelay/strfry) for NWC traffic: reliability + privacy - Default public set for profiles/receipts: damus, nos.lol, nostr.wine, primal
4. Server-side Nostr Service Design
server/src/nostr/
nostr.service.ts // relay pool, publish, sign (managed keys)
nip05.controller.ts // /.well-known/nostr.json
zaps.service.ts // 9734 validation, receipt building
nwc.service.ts // NIP-47 client (subscriptions) + later service mode
nostr-keys.service.ts // managed zap-signing keys (per strategy A)
entities/nostr-identity.entity.ts// env
NOSTR_RELAYS=wss://relay.damus.io,wss://nos.lol,wss://relay.zapapi.com
NOSTR_ZAP_SIGNING_NSEC=<platform zap key for strategy A> // HSM/KMS in prod
NWC_RELAY=wss://relay.zapapi.comKey management
- Platform zap-signing key AWS KMS / env-injected, rotatable
- Per-user: pubkeys only
- If we ever generate keys for users client-side only (nostr-tools in browser)
5. What users experience
Influencer
- Signs up claims
alice@zapapi.com - Pastes npub (or verifies existing) NIP-05 badge everywhere on Nostr
- Followers zap from any client receipts show publicly sats in dashboard instantly
- Bio page shows "Recent supporters" (zap feed)
Follower
- Clicks on Alice's bio/note their wallet opens (LNURL/WebLN/NWC)
- Pays zap appears under the note in seconds
- For subscriptions: connects wallet once with a budget auto-supports monthly
Developer
- Same endpoints work for any app: LNURL-pay + optional Nostr metadata
- Webhook
zap.receivedcarries full zap request JSON for custom logic