尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Pay only for what you watch. Movies, tutorials and exclusive videos you pay for by the second in USDC on Arc. Creators upload, choose a category and set a price per hour. Viewers connect their own wallet, deposit once and watch with no popups. Receipts are settled onchain every hour, and any site can embed it with one line.
Live on Arc mainnet at https://ppv-studio.vercel.app since 2026-10-25, against the contract
0xb5AE…1eF4 (v4, chain 5042, real USDC).
It's a soft launch: deposits are capped at $50 per wallet, and the review worker (originality check, transcripts,
encrypted streaming) and paid agent transcripts are off for now. See MAINNET.md for what differs and
how the cutover was done.
The testnet version of the code is on the testnet branch, against
0x9da4…96e1 on Arc Testnet
(chain 5042002). All addresses are in contracts/deployments.json.
npm install
cp .env.example .env.local # then fill in SESSION_SECRET, CRON_SECRET, SETTLER_PRIVATE_KEY
npm run build && npm start # http://localhost:3000
./data/uploads and metadata in ./data/ppw-db.json. Back up ./data.instrumentation.ts). The settler wallet needs a little
USDC on Arc for gas (a few dollars lasts months).testnet branch. Test USDC for the settler and viewers comes from
https://faucet.circle.com.NEXT_PUBLIC_APP_URL to
it. Sign-in checks the domain, and https enables sign-in inside embeds.npm run seed publishes 3 Creative Commons films from SEED_CREATOR_PRIVATE_KEY, so the catalog isn't
empty on day one.Checks (no funds needed): npx tsx scripts/smoke-testnet.ts http://localhost:3000 (23 checks: pages, framing
headers, Arc reads, SIWE, upload and ranged playback, onchain verification and rejection paths) and
npx tsx scripts/check-eip712.ts (frontend receipt digest equals the contract's hashReceipt()).
Vercel functions can't keep files, so metadata goes to MongoDB Atlas and videos to Cloudflare R2. Browsers upload straight to R2 through signed links, which avoids Vercel's ~4.5 MB request limit, and play through short-lived signed links.
1. MongoDB Atlas (free M0)
0.0.0.0/0, since Vercel has no fixed IPs.MONGODB_URI (put your password in it). MONGODB_DB=payperwatch.
Collections and indexes are created automatically on first use.2. Cloudflare R2
payperwatch-videos and leave it private.R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY and R2_ACCOUNT_ID. Set R2_BUCKET.[{ "AllowedOrigins": ["https://ppv-studio.vercel.app", "http://localhost:3000"],
"AllowedMethods": ["GET", "PUT", "HEAD"], "AllowedHeaders": ["content-type", "range"],
"ExposeHeaders": ["content-length", "content-range", "etag"], "MaxAgeSeconds": 3600 }]
3. Vercel
.env.local (NEXT_PUBLIC_ARC_*, NEXT_PUBLIC_PPW_*,
SESSION_SECRET, CRON_SECRET, SETTLER_PRIVATE_KEY), plus MONGODB_URI, MONGODB_DB, R2_*,
NEXT_PUBLIC_STORAGE_PROVIDER=r2 and NEXT_PUBLIC_APP_URL=https://ppv-studio.vercel.app.NEXT_PUBLIC_APP_URL and redeploy, because it's baked in at build time.4. Hourly payouts. Vercel Hobby only allows daily cron jobs (vercel.json). Milestone settlements
(10/25/50/75/100%) happen on their own while people watch. For the hourly sweep, create a free job at cron-job.org:
GET https://ppv-studio.vercel.app/api/cron/settle, every hour, with header Authorization: Bearer <CRON_SECRET>.
5. Move this PC's videos and data up (once): with the Atlas and R2 values in .env.local, run
npx tsx scripts/migrate-to-cloud.ts (dry run) and then --apply. It uploads data/uploads to R2, copies all
metadata to Atlas, and leaves the local data/ folder untouched.
Checks: npx tsx --conditions=react-server scripts/test-r2.ts (with the R2_* env set) exercises signed upload,
ranged playback and the unsigned-read refusal against your real bucket.
<script src="https://YOUR-HOST/embed.js" data-ppw="catalog" async></script>
| What | Markup |
|---|---|
| One video | <div data-ppw="video" data-video="0x…videoId"></div> |
| Catalog (optionally one category or creator) | <div data-ppw="catalog" data-category="tutorials" data-creator="0x…"></div> |
| Upload flow | <div data-ppw="upload"></div> |
| No script | <iframe src="https://YOUR-HOST/embed/watch/0x…"> |
The embed is an auto-resizing iframe (/embed/* may be framed by any site, the rest of the app may not).
Viewers connect their own wallet inside it. The /developers page has copyable snippets and a live preview,
and every video in the creator dashboard has a "copy embed code" button.
There are no like buttons. A video is rated by how people actually watch it (lib/server/rating.ts):
1 + 3·min(1, t/D) + min(1, (t/D − 1)/2). Sampling it scores about 1, watching to the end 4, and coming back for
two more full watches 5.Creators choose per video, at upload or later in the dashboard, whether viewers can earn for sharing it (off by
default). When it's on, a signed-in viewer who has watched the video can open Share and earn to get a link:
/watch/<id>?ref=<their wallet>. Anyone who watches through it is attributed to them for that video (first link wins,
for 30 days). At settlement the contract pays the referrer 5% (referralBps, owner-adjustable up to 10%) of that
viewer's spend, taken from the creator's share. The viewer's price and the 5% platform fee don't change. With sharing
off, the server refuses referral receipts for that video. (A viewer colluding with a sharer could still sign one and
submit it to the contract directly; the rate is enforced onchain, the per-video opt-out by the platform.)
Rules enforced onchain (contract v3): the referrer can't be the viewer or the creator, and must already have paid that
creator, which stops free self-referral with a fresh wallet. Settlement runs unreferred receipts first so a referrer's
own payment lands before receipts that depend on it. Earnings show under Wallet → Referral earnings.
Platforms can pass their wallet as the referrer in embeds with data-ref="0x…".
Movies, Tutorials, Exclusive (members-style gated content) and Other. Categories are metadata; every video is gated the same way, by pay-per-second onchain.
The design follows Apple's HIG: clarity, deference to content and quiet depth. It uses SF Pro (Text for UI,
Display for large type), with one cream hue for the primary and a caramel secondary derived from it. Light and dark
themes share the same primary, and colours cross-fade between themes. Motion is limited to opacity and transform, and
fixed heights keep loading states from shifting the layout. Icons come from Radix. The landing photos are from
Unsplash, hotlinked and credited per the Unsplash API guidelines (see lib/unsplash.ts).
Font licence: SF Pro is licensed by Apple for use in mockups of Apple-platform software. Before a public launch,
confirm you may serve it on the web, or switch app/layout.tsx to the system stack. -apple-system is already the
first fallback, so Apple devices render the system SF Pro regardless.
| Piece | Where |
|---|---|
Contract address, ABI, EIP-712 domain and types, videoId | lib/contract.ts, lib/abi/PayPerWatch.ts |
| USDC math (bigint base units, never floats) | lib/money.ts |
| Metering and receipt signing | lib/client/meter.ts |
| Session keys (browser, per viewer, spend-capped) | lib/client/session-key.ts |
| Receipt verification (signer, onchain session key, monotonic) | app/api/receipts/route.ts |
| Hourly settlement | lib/server/settle.ts, app/api/cron/settle, instrumentation.ts |
| Storage (local disk, Supabase, Livepeer, Pinata) | app/api/storage/*, app/media/[file], lib/storage/client.ts |
Billing rules (lib/progress.ts, lib/client/meter.ts, lib/server/progress.ts):
sendBeacon), and the server settles onchain as
soon as paid progress crosses 10%, 25%, 50%, 75% and 100%, plus an hourly sweep./api/downloads, /library): pay the unpaid remainder of the full price, settled onchain before
release, then the video is saved in the browser (IndexedDB) for in-app offline playback with a 30-day pass.
Re-downloads within the pass are free. Local uploads stream only via short-lived viewer-bound tokens, and the player
hides the browser's download control. Determined users can still capture a stream; real protection needs DRM.Moving to a new contract: stop the server, run npx tsx scripts/migrate-contract.ts <old address> (archives old
receipts to data/archive/, keeps videos, progress and passes), set the new address and add the old one to
NEXT_PUBLIC_PPW_LEGACY_CONTRACTS. Creators re-publish from the dashboard; viewers withdraw old balances from Wallet.
0x85a5…8c98, SETTLER_PRIVATE_KEY), on mainnet too, by choice.
If that server key leaks, the attacker also controls the contract. Moving ownership to a hardware wallet is one
transferOwnership plus acceptOwnership.contracts/docs/security-review-v4.md), but hasn't had an
outside audit yet.NEXT_PUBLIC_MOCK_MODE=true (a simulated chain) exists only for scripts/e2e-mock.ts.