The rail, for developers
Open-core monorepo: a payer agent, a facilitator, a creator-side integrations package, contracts, and an MV3 extension. Everything below links into the repo — the docs there are authoritative.
Package map
packages/agent/ # @universal-paywall/agent — payer: stake, grant policy, fetchWithPaywall
packages/facilitator/ # @universal-paywall/facilitator — batches metered charges, settles on-chain
packages/integrations/ # @universal-paywall/integrations — creator-side sidecars/plugins/providers
packages/extension/ # @universal-paywall/extension — payer-side MV3 browser-extension adaptor
packages/peertube-plugin/ # peertube-plugin-universal-paywall — published PeerTube view-hook plugin
packages/sdk/ # @universal-paywall/sdk — shared client/types
packages/resource-adapter/ # @universal-paywall/resource-adapter — x402 resource helper
packages/middleware/ # @universal-paywall/middleware — legacy per-payment x402 (see repo History)
contracts/ # Foundry: rail/ (StakeVault, StakeVaultFactory) + legacy vaults The core model: createReporter + Resolve
Every creator-side pattern — sidecar, plugin, proxy, provider — ends at the same call. An adapter's whole job is to map a platform-native event to it:
import { createReporter, mapResolver } from '@universal-paywall/integrations';
const reporter = createReporter({
facilitatorUrl: process.env.FACILITATOR_URL!,
apiKey: process.env.FACILITATOR_API_KEY!,
resolvePayer: mapResolver(JSON.parse(process.env.PAYER_WALLETS || '{}')),
resolveCreator: mapResolver(JSON.parse(process.env.CREATOR_WALLETS || '{}')),
});
// a platform event becomes:
await reporter.report({ payerKey, creatorKey, amount, ref }); Resolve maps a platform-native id to a wallet, and it's async — so a
static mapResolver can be swapped for a live registry like the
MusicBrainz resolver (recording_mbid → artist_mbid → wallet, cached
and rate-limited). The contract: unknown ids resolve to null and the event
is metered-and-skipped — never mischarged.
Build a new integration
The Integration Playbook is the instruction doc: run its "questions script," and the answers select your attachment pattern and give you the facts to implement it.
- A — Discovery. Answer the questions script: can an operator redirect a protocol target by config? Does the platform emit webhooks? Can you proxy the billable request? Is there a plugin loader? Does it fetch an external source? First "yes" picks the pattern (patterns 1–5); otherwise go payer-side (pattern 6). Then pin down the billable event's exact shape, the payer/creator key spaces, and the unit of value.
- B — Build. For sidecar-shaped patterns: a pure
handle<Event>()adapter inpackages/integrations/src/, aRouteinserve.ts, a CLI case soPLATFORM=<yours>runs it, and unit tests with a spy reporter. Published plugins bundle self-contained (esbuild) so they carry no unpublished dependency. - C — Wire the rail. Deploy a
StakeVaultFactory, runup-facilitator, stake + grant with the agent. Your adapter only ever callsreporter.report. - D — Climb the test ladder. L1 unit → L2 HTTP contract (the byte-exact requests the real platform sends) → L3 a real Docker'd instance → L4 the anvil money loop.
Run it locally
# install + unit tests
npm install
npm test --workspaces --if-present
cd contracts && forge test
# a creator-side sidecar (env-driven)
PLATFORM=owncast FACILITATOR_URL=… FACILITATOR_API_KEY=… \
PAYER_WALLETS='{…}' CREATOR_WALLETS='{…}' RATE=1000 npx up-integration
# the facilitator
FACILITATOR_KEY=0x… STAKE_VAULT_FACTORY=0x… npx up-facilitator
# on-chain money loops (anvil :8545 + built contracts)
npm run e2e:anvil -w @universal-paywall/integrations
npm run e2e:owncast -w @universal-paywall/integrations
npm run e2e:mastodon -w @universal-paywall/integrations
npm run e2e:anvil -w @universal-paywall/extension Where to read next
- INTEGRATION-PLAYBOOK.md — the full build guide (questions script included).
- integration-patterns.md — the six permissionless patterns, with the decision guide.
- testing-plan.md — the L1→L4 ladder and per-platform matrix.
- The docs hub — everything else.