TEASEDocs
ProductsClubConcepts

Campaigns & promotions concepts

The mental model behind broadcasts, automation, smart links, the free pool, native OF promos, and referrals — and the traps between look-alike names.

This area covers everything that pushes a message or an offer at your fan base, or routes traffic toward one: mass-DM broadcasts, the separate automation-campaign system, the four fixed auto-messages, Route-Spine smart links, the free-link pool, native OnlyFans promos/bundles, and the referral program. Read this first — the how-to pages assume the ideas below.

Two campaign systems exist, and they are not merged

Broadcast is TEASE's own mass-DM tool: a Campaign row (the plan) plus one CampaignSend row per fan — a throttle-queue the sender worker drains one fan at a time. The sender only ever calls a 1:1 send, never a vendor "broadcast" call — that is the ban-safety property of the whole design. A campaign's mode (segmented | tiered | drip | online_trigger) decides how the audience is picked and sequenced, and its mechanism decides how it's delivered: 1to1 (our paced per-fan send, personalization/tiers/drip all work, lands in the fan's Priority inbox) or native (one OnlyFans native mass message to a list — no per-fan features, lands in "All").

Separately, a Track B automation campaign lives on its own automation_campaigns table — audience × payload (template / sequence / PPV / scheduled post) × schedule. It starts mode='shadow' (renders and logs, sends nothing) and needs a passed soak run before mode can be promoted to 'live' (POST /promote-live); status is a separate field tracking the campaign's own lifecycle (draft/soaking/scheduled/...) and never takes the value 'live' itself.

Coexist, not compete

The automation model's own docstring calls unifying the two systems an open owner decision — they are not merged today, and nothing in the code picks one as "the real one". Building against the wrong table, or expecting a Broadcast campaign to show up in the automation overview (or vice versa), is the single most common integration mistake in this area.

Two safety rails sit under every automated send

A global kill-switch (GET/POST /api/admin/automation/kill-switch) is a cross-tenant, file-backed emergency brake — flipping it halts automation sends for every owner at once. It is separate from each owner's own opt-in kill switch; the file fails open (missing/corrupt state reads as "not active"), so the per-owner opt-in stays the real gate day to day.

Every outbound text write — templates, sequences, PPV offers, scheduled posts — passes through a deterministic, offline content firewall lint that stamps a firewall_state of clean / flagged / blocked. A blocked verdict must never be sent — but the firewall does not reject the write itself (a draft is allowed to hold blocked content); the hard reject happens later, at enqueue. The firewall mechanism itself is shared across Club and is documented in the shared glossary, not repeated here.

The four auto-messages are Track B campaigns you edit, not create

Welcome, subscription-expiring, subscription-expired, and fan-online are four fixed-kind Track B campaigns. A database constraint enforces at most one live row per kind per owner, so POST /api/admin/broadcasts/automessages is an upsert: it edits the one campaign of that kind if it exists, never creates a second. Each kind reads a different threshold from the same send_window_json knob block — within_days (expiring/expired), within_minutes + cooldown_hours (online), resend_after_days (a reminder, on every kind except online, which uses its own cooldown instead). Because these campaigns are Track B underneath, they inherit Track B's shadow-first posture: a shadow log records who each auto-message would have reached, with the real rendered text, while live sending stays off.

A Smartlink (Route-Spine) is one creator-facing /r/<slug> link per domain. Its policy_json decides, per visitor, which arm — paid or ftl (free-trial) — they get: a weighted split, sticky by subnet so a returning visitor keeps the same arm, with per-country overrides. By default the trial arm only fires for a country that has its own permanent trial destination (require_country_link: true) — everywhere else silently gets the paid arm, not a foreign trial. The two old fixed addresses /r/paid and /r/ftl are retired; they still resolve (as aliases into the domain's own Smartlink policy) but nothing in the product offers them anymore.

Not the same object as OnlyFans' own pooled smart link

A completely different, similarly-named feature — OfSmartLink, the registry of onlyfansapi.com-issued pooled smart links, plus its Meta/TikTok CAPI pixel config — is served by a different file (admin/smart_links.py, note the underscore) and keyed on a different model. It is documented separately in the shared glossary. Confusing the two misattributes an endpoint to the wrong feature.

Pool membership is exactly of_campaigns.hidden == 1 — no separate table. Every OnlyFans link a scan finds with no traffic and that the owner (or an auto-hide rule) has hidden becomes a spare in the pool, split into two logical pools by arm (tracking → paid, trial → ftl). Dressing a slot reuses one spare onto a (domain, country, arm) slot: it provisions TEASE's own segment_links row pointing at the spare's already-existing OF offer, binds the domain as that link's source, and the spare leaves the pool — no OnlyFans write, no new mint. It is idempotent (re-dressing an already-live slot just returns the existing row) and reversible: reclaiming a slot returns the link to the pool rather than retiring it. Every write here touches only of_campaigns visibility/layout keys and TEASE's own segment_links / source_link rows — never fan_link or of_transactions — which is what makes the whole mechanism money-safe.

Native OF promo / bundle: TEASE mirrors OnlyFans, it doesn't drive it live

A native promo is OnlyFans' own on-platform offer — either a free_trial (1–30 days) or a first_month percentage discount (5–65%, step 5) — targeted at all_subscribers, new_subscribers, or expired_subscribers. A bundle is a fixed 3/6/12-month subscription slot with its own discount (0–50%, step 5). TEASE never writes either synchronously: every create / stop / delete queues an engine job and answers {"status": "pending", "nonce": ...} immediately — the caller polls /api/admin/promo/ops/{nonce} for queued / done / failed / stale. This is deliberate, not a performance shortcut: a synchronous wait doesn't survive a pod restart or a dropped connection, and a lost wait invites a retry that would create a second live promo on OnlyFans.

Reads of the promo/bundle lists are a mirror of what the engine last saw on the promo page. The envelope's connected field is a plain boolean — true only if something was captured recently (within 3 hours) — so telling apart the three real-world cases takes two fields, not one: connected: false with captured_at: 0 means never captured; connected: false with a real captured_at means the last capture is stale; connected: true with an empty items list means captured recently and genuinely nothing is running.

Offer entry kind: what a fan took, kept beside where they came from

When a fan's OnlyFans subscription-feed row carries a sub_type of trial, promo, or bundle, that fact is stored next to fan_source (where they came from) — never inferred, and never substituted for it. "How they entered" and "where they came from" are different questions with different answers; a promo has no address of its own, so it can never itself be a traffic source.

Impact-before-delete: what breaks, shown before you break it

Before deleting a trial link, a tracking link, a promo, or a bundle, one read (GET /api/admin/promo/impact) answers "what breaks" — chiefly which (domain, country) routing places lose their only link. Its one hard rule: an unmeasurable counter comes back null with a note explaining why, never a 0 — a 0 means "we looked and it was empty", which is a different, more dangerous claim to make right before an irreversible delete.

Referral program: an end-user feature, not an admin one

The referral program (/api/me/referral/*) is unrelated to the workspace-owner admin panel — it is a TEASE-account-level invite-code system for any signed-in user, authenticated by browser session cookie rather than an al_live_* API key. A durable code is minted lazily on first read, and every query is scoped strictly to the caller's own uid: one referrer can never read or claim another's code or stats.

What's next

Broadcast creation/scheduling has its own how-to page, written alongside 1:1 chat — see Create and schedule a broadcast.

Start with creating a smart link, or jump to configuring the four auto-messages if lifecycle messaging is what you need.

On this page