Skip to content

Phase 4 — Targeted Telegram Outreach

Status: implemented, GATED OFF. The engine, templates, inline-button handlers (surveys + help-ticket + CTAs), the scheduled batch Lambda, and the admin Bot Messages editor are all built. Nothing sends to a user until an operator (a) turns a per-segment template ON and (b) flips OUTREACH_ENABLED=true — and the EventBridge schedule ships DISABLED. Until then, scheduled runs are dry-run only; the admin's manual send delivers to a single chat for staging validation.

Goal: turn each computed segment (from the nightly classifier, src/utils/database/segments.mjs) into the right message to the right admin, in Telegram, with 1-tap surveys and a real support path — to recover stuck groups, nudge free-with-value groups, and learn why groups don't convert. Objective: increase free→paid conversion.


Prime directive (this is a live, paying product)

Every send path is gated OFF by default via OUTREACH_ENABLED (env, default "false"). When off, the engine runs in dry-run: it logs exactly what it would send and writes nothing to any user. Nothing reaches a real group until the flag is flipped, per-segment, after a staging test.

ControlMechanism
Master kill switchOUTREACH_ENABLED env (default false → dry-run)
Per-segment enableMESSAGE_TEMPLATES.ENABLED (bool per template)
Frequency cap≤ 1 outreach / chat / 7d; never send the same template key twice to a chat
Don't upsell payersskip PRO/ENTERPRISE chats for cap/upsell segments
Opt-outSETTINGS.OUTREACH_OPTOUT + a "stop these" survey option
Rate limit1 msg/sec staggered batch (well under Telegram limits)
Staging firstvalidate on a test group before enabling any prod segment

1. Data model

  • MESSAGE_TEMPLATES (table already created in .deploy/IaC/db/db.yml): TEMPLATE_KEY (PK = segment id, e.g. filtered_no_report), BODY_HTML, BUTTONS (JSON: survey options + CTA + help), ENABLED (bool), UPDATED_AT, UPDATED_BY. Hardcoded defaults in src/utils/outreachTemplates.mjs are the fallback when no row exists.
  • No new outreach-log table — reuse EVENTS (src/utils/database/events.mjs): OUTREACH_SENT, SURVEY_SHOWN, SURVEY_RESPONSE types already exist. Frequency/dedup reads from these plus two new fields on SEGMENTS: LAST_OUTREACH_AT, LAST_OUTREACH_KEY.
  • Opt-out: new SETTINGS.OUTREACH_OPTOUT flag, respected everywhere.

2. Send engine — src/utils/outreach.mjs (new)

Built on the existing rails:

  • Sending: XBotMessages.sendMessage(chatId, html, keyboard) (Telegraf).
  • Targeting: SETTINGS.NOTIFICATION_ADMINS (admin user IDs to DM); fallback in-chat if none designated.

Functions:

  • resolveTemplate(key)MESSAGE_TEMPLATES row or hardcoded default.
  • renderVars(chatId) (and any future vars) from SETTINGS. No numeric plan/usage values in copy (see §4).
  • buildOutreach(chatId, segment) → rendered HTML + Telegraf inline keyboard (survey buttons + a CTA deep-link into /buy or /setup + a 💬 Get help button).
  • sendOutreach(chatId, key, { dryRun, force }) — the gated core:
    1. OUTREACH_ENABLED check → else dry-run (log only).
    2. OUTREACH_OPTOUT check → skip.
    3. Frequency guard: ≤ 1/chat/7d; never repeat a key; skip upsell keys for PRO chats.
    4. Resolve target (NOTIFICATION_ADMINS DM → fallback in-chat).
    5. XBotMessages.sendMessage(...).
    6. recordEvent(OUTREACH_SENT / SURVEY_SHOWN) + set SEGMENTS.LAST_OUTREACH_*.
    7. Throttle: global 1 msg/sec stagger across a batch.

3. Micro-surveys + the "Get help" ticket path (inline buttons)

New src/commands/outreach.mjs registers the Telegraf action handlers:

  • Surveys — buttons carry callback_data = survey:<key>:<optionId> (e.g. capped → [Too expensive][Don't need more][Missing feature]). bot.action(/^survey:([^:]+):(.+)$/)recordEvent(SURVEY_RESPONSE,{key,option}) → edit the message to a thank-you. A "Missing feature" tap can open a free-text follow-up; a "stop these" option sets SETTINGS.OUTREACH_OPTOUT.

  • 💬 Get help → real support ticket (not a bare "reply here"). Free-text replies in Telegram get lost, so the help button opens a real ticket using the existing system (src/commands/setup/handlers/support.mjs, src/utils/database/tickets.mjs):

    • bot.action(/^outreach:help:(.+)$/) sets the same ticket-input state the /setup → Help & Support → New ticket flow uses. The admin's next message is captured by applySupportNewTicket() → a real TGXBOTTICKETS row, a ticket id returned, admin-visible in the /tickets SPA, and DM-tracked on reply.
    • The ticket is seeded with context: { source:'outreach', segment, templateKey } in the payload, so the admin sees "opened from the installed-no-setup nudge" and knows exactly where the group got stuck.
    • Reuses the existing input-state machine, so it works identically in a group or a DM, survives across messages, and needs no fragile reply-capture logic.

    Every template that offers help carries the 💬 Get help button — never a plain-text "reply here and I'll help" invitation.


4. Default template copy (ships in src/utils/outreachTemplates.mjs)

Tone: kind, accurate expectations, no numeric plan/usage values. First pass:

  • installed_no_setup

    Thanks for adding X Bot to ! 👋 Whenever you're ready, run /setup — it's a quick guided walkthrough where you choose what you'd like to track (a hashtag, cashtag, or an account). Once you've picked your criteria, I'll build your first leaderboard from it.

    Buttons: [ 🚀 Run setup ] [ 💬 Get help ] Rationale: leads with thanks (no guilt framing); makes clear /setup is a guided walkthrough where you choose the criteria first — the leaderboard is built from that choice, not posted automatically. Help routes to a ticket.

  • filtered_no_report

    You're all set up on — reply /report to get your first leaderboard now.

    Buttons: [ 📊 Run report ] [ 💬 Get help ]

  • near_cap

    Your community's on a roll! 🚀 You're getting close to your free monthly limit. Upgrade to PRO to keep your reports flowing without interruption.

    Buttons: [ ⬆️ Upgrade ] [ 💬 Get help ] Rationale: no plan number ("for 1,000" removed) and no usage counts — the message carries no digits at all; it reads as a gentle heads-up, never a metered warning that could quote a stale figure.

  • capped / churned / whale_agency: paywall-on-blocked-report / win-back-with-fresh-data / annual + referral. (Copy TBD, same tone + no digits.)


5. Admin "Bot Messages" page (fills the current stub)

  • Template editor per segment: HTML body + variable palette + button builder
    • ENABLED toggle + a fake-Telegram-bubble preview.
  • Backend: GET/PUT /admin/message-templates[/{key}] in XBotAdmin_Handler.
  • "Send preview to me" (dry-run to the operator's own chat) and, from ChatDetail, a "Send outreach now" one-click (manual, still gated + logged).
  • Outreach dashboard: sends + survey responses (from OUTREACH_SENT / SURVEY_RESPONSE events).

6. Trigger modes (roll out in this order)

  1. Manual one-click from ChatDetail/Behavior — safest; operator picks the chat.
  2. Scheduled batch — new src/Outreach_Handler.mjs Lambda (mirror the classifier Lambda in node-lambdas.yml), daily after the classifier, gated by OUTREACH_ENABLED. Enable per-segment, starting with the two biggest leaks (filtered_no_report, installed_no_setup).
  3. Real-time behavioral (overlaps Phase 5) — e.g. a rescue DM on SETUP_ERRORED. Hot-path; defer until batch is proven.

7. File-level task list

  • src/utils/outreachTemplates.mjs — hardcoded default templates (copy in §4).
  • src/utils/outreach.mjs — send engine (§2).
  • src/commands/outreach.mjssurvey:* + outreach:help:* actions (§3).
  • src/Outreach_Handler.mjs — scheduled batch Lambda (gated) + IaC.
  • src/utils/database/messageTemplates.mjsMESSAGE_TEMPLATES CRUD.
  • src/XBotAdmin_Handler.mjsmessage-templates endpoints + preview/send.
  • admin/src/pages/BotMessagesPage.tsx + admin/src/api/client.ts methods.
  • SEGMENTS field additions: LAST_OUTREACH_AT, LAST_OUTREACH_KEY.
  • SETTINGS.OUTREACH_OPTOUT flag + respect it in sendOutreach.

8. Success metrics

Outreach reply rate, survey completion, and the real prize — LICENSE_CHANGED within 14d of an outreach per segment (measurable thanks to Phase 1 instrumentation). Kill any template whose segment doesn't lift.


9. Rollout sequence

Build gated → staging-group test → enable manual sends to a handful → enable scheduled for filtered_no_report + installed_no_setup → measure reply + conversion → expand per-segment. Disabling OUTREACH_ENABLED stops everything instantly.

X Bot Documentation