Building a Connector¶
This guide walks you through adding support for a new streaming platform to Tessera. A connector is a lightweight adapter (~100 lines) that translates your platform's presence events into Tessera's billing engine.
Prerequisites¶
- Your platform must emit some form of "user joined" and "user left" events (webhooks, WebSocket messages, API polling, etc.).
- You should understand how your platform tracks viewer presence.
The Core Concept: Mapping Events to Endpoints¶
Tessera's core infrastructure is platform-agnostic. It provides fixed billing functions, and your connector's job is simply to translate your platform's native events into the appropriate Tessera payment model.
Every platform emits different signals. A music server emits a "scrobble" when a track is played. A live-streaming server fires a webhook when a viewer joins. A photo gallery resolves a shared link. As a developer, you decide which Tessera payment model best fits your platform's events:
Option A: Continuous Streaming (Per-Second)¶
Best for: Live streams, Video-on-Demand, or time-based access.
- Start the meter: Call sessionService.recordJoin(userId, videoId, ratePerSecond, creatorAddress) when the user starts consuming. (The ratePerSecond and creatorAddress allow you to dynamically price the stream and route funds directly to the specific creator).
- Stop the meter: Call sessionService.recordPartAndSettle(userId) when the user leaves or playback stops.
Option B: One-Off Payments & Tips¶
Best for: Voluntary donations, per-article purchases, photo downloads, or event-driven micro-licenses (e.g., a music scrobble).
- Trigger a Payment: Your frontend or connector can call POST /api/core/tip with the userId, creatorWallet, and amount.
- This executes a one-time, off-chain settlement directly from the user's Gateway balance to the creator.
You can implement either of these models, or both, depending on what data structures and events your platform exposes.
The Frontend & Identity Concept¶
Tessera provides a universal, platform-agnostic UI (src/ui/paywall.js) that handles wallets, CCTP bridging, and Circle sessions. You do not need to build a crypto frontend from scratch.
Your connector has two responsibilities regarding the frontend:
1. Serve and Inject: Serve the src/ui directory as static assets and inject the script into your platform's HTML response using a reverse proxy. The reverse proxy will route traffic to your platform's upstreamUrl (defined in the configuration).
2. Identity Synchronization (The userId rule): If your platform tracks users, you must ensure that the userId in the frontend exactly matches the userId emitted by your backend webhooks. You can do this by injecting window.PLATFORM_USER_ID = 'user_123' into the HTML before the paywall loads. If left undefined, the paywall generates an anonymous local ID, which is fine for anonymous tips but will not map correctly to backend presence events.
Step 1: Create the Connector Directory¶
src/connectors/
└── your-platform/
├── index.ts # Implements the Connector interface
├── webhooks.ts # Translates platform events → engine calls
└── public/ # (Optional) Frontend paywall assets
Step 2: Implement the Connector Interface¶
Create src/connectors/your-platform/index.ts:
import express from 'express';
import path from 'path';
import { createProxyMiddleware } from 'http-proxy-middleware';
import * as cheerio from 'cheerio';
import type { Connector, ConnectorConfig } from '../../core/types';
import { sessionService } from '../../core/session';
const myConnector: Connector = {
name: 'YourPlatform',
register(app: express.Express, config: ConnectorConfig): void {
// 1. Serve the universal frontend paywall assets
app.use('/your-platform-assets', express.static(path.join(__dirname, '..', '..', 'ui')));
// 2. Register your webhook/event listener
app.post('/api/connectors/your-platform/webhook', (req, res) => {
const { event, userId } = req.body;
if (event === 'viewer_joined') {
sessionService.recordJoin(userId);
} else if (event === 'viewer_left') {
sessionService.recordPartAndSettle(userId).catch(console.error);
}
res.json({ status: 'ok' });
});
// 3. Set up a reverse proxy using config.upstreamUrl to inject the paywall
app.use('/', createProxyMiddleware({
target: config.upstreamUrl,
changeOrigin: true,
selfHandleResponse: true,
on: {
proxyRes: async (responseBuffer, proxyRes, req, res) => {
const contentType = proxyRes.headers['content-type'];
if (contentType && contentType.includes('text/html')) {
const html = responseBuffer.toString('utf8');
const $ = cheerio.load(html);
// Inject the identity and the paywall script
$('body').append(`<script>window.PLATFORM_USER_ID = 'dynamic_user_id_here';</script>`);
$('body').append(`<script src="/your-platform-assets/paywall.js"></script>`);
const modifiedHtml = $.html();
res.setHeader('Content-Length', Buffer.byteLength(modifiedHtml));
return modifiedHtml;
}
return responseBuffer;
}
}
}));
}
};
export default myConnector;
The two critical calls are:
- sessionService.recordJoin(userId) - starts the billing meter
- sessionService.recordPartAndSettle(userId) - stops the meter, calculates cost, refunds remaining balance
Everything else (Gateway deposit, x402 payment, withdrawal) is handled automatically by the core engine.
Step 3: Register in the Connector Registry¶
Add one line to src/server.ts:
const CONNECTOR_REGISTRY: Record<string, () => Promise<{ default: Connector }>> = {
owncast: () => import('./connectors/owncast'),
'your-platform': () => import('./connectors/your-platform'), // ← add this
};
Step 4: Enable in Config¶
Add your connector to src/tessera.config.ts:
connectors: [
{
name: 'your-platform',
upstreamUrl: 'http://localhost:9000',
ratePerSecond: 0.0001,
},
],
Step 5: Test¶
- Start your platform
- Start Tessera:
npx ts-node src/index.ts - Trigger a "viewer joined" event from your platform
- Verify
[Session] 🟢 Session startedappears in the terminal - Trigger a "viewer left" event
- Verify
[Session] 🔴 User partedand[Session] ✅ Refund completeappear
Step 6: Multi-Tenant Architecture Rules¶
To ensure Tessera can run multiple connectors simultaneously (e.g., Owncast and PeerTube on the same server) without routing collisions, you MUST adhere to these two architectural rules:
- Asset Routing Convention: Any frontend assets (like
paywall.jsorpaywall.css) must be served under a route named exactly/[your-platform-name]-assets. The core reverse proxies automatically ignore any routes matching/*-assets/to prevent swallowing requests meant for other connectors. - Reverse Proxy Limitations: While Tessera can run 10 connectors simultaneously, only one connector can mount a global catch-all reverse proxy (
app.use('/')) per instance. If your platform requires a root proxy (like Owncast), users cannot enable another platform that also requires a root proxy on the same Tessera port.
Reference: Owncast Connector¶
The Owncast connector in src/connectors/owncast/ is the reference implementation. It demonstrates:
| File | Purpose |
|---|---|
index.ts |
Implements Connector interface, mounts routes + proxy |
webhooks.ts |
Translates Owncast's USER_JOINED / USER_PARTED webhooks |
proxy.ts |
Reverse proxy that injects paywall.js into Owncast's HTML |
types.ts |
TypeScript types matching Owncast's Go webhook structs |
public/paywall.js |
Browser-side paywall (MetaMask connect, deposit, session UI) |
public/paywall.css |
Paywall styling |