Real-time per-second micropayments for video streaming, powered by Solana + USDC
Problem · Solution · Features · How It Works · Quick Start · Architecture · Database · API · Tech Stack
Streaming platforms today operate on an all-or-nothing model. You pay a flat monthly subscription whether you watch 200 hours or 2. Creators are paid through opaque algorithms that have no direct relationship with how much value a viewer actually got from their content.
This creates three fundamental problems:
| Who | Problem |
|---|---|
| Viewers | Overpay for content they barely use. Subsidize videos they never finish. |
| Creators | Revenue is decoupled from actual viewer engagement and value delivered. |
| The Market | No honest price signal exists to reflect how much of a video is worth watching. |
What if you could pay a creator exactly for what you watched — streamed in real-time, settled on a blockchain, down to the second?
PayPerPlay is a Chrome extension + Node.js backend that introduces per-second streaming payments for video content using USDC (a digital dollar stablecoin) on Solana.
Instead of subscriptions, viewers pay micro-amounts in real-time as they watch. Close a video halfway? You pay for half. The price itself is shaped by community engagement — if most people only finish 40% of a video, the price adjusts downward automatically to reflect that.
We needed a payment rail that could handle frequent tiny transactions with near-instant settlement and near-zero fees. Solana delivers all three:
| Requirement | Solana Capability |
|---|---|
| Micropayments (fractions of a cent) | Transactions cost ~$0.00025 in SOL fees |
| Speed (payments every 5 seconds) | ~400ms block time, finality in seconds |
| Stablecoin support | USDC is a native SPL token — 1 USDC = exactly $1 |
| Developer experience | First-class @solana/web3.js and @solana/spl-token SDKs |
| Reliability | Battle-tested infrastructure with high uptime |
Payments are denominated in USDC so viewers and creators deal in familiar dollar amounts. Every transaction is permanently recorded on Solana with a public transaction signature, viewable on Solana Explorer.
- Per-second pricing — charges accrue only while the video is actively playing
- Streaming payments — USDC micropayments sent every 5 seconds via Solana SPL token transfer
- Community-driven pricing — average watch ratio across all viewers dynamically adjusts the price
- Multi-platform — works on YouTube and Amazon Prime Video
- Phantom wallet onboarding — connect your Phantom wallet in one click
- Session history — popup UI shows full transaction history with amounts, durations, and statuses
- Admin dashboard — override prices, adjust watch ratios, inspect all sessions
- On-chain audit trail — every payment has a Solana transaction signature, verifiable at explorer.solana.com
- Devnet safe — runs entirely on Solana devnet with test USDC, no real money involved
Viewer opens a YouTube/Prime Video page
|
v
┌──────────────────────┐
│ Pricing Gate │ Extension fetches price from backend
│ $3.93 for 32min │ (base rate × community watch ratio)
│ 0.2001¢ / second │
│ │
│ [Start Watching] │
│ [Decline] │
└──────────┬────────────┘
│ Viewer clicks Start Watching
v
┌──────────────────────────────────────────┐
│ Active Session │
│ │
│ Every 1s: accumulate watch time │
│ Every 5s: send heartbeat to backend ──────> Backend calculates
│ with seconds watched │ prorated USDC amount
│ │ │
│ Badge shows live cost: [$0.04] │ v
│ │ Solana SPL Transfer
│ │ Viewer ──> Creator
│ │ tx_hash recorded in DB
└──────────────┬───────────────────────────┘
│ Video ends / viewer stops
v
┌───────────────────────────┐
│ Session Complete │
│ Final payment sent │ Remaining balance settled
│ Total: $0.18 for 1:32 │ Session marked 'completed'
└───────────────────────────┘
totalPrice = (baseCentsPerSecond × durationSeconds) × (avgWatchRatio / 100)
Where:
- baseCentsPerSecond =
0.2cents/second (configurable per video) - durationSeconds = total length of the video
- avgWatchRatio = average percentage of the video watched across all viewers (0–100)
Examples:
| Video | Duration | Watch Ratio | Price |
|---|---|---|---|
| Popular tutorial (people finish it) | 10 min | 90% | $1.08 |
| Clickbait video (people leave early) | 10 min | 20% | $0.24 |
| Standard video | 32 min | 98% | $3.93 |
Content that people actually finish costs more. Content people abandon costs less. The market speaks.
USDC has 6 decimal places on Solana. The conversion formula is:
microUsdc = Math.round(amountCents × 10,000)
| Amount | Micro-USDC | Actual USDC |
|---|---|---|
| 0.2 cents | 2,000 | 0.000002 USDC |
| 1 cent | 10,000 | 0.0001 USDC |
| 100 cents | 1,000,000 | 1.000000 USDC |
- Node.js >= 18
- npm >= 9
- Google Chrome
- Phantom wallet browser extension (from phantom.app)
git clone https://github.com/khuzaymaa918/PayPerPlay.git
cd PayPerPlay
npm installnpx ts-node backend/scripts/solana-setup.tsThis script:
- Generates two Solana keypairs (viewer + creator)
- Writes them to
.envimmediately - Checks SOL balances
- Creates USDC Associated Token Accounts (ATAs) for both wallets if funded
The Solana devnet RPC airdrop is rate-limited. If it fails, fund both wallets manually:
- Go to https://faucet.solana.com → select Devnet → airdrop 2 SOL to each address printed by the script
- Re-run
npx ts-node backend/scripts/solana-setup.ts— it reloads existing keys and creates the ATAs
1. Go to https://faucet.circle.com
2. Select network: Solana Devnet
3. Paste the Viewer public key printed by the setup script
4. Request USDC — arrives in ~30 seconds
npm run dev:backendThe API server starts at http://localhost:4000. SQLite migrations run automatically on first launch. You should see:
[solana] Connected to https://api.devnet.solana.com
[solana] Viewer : <address> (2.4980 SOL)
[PayPerPlay] Backend running on http://localhost:4000
npm run build:extension- Open
chrome://extensions - Enable Developer mode (top-right toggle)
- Click Load unpacked
- Select the
extension/distfolder
- Click the StreamPay extension icon — the onboarding page opens
- If Phantom injects into the page, click Connect Phantom
- If not (common on extension pages), open Phantom, copy your wallet address, paste it into the input field
- Click Connect
Open YouTube or Prime Video. Navigate to any video. The StreamPay pricing gate appears over the player. Click Start Watching — USDC payments stream every 5 seconds.
Check the backend terminal to see live transaction logs:
[solana] Payment SUCCESS: 1¢ (0.010000 USDC)
[solana] TX: https://explorer.solana.com/tx/...?cluster=devnet
┌─────────────────────────────────────────────────────────┐
│ Chrome Extension (MV3) │
│ │
│ ┌──────────────┐ ┌────────────────┐ ┌─────────────┐ │
│ │ Content │ │ Service Worker │ │ Popup │ │
│ │ Script │ │ (Background) │ │ │ │
│ │ │ │ │ │ - Tx history│ │
│ │ - Gate UI │ │ - CORS proxy │ │ - Totals │ │
│ │ - Meter │ │ - Wallet store │ │ - Sessions │ │
│ │ - Badge │ │ - Badge ctrl │ │ │ │
│ │ - Platform │ │ │ │ │ │
│ │ detection │ │ │ │ │ │
│ └──────┬────────┘ └──────┬─────────┘ └─────────────┘ │
│ │ │ │
└─────────┼──────────────────┼──────────────────────────────┘
│ REST / JSON │
└────────┬──────────┘
│
┌────────────▼───────────────────────────┐
│ Backend (Express + TypeScript) │
│ │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ Routes │ │ Services │ │
│ │ │ │ │ │
│ │ /price │ │ PricingEngine │ │
│ │ /sessions│ │ SolanaPayment │ │
│ │ /solana │ │ Provider │ │
│ │ /onboard │ │ │ │
│ │ /admin │ └────────┬─────────┘ │
│ └──────────┘ │ │
│ │ │
│ ┌──────────────────┐ │ │
│ │ SQLite Database │ │ │
│ │ │ │ │
│ │ videos │ │ │
│ │ watch_sessions │ │ │
│ │ watch_events │ │ │
│ │ payment_ledger │ │ │
│ └──────────────────┘ │ │
└────────────────────────-┼──────────────┘
│ SPL Token Transfer
│
┌────────────────▼──────────────────┐
│ Solana Devnet │
│ │
│ Viewer Wallet ──USDC──> Creator │
│ │
│ Settlement: ~400ms │
│ Fee: ~$0.00025 in SOL │
│ Every tx: viewable on Explorer │
└────────────────────────────────────┘
| Component | Tech | Role |
|---|---|---|
| Content Script | TypeScript, Shadow DOM | Detects videos, renders pricing gate, meters watch time, sends heartbeats |
| Service Worker | Chrome MV3 Background | Proxies API calls (CORS bypass), stores Phantom wallet public key |
| Popup | HTML + TypeScript | Shows session history and total USDC spent |
| Onboarding | HTML + TypeScript | Phantom wallet connect + manual pubkey fallback |
| Backend API | Express.js, TypeScript | Pricing, session lifecycle, payment orchestration |
| Pricing Engine | Pure TypeScript function | Computes price from base rate × community watch ratio |
| Payment Provider | @solana/web3.js + @solana/spl-token | Creates USDC SPL transfers, signs + submits to Solana devnet |
| Database | SQLite (better-sqlite3) | Videos, sessions, events, payment ledger |
The extension uses a platform abstraction layer to support multiple streaming sites:
| Platform | Video ID | Navigation Detection |
|---|---|---|
| YouTube | URL param ?v= |
yt-navigate-finish event |
| Prime Video | ASIN from URL path | URL polling |
PayPerPlay/
├── backend/
│ ├── src/
│ │ ├── db/
│ │ │ ├── connection.ts # SQLite setup (WAL mode, foreign keys)
│ │ │ ├── migrate.ts # Auto-migration runner on startup
│ │ │ └── migrations/ # Numbered SQL migration files
│ │ ├── models/
│ │ │ ├── video.ts # Video CRUD + pricing queries
│ │ │ ├── session.ts # Session lifecycle management
│ │ │ └── event.ts # Watch event logging
│ │ ├── services/
│ │ │ ├── payment-provider.ts # Solana USDC payment singleton
│ │ │ └── pricing.ts # Price computation engine
│ │ ├── routes/
│ │ │ ├── price.ts # GET /api/videos/:id/price
│ │ │ ├── sessions.ts # Session CRUD + heartbeat handler
│ │ │ ├── solana.ts # Solana status + payment history
│ │ │ ├── admin.ts # Admin dashboard + overrides
│ │ │ └── onboarding.ts # Viewer registration + SOL airdrop
│ │ ├── middleware/
│ │ │ ├── install-id.ts # X-Install-Id header extraction
│ │ │ └── admin-auth.ts # Token-based admin auth
│ │ ├── views/
│ │ │ └── admin.html # Admin dashboard UI
│ │ ├── app.ts # Express app configuration
│ │ └── index.ts # Server entry point
│ ├── scripts/
│ │ └── solana-setup.ts # Devnet wallet provisioning script
│ └── data/
│ └── PayPerPlay.db # SQLite database (auto-created)
│
├── extension/
│ ├── src/
│ │ ├── background/
│ │ │ └── service-worker.ts # Message handler, Phantom wallet store
│ │ ├── content/
│ │ │ ├── main.ts # Video detection orchestrator
│ │ │ ├── gate.ts # Pricing gate overlay (Shadow DOM)
│ │ │ ├── meter.ts # Watch time accumulation + heartbeat loop
│ │ │ ├── badge.ts # Extension icon badge with live cost
│ │ │ ├── platform.ts # Platform abstraction layer
│ │ │ ├── youtube-utils.ts # YouTube-specific DOM selectors
│ │ │ └── prime-utils.ts # Prime Video-specific DOM selectors
│ │ ├── popup/
│ │ │ └── popup.ts # Transaction history popup UI
│ │ ├── onboarding/
│ │ │ └── onboarding.ts # Phantom connect + manual fallback
│ │ └── shared/
│ │ ├── api-client.ts # REST client routed via service worker
│ │ ├── types.ts # Shared TypeScript interfaces
│ │ └── constants.ts # API URL, intervals, Solana constants
│ └── dist/ # Built extension (load this in Chrome)
│
├── static/
│ └── extension/
│ ├── manifest.json # Chrome MV3 manifest
│ ├── popup.html # Popup shell
│ ├── onboarding.html # Onboarding shell
│ └── icons/ # Extension icons (16/48/128px)
│
├── .env # Solana devnet config (git-ignored)
├── .env.example # Template showing required variables
└── package.json # npm workspaces root
PayPerPlay uses SQLite with WAL mode for concurrent reads. The schema is managed through numbered SQL migrations that run automatically on every backend startup.
videos
video_id TEXT PRIMARY KEY
title TEXT
channel TEXT
duration_seconds INT
avg_watch_ratio REAL -- community average, updated per session
manual_avg_watch_ratio REAL -- admin override
override_price REAL -- admin price override in cents
watch_sessions
session_id TEXT PRIMARY KEY
install_id TEXT -- identifies the extension instance
video_id TEXT REFERENCES videos
status TEXT -- active | completed | declined
price_quoted REAL -- price shown at gate (cents)
price_final REAL -- actual amount paid (cents)
seconds_watched INT
amount_streamed REAL -- running total paid so far (cents)
watch_events
event_id INT PRIMARY KEY
session_id TEXT REFERENCES watch_sessions
event_type TEXT -- play | pause | seek | heartbeat | end
timestamp_seconds REAL
metadata TEXT
payment_ledger
id INT PRIMARY KEY
session_id TEXT REFERENCES watch_sessions
amount_cents REAL -- payment amount in cents
rlusd_amount TEXT -- USDC amount as string (e.g. "0.010000")
tx_hash TEXT -- Solana transaction signature (base58)
tx_type TEXT -- stream | final | refund
created_at TEXT
Every row in payment_ledger maps to one Solana transaction. The tx_hash column stores the base58 Solana signature, viewable at:
https://explorer.solana.com/tx/{tx_hash}?cluster=devnet
The SolanaPaymentProvider singleton in backend/src/services/payment-provider.ts handles all on-chain activity:
- On startup, it loads the viewer keypair from
VIEWER_SECRET_KEYin.env - On each heartbeat, it calls
sendPayment(amountCents):- Gets or creates the viewer's Associated Token Account (ATA) for USDC
- Gets or creates the creator's ATA for USDC
- Builds an SPL token
transferInstruction - Signs and submits with
sendAndConfirmTransaction - Logs the Solana Explorer URL for every transaction
| Wallet | Role | Who controls it |
|---|---|---|
Viewer wallet (VIEWER_* in .env) |
Backend hot-wallet that sends USDC payments | Backend server |
Creator wallet (CREATOR_* in .env) |
Receives USDC payments | Backend server (simulated) |
| Phantom wallet | Your identity in the extension | You (via Phantom) |
The Phantom wallet is used for user identification only. Actual USDC transfers happen from the backend viewer keypair.
USDC on Solana lives in Associated Token Accounts (ATAs) — one per wallet, one per token. The setup script creates ATAs for both viewer and creator. If an ATA doesn't exist when a payment is attempted, the payment provider creates it automatically (using the viewer's SOL to pay the ~0.002 SOL rent).
On first install, the extension opens the onboarding page automatically.
- Detect Phantom — the page polls
window.solanafor up to 2 seconds - Connect — if Phantom is detected,
window.solana.connect()is called and the public key is stored - Manual fallback — if Phantom doesn't inject (common on
chrome-extension://pages), a text field appears to paste your Phantom address directly - Register — the backend creates a USDC ATA for the viewer's Phantom address
- Status — shows SOL balance, USDC balance, and ATA address
- Ready — viewer can navigate to YouTube/Prime Video and start watching
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/videos/:id/price?title=&channel=&duration= |
Get price for a video |
Response:
{
"videoId": "dQw4w9WgXcQ",
"priceCents": 393,
"centsPerSecond": 0.2001,
"avgWatchRatio": 98,
"durationSeconds": 1964
}| Method | Endpoint | Description |
|---|---|---|
POST |
/api/sessions |
Start a new watch session |
POST |
/api/sessions/decline |
Log a declined video |
GET |
/api/sessions/history |
Session history for this extension install |
POST |
/api/sessions/:id/events |
Log a watch event (heartbeat triggers payment) |
POST |
/api/sessions/:id/end |
End session and send final settlement |
All session endpoints require the X-Install-Id header (set automatically by the extension).
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/solana/status |
Connection status, SOL + USDC balances for both wallets |
GET |
/api/solana/payments |
All payment ledger entries (newest first) |
GET |
/api/solana/payments/:sessionId |
All payments for a specific session |
Status response:
{
"connected": true,
"network": "devnet",
"viewerAddress": "G3Fniz6...",
"creatorAddress": "FaAesMP...",
"viewerSolBalance": 2.498,
"viewerUsdcBalance": "10.000000",
"creatorUsdcBalance": "0.041200"
}| Method | Endpoint | Description |
|---|---|---|
POST |
/api/onboarding/register-viewer |
Register Phantom pubkey, create USDC ATA |
GET |
/api/onboarding/status?address= |
SOL balance, USDC balance, ATA address |
POST |
/api/onboarding/airdrop-sol |
Request 1 devnet SOL (Solana devnet only) |
| Method | Endpoint | Description |
|---|---|---|
GET |
/admin |
Admin dashboard (HTML UI) |
GET |
/api/admin/videos |
List all tracked videos |
PUT |
/api/admin/videos/:id/override |
Set a fixed price override |
PUT |
/api/admin/videos/:id/ratio |
Set a manual watch ratio |
Admin endpoints require the X-Admin-Token header matching ADMIN_TOKEN in .env.
Generated by backend/scripts/solana-setup.ts. See .env.example for the template.
| Variable | Description |
|---|---|
SOLANA_RPC_URL |
Solana RPC endpoint (default: https://api.devnet.solana.com) |
SOLANA_WS_URL |
Solana WebSocket endpoint |
USDC_MINT |
Devnet USDC mint address (4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU) |
VIEWER_PUBLIC_KEY |
Viewer wallet public key (base58) |
VIEWER_SECRET_KEY |
Viewer wallet secret key (JSON array of 64 bytes) |
CREATOR_PUBLIC_KEY |
Creator wallet public key (base58) |
CREATOR_SECRET_KEY |
Creator wallet secret key (JSON array of 64 bytes) |
PORT |
Backend port (default: 4000) |
ADMIN_TOKEN |
Secret token for admin dashboard access |
| Constant | Value | Description |
|---|---|---|
API_BASE |
http://localhost:4000/api |
Backend URL |
HEARTBEAT_INTERVAL_MS |
5000 |
How often payments are sent (ms) |
METER_TICK_MS |
1000 |
Watch time accumulation tick (ms) |
SOLANA_NETWORK |
devnet |
Solana network identifier |
USDC_MINT |
4zMMC9... |
Devnet USDC mint address |
USDC_DECIMALS |
6 |
USDC decimal places on Solana |
npm run dev:backendUses nodemon + ts-node. Auto-restarts on any .ts or .sql file change. SQLite database is created at backend/data/PayPerPlay.db on first run.
npm run build:extensionUses esbuild for sub-second bundling. After building, go to chrome://extensions and click the reload button on the StreamPay card.
npx ts-node backend/scripts/solana-setup.tsReloads existing keys from .env on re-runs — will not regenerate keypairs if .env already exists. Run this after funding wallets with SOL to complete ATA creation.
curl http://localhost:4000/api/solana/status | json_pphttp://localhost:4000/admin
| Layer | Technology | Why |
|---|---|---|
| Language | TypeScript (strict) | Type safety across the entire stack |
| Backend | Express.js | Lightweight, well-understood HTTP framework |
| Database | SQLite via better-sqlite3 | Zero-config, single-file, WAL mode for concurrency |
| Blockchain | Solana (devnet) | Fast settlement (~400ms), near-zero fees, ideal for micropayments |
| Payments | @solana/web3.js + @solana/spl-token | Official Solana SDKs for keypair management and SPL token transfers |
| Stablecoin | USDC (SPL token) | USD-pegged, 6 decimals, widely used — viewers pay in real dollars |
| Wallet | Phantom | Most popular Solana wallet, simple window.solana API |
| Extension | Chrome Manifest V3 | Modern extension platform with service workers |
| Build | esbuild | Sub-second extension builds |
| UI | Shadow DOM | Style isolation — gate overlay can't be broken by host page CSS |
| Monorepo | npm workspaces | Single install, shared dependencies |
- Mainnet deployment with real USDC
- Direct Phantom signing (viewer pays from their own wallet, not backend keypair)
- Creator dashboard with earnings analytics
- Multi-creator payment splitting
- Firefox and Edge extension support
- Creator-set pricing tiers
- Viewer reputation and loyalty rewards
- Additional platform support (Twitch, Vimeo, Netflix)
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Run
npx ts-node backend/scripts/solana-setup.tsto provision devnet wallets - Make your changes
- Build and test the extension in Chrome
- Commit:
git commit -m 'Add my feature' - Push:
git push origin feature/my-feature - Open a Pull Request
MIT License. See LICENSE for details.
- Solana — the blockchain powering PayPerPlay's payment layer
- USDC — Circle's USD stablecoin for real-dollar micropayments
- @solana/web3.js — official Solana JavaScript SDK
- @solana/spl-token — SPL token transfer library
- Phantom — Solana wallet used for viewer identity
- esbuild — blazing-fast bundler for the Chrome extension
- better-sqlite3 — synchronous SQLite for Node.js
Built for a fairer streaming economy — pay only for what you watch.