⚠️ 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:
+
+ - Download the .smsg file from the link above
+ - Go to demo.dapp.fm
+ - Click "Fan" tab, then "Unlock Licensed Content"
+ - Paste the file and enter your license key
+
+
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)