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
This commit is contained in:
parent
bd7e8b3040
commit
8486242fd8
4 changed files with 1006 additions and 13 deletions
|
|
@ -636,9 +636,9 @@ Local playback Third-party hosting
|
||||||
|
|
||||||
### 8.3 Future Work
|
### 8.3 Future Work
|
||||||
- [ ] Multi-bitrate adaptive streaming (like HLS/DASH but encrypted)
|
- [ ] Multi-bitrate adaptive streaming (like HLS/DASH but encrypted)
|
||||||
- [ ] Payment integration examples (Stripe, Gumroad)
|
- [x] Payment integration examples (see `docs/payment-integration.md`)
|
||||||
- [ ] IPFS distribution guide
|
- [x] IPFS distribution guide (see `docs/ipfs-distribution.md`)
|
||||||
- [ ] Demo page "Streaming" tab for v3 showcase
|
- [x] Demo page "Streaming" tab for v3 showcase
|
||||||
|
|
||||||
## 9. Usage Examples
|
## 9. Usage Examples
|
||||||
|
|
||||||
|
|
|
||||||
235
demo/index.html
235
demo/index.html
|
|
@ -971,6 +971,154 @@
|
||||||
color: #888;
|
color: #888;
|
||||||
margin-top: 0.5rem;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -986,7 +1134,7 @@
|
||||||
<button class="mode-btn" data-mode="streaming">📡 Streaming</button>
|
<button class="mode-btn" data-mode="streaming">📡 Streaming</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; gap: 1rem;">
|
<div class="tagline-row" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; gap: 1rem;">
|
||||||
<p class="tagline" style="margin: 0;">Zero-Trust DRM for Independent Artists</p>
|
<p class="tagline" style="margin: 0;">Zero-Trust DRM for Independent Artists</p>
|
||||||
<div style="background: rgba(255, 193, 7, 0.15); border: 1px solid rgba(255, 193, 7, 0.4); border-radius: 8px; padding: 0.4rem 0.75rem; font-size: 0.75rem; color: #ffc107;">
|
<div style="background: rgba(255, 193, 7, 0.15); border: 1px solid rgba(255, 193, 7, 0.4); border-radius: 8px; padding: 0.4rem 0.75rem; font-size: 0.75rem; color: #ffc107;">
|
||||||
⚠️ Demo pre-seeded with keys for protocol demonstration
|
⚠️ Demo pre-seeded with keys for protocol demonstration
|
||||||
|
|
@ -1583,10 +1731,14 @@
|
||||||
'../examples/demo-sample.smsg': '6ZhMQ034bT6maHqMaJejoxDMpfaOQvq5',
|
'../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_URL = 'https://demo.dapp.fm/demo-track.smsg';
|
||||||
const DEMO_PASSWORD = DEMO_TRACKS[DEMO_URL];
|
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 wasmReady = false;
|
||||||
let manifest = null;
|
let manifest = null;
|
||||||
|
|
||||||
|
|
@ -1678,9 +1830,9 @@
|
||||||
btn.textContent = 'Loading...';
|
btn.textContent = 'Loading...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch encrypted content
|
// Fetch encrypted content (Vi mascot demo)
|
||||||
setProgress(10, 'Fetching encrypted content...');
|
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');
|
if (!response.ok) throw new Error('Demo file not found');
|
||||||
|
|
||||||
const contentLength = response.headers.get('content-length');
|
const contentLength = response.headers.get('content-length');
|
||||||
|
|
@ -1719,7 +1871,7 @@
|
||||||
|
|
||||||
// Decrypt using binary API - no base64, pure zstd speed!
|
// Decrypt using binary API - no base64, pure zstd speed!
|
||||||
setProgress(70, 'Decrypting (zstd)...');
|
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...');
|
setProgress(95, 'Preparing player...');
|
||||||
displayMedia(msg);
|
displayMedia(msg);
|
||||||
|
|
@ -2078,6 +2230,48 @@
|
||||||
|
|
||||||
// ========== MODE SWITCHING ==========
|
// ========== MODE SWITCHING ==========
|
||||||
let profileLoaded = false;
|
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 => {
|
document.querySelectorAll('.mode-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
|
|
@ -2097,6 +2291,11 @@
|
||||||
if (mode === 'streaming' && wasmReady && !streamingHeaderInfo) {
|
if (mode === 'streaming' && wasmReady && !streamingHeaderInfo) {
|
||||||
initStreamingDemo();
|
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
|
// Generate license
|
||||||
async function artistGenerateLicense() {
|
async function artistGenerateLicense() {
|
||||||
if (!wasmReady) return;
|
|
||||||
|
|
||||||
const btn = document.getElementById('artist-generate-btn');
|
const btn = document.getElementById('artist-generate-btn');
|
||||||
btn.disabled = true;
|
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...';
|
btn.textContent = 'Generating...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -2450,10 +2661,10 @@
|
||||||
setTimeout(() => btn.textContent = 'Copy', 1500);
|
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() {
|
function checkArtistGenerateReady() {
|
||||||
const hasMasterPassword = document.getElementById('artist-master-password').value.length > 0;
|
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);
|
document.getElementById('artist-master-password').addEventListener('input', checkArtistGenerateReady);
|
||||||
|
|
@ -2462,7 +2673,11 @@
|
||||||
initWasm = async function() {
|
initWasm = async function() {
|
||||||
await originalInitWasm();
|
await originalInitWasm();
|
||||||
if (wasmReady) {
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
281
docs/ipfs-distribution.md
Normal file
281
docs/ipfs-distribution.md
Normal file
|
|
@ -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'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>My Album - Download</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>My Album</h1>
|
||||||
|
<p>Download: <a href="https://ipfs.io/ipfs/QmAlbumCID">IPFS</a></p>
|
||||||
|
<p>After purchase, you'll receive your license key via email.</p>
|
||||||
|
<p><a href="https://demo.dapp.fm">Play with license key</a></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
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/)
|
||||||
497
docs/payment-integration.md
Normal file
497
docs/payment-integration.md
Normal file
|
|
@ -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: `
|
||||||
|
<h1>Thank you for your purchase!</h1>
|
||||||
|
<p><strong>Download:</strong> <a href="${product.downloadUrl}">${product.name}</a></p>
|
||||||
|
<p><strong>License Key:</strong> <code>${product.password}</code></p>
|
||||||
|
<p><strong>How to play:</strong></p>
|
||||||
|
<ol>
|
||||||
|
<li>Download the .smsg file from the link above</li>
|
||||||
|
<li>Go to <a href="https://demo.dapp.fm">demo.dapp.fm</a></li>
|
||||||
|
<li>Click "Fan" tab, then "Unlock Licensed Content"</li>
|
||||||
|
<li>Paste the file and enter your license key</li>
|
||||||
|
</ol>
|
||||||
|
<p>This is your permanent license - save this email!</p>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(`
|
||||||
|
<h1>Thank you!</h1>
|
||||||
|
<p>Download: <a href="${product.downloadUrl}">${product.name}</a></p>
|
||||||
|
<p>License Key: <code>${product.password}</code></p>
|
||||||
|
`);
|
||||||
|
} 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
|
||||||
|
<!-- PayPal Buy Button -->
|
||||||
|
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
|
||||||
|
<input type="hidden" name="cmd" value="_xclick">
|
||||||
|
<input type="hidden" name="business" value="artist@example.com">
|
||||||
|
<input type="hidden" name="item_name" value="My Album - Digital Download">
|
||||||
|
<input type="hidden" name="item_number" value="album-001">
|
||||||
|
<input type="hidden" name="amount" value="9.99">
|
||||||
|
<input type="hidden" name="currency_code" value="USD">
|
||||||
|
<input type="hidden" name="notify_url" value="https://yoursite.com/paypal-ipn">
|
||||||
|
<input type="hidden" name="return" value="https://yoursite.com/thank-you">
|
||||||
|
<input type="submit" value="Buy Now - $9.99">
|
||||||
|
</form>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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: `
|
||||||
|
<p>Download: <a href="https://ipfs.io/ipfs/QmYourCID">My Album</a></p>
|
||||||
|
<p>License Key: <code>PMVXogAJNVe_DDABfTmLYztaJAzsD0R7</code></p>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
Loading…
Add table
Reference in a new issue