How the rail works
Four actors, one invariant. A supporter stakes once; every metered platform event settles on-chain to the creator — payee balance += rate × units.
The four actors
1. The payer — @universal-paywall/agent
The supporter (a viewer, a listener, or an AI agent acting for one) stakes USDC in
their own StakeVault and grants the facilitator a bounded,
revocable policy: a spending cap and an expiry. Nothing is custodied —
funds stay in the vault until a valid, in-policy charge settles them. Revoke the
grant and spending stops.
2. The platform event
The platform keeps doing what it already does: PeerTube counts a view, Owncast sees a viewer join and part, Navidrome scrobbles a play, Jellyfin reports a playback stop, Immich serves a shared-link photo. The platform is never forked — the rail listens at a boundary it already exposes.
3. The integration — @universal-paywall/integrations
A small adapter — a sidecar, a published plugin, a reverse proxy, or an external
provider — translates that platform-native event into one call:
reporter.report({ payerKey, creatorKey, amount, ref }).
Identity resolvers map platform ids to wallets (for music, live MusicBrainz:
recording_mbid → artist_mbid → wallet). Unknown ids are
metered-and-skipped — nobody is ever mischarged.
4. The facilitator — @universal-paywall/facilitator
The facilitator batches metered charges and calls StakeVault.settle
on-chain. Settlement is feeless per event — no processor cut, no
rent in the rail — and only charges within the payer's granted cap and expiry can
settle.
Permissionless attachment — the six patterns
The core principle: attach at a boundary the platform already exposes — its config, its outbound events, its request path, its plugin loader, its external-data slots, or the client. The platform's source is never modified.
| # | Pattern | Idea | Proven on |
|---|---|---|---|
| 1 | Config redirect | The platform already speaks a protocol to some upstream; we implement that protocol and the operator redirects one config value at us. The platform can't tell. | Navidrome (ND_LISTENBRAINZ_BASEURL), Subsonic |
| 2 | Event sidecar | The platform emits webhooks/notifications; we register our endpoint for the events that matter. | Owncast (admin webhook), Jellyfin (official Webhook plugin), RSSHub (crawler boundary) |
| 3 | Reverse proxy | Sit transparently in the request path; meter the requests whose serving is the billable event. | Immich (shared-link resolves) |
| 4 | Published plugin | Where the platform has a plugin loader, ship a published plugin into that sanctioned slot — still not a source edit. | PeerTube (action:api.video.viewed) |
| 5 | External provider | The platform fetches an external data source; we run that source. | Mastodon (DONATION_CAMPAIGNS_URL) |
| 6 | Payer-side adaptor | Nothing on the platform at all — pay on the user's behalf from the client. | Browser extension (MV3) |
Full write-up: the six permissionless attachment patterns — including why a core PR to the platform is the wrong shape for this (upstreams merge donation pointers, but reject per-user payment plumbing; the durable integration is one you ship yourself at a boundary the platform already exposes).
Tips vs. paywall — a mode, not a headline
The same rail runs two modes, and tip mode is the default framing:
- Tip / support mode — content stays open; supporters who have staked automatically send a small amount per view, per listen, or per minute. Think "support-while-you-watch," not a gate.
- Paywall mode — the resource is gated (x402
402 Payment Required) and the payer agent or browser extension pays to unlock it.
Creators on community platforms overwhelmingly asked for support mechanisms, not gates — so the integrations default to metering support, and gating is opt-in.
Credibility: how each integration is proven
Every platform climbs the same four-layer ladder before its "tested" badge turns green — an integration is "done" only when its layer-4 money loop settles on-chain:
| Layer | What it proves |
|---|---|
| L1 Unit | the adapter maps a platform event to the right charge arguments |
| L2 HTTP contract | the running sidecar answers real platform-shaped HTTP correctly (status, body, headers) |
| L3 Real instance | a real Docker'd platform instance, wired per its recipe, actually calls the sidecar on a real event |
| L4 On-chain money loop | stake + grant → event → charge → batch → settle → the payee's balance increased by exactly rate × units |
The per-platform matrix lives in the repo: testing-plan.md.