From e92ef7fef9f5d9267937e55abfaaffec7d83a130 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 29 Jul 2026 12:43:46 +0000 Subject: [PATCH] feat: Cashu marketplace for paid episodes, resale, and cross-instance discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Paid episodes: - server/services/cashu.ts: self-custodied Cashu wallet against a configured mint (NUT-04 mint quote -> bolt11 invoice -> mint/store proofs -> LNURL-pay payout on withdraw). Buyers pay a plain Lightning invoice, no Cashu wallet needed on their end. - routes/marketplace.ts: purchase/confirm flow, download-url paywall gate, reseller certification/revocation, earnings ledger + withdraw. - services/marketplace.ts: producer + certified-reseller source resolution, with a naive per-seller sales-count reputation signal. Discovery: - services/rss.ts: RSS tag on priced episodes (producer + resellers, price, sales count, url) so pricing/sources are discoverable straight from the feed, not just a separate API call. Locked episodes point their at an info page instead of the raw file. - routes/feeds.ts: /catalog.opml lists every podcast this instance hosts, for peer podsteadr servers or any OPML-aware crawler to discover without a central directory. Frontend: episode wizard price/reseller controls, sources display, earnings dashboard. Also fixes CORS and a container-image reference: - app.ts: register @fastify/cors, open on the catalog/feed/sources endpoints (already deliberately public/crawlable) and on purchase/confirm (already accept stateless NIP-98 header auth for exactly this case). Credentials stay off, so the session cookie never crosses origins — cookie-authed admin routes stay same-origin-only. Needed so external clients like podsteadr-player can browse/buy/play from a different origin. - docker-compose.yml: fully qualify the mediamtx image reference (docker.io/bluenviron/mediamtx:1.19.2) — some Podman hosts have no unqualified-search registry configured and fail to resolve short names. Co-Authored-By: Claude Sonnet 5 --- .env.example | 3 + docker-compose.yml | 3 +- frontend/src/App.vue | 1 + frontend/src/components/EpisodeSources.vue | 182 ++++++++++ frontend/src/router.ts | 2 + frontend/src/views/EarningsView.vue | 86 +++++ frontend/src/views/EpisodeDetailView.vue | 131 +++++++ frontend/src/views/wizard/EpisodeWizard.vue | 10 + scripts/e2e.mjs | 72 ++++ server/package-lock.json | 103 ++++++ server/package.json | 2 + server/src/app.ts | 13 + server/src/config.ts | 1 + server/src/db/migrations.ts | 58 ++++ server/src/routes/feeds.ts | 19 +- server/src/routes/marketplace.test.ts | 360 ++++++++++++++++++++ server/src/routes/marketplace.ts | 332 ++++++++++++++++++ server/src/routes/podcasts.ts | 25 +- server/src/routes/settings.ts | 4 +- server/src/services/blossom.ts | 5 + server/src/services/cashu.ts | 168 +++++++++ server/src/services/marketplace.ts | 49 +++ server/src/services/nostr.ts | 53 +++ server/src/services/rss.test.ts | 54 ++- server/src/services/rss.ts | 59 +++- server/src/services/settings.ts | 2 + server/src/types.ts | 46 +++ 27 files changed, 1825 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/EpisodeSources.vue create mode 100644 frontend/src/views/EarningsView.vue create mode 100644 frontend/src/views/EpisodeDetailView.vue create mode 100644 server/src/routes/marketplace.test.ts create mode 100644 server/src/routes/marketplace.ts create mode 100644 server/src/services/cashu.ts create mode 100644 server/src/services/marketplace.ts diff --git a/.env.example b/.env.example index eda3722..1e95381 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,6 @@ BLOSSOM_URL_DEFAULT=http://${PUBLIC_HOST}:8098 # Default nostr relays for NIP-53 live-event announcements (comma separated, # changeable at runtime in Settings) NOSTR_RELAYS=wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band + +# Cashu mint used to settle paid-episode purchases (changeable at runtime in Settings) +CASHU_MINT_URL_DEFAULT=https://mint.minibits.cash/Bitcoin diff --git a/docker-compose.yml b/docker-compose.yml index 3c15256..da0aa90 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,7 @@ services: BLOSSOM_URL_DEFAULT: ${BLOSSOM_URL_DEFAULT:-http://localhost:8098} BLOSSOM_URL_INTERNAL: http://blossom:3000 NOSTR_RELAYS: ${NOSTR_RELAYS:-wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band} + CASHU_MINT_URL_DEFAULT: ${CASHU_MINT_URL_DEFAULT:-https://mint.minibits.cash/Bitcoin} volumes: - podsteadr-data:/data - mediamtx-recordings:/recordings:ro @@ -28,7 +29,7 @@ services: - blossom mediamtx: - image: bluenviron/mediamtx:1.19.2 + image: docker.io/bluenviron/mediamtx:1.19.2 container_name: podsteadr-mediamtx restart: unless-stopped ports: diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 177ddf7..16595c2 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -20,6 +20,7 @@ async function logout() {