# Zap-Gated Premium Templates (with NWC + Platform Split) — Feature Plan > **Status:** Phases 0–3 (noble crypto bundle, NWC client + wallet > settings, platform-fee plumbing, tag-only feed UI) shipped on the > `zap-gated` branch and are reused as-is. The tag-only gate they > produced turned out to be too leaky to ship as the final model — > the design payload itself is still publicly readable. The > canonical spec for the encryption layer + buyer purchase vault > that closes that gap is now **`PREMIUM_DESIGNS_FEATURE.md`** > (the original "static soft-gate + buyer vault" design). Read > that document for the architecture going forward; this file is > retained for the Phases 0–3 history and the NWC/tip integration > notes. > > This document also superseded the original > `ZAP_GATED_TEMPLATES_FEATURE.md` and `ZAPS_NWC_FEATURE.md`. They > remain in `docs/` with a "superseded" banner for historical > reference. --- ## Goals ### Product goal A creator can mark a template as "premium" with a minimum sat amount. Other users see a ⚡ lock on the feed card instead of Import. Tapping the lock pays the creator and the platform in a single user gesture, and unlocks the import button (cached locally so they don't pay again). ### Why the split is mandatory The platform fee is the entire revenue model for this feature. Every unlock MUST split: creator gets `(100% − fee)`, platform gets `fee%`. There is no creator-only fallback path. ### Non-goals (v1) - **Refunds.** If the platform invoice fails after the creator invoice settles, we surface the error and ask the user to retry the platform leg — no auto-refund. - **Tipping splits.** The existing LNURL tip flow still goes 100% to the creator. Splits are scoped to zap-gated unlocks only. - **Non-NWC payment paths.** v1 requires NWC to be configured. No bolt11 QR fallback (would re-introduce the two-invoice clunky UX this whole arc is meant to avoid). - **Re-distribution prevention.** A paid buyer can re-share the decryption key. The encryption protects against drive-by reading / scraping, not against determined piracy. No client-side scheme closes this gap without a server. ### Content model (CORRECTED — what came after this doc) Phases 0–3 originally shipped a **tag-only** gate: the design content was published in plaintext in the kind-30078 event; Artstr's UI just refused to import it. Two problems surfaced in real-world testing: 1. **Other clients can ignore the tag** and import freely. 2. **Even inside Artstr** there are multiple import paths (feed card, preview pane Use/Fork, preview pane Save JSON) — gating only the feed-card button leaves the others as full bypasses. Patching every UI surface still doesn't address (1). The canonical fix is the **static soft-gate + buyer purchase vault** model documented in **`PREMIUM_DESIGNS_FEATURE.md`**. That spec encrypts the design payload at publish time with a key derivable from public-plus-obfuscated material in the static client, gated behind valid split-zap receipts. Decryption keys are persisted in a buyer-owned NIP-44 encrypted kind-30078 vault for cross-device re-open. The spec also covers the watermarked preview and the honest "soft gate, not strong DRM" copy. --- ## Phased delivery The phases build on each other. Each has an explicit ship gate so work can be paused between phases for review. ### Phase 0 — Crypto bundle *(~3–5h)* The existing codebase signs/encrypts only via `window.nostr.*` (the user's NIP-07 extension). NWC requires signing kind-23194 events with the **NWC secret** (a different key, embedded in the wallet URI), which the extension can't do. So we need a bundled cryptography implementation. **Deliverables** - Inline a minimal `@noble/secp256k1` + `@noble/hashes` subset (schnorr sign/verify, ECDH for NIP-04 shared-secret). - Inline a NIP-04 helper using `crypto.subtle` AES-CBC + the noble ECDH output. - Verify against the canonical NIP-04 test vectors in code comments. **Ship gate** Round-trip encrypt/decrypt a known plaintext using the test-vector keys → produces the test-vector ciphertext. --- ### Phase 1 — NWC connection *(~5–7h)* **Protocol summary (NIP-47)** The wallet exposes a connection URI: ``` nostr+walletconnect://?relay=&secret=[&lud16=] ``` - `wallet-service-pubkey` — the wallet's NIP-47 service pubkey (hex). - `relay` — a dedicated relay both ends listen on. May appear MORE than once; we publish/subscribe on all of them for redundancy. - `secret` — a 32-byte hex privkey that the client uses to sign requests (this is NOT the user's identity key). May be URL-encoded. - `lud16` — optional Lightning address some wallets (incl. CoinOS) include for convenience; we cache it but don't depend on it. **Capability discovery (kind-13194)** The wallet publishes a kind-13194 "info" event listing supported methods (e.g. `pay_invoice get_balance get_info lookup_invoice`). On connect we subscribe to that event and verify `pay_invoice` is in the list — otherwise we surface "This wallet doesn't support payments via NWC" before the user is allowed to proceed. **Request lifecycle** 1. Build a kind-23194 event: `content` = NIP-04(`{"method":"pay_invoice","params":{"invoice":"..."}}`) encrypted to the wallet service pubkey, with the NWC secret as sender; tag `["p", walletServicePubkey]`. 2. Sign with the NWC secret (schnorr). 3. Publish to the NWC relay. 4. Subscribe `{ kinds: [23195], '#p': [], '#e': [] }`. 5. Decrypt the response with the NWC secret; parse JSON. Possible results: `{ "result_type": "pay_invoice", "result": { "preimage": "..." } }` or `{ "error": { "code": "...", "message": "..." } }`. **Deliverables** - `parseNwcUri(uri)` → `{ servicePubkey, relays[], secret, lud16? }` or null. Handles multiple `relay` params and URL-encoded secrets. - `nwcConnect(uri)` — store creds, open WS to each relay, fetch the kind-13194 capability list, send a `get_info` request to retrieve the wallet's alias + pubkey. - `nwcPayInvoice(bolt11, { timeoutMs })` → resolves to `{ preimage }` or rejects with `{ code, message }`. Common codes worth special-casing: `INSUFFICIENT_BALANCE`, `QUOTA_EXCEEDED` / `BUDGET_EXCEEDED` (wallet's per-connection cap hit), `PAYMENT_FAILED`, `NOT_FOUND` (invoice mistyped), `INTERNAL`. - `nwcLookupInvoice(paymentHash)` → returns settlement state. Used during retry of a half-failed unlock so we never double-pay. - Credentials live in `localStorage` under `casewrap-nwc-connection` (single-record; new paste replaces old). Trade-off acknowledged in the threat-model section. **UX (the Settings panel)** A new "Wallet" section in the Community settings, two-state: *Not connected:* ``` ┌────────────────────────────────────────────┐ │ Lightning wallet — not connected │ │ │ │ To unlock premium templates, connect a │ │ NIP-47 wallet. Recommended: CoinOS │ │ (free, web-based, no install). │ │ │ │ [Set up with CoinOS →] │ │ [ Paste connection string ▾ ] │ └────────────────────────────────────────────┘ ``` The CoinOS button opens a short modal: > **1.** Sign up at [coinos.io](https://coinos.io) (no install). > **2.** Open the [Wallet Connect](https://coinos.io/settings/connect) > page directly, or browse to *Settings → Connect → Nostr Wallet > Connect*. > **3.** Set a budget that comfortably covers the unlocks you expect > — at least a few thousand sats. *(Hint inline.)* > **4.** Copy the connection string (`nostr+walletconnect://…`). > **5.** Paste below. > *[input]* *[Connect]* The "Paste connection string" disclosure expands to the same input with no per-wallet instructions — works for Alby Hub, Mutiny, Phoenix, Cashu.me, Boltz, etc. **Mobile note:** on a phone, the user copies the NWC URI from their wallet app (Alby Go, Mutiny, Cashu.me's "Connect" share sheet, etc.) and pastes into this same field. We don't try to detect / deep-link. *Connected:* ``` ┌────────────────────────────────────────────┐ │ ⚡ Connected to │ │ Last test: