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.

  1. 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.
  2. B — Build. For sidecar-shaped patterns: a pure handle<Event>() adapter in packages/integrations/src/, a Route in serve.ts, a CLI case so PLATFORM=<yours> runs it, and unit tests with a spy reporter. Published plugins bundle self-contained (esbuild) so they carry no unpublished dependency.
  3. C — Wire the rail. Deploy a StakeVaultFactory, run up-facilitator, stake + grant with the agent. Your adapter only ever calls reporter.report.
  4. 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