Sponsors & the sponsor overlay

The Sponsors module lets a workspace sell on-stream placements to advertisers without touching the video feed. Creatives render through a transparent web page that each creator adds to OBS as a Browser Source; the page polls the platform and shows the right creative at the right time with an always-on disclosure label. Impressions are counted server-side against the live poller's viewer counts.

Nothing is relayed or spliced into the stream. Platform ad breaks (Twitch/YouTube) are untouched and unrelated.

Concepts

ThingWhat it is
SponsorThe advertiser (name, site, logo, contact). Workspace-scoped; plan limit limits.sponsors.
CampaignA flight for one sponsor: placement, cadence (display_seconds every interval_seconds), optional start/end, disclosure text, rate (none / cpm / flat), optional CPM budget cap, and creator targeting (all profiles or a chosen set). Statuses: draft → active ⇄ paused → ended.
CreativeAn image and/or text unit (headline, body, CTA) with a rotation weight. A campaign cannot be activated without an active creative.
ImpressionOne showing of one creative on one creator's overlay in one time slot: shown_at, seconds, viewers (profile's live_viewers at that moment), is_live (was the live poller reporting the creator live). Unique per (creative, profile, slot) so repeated polls never double count.

Viewer-impressions = Σ viewers over impressions; this is the billable unit for CPM. Estimated value = viewer-impressions ÷ 1,000 × CPM rate, or the flat fee.

Creator opt-in (enable / disable)

Every creator profile has its own overlay URL and an on/off switch at /dashboard/sponsors/overlay (staff and above). While a profile is disabled, its overlay serves nothing and nothing is counted, even if campaigns target it. Campaign targeting is the workspace admin's side of the same gate: a creative appears only when the creator has enabled the overlay and the campaign targets that profile (or all profiles).

The overlay URL contains a random token that is the only credential. Regenerate URL invalidates the old one. Enable/disable and rotation are audited (sponsor_overlay.*).

Overlay

  • GET /overlay/{token} — transparent 1920×1080 page for OBS. Add as Browser Source, width 1920, height 1080. ?preview=1 cycles every creative of every eligible campaign every 8 s without counting anything.
  • GET /overlay/{token}/next.json — what to show now: {show, remaining, poll_in, campaign:{id,name,sponsor,sponsor_logo,placement,disclosure}, creative:{id,kind,image_url,headline,body,cta_text,cta_url}} or {show:false, reason, poll_in}. Rate limited (public bucket). 404 for unknown tokens.

Selection is deterministic per time slot: for each eligible campaign, slot = floor(now / interval); the creative shows during the first display_seconds of each slot, and the creative index is slot mod weighted-pool-size, so all overlays of a workspace agree and a re-poll during the same showing returns the same answer. When several campaigns collide in a slot, one is chosen round-robin by slot number. Placements: banner (bottom centre), corner (bottom right card), lower_third (bottom left).

Disclosure: the campaign's disclosure_text is rendered on every showing. Creators should also set the platform's branded-content flag (Twitch "Branded Content", YouTube "paid promotion", KICK equivalent) when streaming with sponsors enabled — that is the creator's obligation, the overlay only makes the on-screen part automatic.

Reporting

/dashboard/sponsors shows 30-day totals and the campaign table; each campaign page shows daily impressions, per-creative and per-creator breakdowns, estimated value, and a CSV export of raw impressions (shown_at_utc, seconds, viewers, stream_live, creative_id, creative, profile). Exports are audited.

is_live = 0 impressions mean the overlay was running while the live poller did not see the creator live (no connected account, poll lag, or OBS open while offline). Bill on verified-live impressions unless the sponsor agreed otherwise.

Money

The module produces the numbers to invoice; it does not move money yet. Invoice the sponsor from Stripe (or the store as a one-off product) and pay creators manually. Automated creator payouts need Stripe Connect — an open decision in CLAUDE.md.

Tables

sponsors, sponsor_campaigns, sponsor_creatives, sponsor_impressions (all tenant-scoped) and two columns on creator_profiles: sponsors_enabled, overlay_token.