From 8486242fd81295fe51ba7c9b432e7dc408483092 Mon Sep 17 00:00:00 2001 From: snider Date: Tue, 13 Jan 2026 15:17:22 +0000 Subject: [PATCH] docs: add IPFS and payment integration guides + artist mode polish - Add docs/ipfs-distribution.md: complete guide for IPFS hosting - Installation, pinning services, gateways, best practices - Full album release workflow example - Add docs/payment-integration.md: Stripe, Gumroad, PayPal examples - Webhook handlers for automated license delivery - Serverless options (Vercel/Netlify) - Manual workflow for non-technical artists - Demo artist mode improvements: - WASM loads on-demand (fixes 6s delay on 4G) - Generate button enabled by password only - Vi demo preloads when WASM ready - Update RFC-001 section 8.3: mark completed items --- RFC-001-OSS-DRM.md | 6 +- demo/index.html | 235 ++++++++++++++++- docs/ipfs-distribution.md | 281 ++++++++++++++++++++ docs/payment-integration.md | 497 ++++++++++++++++++++++++++++++++++++ 4 files changed, 1006 insertions(+), 13 deletions(-) create mode 100644 docs/ipfs-distribution.md create mode 100644 docs/payment-integration.md diff --git a/RFC-001-OSS-DRM.md b/RFC-001-OSS-DRM.md index e7e8dfd..d7e147e 100644 --- a/RFC-001-OSS-DRM.md +++ b/RFC-001-OSS-DRM.md @@ -636,9 +636,9 @@ Local playback Third-party hosting ### 8.3 Future Work - [ ] Multi-bitrate adaptive streaming (like HLS/DASH but encrypted) -- [ ] Payment integration examples (Stripe, Gumroad) -- [ ] IPFS distribution guide -- [ ] Demo page "Streaming" tab for v3 showcase +- [x] Payment integration examples (see `docs/payment-integration.md`) +- [x] IPFS distribution guide (see `docs/ipfs-distribution.md`) +- [x] Demo page "Streaming" tab for v3 showcase ## 9. Usage Examples diff --git a/demo/index.html b/demo/index.html index e4a40d6..cea98ca 100644 --- a/demo/index.html +++ b/demo/index.html @@ -971,6 +971,154 @@ color: #888; margin-top: 0.5rem; } + + /* Mobile Responsive */ + @media (max-width: 768px) { + .container { + padding: 0.75rem 1rem; + } + + .header-row { + flex-direction: column; + gap: 0.75rem; + align-items: stretch; + } + + .logo { + font-size: 2.5rem; + text-align: center; + } + + .mode-switcher { + width: 100%; + justify-content: center; + flex-wrap: wrap; + gap: 4px; + } + + .mode-btn { + padding: 0.4rem 0.75rem; + font-size: 0.75rem; + flex: 1; + justify-content: center; + min-width: 0; + } + + .value-prop { + display: none !important; + } + + .tagline-row { + display: none !important; + } + + .two-col { + grid-template-columns: 1fr; + gap: 1rem; + } + + /* Profile layout - video first on mobile */ + .profile-layout { + display: flex !important; + flex-direction: column; + gap: 1rem; + min-height: auto !important; + } + + .profile-main { + order: -1; + } + + .profile-sidebar { + text-align: center; + padding: 1rem; + } + + .profile-avatar { + width: 120px !important; + height: 120px !important; + font-size: 2.5rem !important; + } + + .profile-name { + font-size: 1.25rem !important; + } + + .profile-links { + justify-content: center !important; + flex-wrap: wrap; + } + + .profile-links a { + font-size: 0.75rem !important; + padding: 0.3rem 0.6rem !important; + } + + /* Tabs */ + .tab-buttons { + flex-wrap: wrap; + } + + .tab-btn { + flex: 1; + min-width: 50%; + font-size: 0.8rem; + padding: 0.6rem; + } + + /* Video */ + .profile-hero { + min-height: 200px !important; + } + + #profile-video { + max-height: 250px; + } + + /* Forms */ + .form-row { + flex-direction: column !important; + } + + .form-row input, + .form-row button { + width: 100% !important; + } + + /* Streaming grid */ + .chunk-grid { + grid-template-columns: repeat(5, 1fr) !important; + gap: 4px !important; + } + + .chunk-cell { + font-size: 0.6rem !important; + padding: 4px !important; + } + } + + @media (max-width: 480px) { + .logo { + font-size: 2rem; + } + + .mode-btn { + padding: 0.35rem 0.5rem; + font-size: 0.7rem; + } + + .value-prop { + grid-template-columns: 1fr; + } + + .tagline { + font-size: 0.85rem; + } + + .chunk-grid { + grid-template-columns: repeat(4, 1fr) !important; + } + } @@ -986,7 +1134,7 @@ -
+

Zero-Trust DRM for Independent Artists

⚠️ Demo pre-seeded with keys for protocol demonstration @@ -1583,10 +1731,14 @@ '../examples/demo-sample.smsg': '6ZhMQ034bT6maHqMaJejoxDMpfaOQvq5', }; - // Active demo - CDN for profile, local for testing + // Profile uses CDN track (music video) const DEMO_URL = 'https://demo.dapp.fm/demo-track.smsg'; const DEMO_PASSWORD = DEMO_TRACKS[DEMO_URL]; + // Fan/Artist tabs use local mascot video (Vi) + const VI_DEMO_URL = './demo-sample.smsg'; + const VI_DEMO_PASSWORD = DEMO_TRACKS[VI_DEMO_URL]; + let wasmReady = false; let manifest = null; @@ -1678,9 +1830,9 @@ btn.textContent = 'Loading...'; try { - // Fetch encrypted content + // Fetch encrypted content (Vi mascot demo) setProgress(10, 'Fetching encrypted content...'); - const response = await fetch(DEMO_URL); + const response = await fetch(VI_DEMO_URL); if (!response.ok) throw new Error('Demo file not found'); const contentLength = response.headers.get('content-length'); @@ -1719,7 +1871,7 @@ // Decrypt using binary API - no base64, pure zstd speed! setProgress(70, 'Decrypting (zstd)...'); - const msg = await BorgSMSG.decryptBinary(allChunks, DEMO_PASSWORD); + const msg = await BorgSMSG.decryptBinary(allChunks, VI_DEMO_PASSWORD); setProgress(95, 'Preparing player...'); displayMedia(msg); @@ -2078,6 +2230,48 @@ // ========== MODE SWITCHING ========== let profileLoaded = false; + let artistDemoLoaded = false; + + // Preload Vi demo for Artist mode + async function preloadArtistDemo() { + if (artistDemoLoaded || !wasmReady) return; + artistDemoLoaded = true; + + const info = document.getElementById('artist-content-info'); + info.textContent = 'Loading Vi demo...'; + info.style.color = '#999'; + + try { + const response = await fetch(VI_DEMO_URL); + if (!response.ok) throw new Error('Demo file not found'); + + const buffer = await response.arrayBuffer(); + const bytes = new Uint8Array(buffer); + const msg = await BorgSMSG.decryptBinary(bytes, VI_DEMO_PASSWORD); + + if (msg.attachments && msg.attachments.length > 0) { + const att = msg.attachments[0]; + // Convert binary data to base64 for artistContentData + let base64 = ''; + const chunkSize = 32768; + for (let i = 0; i < att.data.length; i += chunkSize) { + const chunk = att.data.subarray(i, Math.min(i + chunkSize, att.data.length)); + base64 += String.fromCharCode.apply(null, chunk); + } + artistContentData = btoa(base64); + artistContentName = att.name || 'vi-demo.mp4'; + artistContentMime = att.mime || 'video/mp4'; + + info.textContent = artistContentName + ' (' + (att.data.length / 1024 / 1024).toFixed(1) + ' MB) - Vi Demo Ready'; + info.style.color = '#00ff94'; + } + } catch (err) { + console.warn('Failed to preload artist demo:', err); + info.textContent = 'Demo unavailable - upload your own file'; + info.style.color = '#ff9500'; + artistDemoLoaded = false; + } + } document.querySelectorAll('.mode-btn').forEach(btn => { btn.addEventListener('click', () => { @@ -2097,6 +2291,11 @@ if (mode === 'streaming' && wasmReady && !streamingHeaderInfo) { initStreamingDemo(); } + + // Preload Vi demo if WASM already loaded (don't auto-load WASM - it's too slow on 4G) + if (mode === 'artist' && wasmReady && !artistDemoLoaded) { + preloadArtistDemo(); + } }); }); @@ -2279,10 +2478,22 @@ // Generate license async function artistGenerateLicense() { - if (!wasmReady) return; - const btn = document.getElementById('artist-generate-btn'); btn.disabled = true; + + // Load WASM on-demand if not ready + if (!wasmReady) { + btn.textContent = 'Loading WASM...'; + try { + await initWasm(); + } catch (err) { + btn.textContent = 'Generate License'; + btn.disabled = false; + alert('Failed to load WASM: ' + err.message); + return; + } + } + btn.textContent = 'Generating...'; try { @@ -2450,10 +2661,10 @@ setTimeout(() => btn.textContent = 'Copy', 1500); }); - // Enable generate button when WASM ready AND master password set + // Enable generate button when master password set (WASM loads on-demand) function checkArtistGenerateReady() { const hasMasterPassword = document.getElementById('artist-master-password').value.length > 0; - document.getElementById('artist-generate-btn').disabled = !wasmReady || !hasMasterPassword; + document.getElementById('artist-generate-btn').disabled = !hasMasterPassword; } document.getElementById('artist-master-password').addEventListener('input', checkArtistGenerateReady); @@ -2462,7 +2673,11 @@ initWasm = async function() { await originalInitWasm(); if (wasmReady) { - checkArtistGenerateReady(); + // Preload Vi demo if we're on Artist mode + const artistActive = document.querySelector('.mode-btn[data-mode="artist"]').classList.contains('active'); + if (artistActive && !artistDemoLoaded) { + preloadArtistDemo(); + } } }; diff --git a/docs/ipfs-distribution.md b/docs/ipfs-distribution.md new file mode 100644 index 0000000..74a6f51 --- /dev/null +++ b/docs/ipfs-distribution.md @@ -0,0 +1,281 @@ +# IPFS Distribution Guide + +This guide explains how to distribute your encrypted `.smsg` content via IPFS (InterPlanetary File System) for permanent, decentralized hosting. + +## Why IPFS? + +IPFS is ideal for dapp.fm content because: + +- **Permanent links** - Content-addressed (CID) means the URL never changes +- **No hosting costs** - Pin with free services or self-host +- **Censorship resistant** - No single point of failure +- **Global CDN** - Content served from nearest peer +- **Perfect for archival** - Your content survives even if you disappear + +Combined with password-as-license, IPFS creates truly permanent media distribution: + +``` +Artist uploads to IPFS → Fan downloads from anywhere → Password unlocks forever +``` + +## Quick Start + +### 1. Install IPFS + +**macOS:** +```bash +brew install ipfs +``` + +**Linux:** +```bash +wget https://dist.ipfs.tech/kubo/v0.24.0/kubo_v0.24.0_linux-amd64.tar.gz +tar xvfz kubo_v0.24.0_linux-amd64.tar.gz +sudo mv kubo/ipfs /usr/local/bin/ +``` + +**Windows:** +Download from https://dist.ipfs.tech/#kubo + +### 2. Initialize and Start + +```bash +ipfs init +ipfs daemon +``` + +### 3. Add Your Content + +```bash +# Create your encrypted content first +go run ./cmd/mkdemo my-album.mp4 my-album.smsg + +# Add to IPFS +ipfs add my-album.smsg +# Output: added QmX...abc my-album.smsg + +# Your content is now available at: +# - Local: http://localhost:8080/ipfs/QmX...abc +# - Gateway: https://ipfs.io/ipfs/QmX...abc +``` + +## Distribution Workflow + +### For Artists + +```bash +# 1. Package your media +go run ./cmd/mkdemo album.mp4 album.smsg +# Save the password: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7 + +# 2. Add to IPFS +ipfs add album.smsg +# added QmYourContentCID album.smsg + +# 3. Pin for persistence (choose one): + +# Option A: Pin locally (requires running node) +ipfs pin add QmYourContentCID + +# Option B: Use Pinata (free tier: 1GB) +curl -X POST "https://api.pinata.cloud/pinning/pinByHash" \ + -H "Authorization: Bearer YOUR_JWT" \ + -H "Content-Type: application/json" \ + -d '{"hashToPin": "QmYourContentCID"}' + +# Option C: Use web3.storage (free tier: 5GB) +# Upload at https://web3.storage + +# 4. Share with fans +# CID: QmYourContentCID +# Password: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7 +# Gateway URL: https://ipfs.io/ipfs/QmYourContentCID +``` + +### For Fans + +```bash +# Download via any gateway +curl -o album.smsg https://ipfs.io/ipfs/QmYourContentCID + +# Or via local node (faster if running) +ipfs get QmYourContentCID -o album.smsg + +# Play with password in browser demo or native app +``` + +## IPFS Gateways + +Public gateways for sharing (no IPFS node required): + +| Gateway | URL Pattern | Notes | +|---------|-------------|-------| +| ipfs.io | `https://ipfs.io/ipfs/{CID}` | Official, reliable | +| dweb.link | `https://{CID}.ipfs.dweb.link` | Subdomain style | +| cloudflare | `https://cloudflare-ipfs.com/ipfs/{CID}` | Fast, cached | +| w3s.link | `https://{CID}.ipfs.w3s.link` | web3.storage | +| nftstorage.link | `https://{CID}.ipfs.nftstorage.link` | NFT.storage | + +**Example URLs for CID `QmX...abc`:** +``` +https://ipfs.io/ipfs/QmX...abc +https://QmX...abc.ipfs.dweb.link +https://cloudflare-ipfs.com/ipfs/QmX...abc +``` + +## Pinning Services + +Content on IPFS is only available while someone is hosting it. Use pinning services for persistence: + +### Free Options + +| Service | Free Tier | Link | +|---------|-----------|------| +| Pinata | 1 GB | https://pinata.cloud | +| web3.storage | 5 GB | https://web3.storage | +| NFT.storage | Unlimited* | https://nft.storage | +| Filebase | 5 GB | https://filebase.com | + +*NFT.storage is designed for NFT data but works for any content. + +### Pin via CLI + +```bash +# Pinata +export PINATA_JWT="your-jwt-token" +curl -X POST "https://api.pinata.cloud/pinning/pinByHash" \ + -H "Authorization: Bearer $PINATA_JWT" \ + -H "Content-Type: application/json" \ + -d '{"hashToPin": "QmYourCID", "pinataMetadata": {"name": "my-album.smsg"}}' + +# web3.storage (using w3 CLI) +npm install -g @web3-storage/w3cli +w3 login your@email.com +w3 up my-album.smsg +``` + +## Integration with Demo Page + +The demo page can load content directly from IPFS gateways: + +```javascript +// In the demo page, use gateway URL +const ipfsCID = 'QmYourContentCID'; +const gatewayUrl = `https://ipfs.io/ipfs/${ipfsCID}`; + +// Fetch and decrypt +const response = await fetch(gatewayUrl); +const bytes = new Uint8Array(await response.arrayBuffer()); +const msg = await BorgSMSG.decryptBinary(bytes, password); +``` + +Or use the Fan tab with the IPFS gateway URL directly. + +## Best Practices + +### 1. Always Pin Your Content + +IPFS garbage-collects unpinned content. Always pin important files: + +```bash +ipfs pin add QmYourCID +# Or use a pinning service +``` + +### 2. Use Multiple Pins + +Pin with 2-3 services for redundancy: + +```bash +# Pin locally +ipfs pin add QmYourCID + +# Also pin with Pinata +curl -X POST "https://api.pinata.cloud/pinning/pinByHash" ... + +# And web3.storage as backup +w3 up my-album.smsg +``` + +### 3. Share CID + Password Separately + +``` +Download: https://ipfs.io/ipfs/QmYourCID +License: [sent via email/DM after purchase] +``` + +### 4. Use IPNS for Updates (Optional) + +IPNS lets you update content while keeping the same URL: + +```bash +# Create IPNS name +ipfs name publish QmYourCID +# Published to k51...xyz + +# Your content is now at: +# https://ipfs.io/ipns/k51...xyz + +# Update to new version later: +ipfs name publish QmNewVersionCID +``` + +## Example: Full Album Release + +```bash +# 1. Create encrypted album +go run ./cmd/mkdemo my-album.mp4 my-album.smsg +# Password: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7 + +# 2. Add to IPFS +ipfs add my-album.smsg +# added QmAlbumCID my-album.smsg + +# 3. Pin with multiple services +ipfs pin add QmAlbumCID +w3 up my-album.smsg + +# 4. Create release page +cat > release.html << 'EOF' + + +My Album - Download + +

My Album

+

Download: IPFS

+

After purchase, you'll receive your license key via email.

+

Play with license key

+ + +EOF + +# 5. Host release page on IPFS too! +ipfs add release.html +# added QmReleaseCID release.html +# Share: https://ipfs.io/ipfs/QmReleaseCID +``` + +## Troubleshooting + +### Content Not Loading + +1. **Check if pinned**: `ipfs pin ls | grep QmYourCID` +2. **Try different gateway**: Some gateways cache slowly +3. **Check daemon running**: `ipfs swarm peers` should show peers + +### Slow Downloads + +1. Use a faster gateway (cloudflare-ipfs.com is often fastest) +2. Run your own IPFS node for direct access +3. Pre-warm gateways by accessing content once + +### CID Changed After Re-adding + +IPFS CIDs are content-addressed. If you modify the file, the CID changes. For the same content, the CID is always identical. + +## Resources + +- [IPFS Documentation](https://docs.ipfs.tech/) +- [Pinata Docs](https://docs.pinata.cloud/) +- [web3.storage Docs](https://web3.storage/docs/) +- [IPFS Gateway Checker](https://ipfs.github.io/public-gateway-checker/) diff --git a/docs/payment-integration.md b/docs/payment-integration.md new file mode 100644 index 0000000..740c855 --- /dev/null +++ b/docs/payment-integration.md @@ -0,0 +1,497 @@ +# Payment Integration Guide + +This guide shows how to sell your encrypted `.smsg` content and deliver license keys (passwords) to customers using popular payment processors. + +## Overview + +The dapp.fm model is simple: + +``` +1. Customer pays via Stripe/Gumroad/PayPal +2. Payment processor triggers webhook or delivers digital product +3. Customer receives password (license key) +4. Customer downloads .smsg from your CDN/IPFS +5. Customer decrypts with password - done forever +``` + +No license servers, no accounts, no ongoing infrastructure. + +## Stripe Integration + +### Option 1: Stripe Payment Links (Easiest) + +No code required - use Stripe's hosted checkout: + +1. Create a Payment Link in Stripe Dashboard +2. Set up a webhook to email the password on successful payment +3. Host your `.smsg` file anywhere (CDN, IPFS, S3) + +**Webhook endpoint (Node.js/Express):** + +```javascript +const express = require('express'); +const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); +const nodemailer = require('nodemailer'); + +const app = express(); + +// Your content passwords (store securely!) +const PRODUCTS = { + 'prod_ABC123': { + name: 'My Album', + password: 'PMVXogAJNVe_DDABfTmLYztaJAzsD0R7', + downloadUrl: 'https://ipfs.io/ipfs/QmYourCID' + } +}; + +app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => { + const sig = req.headers['stripe-signature']; + const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; + + let event; + try { + event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret); + } catch (err) { + return res.status(400).send(`Webhook Error: ${err.message}`); + } + + if (event.type === 'checkout.session.completed') { + const session = event.data.object; + const customerEmail = session.customer_details.email; + const productId = session.metadata.product_id; + const product = PRODUCTS[productId]; + + if (product) { + await sendLicenseEmail(customerEmail, product); + } + } + + res.json({received: true}); +}); + +async function sendLicenseEmail(email, product) { + const transporter = nodemailer.createTransport({ + // Configure your email provider + service: 'gmail', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS + } + }); + + await transporter.sendMail({ + from: 'artist@example.com', + to: email, + subject: `Your License Key for ${product.name}`, + html: ` +

Thank you for your purchase!

+

Download: ${product.name}

+

License Key: ${product.password}

+

How to play:

+
    +
  1. Download the .smsg file from the link above
  2. +
  3. Go to demo.dapp.fm
  4. +
  5. Click "Fan" tab, then "Unlock Licensed Content"
  6. +
  7. Paste the file and enter your license key
  8. +
+

This is your permanent license - save this email!

+ ` + }); +} + +app.listen(3000); +``` + +### Option 2: Stripe Checkout Session (More Control) + +```javascript +const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + +// Create checkout session +app.post('/create-checkout', async (req, res) => { + const { productId } = req.body; + + const session = await stripe.checkout.sessions.create({ + payment_method_types: ['card'], + line_items: [{ + price: 'price_ABC123', // Your Stripe price ID + quantity: 1, + }], + mode: 'payment', + success_url: 'https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}', + cancel_url: 'https://yoursite.com/cancel', + metadata: { + product_id: productId + } + }); + + res.json({ url: session.url }); +}); + +// Success page - show license after payment +app.get('/success', async (req, res) => { + const session = await stripe.checkout.sessions.retrieve(req.query.session_id); + + if (session.payment_status === 'paid') { + const product = PRODUCTS[session.metadata.product_id]; + res.send(` +

Thank you!

+

Download: ${product.name}

+

License Key: ${product.password}

+ `); + } else { + res.send('Payment not completed'); + } +}); +``` + +## Gumroad Integration + +Gumroad is perfect for artists - handles payments, delivery, and customer management. + +### Setup + +1. Create a Digital Product on Gumroad +2. Upload a text file or PDF containing the password +3. Set your `.smsg` download URL in the product description +4. Gumroad delivers the password file on purchase + +### Product Setup + +**Product Description:** +``` +My Album - Encrypted Digital Download + +After purchase, you'll receive: +1. A license key (in the download) +2. Download link for the .smsg file + +How to play: +1. Download the .smsg file: https://ipfs.io/ipfs/QmYourCID +2. Go to https://demo.dapp.fm +3. Click "Fan" → "Unlock Licensed Content" +4. Enter your license key from the PDF +``` + +**Delivered File (license.txt):** +``` +Your License Key: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7 + +Download your content: https://ipfs.io/ipfs/QmYourCID + +This is your permanent license - keep this file safe! +The content works offline forever with this key. + +Need help? Visit https://demo.dapp.fm +``` + +### Gumroad Ping (Webhook) + +For automated delivery, use Gumroad's Ping feature: + +```javascript +const express = require('express'); +const app = express(); + +app.use(express.urlencoded({ extended: true })); + +// Gumroad sends POST to this endpoint on sale +app.post('/gumroad-ping', (req, res) => { + const { + seller_id, + product_id, + email, + full_name, + purchaser_id + } = req.body; + + // Verify it's from Gumroad (check seller_id matches yours) + if (seller_id !== process.env.GUMROAD_SELLER_ID) { + return res.status(403).send('Invalid seller'); + } + + const product = PRODUCTS[product_id]; + if (product) { + // Send custom email with password + sendLicenseEmail(email, product); + } + + res.send('OK'); +}); +``` + +## PayPal Integration + +### PayPal Buttons + IPN + +```html + +
+ + + + + + + + + +
+``` + +**IPN Handler:** + +```javascript +const express = require('express'); +const axios = require('axios'); + +app.post('/paypal-ipn', express.urlencoded({ extended: true }), async (req, res) => { + // Verify with PayPal + const verifyUrl = 'https://ipnpb.paypal.com/cgi-bin/webscr'; + const verifyBody = 'cmd=_notify-validate&' + new URLSearchParams(req.body).toString(); + + const response = await axios.post(verifyUrl, verifyBody); + + if (response.data === 'VERIFIED' && req.body.payment_status === 'Completed') { + const email = req.body.payer_email; + const itemNumber = req.body.item_number; + const product = PRODUCTS[itemNumber]; + + if (product) { + await sendLicenseEmail(email, product); + } + } + + res.send('OK'); +}); +``` + +## Ko-fi Integration + +Ko-fi is great for tips and single purchases. + +### Setup + +1. Enable "Commissions" or "Shop" on Ko-fi +2. Create a product with the license key in the thank-you message +3. Link to your .smsg download + +**Ko-fi Thank You Message:** +``` +Thank you for your purchase! + +Your License Key: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7 + +Download: https://ipfs.io/ipfs/QmYourCID + +Play at: https://demo.dapp.fm (Fan → Unlock Licensed Content) +``` + +## Serverless Options + +### Vercel/Netlify Functions + +No server needed - use serverless functions: + +```javascript +// api/stripe-webhook.js (Vercel) +import Stripe from 'stripe'; +import { Resend } from 'resend'; + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); +const resend = new Resend(process.env.RESEND_API_KEY); + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).end(); + } + + const sig = req.headers['stripe-signature']; + const event = stripe.webhooks.constructEvent( + req.body, + sig, + process.env.STRIPE_WEBHOOK_SECRET + ); + + if (event.type === 'checkout.session.completed') { + const session = event.data.object; + + await resend.emails.send({ + from: 'artist@yoursite.com', + to: session.customer_details.email, + subject: 'Your License Key', + html: ` +

Download: My Album

+

License Key: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7

+ ` + }); + } + + res.json({ received: true }); +} + +export const config = { + api: { bodyParser: false } +}; +``` + +## Manual Workflow (No Code) + +For artists who don't want to set up webhooks: + +### Using Email + +1. **Gumroad/Ko-fi**: Set product to require email +2. **Manual delivery**: Check sales daily, email passwords manually +3. **Template**: + +``` +Subject: Your License for [Album Name] + +Hi [Name], + +Thank you for your purchase! + +Download: [IPFS/CDN link] +License Key: [password] + +How to play: +1. Download the .smsg file +2. Go to demo.dapp.fm +3. Fan tab → Unlock Licensed Content +4. Enter your license key + +Enjoy! This license works forever. + +[Artist Name] +``` + +### Using Discord/Telegram + +1. Sell via Gumroad (free tier) +2. Require customers join your Discord/Telegram +3. Bot or manual delivery of license keys +4. Community building bonus! + +## Security Best Practices + +### 1. One Password Per Product + +Don't reuse passwords across products: + +```javascript +const PRODUCTS = { + 'album-2024': { password: 'unique-key-1' }, + 'album-2023': { password: 'unique-key-2' }, + 'single-summer': { password: 'unique-key-3' } +}; +``` + +### 2. Environment Variables + +Never hardcode passwords in source: + +```bash +# .env +ALBUM_2024_PASSWORD=PMVXogAJNVe_DDABfTmLYztaJAzsD0R7 +STRIPE_SECRET_KEY=sk_live_... +``` + +### 3. Webhook Verification + +Always verify webhooks are from the payment provider: + +```javascript +// Stripe +stripe.webhooks.constructEvent(body, sig, secret); + +// Gumroad +if (seller_id !== MY_SELLER_ID) reject(); + +// PayPal +verify with IPN endpoint +``` + +### 4. HTTPS Only + +All webhook endpoints must use HTTPS. + +## Pricing Strategies + +### Direct Sale (Perpetual License) + +- Customer pays once, owns forever +- Single password for all buyers +- Best for: Albums, films, books + +### Time-Limited (Streaming/Rental) + +Use dapp.fm Re-Key feature: + +1. Encrypt master copy with master password +2. On purchase, re-key with customer-specific password + expiry +3. Deliver unique password per customer + +```javascript +// On purchase webhook +const customerPassword = generateUniquePassword(); +const expiry = Date.now() + (24 * 60 * 60 * 1000); // 24 hours + +// Use WASM or Go to re-key +const customerVersion = await rekeyContent(masterSmsg, masterPassword, customerPassword, expiry); + +// Deliver customer-specific file + password +``` + +### Tiered Access + +Different passwords for different tiers: + +```javascript +const TIERS = { + 'preview': { password: 'preview-key', expiry: '30s' }, + 'rental': { password: 'rental-key', expiry: '7d' }, + 'own': { password: 'perpetual-key', expiry: null } +}; +``` + +## Example: Complete Stripe Setup + +```bash +# 1. Create your content +go run ./cmd/mkdemo album.mp4 album.smsg +# Password: PMVXogAJNVe_DDABfTmLYztaJAzsD0R7 + +# 2. Upload to IPFS +ipfs add album.smsg +# QmAlbumCID + +# 3. Create Stripe product +# Dashboard → Products → Add Product +# Name: My Album +# Price: $9.99 + +# 4. Create Payment Link +# Dashboard → Payment Links → New +# Select your product +# Get link: https://buy.stripe.com/xxx + +# 5. Set up webhook +# Dashboard → Developers → Webhooks → Add endpoint +# URL: https://yoursite.com/api/stripe-webhook +# Events: checkout.session.completed + +# 6. Deploy webhook handler (Vercel example) +vercel deploy + +# 7. Share payment link +# Fans click → Pay → Get email with password → Download → Play forever +``` + +## Resources + +- [Stripe Webhooks](https://stripe.com/docs/webhooks) +- [Gumroad Ping](https://help.gumroad.com/article/149-ping) +- [PayPal IPN](https://developer.paypal.com/docs/ipn/) +- [Resend (Email API)](https://resend.com/) +- [Vercel Functions](https://vercel.com/docs/functions)