From 007ad1d1e27e888ff0d08bac36bde305da3cee23 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:11:35 -0400 Subject: [PATCH] Cleanup: drop superseded specs + refresh README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed three superseded spec files now that the work they described is either shipped (under newer specs) or formally replaced: docs/ZAP_GATED_TEMPLATES_FEATURE.md docs/ZAPS_NWC_FEATURE.md docs/ZAP_GATED_PREMIUM_FEATURE.md The history is preserved in git. The current canonical premium plan is docs/PREMIUM_DESIGNS_FEATURE.md (which absorbed the zap-gated+NWC infrastructure plus added the soft-gate KDF + encrypted envelopes + purchase vault). README's docs paragraph now lists every active spec by status — shipped / partial / drafted — and adds the transparency-iframe baseline stub that landed alongside the embed feature. --- README.md | 6 +- docs/ZAPS_NWC_FEATURE.md | 372 ---------------------- docs/ZAP_GATED_PREMIUM_FEATURE.md | 470 ---------------------------- docs/ZAP_GATED_TEMPLATES_FEATURE.md | 261 --------------- 4 files changed, 5 insertions(+), 1104 deletions(-) delete mode 100644 docs/ZAPS_NWC_FEATURE.md delete mode 100644 docs/ZAP_GATED_PREMIUM_FEATURE.md delete mode 100644 docs/ZAP_GATED_TEMPLATES_FEATURE.md diff --git a/README.md b/README.md index c23d942..9fab256 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,11 @@ Spec lives in `docs/PREMIUM_DESIGNS_FEATURE.md`. - `src/index.html` — the complete single-file app. Open it directly in a browser, or serve it from any static host. Vercel rewrites in `vercel.json` map `/share/:id` and `/u/:npub` to the SPA entry. - `src/vendor/qrcode.min.js` — vendored QR encoder (used for Lightning tip invoices). The GIF89a encoder for pixel-art animation export is implemented inline in `index.html`. -- `docs/` — feature specs. `PEN_TOOL_FEATURE.md` covers the shipped pen / pencil / vector tooling; `SLIDE_DECK_FEATURE.md` covers the shipped slide / deck / presenter system (Phases A–D; dual-window presenter is future); `PIXEL_ART_FEATURE.md` covers the shipped palette-indexed pixel-art system (Phases A–D); `PREMIUM_DESIGNS_FEATURE.md` covers the shipped premium-encrypted-designs system (NWC split-zap + soft-gate + purchase vault); the older `ZAP_GATED_PREMIUM_FEATURE.md` / `ZAP_GATED_TEMPLATES_FEATURE.md` / `ZAPS_NWC_FEATURE.md` carry "superseded" banners and remain for history. Badges, collections, private publishing (Phases 3–4), and Stacks are still unbuilt. +- `src/transparency-iframe.html` — minimal transparent stub used by the embed transparency baseline tests. +- `docs/` — feature specs. + - **Shipped:** `PEN_TOOL_FEATURE.md` (pen / pencil / vector tooling), `SLIDE_DECK_FEATURE.md` (slide / deck / presenter, Phases A–D; dual-window presenter still future), `PIXEL_ART_FEATURE.md` (palette-indexed pixel art, Phases A–D), `PREMIUM_DESIGNS_FEATURE.md` (NWC split-zap + soft-gate + purchase vault), `LINKED_DESIGNS_FEATURE.md` (cross-reference imports + auto-update + attribution chain), `PPTX_IMPORT_FEATURE.md` (Phases 0–7 of PowerPoint deck import), `CHART_IMPORT_FEATURE.md` (Phases 1–4 of native OOXML chart rendering), `EXTERNAL_EMBED_FEATURE.md` (iframe-able read-only viewer with code generator). + - **Partial:** `PRIVATE_PUBLISHING_FEATURE.md` (Phases 1+2 shipped; share-with-pubkeys + gift-wrap metadata privacy deferred). + - **Drafted-but-unbuilt:** `STACKS_FEATURE.md`, `BADGES_FEATURE.md`, `COLLECTIONS_FEATURE.md`. - `TODO.md` — running list of deferred work and cleanup items. ## Schema diff --git a/docs/ZAPS_NWC_FEATURE.md b/docs/ZAPS_NWC_FEATURE.md deleted file mode 100644 index fbdada9..0000000 --- a/docs/ZAPS_NWC_FEATURE.md +++ /dev/null @@ -1,372 +0,0 @@ -# Zaps via Nostr Wallet Connect (NWC) Feature Plan - -> **⚠️ SUPERSEDED.** This draft is kept for historical context only. -> NWC is no longer being built as a standalone feature — it's the -> wallet substrate for `ZAP_GATED_PREMIUM_FEATURE.md` (which is the -> canonical merged plan). General-purpose NWC zaps for the existing -> tip flow remain a possible follow-up after that ships; see the -> "Open questions" section of the merged doc. - -## Overview -Allow users to zap template creators (and specific templates) directly from the community -feed using Nostr Wallet Connect (NIP-47). The user connects their Lightning wallet once via -a connection string, then taps ⚡ on any template card to send sats to the creator. No -backend required — NWC runs entirely over WebSockets through the user's own wallet relay. - -## Complexity -**Moderate–High** — Estimated 6–10 hours -- NWC connection + pay_invoice: ~3 hours -- NIP-57 zap request + LNURL flow: ~3 hours -- UI (zap button, amount modal, settings): ~2 hours -- Crypto dependency setup: ~1 hour - ---- - -## Protocol Overview - -Two NIPs work together here: - -| NIP | Purpose | -|-----|---------| -| NIP-47 (NWC) | App requests a payment from the user's wallet over an encrypted relay channel | -| NIP-57 | Defines what a "zap" is — a Lightning payment linked to a Nostr event or profile | - -**The payment chain:** -``` -User clicks ⚡ - → app fetches creator's Lightning address from their cached kind 0 profile (lud16) - → app queries the creator's LNURL-pay endpoint for a callback URL - → app builds a NIP-57 zap request (kind 9734) and signs it via NIP-07 - → app POSTs to the LNURL callback → gets back a bolt11 invoice - → app sends pay_invoice to user's wallet via NWC (NIP-47) - → wallet pays, publishes kind 9735 zap receipt to relays -``` - ---- - -## Key Components - -### 1. NWC Wallet Connection (NIP-47) - -#### Connection String Format -Wallets (Alby, Mutiny, Zeus, Cashu) generate a URI the user pastes in once: -``` -nostr+walletconnect://?relay=wss%3A%2F%2Frelay.example.com&secret= -``` - -| Part | Meaning | -|------|---------| -| `walletPubkey` | Hex pubkey of the wallet service | -| `relay` | WebSocket URL of the relay the wallet listens on | -| `secret` | App-side private key (hex) — used to sign requests and decrypt responses | - -#### Storage -- Store connection string in `localStorage` key `casewrap-nwc-uri` -- **Never log or expose the secret** — treat it like a password -- Derive the app pubkey from the secret at runtime for signing - -#### Core parse function -```javascript -function parseNWCUri(uri) { - // Returns { walletPubkey, relay, secret } or null if invalid - const url = new URL(uri.replace('nostr+walletconnect://', 'https://')); - return { - walletPubkey: url.hostname, - relay: url.searchParams.get('relay'), - secret: url.searchParams.get('secret'), - }; -} -``` - ---- - -### 2. NWC pay_invoice Request (NIP-47) - -NWC uses encrypted Nostr events to pass commands to the wallet. The app encrypts a JSON -command as kind 23194, publishes it to the wallet relay, and waits for a kind 23195 -response from the wallet. - -#### Encryption -NWC uses **NIP-04 encryption** (ECDH shared secret → AES-128-CBC). -- Shared secret = ECDH(appSecret, walletPubkey).x -- This is the same cipher used elsewhere in Nostr but keyed with the NWC secret, NOT the - user's NIP-07 key — so `window.nostr.nip04` cannot be used here. -- Requires a small crypto library (see Crypto Dependencies section). - -#### Request event (kind 23194) -```json -{ - "kind": 23194, - "pubkey": "", - "tags": [["p", ""]], - "content": "", - "created_at": , - "id": "", - "sig": "" -} -``` - -Decrypted content: -```json -{ - "method": "pay_invoice", - "params": { "invoice": "lnbc..." } -} -``` - -#### Response event (kind 23195) -Wallet sends this back, encrypted to appPubkey: -```json -{ - "result_type": "pay_invoice", - "result": { "preimage": "" } -} -// OR on failure: -{ - "result_type": "pay_invoice", - "error": { "code": "INSUFFICIENT_BALANCE", "message": "..." } -} -``` - -#### Core send function -```javascript -async function sendNWCPayment(bolt11) { - // 1. Parse stored NWC URI - // 2. Encrypt pay_invoice request with NIP-04 using appSecret - // 3. Build, sign, and publish kind 23194 to wallet relay - // 4. Open subscription for kind 23195 from walletPubkey, tagged with request event ID - // 5. Wait up to 30s for response, decrypt, return result or throw error -} -``` - -Note: The app signs kind 23194 events with the NWC `secret` key — NOT the user's NIP-07 key. -All NWC signing is done with the noble-secp256k1 library directly. - ---- - -### 3. NIP-57 Zap Request + LNURL Flow - -Before paying, the app must obtain a bolt11 invoice from the creator's Lightning address. -This follows the LNURL-pay + NIP-57 protocol: - -#### Step A — Resolve the creator's Lightning address (lud16) -The creator's `lud16` field in their kind 0 profile (e.g. `alice@getalby.com`) maps to a URL: -```javascript -function lud16ToUrl(lud16) { - const [name, domain] = lud16.split('@'); - return `https://${domain}/.well-known/lnurlp/${name}`; -} -``` -If the creator only has `lud06` (bech32 LNURL), decode it: -```javascript -function decodeLNURL(lud06) { - // bech32-decode lud06 → UTF-8 URL string -} -``` - -#### Step B — Fetch LNURL-pay metadata -GET the resolved URL. Check `allowsNostr: true` to confirm zap support: -```json -{ - "callback": "https://getalby.com/lnurlp/alice/callback", - "minSendable": 1000, - "maxSendable": 100000000000, - "allowsNostr": true, - "nostrPubkey": "" -} -``` - -#### Step C — Build and sign the zap request (kind 9734) -This event is attached to the payment so the recipient sees who zapped them and what for: -```json -{ - "kind": 9734, - "pubkey": "", - "tags": [ - ["relays", "wss://relay.damus.io", "wss://nos.lol"], - ["amount", "21000"], - ["lnurl", ""], - ["p", ""], - ["e", ""] - ], - "content": "Love this design! ⚡" -} -``` -Sign via `window.nostr.signEvent()` (NIP-07) — this DOES use the user's NIP-07 key. - -#### Step D — Fetch the invoice -``` -GET {callback}?amount=21000&nostr={encodeURIComponent(JSON.stringify(signedZapRequest))}&lnurl={lnurl} -``` -Response: `{ "pr": "lnbc21n1p..." }` - -#### Step E — Pay via NWC -Pass the `pr` bolt11 string to `sendNWCPayment()`. - -```javascript -async function zapCreator(recipientPubkey, eventId, amountSats, message) { - const profile = state.community.profiles[recipientPubkey]; - if (!profile?.lud16 && !profile?.lud06) throw new Error('Creator has no Lightning address'); - const lnurlInfo = await fetchLNURLPayInfo(profile.lud16 ?? profile.lud06); - if (!lnurlInfo.allowsNostr) throw new Error('Creator wallet does not support zaps'); - const zapRequest = await buildAndSignZapRequest(recipientPubkey, eventId, amountSats * 1000, message); - const invoice = await fetchZapInvoice(lnurlInfo, zapRequest, amountSats * 1000); - return await sendNWCPayment(invoice); -} -``` - ---- - -### 4. Crypto Dependencies - -NWC requires: -- **Schnorr signing** (sign kind 23194 events with appSecret) -- **NIP-04 encryption** (ECDH + AES-128-CBC) -- **Event ID hashing** (SHA-256 of canonical serialization) - -The app has no build tools, so load from CDN via ES module imports or ` -``` - -These two packages together are ~20 KB minified and have no further dependencies. -They are widely used across the Nostr ecosystem. - -For bech32 decoding of `lud06` LNURL values, use: -```html - -``` - ---- - -### 5. UI - -#### Settings Panel — Wallet Connection -Add a "Lightning Wallet" section to the existing settings/profile panel: - -``` -┌─────────────────────────────────────────┐ -│ ⚡ Lightning Wallet │ -│ │ -│ [Paste NWC connection string… ] [Connect] │ -│ │ -│ Connected: Alby (alice@getalby.com) │ -│ Balance: 12,450 sats [Disconnect] │ -└─────────────────────────────────────────┘ -``` - -- "Connect" parses and stores the NWC URI; optionally fires a `get_balance` request -- Balance display is optional — costs one extra NWC round-trip; hide if user prefers -- "Disconnect" clears localStorage key and hides zap buttons site-wide - -#### Zap Button on Template Cards -Add to the `reactionRow` in both `renderCommunity()` feed cards and `renderCommunityDetail()`: - -``` -[👍 12] [💬 3] [🔁 5] [⚡ Zap] -``` - -- Show ⚡ button only when the creator's profile has `lud16` or `lud06` -- If NWC is not connected, clicking ⚡ opens the settings panel to prompt setup -- Show "Zap creator" tooltip on hover - -#### Zap Amount Modal -Opens on ⚡ click: - -``` -┌──────────────────────────────┐ -│ ⚡ Zap alice@getalby.com │ -│ │ -│ [ 21 ] [ 100 ] [ 500 ] │ -│ [1000 ] [ Custom… ] │ -│ │ -│ Message (optional): │ -│ [________________________] │ -│ │ -│ [Cancel] [Send Zap] │ -└──────────────────────────────┘ -``` - -- Preset amounts in sats (21 is the canonical "nice" zap amount in Nostr culture) -- Custom amount field with min/max validation against LNURL `minSendable`/`maxSendable` -- Message populates the `content` field of the kind 9734 zap request -- "Send Zap" button shows spinner while in flight, then ✅ success or ❌ error message - ---- - -## State Additions - -```javascript -// Add to state object: -nwc: { - uri: '', // raw connection string (from localStorage on load) - connected: false, // true after successful parse + optional balance check - balance: null, // sats, or null if not fetched / not supported -} -``` - ---- - -## Performance Considerations -- LNURL fetch and NWC round-trip each take ~1–3 seconds — always show a loading state -- NWC WebSocket opens on-demand per payment and closes immediately after — no persistent connection -- Creator profile `lud16` is already in `state.community.profiles` — no extra relay query needed -- `get_balance` is optional; skip it to save a round-trip on wallet connect -- bech32/secp256k1 libs load once on page init; zap operations after that are <5ms local work - ---- - -## Limitations -- Requires creator to have a Lightning address (`lud16` or `lud06`) in their Nostr profile -- Requires creator's wallet/service to support NIP-57 (`allowsNostr: true`) -- NWC secret gives partial spend authority — user should use a budget-limited NWC URI (Alby and Mutiny both support spending limits on NWC connections) -- `get_balance` and multi-pay are optional NWC methods — not all wallets implement them -- Zap receipts (kind 9735) are published by the recipient's wallet, not the sender — the app can listen for them but cannot guarantee delivery -- No offline support — both LNURL fetch and NWC require live network - ---- - -## Security Notes -- Store the NWC URI in `localStorage` only — never send it to any server -- Never log `uri` or `secret` to console -- The NWC `secret` grants payment authority up to the wallet's configured spending limit — document this clearly in the UI so users understand what they're connecting -- LNURL fetches go to third-party servers (the creator's wallet provider) — no PII is sent beyond the zap request event (which is a public Nostr event anyway) - ---- - -## Future Enhancements -- Zap splits: send a percentage to both template creator and original asset uploader (NIP-57 supports multiple `p` tags) -- Zap leaderboard on template detail: show top zappers (parse kind 9735 receipts) -- Recurring/scheduled zaps for favorite creators -- Show total sats zapped on each template card (sum kind 9735 receipts) -- Cashu/eCash zaps for sub-sat amounts (separate NIP, future consideration) -- WebLN fallback: if user has a WebLN browser extension instead of NWC, use `window.webln.sendPayment()` as a simpler alternative - ---- - -## Testing Checklist -- [ ] NWC URI parses correctly (walletPubkey, relay, secret extracted) -- [ ] Invalid / malformed NWC URI shows clear error in settings -- [ ] `get_balance` request succeeds and displays balance (if wallet supports it) -- [ ] Disconnect clears stored URI and hides zap buttons -- [ ] ⚡ button only shows on cards where creator has lud16/lud06 -- [ ] ⚡ click on card without NWC connected opens wallet setup flow -- [ ] LNURL fetch resolves correctly for standard lud16 addresses -- [ ] LNURL fetch resolves correctly for bech32 lud06 addresses -- [ ] `allowsNostr: false` shows friendly "creator wallet doesn't support zaps" message -- [ ] Zap request (kind 9734) is signed via NIP-07 and includes correct tags -- [ ] Invoice is fetched from LNURL callback with correct query params -- [ ] NWC pay_invoice request is encrypted and published to wallet relay -- [ ] Successful payment shows ✅ confirmation in modal -- [ ] NWC error codes (INSUFFICIENT_BALANCE, etc.) show readable messages -- [ ] Amount presets respect LNURL minSendable/maxSendable limits -- [ ] Custom amount validates against min/max before sending -- [ ] NWC connection secret is never logged to console -- [ ] Page reload restores NWC connected state from localStorage diff --git a/docs/ZAP_GATED_PREMIUM_FEATURE.md b/docs/ZAP_GATED_PREMIUM_FEATURE.md deleted file mode 100644 index 664ef53..0000000 --- a/docs/ZAP_GATED_PREMIUM_FEATURE.md +++ /dev/null @@ -1,470 +0,0 @@ -# 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: