Files
client/plans/nip61-nutzaps.md
2026-04-17 16:52:51 -04:00

14 KiB

NIP-61 Nutzaps — Implementation Plan

What is NIP-61?

NIP-61 defines Nutzaps — Cashu ecash-based zaps on Nostr. Instead of paying a Lightning invoice (NIP-57), the sender creates Cashu token proofs locked to the recipient's pubkey (P2PK) and publishes them as a kind 9321 event. The recipient redeems the proofs from the mint.

Key Differences from NIP-57 Zaps

Aspect NIP-57 Zaps NIP-61 Nutzaps
Payment Lightning invoice via LNURL Cashu proofs in Nostr event
Requires Recipient has lud16/lud06 Recipient has kind 10019 mint list
Event kind 9734 request + 9735 receipt 9321 nutzap event
Custody Lightning wallet provider Self-custodied ecash
Privacy LNURL server sees payment Mint sees redemption, not sender
Offline Recipient must have LN node Proofs wait on relays

NIP-61 Event Structure (kind 9321)

{
  "kind": 9321,
  "content": "optional comment",
  "tags": [
    ["proof", "{...serialized proof JSON...}"],
    ["u", "https://mint.example.com"],
    ["e", "<event-id-being-zapped>"],
    ["p", "<recipient-pubkey>"],
    ["amount", "100"],
    ["unit", "sat"],
    ["alt", "This is a nutzap"]
  ]
}

Kind 10019 — CashuMintList (Recipient Signal)

A replaceable event published by users who want to receive nutzaps:

{
  "kind": 10019,
  "tags": [
    ["mint", "https://mint.minibits.cash/Bitcoin"],
    ["mint", "https://mint.coinos.io"],
    ["relay", "wss://relay.damus.io"],
    ["pubkey", "<optional-p2pk-pubkey>"]
  ]
}

Current State of the App

What Already Exists

  1. NIP-60 Cashu Wallet — Full wallet on cashu.html with cashu-wallet.mjs controller. Direct cashu-ts operations in ndk-worker.js — send, receive, deposit, pay invoice, proof management.

  2. NIP-57 Zap Flowzaps.mjs handles LNURL resolution, zap request creation (kind 9734), invoice fetching. post-interactions.mjs has handleZapClick() that prompts for amount, creates invoice, pays via walletPayInvoice().

  3. NDK Bundle Includes NIP-61 Classes — The ndk-entry.js re-exports from ndk/wallet/src and ndk/core/src which include:

    • NDKNutzap — Kind 9321 event class with proof management, P2PK, validation
    • NDKCashuMintList — Kind 10019 event class
    • NDKNutzapMonitor — Monitors incoming nutzaps and auto-redeems
    • NDKZapper.zapNip61() — NIP-61 zap method in the zapper
    • Nutzap validation, proof spend state checking, grouping utilities
  4. cashu-ts ExportsCashuMint, CashuWallet, getDecodedToken, getEncodedTokenV4 are all available in the bundle.

  5. Worker Message API — All wallet operations go through init-ndk.mjs → SharedWorker message protocol. The worker has direct access to cashu-ts and NDK.

What's Missing for NIP-61

  1. Sending nutzaps — No way to create P2PK-locked proofs and publish kind 9321 events
  2. Publishing kind 10019 — No way to advertise mint list for receiving nutzaps
  3. Receiving/redeeming nutzaps — No nutzap monitor or redemption flow
  4. Detecting recipient nutzap support — No check for kind 10019 before offering nutzap option
  5. UI integration — Zap button only does NIP-57; no nutzap option in the zap flow

Architecture

flowchart TD
    subgraph Sending a Nutzap
        A[User clicks Zap on a post] --> B{Recipient has kind 10019?}
        B -->|Yes| C[Show nutzap option in zap modal]
        B -->|No| D[NIP-57 Lightning zap only]
        C --> E[User picks amount]
        E --> F[Worker: create P2PK proofs via cashu-ts]
        F --> G[Worker: build and sign kind 9321 event]
        G --> H[Worker: publish to relays]
    end

    subgraph Receiving Nutzaps
        I[Worker subscription: kind 9321 where p = me] --> J[Validate nutzap]
        J --> K[Check proof spend state at mint]
        K -->|Unspent| L[Redeem proofs via cashu-ts]
        L --> M[Store proofs in wallet / kind 7375]
        M --> N[Broadcast balance update]
        K -->|Already spent| O[Mark as spent / ignore]
    end

    subgraph Publishing Mint List
        P[cashu.html Settings] --> Q[User configures mints + relays]
        Q --> R[Publish kind 10019 event]
    end

Implementation Plan

Phase 1: Publish Kind 10019 Mint List

Enable the user to advertise their nutzap preferences so others can send them nutzaps.

Files modified:

Worker handler logic:

  1. Build a kind 10019 event with mint tags from the wallet's configured mints
  2. Add relay tags from the user's connected relays
  3. Optionally add a pubkey tag for P2PK (the wallet's cashu pubkey)
  4. Sign and publish the event

Phase 2: Detect Recipient Nutzap Support

Before sending a nutzap, check if the recipient has published a kind 10019 event.

Files modified:

Worker handler logic:

  1. Fetch kind 10019 event for the given pubkey
  2. Parse mint URLs, relay URLs, and optional P2PK pubkey
  3. Return { mints, relays, p2pk } or null if no mint list found

Phase 3: Send Nutzaps (Kind 9321)

Create P2PK-locked Cashu proofs and publish them as a nutzap event.

Files modified:

Worker handler logic:

  1. Receive: amount, recipientPubkey, recipientMints, recipientP2pk, eventId (optional), comment
  2. Find a mint in common between sender wallet and recipient mint list
  3. If no common mint, attempt cross-mint transfer (send from our mint, swap into recipient mint)
  4. Create P2PK-locked proofs using cashu-ts wallet.send() with P2PK spending conditions
  5. Build proof tags with replay protection (e tag for event ID, P tag for sender pubkey in proof secret)
  6. Construct kind 9321 event with: proof tags, u (mint), p (recipient), e (event), amount, unit, alt, comment tag
  7. Sign via MessageBasedSigner and publish
  8. Update local proof store (remove spent proofs)
  9. Return nutzap event details and updated balance

P2PK proof creation (cashu-ts):

const { keep, send } = await cashuWallet.send(amount, proofs, {
    pubkey: recipientP2pk,  // Lock to recipient
    tags: [
        ['e', eventId],           // Replay protection
        ['P', senderPubkey]       // Sender verification
    ]
});

Phase 4: Receive and Redeem Nutzaps

Monitor for incoming nutzaps and automatically redeem them.

Files modified:

Worker logic:

  1. At startup (after wallet hydration), subscribe to { kinds: [9321], '#p': [currentPubkey] }
  2. On new event: parse as NDKNutzap, validate (check proofs exist, single mint, single recipient)
  3. Check proof spend state at the mint via cashuWallet.checkProofsStates()
  4. If unspent: redeem P2PK proofs using the wallet's private key
  5. Add redeemed proofs to directProofStore, publish updated kind 7375 events
  6. Broadcast nutzapReceived message with amount, sender, comment
  7. Broadcast updated balance
  8. Track redeemed nutzap IDs to avoid double-processing

P2PK redemption (cashu-ts):

const proofs = nutzap.proofs;
const redeemed = await cashuWallet.receive(proofs, {
    privkey: walletPrivateKey  // Needed to unlock P2PK proofs
});

Key consideration: P2PK redemption requires a private key. The current wallet uses window.nostr for signing (NIP-07/NIP-46), which doesn't expose the raw private key. Options:

  • Option A: Generate a separate Cashu-specific keypair stored encrypted in kind 17375 wallet metadata. The pubkey tag in kind 10019 advertises this key. This is the approach NDK's NDKCashuMintList.p2pk supports.
  • Option B: Use the nostr private key directly (only works with NIP-07 extensions that expose it, which most don't).
  • Option A is recommended — it's what the NIP-61 spec and NDK implementation expect.

Phase 5: UI Integration — Zap Modal Enhancement

Update the zap flow to offer nutzap as an option when the recipient supports it.

Files modified:

Flow change in handleZapClick():

  1. Before showing the zap modal, fetch recipient's kind 10019 mint list
  2. If mint list exists AND user has a Cashu wallet with balance:
    • Show "Nutzap" toggle/option in the zap details modal
    • Default to nutzap if both parties share a mint
  3. If nutzap selected: call walletSendNutzap() instead of the NIP-57 flow
  4. If NIP-57 selected (or no mint list): existing Lightning zap flow

Phase 6: Display Nutzaps in Post Interactions

Show received nutzaps alongside regular zaps in the post interaction counts.

Files modified:

Changes:

  1. Add kind 9321 to the subscription filter alongside kind 9735
  2. When a kind 9321 event arrives, parse it as a nutzap: extract amount from proof tags, sender from pubkey, comment from content
  3. Add to the same state.zaps array (or a parallel state.nutzaps array) and include in zapTotal
  4. Display nutzap indicator (e.g., different icon or label) to distinguish from Lightning zaps

Phase 7: Cashu Keypair Management

Generate and store a Cashu-specific P2PK keypair for nutzap reception.

Files modified:

Logic:

  1. When wallet is created or first nutzap setup: generate a secp256k1 keypair
  2. Store the private key encrypted (NIP-44) in the kind 17375 wallet event content
  3. The public key (with "02" prefix for Cashu) goes in the kind 10019 pubkey tag
  4. On wallet load: decrypt and cache the private key for nutzap redemption
  5. The NDKNutzapMonitor accepts private keys via addPrivkey() — we feed it this key

Technical Considerations

Mint Compatibility

  • Sender and recipient should share at least one mint for direct nutzaps
  • If no common mint: cross-mint transfer is possible but adds complexity and fees
  • The UI should indicate when mints are compatible vs. when a swap is needed

Proof Replay Protection

  • Each proof's secret should contain an e tag matching the event being zapped
  • This prevents the same proof from being used in multiple nutzap events
  • The P tag in the proof secret identifies the sender for verification

Privacy Benefits

  • The mint only sees proof redemption, not the Nostr event or social context
  • No LNURL server involved — no third party sees the payment
  • Sender and recipient interact only through Nostr relays and the Cashu mint

Error Handling

  • Mint offline: queue nutzap for retry, show pending state
  • Proofs already spent: mark nutzap as invalid, notify user
  • No common mint: offer to swap or fall back to NIP-57
  • Insufficient balance: show balance and suggest deposit

Files Summary

File Changes
www/ndk-worker.js handlePublishMintList, handleFetchMintList, handleWalletSendNutzap, handleRedeemNutzap, nutzap subscription, P2PK keypair management
www/js/init-ndk.mjs walletPublishMintList(), fetchRecipientMintList(), walletSendNutzap(), walletRedeemNutzap(), ndkNutzapReceived event
www/js/zaps.mjs resolveNutzapSpecForPubkey(), sendNutzap(), update promptZapDetails() with nutzap toggle
www/js/cashu-wallet.mjs publishMintList(), nutzap event listeners, P2PK pubkey exposure
www/js/post-interactions.mjs Subscribe to kind 9321, parse nutzap amounts, update zap totals, nutzap routing in handleZapClick()
www/cashu.html Mint list publish button, nutzap reception settings, incoming nutzap display
build/ndk-entry.js Verify NDKNutzap, NDKCashuMintList exports (likely already included via wildcard)

Dependency Chain

flowchart LR
    P7[Phase 7: Cashu Keypair] --> P1[Phase 1: Publish 10019]
    P1 --> P4[Phase 4: Receive Nutzaps]
    P2[Phase 2: Detect Support] --> P3[Phase 3: Send Nutzaps]
    P3 --> P5[Phase 5: UI Integration]
    P4 --> P6[Phase 6: Display in Posts]
    P5 --> P6

Phases 1-2 and Phase 7 can be worked on in parallel. Phase 3 depends on Phase 2. Phase 4 depends on Phase 1 and 7. Phases 5-6 depend on everything else.