Перейти до вмісту

Маркетинговий блюпринт (email + Telegram)

0. П’ять рішень, що перекривають усе інше

Section titled “0. П’ять рішень, що перекривають усе інше”
  1. Telegram стартує першим; email пропускає цикл 2026. Сьогодні 2026-06-06; пік вступної кампанії — кінець липня — серпень (~7 тижнів). Warm-up холодного домену займає 4–6 тижнів, і нічого (домен, ESP-акаунт, DNS, hygiene) не провіжнено. Email фізично не може бути безпечно готовим до revenue в 2026. ~5k Telegram-користувачів вже API-enforced opt-in, проходять broadcast за ~3 хвилини й дають ~3× email-revenue на одного отримувача. Цикл 2026 = Telegram для revenue; email = побудова чистого згоданого списку до 2027. (SRE-критик high; Finance-критик critical.)
  2. Жодного сенду до legal sign-off; прибрати «транзакційний re-permission» bypass. Re-permission-email — це комерційна комунікація. Фікція з lawful basis — найбільша поодинока експозиція програми. (Deliverability/право-критик — центральний дефект.)
  3. Engagement-події fail-closed. Prod-lake-house не існує. Поки його нема, engagement-події дропаються+рахуються, ніколи не пишуться у спільний RDS. (SRE-критик critical.)
  4. Один канонічний consent ledger, keyed by channel_identity_id. Видалити конкуруючі схеми. (Deliverability/право-критик critical; Data-model-критик.)
  5. Витягнути реальний ARPPU + intent-розподіл до того, як писати ще код. Кожна revenue-цифра тримається на недокументованому placeholder. (Finance-критик critical.)

ЧАСТИНА I — ТЕХНІЧНА ІНФРАСТРУКТУРА

Section titled “ЧАСТИНА I — ТЕХНІЧНА ІНФРАСТРУКТУРА”

1. Форма системи та дві storage-площини

Section titled “1. Форма системи та дві storage-площини”

abitly-marketing — це NestJS + BullMQ воркер на ECS Fargate (патерн як у Abitly API). Він володіє маркетинговою оркестрацією та станом; він купує доставку email за swappable-транспортом і переюзає @abitlybot для Telegram. Дві storage-площини, ніколи не змішуються (ADR-0002):

ПлощинаЖиве вТримаєОбсяг
Операційний стансхема marketing на спільному RDS (abitly_prod_db, db.t4g.small), обмежений пул max=5consent ledger, suppression, стан сенду, сегменти, кампанії, атрибуція, fact-packs, статус генераціїнизький
Engagement / телеметріяLake-house (Lambda→Firehose→S3 Iceberg) — fail-closed, поки не задеплоєний у prodкожен sent/delivered/open/click/bounce, AI-логи, відмови валідаторависокий, append-only

1.1 Fail-closed engagement boundary (закриває SRE-critical #1)

Section titled “1.1 Fail-closed engagement boundary (закриває SRE-critical #1)”

Lake-house тільки dev (abitly-analytics: «Прод — ❌ не задеплоєний»). Дизайн не має вдавати, що він існує:

  • Прапор LAKEHOUSE_PROD_READY гейтить engagement-шлях. Поки false, engagement-події дропаються й рахуються (лічильник у Valkey) і ніколи не падають у RDS як fallback.
  • Запуск (Telegram + suppression + consent + one-click unsub) не потребує lake-house. Deliverability/health вимірюється з native-сигналів самого каналу (Telegram 403/blocked rate; для email пізніше — SES SNS complaint/bounce events).
  • Прийнятий trade-off: цикл 2026 стартує без open/click аналітичних дашбордів. Інструментуємо лише тонкі native-сигнали. Повна attribution-площина на lake-house гейтиться на тому, що prod-стек аналітики поїде.

2. Telegram broadcast stack (revenue-канал циклу 2026)

Section titled “2. Telegram broadcast stack (revenue-канал циклу 2026)”

Побудований на наявному @abitlybot (aiogram 3, AWS ECS). Дві мови, два Redis-сховища → оркестратор командує, бот виконує через HTTP control-plane, ніколи спільну чергу (BullMQ — це TS-only Redis-протокол, який Python-бот не може споживати; sidecar-Redis бота навмисно ізольований).

abitly-marketing (NestJS, BullMQ on Valkey)
mkt.campaign.plan → mkt.tg.materialize (grounded+validated) → mkt.tg.dispatch
│ HTTP POST (mTLS/HMAC, internal VPC only)
@abitlybot control-plane (aiohttp :3001)
BroadcastRunner (resumable state in telegram schema) → MessageSender (30/s global, 403-suppress, 429-retry)
│ reciprocal webhooks (progress / consent / suppression)
▼ back to marketing + engagement → lake-house (fail-closed)

Ключові рішення (збережені з компонентного дизайну):

  • Переюз MessageSender (30/s aiolimiter, concurrency 5, 429-retry, 403-drop). Додати вкладений маркетинговий бюджет ~20/s, щоб broadcast ніколи не голодом виморював транзакційні/support-відповіді — Telegram-аналог окремого email-сабдомену. 429 заморожує весь токен, тож retry_after > 120s → abort + alert.
  • Resumable стан broadcast у Postgres-схемі бота telegram (broadcast / broadcast_item), не в ефемерному sidecar-Redis — переживає ECS rolling deploy. At-most-once на контакт через UNIQUE(broadcast_id, chat_id) + pending→sent.
  • Бот — тупий виконавець. Маркетинг pre-фільтрує по consent+suppression і шле готові (chat_id, contact_id, payload). Жодних hot cross-schema marketing↔telegram join (ADR-0002).
  • Deep-link-атрибуція через m_<campaign>_<source>_<contactToken> (≤64 символів; contactToken — підписаний короткий токен, що резолвиться маркетингом, не сирий UUID).
  • Mini App — це checkout-поверхня (кнопка web_app, startapp ≤512 символів) для Premium/Subscription/Tokens через Monobank. Telegram Stars / Paid Broadcasts НЕ використовуються.

Закриття знахідок критиків:

  • Ризик spam-report (Bot ToS 5.2b обмежує весь токен). Хвиля 1 йде лише на найнедавніше активних користувачів бота, веде з реальною цінністю (free-tokens carrot), робить inline-opt-out найпомітнішою дією і моніторить 403/blocked як real-time spam-проксі — сплеск 403 передує report-driven обмеженню → auto-pause. Давно сплячі Start-користувачі НЕ входять у хвилю 1. (Deliverability-критик high.)
  • Ізоляція репутації залежить від runtime-розділення. Маркетинговий Telegram-воркер працює в окремому процесі від @abitlybot; маркетинговий 429 паузить лише маркетинговий бюджет. Верифіковано на деплої, не припускається. (Orchestration-ризик.)
  • Запас одного ECS-таску бота (256/512, desired_count=1). Переглянути розмір таску до того, як phase-2 паралельні broadcast зіткнуться з 07:00 APScheduler cron. (Telegram open item — task-size review гейтиться перед масштабуванням.)
  • Мережевий шлях. :3001 — internal-VPC-only з mTLS чи HMAC shared-secret; ніколи публічний endpoint (поверхня forged-broadcast).

3. Email-доставка та deliverability (будуємо під 2027, провіжнимо зараз)

Section titled “3. Email-доставка та deliverability (будуємо під 2027, провіжнимо зараз)”

Email відкладено для revenue в 2026, але провіжниться під час off-peak, щоб бути прогрітим до весни 2027. Механіка deliverability коректна; виправлення стосуються lawful basis, build-vs-buy і незалежності від lake-house.

3.1 Спершу ВЕРИФІКУВАТИ транзакційний baseline (закриває Deliverability-critical #2)

Section titled “3.1 Спершу ВЕРИФІКУВАТИ транзакційний baseline (закриває Deliverability-critical #2)”

До публікації будь-якого DNS твердження про ізоляцію треба верифікувати, а не припускати. Транзакційний SMTP-провайдер і From-домен недокументовані (email-analytics: provider = TODO). Дія: прочитати /abitly/prod/backend/SMTP_HOST + SMTP_FROM; виконати dig TXT abitly.org, dig TXT _dmarc.abitly.org, перевірити наявні DKIM-селектори; підтвердити, що транзакційний email НЕ шлеться вже як @abitly.org з apex SPF, з яким новий ESP include зіткнеться або переб’є ліміт 10 lookup. Задокументувати реального транзакційного відправника й закрити TODO.

3.2 Домен і DNS (механіка незмінна)

Section titled “3.2 Домен і DNS (механіка незмінна)”
  • Маркетинг шле з нового холодного сабдомену news.abitly.org (From), bounce.news.abitly.org (custom MAIL FROM, SPF -all), link.news.abitly.org (tracking, відкладено — див. нижче). Apex і транзакційна репутація ніколи не зачіпаються.
  • SES Easy DKIM (3 CNAME, d= вирівняно на news.) — але див. рішення по ESP. DMARC на news.abitly.org піднімається p=none → quarantine → reject, гейтиться на обов’язковому RUA-моніторингу (Postmark digest / dmarcian free tier). Усі записи DNS-only (grey cloud) у зоні Cloudflare.

3.3 ESP: Brevo на перший цикл, за swappable-транспортом (ПЕРЕГЛЯНУТО — закриває SRE-medium)

Section titled “3.3 ESP: Brevo на перший цикл, за swappable-транспортом (ПЕРЕГЛЯНУТО — закриває SRE-medium)”

Компонентний дизайн рекомендував SES; SRE-критик коректно перевертає це для циклу 1. Рішення: Brevo (EU-native) для re-permission-запуску. Обґрунтування, переважене на engineer-hours, а не на ~$3/cycle різниці у вартості:

  • Заголовкова перевага SES — native SES→Firehose у lake-house — moot, поки lake-house не в prod.
  • Brevo купує GDPR-преференс-центр, hosted unsubscribe, consent UI і suppression з першого дня — найшвидший безпечний перший сенд для команди з однієї людини.
  • Обидва сидять за тим самим портом EmailTransport. Перенести доставку на SES пізніше, коли prod-lake-house зробить native event-шлях реальним, а обсяг виправдає in-house build.
  • НЕ будуй абстракцію на 3 адаптери зараз. Постав один Brevo-адаптер за інтерфейсом; додай SES при міграції. (Finance-критик high.)
  • Без dedicated IP при ~30k сезонних (далеко нижче порога ~100–200k/міс). Додати seed-list inbox-placement тестування (Gmail/Outlook/Yahoo/Apple) на кожному кроці warm-up як випереджальний індикатор — opens MPP-роздуті, а complaints запізнюються. (Deliverability-критик medium.)

3.4 List hygiene з порогом engagement-recency (закриває Deliverability-medium)

Section titled “3.4 List hygiene з порогом engagement-recency (закриває Deliverability-medium)”
  • ZeroBounce-batch на 30k — але застосувати ту саму EU-residency прискіпливість, що й до ESP (підтвердити DPA + EU-processing ~30k email неповнолітніх, або взяти EU-верифікатор). Це також не launch-critical шлях: hygiene-ити холодний хвіст, а не сенд-1.
  • Жорстке правило mailability для циклу 1: catch-all/unknown потребують позитивного свіжого сигналу активності (недавній логін / Telegram-активність / недавній OfferApplicant) перед будь-яким сендом. Engagement-recency, а не лише статус ZeroBounce, — це поріг.

3.5 Зменшення blast-radius холодного сенду (закриває Deliverability-critical, ризик complaint)

Section titled “3.5 Зменшення blast-radius холодного сенду (закриває Deliverability-critical, ризик complaint)”

Найбільш complaint-схильне повідомлення, яке програма колись пошле, НЕ має бути warm-up-payload на домені з нульовою репутацією:

  1. Запустити re-permission СПЕРШУ на уже згоданих каналах (Telegram Start, in-app/on-login web-банер, преференс-центр), щоб зменшити cold-email залишок.
  2. Cold-email лише на найнедавніше активних впізнавачів у крихітній (~500) засіяній першій когорті.
  3. Виміряти реальний complaint rate через SES/ESP real-time events за 72 год; вимагати <0.1% до розширення. Розглядати когорту 1 як експеримент із репутацією, не як кампанію.
  4. Day-1 circuit-breaker читає ESP-native real-time complaint/bounce events, НЕ Google Postmaster (порожній/із затримкою перший ~тиждень). (Deliverability+SRE: day-1 circuit-breaker читає порожню телеметрію.)

3.6 Загартування one-click unsubscribe (закриває Deliverability-high)

Section titled “3.6 Загартування one-click unsubscribe (закриває Deliverability-high)”
  • RFC 8058 one-click з email #1, миттєво honored, заголовки запечені в транспорт.
  • Виключити POST-endpoint з Cloudflare Bot Fight Mode / WAF (автоматизований POST від Gmail/Yahoo, який отримує challenge = порушення на >2 дні). Протестувати автоматизований POST з реальних seed-акаунтів Gmail/Yahoo/Apple до сенду 1.
  • Лишити stateless HMAC-токен, але додати rate-limiting per token/IP, логувати кожен unsub із IP-hash для детекції зловживань і використати versioned/rotatable secret, щоб витік був revocable без поломки вже надісланих лінків. Fail-open на suppress; масовий forged unsub має бути детектованим і operator-reversible.

3.7 Відкладено для циклу 1 (вирізи скоупу)

Section titled “3.7 Відкладено для циклу 1 (вирізи скоупу)”

Branded click-tracking (link.news. і footgun із CF-проксі, який він несе), BIMI/VMC, in-house MJML CI-пайплайн (натомість Brevo-templating) і 3 Metabase-борди. Open rates все одно MPP-роздуті й де-пріоритизовані.


4. Дані, identity та сегментація

Section titled “4. Дані, identity та сегментація”

4.1 Identity-модель (збережено)

Section titled “4.1 Identity-модель (збережено)”

Контакт = BaseUser (D2). Канали висять на контакті як рядки channel_identity. Telegram-користувачі, які ніколи не злінкували web-акаунт, стають Telegram-only orphan-контактами (base_user_id = NULL). Пізнє лінкування на auth зливає orphan у email-rooted контакт, перенаправляючи рядки, keyed by channel_identity_id. Жодного probabilistic stitching — лінкування детерміноване на auth (GDPR-чисте, операбельне для bus-factor-1). Кількість orphan-vs-linked після першого sync відповідає на відкрите питання «5k TG ↔ email overlap».

Section titled “4.2 Один канонічний consent ledger (закриває Consent-critical неузгодженість)”

По доках існувало три несумісні consent-схеми. Канонічна модель, на яку посилаються ВСІ компоненти:

-- append-only ledger (GDPR Art.7 "demonstrate consent") — the legal record
CREATE TABLE marketing.consent_event (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
channel_identity_id bigint NOT NULL REFERENCES marketing.channel_identity(id), -- KEYED HERE, not contact_id
channel text NOT NULL, -- email | telegram
status text NOT NULL, -- granted | withdrawn | pending
source text NOT NULL, -- re_permission | preference_center | start_deeplink | signup_transactional
scope text[] NOT NULL DEFAULT '{}',
consent_text_version text NOT NULL, -- exact wording shown (incl. lawful-basis blessed by legal)
is_minor boolean, -- NEW: derived from birth year / NMT cohort (minors ADR)
occurred_at timestamptz NOT NULL, -- ISO-8601 UTC
created_at timestamptz NOT NULL DEFAULT now()
);
-- fast projection, what the send pipeline's ConsentGuard reads
CREATE TABLE marketing.consent_current (
channel_identity_id bigint PRIMARY KEY REFERENCES marketing.channel_identity(id),
channel text NOT NULL, status text NOT NULL, scope text[] NOT NULL DEFAULT '{}',
is_minor boolean, granted_at timestamptz, withdrawn_at timestamptz,
updated_at timestamptz NOT NULL DEFAULT now()
);
  • Keyed by channel_identity_id, щоб consent/suppression пережили late-link merge (contact_id-keying псується на merge — Data-model-критик).
  • Telegram-ідентичності засіюються як granted (Start = API-enforced досяжність+згода). Email-ідентичності стартують pending і перемикаються на granted лише через legally-blessed re-permission (НЕ self-declared транзакційний bypass).
  • is_minor — нове (AI-safety-критик critical — див. §5.3 і ADR по неповнолітніх).

4.3 Suppression (збережено, redundant-by-design)

Section titled “4.3 Suppression (збережено, redundant-by-design)”

Постійний, channel-scoped, остання лінія безпеки, що перевіряється останньою в send pipeline. Sinks: email hard-bounce/complaint/unsubscribe; Telegram 403-blocked / /stop; ZeroBounce invalid/spamtrap/abuse pre-seeded. Переживає ESP-swap (тримається окремо від власного списку ESP). Оптимізація hot-path (закриває SRE-high connection budget): per-send suppression-перевірка читає Valkey set/bloom filter (перебудований із durable-таблиці), не RDS round-trip на отримувача.

Матеріалізовані snapshot (segment_membership keyed by computed_run_id), скомпільовані з декларативного rule_jsonb над денормалізованими read-model колонками — ніколи живі abitly join. Suppression + consent завжди ANDяться в скомпільований SQL, тож сегмент структурно не може включити незгоданий чи suppressed контакт. Таксономія = lifecycle stage (primary) × intent cluster × engagement × value (не RFM — single-cycle термінальна аудиторія). Див. матрицю сегмент→offer у Частині II §3.

4.5 Read-model sync (збережено, batch-first)

Section titled “4.5 Read-model sync (збережено, batch-first)”

Watermarked paged SELECT по abitly.updatedAt під обмеженим пулом, поза send hot-path, тільки off-peak під час send-вікна. CDC (RDS logical replication чи API domain events) зарезервовано на коли L3-latency цього вимагатиме. Strapi-факти НЕ синхронізуються — fetch на рендері (ADR-0003). Open: верифікувати, що abitly-таблиці експонують надійний updatedAt; інакше нічний повний diff у тимчасову marketing staging-таблицю.


5. Пайплайн AI-персоналізації

Section titled “5. Пайплайн AI-персоналізації”

Модель пише ТІЛЬКИ переконливу обгортку; УСІ факти — детерміновані merge-поля, валідовані проти структурованого input (ADR-0003). Fact-Pack (Zod-валідований immutable snapshot, окремі L2_SEGMENT / L3_CONTACT форми), placeholder-preferred рендеринг, surface-form валідатор, обмежений regenerate→fallback-to-approved-template цикл, Haiku-L2/Sonnet-L3 Batch-routing (~$30–50/cycle), prompt-caching, eval harness і template-level approval queue — усе збережено. Чотири нові gate закривають AI-safety-критика:

5.1 Advice-correctness gate (закриває AI-safety critical #1)

Section titled “5.1 Advice-correctness gate (закриває AI-safety critical #1)”

Surface-валідатор доводить, що факт існує у pack, ніколи що факти коректно скомбіновані. Найгірший провал EdTech-для-підлітків — добре grounded, але оманлива порада («твій бал 169 вище прохідного 168.5 — ти вступиш»). Додати детермінований gate ВИЩЕ за surface-валідатор:

  1. Slot-provenance binding — число в тілі має трасуватися до конкретного слота, на який посилається його речення, а не просто десь існувати в pack.
  2. Verdict cross-check — якщо текст натякає «вище / вступиш», але fit_vs_passing != 'above' → REJECT.
  3. Жорстка заборона predictive/causal admission-тверджень (вступиш / гарантовано / точно пройдеш / 100%) незалежно від grounding.
  4. Atomic offer triple — залочити (offer, deadline, score), щоб модель не могла причепити неправильний дедлайн до правильного ЗВО.

5.2 Untrusted user-merge значення (закриває AI-safety critical #3)

Section titled “5.2 Untrusted user-merge значення (закриває AI-safety critical #3)”

first_name user-контрольоване → вектор prompt-injection / XSS / abuse, що тривіально проходить валідатор (first_name == contact.first_name). Трактувати всі user-sourced merge-значення як untrusted: HTML/Telegram-HTML escape + charset allowlist (кирилиця/латиниця, дефіс — відкидати фігурні дужки, кутові дужки, URL, control-символи) на ОБОХ етапах — складання prompt і рендер. Обов’язкова sampled human review auto-approved L3-інстансів (зараз жодна людина не читає per-contact L3-текст).

Section titled “5.3 Minors guardrail (закриває AI-safety critical #2 — також legal ADR)”

Аудиторія — 16–18 років; не всі 18. Додати is_minor у Fact-Pack + consent ledger (виведено з року народження / NMT-когорти). Для відомих неповнолітніх: жодного deadline-urgency high-pressure paywall framing, м’якший CTA, parental/refund disclosure у sender-ID блоці. Legal review AI-персоналізованого платного продажу неповнолітнім конкретно. Launch blocker, не TODO.

5.4 Fact-Pack source-data QA (закриває AI-safety high)

Section titled “5.4 Fact-Pack source-data QA (закриває AI-safety high)”

Fact-Pack — це власна ground truth валідатора — ніщо не перевіряє вихідні факти. Додати plausibility-assertion перед генерацією: дедлайн у межах вступного вікна; прохідний бал у діапазоні NMT 100–200, позначений роком («прохідний 2025»); competitive_score перерахований і звірений із предметними балами; offer.university/speciality крос-консистентні. Pre-send freshness-перевірка §5.5 детектує зміну, не хибність — потрібні обидві.

5.5 Збережена safety-механіка

Section titled “5.5 Збережена safety-механіка”
  • Уся арифметика попередньо порахована детерміновано; модель ніколи не рахує/порівнює числа.
  • Eval: використати іншу model family як judge, ніж як генератор (уникнути same-family змови), відкалібрувати пороги ≥4.0 проти реальних UA-teen-оцінених семплів до того, як їм довіряти, додати adversarial-зрізи для класів semantic-correctness + неповнолітні й розставити blind human spot-checks.
  • Model-version pinning + canary-генерація на batch, що стверджує: placeholder-дисципліна не зрегресувала; протестований fallback-модель на retirement у пік сезону.
  • Batch-API + 1-год prompt cache стек; задокументувати синхронний (full-price) fallback-шлях для time-critical L3, коли 24h Batch-latency зіткнеться з 3-денним дедлайном.

6. Engine оркестрації та безпеки

Section titled “6. Engine оркестрації та безпеки”

Ядро виконання: кожне повідомлення кожного каналу проходить ОДИН упорядкований send pipeline. Безпека — це властивість шляху, не caller — властивість, що дозволяє одному інженеру спати.

6.1 Єдиний send pipeline (збережено, з новим rail)

Section titled “6.1 Єдиний send pipeline (збережено, з новим rail)”
0 KillSwitch → 1 Suppression → 2 Consent → 3 FrequencyCap → 4 QuietHours →
5 SendWindow → 6 AdviceCorrectness(L3) → 7 WarmupBudget(LAST)
  • Транспорти package-private для send-воркера — ніщо не може обійти rails.
  • Warmup budget — ОСТАННІЙ, тож DROP/DEFER ранішими rails ніколи не споживає денні warm-up токени → cold-domain ramp рахує лише реальні сенди.
  • AdviceCorrectness (новий rail 6, §5.1) виконується перед тим, як бюджет витрачено на оманливий L3.
  • Fail-closed: якщо suppression/consent нечитабельні (RDS-blip), rails DEFER, ніколи не шлють.

6.2 Два режими сходяться на одному шляху

Section titled “6.2 Два режими сходяться на одному шляху”

Batch Campaigns (D9 v1) заморожують snapshot сегмента (send_batch, idempotent/resumable); event-triggered proto-Journeys (дедлайн T-72h, L3) — це Campaign-of-one, keyed contact × journey × offer × deadline. Deadline-тригер Telegram-first (≈10× email CTR, миттєвий), email — fallback.

6.3 Idempotency, kill-switch, warm-up (збережено)

Section titled “6.3 Idempotency, kill-switch, warm-up (збережено)”
  • jobId = campaign×contact×channel (або journey key) + UNIQUE idem_key на send_log з claim-before-send (INSERT ... ON CONFLICT DO NOTHING) → re-runs/replays/crashes ніколи не дублюють сенд. Reaper звіряє застряглі claimed-рядки.
  • Three-scope kill-switch (global / per-channel / per-campaign) як Valkey-прапори, перевіряються першими; rail 0 DEFERить, тож перемикання назад дренажить заморожену чергу без втрати стану. Auto-tripped complaint circuit-breaker’ами (0.10% warn / 0.30% hard) і retry_after>120s.
  • Warm-up token bucket (per-second rate + per-day cap із таблиці warmup_schedule) робить сам оркестратор ramp-контролером — без ручного pacing.

6.4 Дисципліна connection-budget (закриває SRE-high)

Section titled “6.4 Дисципліна connection-budget (закриває SRE-high)”
  • Маркетинговий пул max=5; sync-воркер + send-воркери ділять його під бюджет.
  • Suppression/consent hot-path reads йдуть у Valkey-кеші, не RDS round-trips.
  • Global free-connection-slot alarm на спільному інстансі (задокументований failure-mode «too many connections»). Розглянути PgBouncer, якщо з’явиться contention.

6.5 Алертинг розв’язаний з broadcaster (закриває SRE-high)

Section titled “6.5 Алертинг розв’язаний з broadcaster (закриває SRE-high)”

Основні deliverability circuit-breaker алерти йдуть через CloudWatch Alarm → SNS → email/SMS, незалежно від @abitlybot (який і broadcaster, І один ECS-таск — алертинг через нього ділить failure-domain із тим, що він моніторить). Telegram-алерти — лише вторинна зручність.


7. Атрибуція та аналітика (радикально обрізано для циклу 1)

Section titled “7. Атрибуція та аналітика (радикально обрізано для циклу 1)”

7.1 Цикл 1 — мінімальне чесне вимірювання

Section titled “7.1 Цикл 1 — мінімальне чесне вимірювання”
  • Stamp на джерелі: send_id — join-ключ; (campaign_id, contact_id, send_id, variant_id, segment_id) їдуть каналом через deep-link (as_sid/as_cid, HMAC-signed) і custom args Brevo.
  • Операційний last-touch (D11) для «кому зарахувати» всередині маркетингованої когорти — збережено, але позначено як over-crediting owned-каналів (абітурієнт вступив би все одно).
  • Telegram-конверсія = click/Mini-App entry, ніколи opens (нема read API; MPP-free).
  • Точність атрибуції гейтиться на cross-repo зміні: abitly-api-v2 /payments/create має протягнути as_sid/as_cid у metadata Monobank-інвойсу → webhook conversion_purchased. Підтвердити ownership/timing як Phase-0 gate; якщо не приземлиться, прийняти fuzzy 7-day time-window last-touch для циклу 1 і не будувати deep-link attribution marts, що від нього залежать.

7.2 ВИРІЗАНО для циклу 1 (закриває Finance-high + AI-safety-medium)

Section titled “7.2 ВИРІЗАНО для циклу 1 (закриває Finance-high + AI-safety-medium)”

Universal 10% holdout + CUPED + dbt program-lift marts. При ~145–245 покупках/cycle 10%-holdout бачить кілька конверсій — lift CI надто широкий, щоб діяти в межах одного термінального циклу, і він жертвує ~$200–400 рідкісного revenue, щоб виміряти revenue, якого ледь є. Фреймити holdout як метод циклу 2+, не вердикт циклу 1. Для near-term сигналу — простий контроль на єдиному найбільшому за обсягом flow (deadline-тригер) і опора на per-channel A/B (TG vs email deadline-тригер).

7.3 A/B (збережено, відкладено до фази 2)

Section titled “7.3 A/B (збережено, відкладено до фази 2)”

Bayesian probability-to-beat (не frequentist NHST) — толерує peeking, операбельне для однієї людини. Порядок тестів: subject → preheader → CTA → send-time → sender. Найважливіший cold-start «тест» — re-permission copy + unsubscribe UX (guardrail 0.10% > open rate).


ЧАСТИНА II — БІЗНЕС / REVENUE-ПЛАН

Section titled “ЧАСТИНА II — БІЗНЕС / REVENUE-ПЛАН”

1. Витягнути несучі цифри ДО того, як ще код

Section titled “1. Витягнути несучі цифри ДО того, як ще код”

Два half-day запити гейтять go/no-go і перезапускають усю модель:

  1. Реальний Monobank ARPPU per SKU + realized free→paid conversion з наявних /payments даних. Кожна revenue-цифра тримається на недокументованому placeholder ARPPU=300 UAH (~$7.50). Якщо реальний ARPPU, скажімо, 150 UAH, base case вдвічі менший, а email build не захищається.
  2. OfferApplicant intent-cluster розподіл по lifecycle stage. Усі розміри сегментів нижче — гіпотези, поки цього нема.

2. Монетизація (переюз Monobank; жодного білінгу не будуємо)

Section titled “2. Монетизація (переюз Monobank; жодного білінгу не будуємо)”
SKUТипПсихологіяСезонний пікНотатка
TokensOne-off consumableLow-friction вхід, сигнал willingness-to-payЧер–Сер«Передні двері» для невизначених
PremiumOne-offІмпульс на дедлайні; «обери прямо зараз»Кін. лип–серДефолтний hero offer; контекстний paywall ≈ 2× conv
SubscriptionRecurringСпокій, вищий LTVВесна→cycleДив. ризик

Packaging ladder: Free → Tokens → Premium (контекстний paywall на дедлайні моніторованого offer) → Subscription. Re-permission carrot = free tokens / Premium-trial (дай цінність за згоду).

3. Матриця сегмент → offer (lifecycle × intent; розміри — гіпотези)

Section titled “3. Матриця сегмент → offer (lifecycle × intent; розміри — гіпотези)”
Сегмент (stage × intent)Hero offerКаналТригер / кутPers.
Active × медицина (висока конкуренція)Premium → SubscriptionTG+email«дедлайн на [offer]; прохідний росте — перевір шанси»L3
Active × IT/інженеріяPremiumTG+email«твій NMT-профіль vs прохідний [offer]»L3
Active × broad/невизначеніTokens → Premiumemail«обери серед offers, які моніториш»L2
Sleeping × any (найбільший)Re-engage → Premiumemail→TG«сезон 2027 стартував; offers, які ти трекав, оновлено»L2
Sleeping × no intentАктивація (трекни 1-й offer)email«почни трекати — перевір шанси безкоштовно»L2
NMT-prep (весна)Tokens / Premium-trialemail+TG«N днів до NMT; перевір готовність»L2
Enrolled (попередній цикл)Studsearchemail+TG«вітаємо зі вступом до [ЗВО] — лиши відгук / дивись insights»L2
No consent (всі, pre-C0)Re-permission carrotспершу згодані канали«налаштуй свої alerts на 2027 + free tokens»L2

4. Портфель кампаній (flows > newsletters), переупорядковано

Section titled “4. Портфель кампаній (flows > newsletters), переупорядковано”
#FlowТригерКаналПріоритетКоли live
C0Re-permission + преференс-центрстарт циклузгодані канали → cold-email хвістP0 (blocker)Після legal sign-off
F1Deadline-тригер (моніторований offer)дедлайн через N днівTG-firstP0 — #1 RPRФаза 1 (Telegram) — до кінця липня
F2Welcome / активаціяconsent / новий StartTG+emailP1Фаза 1
F3Intent-nurturecalendar × intentemailP1Фаза 2
F4Conversion push (пік)кін. лип–серTG+emailP1Фаза 2
F5Win-back / next-season60–120д неактивностіemail+TGP2Фаза 3
F6Studsearch cross-sellвиявлено вступemail+TGP2Phase 0 dependency (див. ризик)
F7Off-season valueмаксимум раз/місяцьemailP3Фаза 4

5. Математика воронки (плануй на КОНСЕРВАТИВНУ колонку для циклу 1)

Section titled “5. Математика воронки (плануй на КОНСЕРВАТИВНУ колонку для циклу 1)”

Email-воронка стекає п’ять «base-case» факторів; cold first-cycle перформанс кластеризується на консервативному кінці, а 28%-of-openers re-permission opt-in на 2-річно-stale списку — щедро (5–15% реалістично), стискаючи згоданий базис із ~1,630 до ~400–800.

Re-permission (C0, разово, консервативно): 30k raw → ~24k valid (hygiene) → ~90% delivered → ~18% open → ~15% opt-in серед openers ≈ ~580 згоданих email. Telegram: ~5k Start-нуто, ~90% reachable, inline grant.

Telegram per-send (base): delivered ~93% × CTR ~10% × conv ~6% = ~0.56% net purchase; RPR @300₴ ≈ 1.67₴ (~3× email).

Cycle-1 attributed revenue (з усіма flows, не лише broadcast):

СценарійUSD / cycle
Консервативний~$0.8k–$1.5k
Base~$1.8k–$4k
Оптимістичний (сильний L3 deadline flow)~$5k–$8k

Telegram несе більшість. Цикл 1 — це інвестиційний цикл (чистий список + прогрітий домен + перші engagement-дані); реальне зростання — цикл 2+. Найбільші абсолютні важелі — ARPPU і розмір згоданого базису, не open rate.

6. Guardrails (circuit-breakers, не плитки дашборду)

Section titled “6. Guardrails (circuit-breakers, не плитки дашборду)”
GuardrailЦільHard limitДія
Complaint rate<0.10%0.30%auto-pause черги + alert
Bounce rate (post-hygiene)<2%hold warm-up advance
Unsubscribe rate<0.5%flag шаблону/сегмента
TG 403/blocked surgereal-time spam-проксі → pause хвилю
TG retry_after>120spause маркетингову чергу + abort

7. Frequency caps — ВСТАНОВИТИ ЗАРАЗ, не TODO (закриває all-critic знахідку)

Section titled “7. Frequency caps — ВСТАНОВИТИ ЗАРАЗ, не TODO (закриває all-critic знахідку)”

Fatigue названо #1 драйвером unsub, проте cap — TODO по чотирьох компонентах, що робить rail no-op. v1-значення, вирішені зараз: email ≤2/тиждень, Telegram ≤3/тиждень, єдиний cross-channel бюджет, keyed на contact = BaseUser, плюс quiet hours 22:00–08:00 Europe/Kyiv. (Per-contact timezone відкладено; прийнятно для майже-100% української аудиторії.)

8. Операційна модель bus-factor-1

Section titled “8. Операційна модель bus-factor-1”

Кілька довговічних evergreen-flows, що переозброюються щоцикл > багато hand-crafted кампаній. Self-serve преференс-центр offload-ить тюнінг частоти. Swappable-ESP абстракція (один адаптер зараз). Одне approval на шаблон (D8); Telegram inline Approve/Reject життєздатний. Сезонний cadence залочений на NMT-цикл із навмисно тихим off-season для захисту прогрітого домену. Додати complaint/DSAR/erasure runbook і blocklist-remediation runbook (Spamhaus/SNDS delisting) до cold-сенду.


9. Поетапна дорожня карта

Section titled “9. Поетапна дорожня карта”
ФазаСкоупГейтиться на
0 — Foundationsсхема marketing (канонічний consent ledger + suppression + send_log), Valkey suppression cache, bot control-plane :3001, fail-closed engagement boundary, frequency caps встановлені, ARPPU+intent витягнуті, transactional-DNS baseline верифікований, рішення по enrollment-сигналу + payments-as_sidШлях legal sign-off визначений
1 — Telegram revenue (2026)TG re-permission (inline grant), F1 deadline-тригер (Telegram), F2 welcome, deep-link атрибуція, real-time 403 моніторингLegal sign-off; F6 enrollment-сигнал
2 — Email list-build (до 2027)Brevo + news. DNS + warm-up (consented-first, крихітна засіяна когорта, <0.1% complaint gate), преференс-центр (Brevo), one-click unsub загартований, L2 segment copy + approval queueDNS baseline; ESP DPA
3 — L3 + вимірюванняEmail deadline-тригер, advice-correctness gate live, per-flow контроль, lake-house prod (якщо поїхав) → engagement+attribution martsLake-house prod deploy
4 — Journeys v2event-triggered сценарії, send-time оптимізація, next-best-offer, universal holdout + lift (cycle 2 spend виправдовує)Зрілі engagement-дані

10. Що лишається vs що вирізано

Section titled “10. Що лишається vs що вирізано”

Збережено (справді несуче для bus-factor-1): ізоляція транзакційної репутації (сабдомен + вкладений Telegram-бюджет); suppression-таблиця, що переживає ESP-swap; grounded-generation валідатор як compliance gate; єдиний упорядкований send-pipeline-as-chokepoint; resumable Telegram broadcast-стан; детермінована resolution identity; AI-вартість rightsized (~$30–50/cycle, похибка округлення).

Вирізано/відкладено для циклу 1: email як revenue-канал 2026; universal holdout + CUPED + lift marts; 3-адаптерна ESP-абстракція; in-house преференс-центр + MJML-пайплайн (купуємо Brevo); BIMI; branded click-tracking; 3 Metabase-борди; F1 із Фази 3 (перенесено на Фазу 1).

  1. Тиждень 1 — Загейтити програму: витягнути реальний Monobank ARPPU per SKU + free→paid conversion і OfferApplicant intent-розподіл; перезапустити модель воронки. Відкрити legal review для re-permission lawful basis + позиції щодо неповнолітніх. Верифікувати транзакційний baseline (dig abitly.org TXT/_dmarc/DKIM; прочитати SSM SMTP_HOST/SMTP_FROM) і закрити email-analytics TODO.
  2. Тиждень 2 — Вирішити несучі залежності: підтвердити джерело сигналу «виявлено вступ» для Studsearch cross-sell (F6) і owner/timeline зміни abitly-api /payments, щоб протягнути as_sid/as_cid. Встановити frequency caps (email≤2/тиждень, TG≤3/тиждень, cross-channel на BaseUser) + quiet hours. Підняти схему marketing із канонічним ledger consent_event + проєкцією consent_current, keyed by channel_identity_id.
  3. Тиждень 3 — Побудувати read-model sync (batch, watermarked, bounded pool, off-peak) і виконати перший sync; відзвітувати orphan-vs-linked Telegram counts (відповідає на 5k↔email overlap). Підняти Valkey suppression/consent кеші й fail-closed engagement boundary (прапор LAKEHOUSE_PROD_READY, drop+count).
  4. Тиждень 4 — Побудувати bot control-plane :3001 (mTLS/HMAC, internal VPC) + reciprocal webhooks; таблиці telegram.broadcast/broadcast_item + resumable runner; вкладений 20/s маркетинговий limiter верифіковано окремо від транзакційного шляху. Зв’язати єдиний send pipeline з усіма rails (вкл. advice-correctness + minors guard).
  5. Тиждень 5 — В очікуванні legal sign-off: побудувати Telegram re-permission flow (inline grant + carrot), deep-link m_ namespace + резолюцію токена, real-time 403/blocked spam-proxy монітор з auto-pause. CloudWatch→SNS алертинг, розв’язаний з ботом.
  6. Тиждень 6 — На legal sign-off: послати Telegram re-permission спершу найнедавніше активним користувачам (НЕ давно сплячим); моніторити сплеск 403 і opt-out; ітерувати copy. Побудувати F2 welcome. Підняти AI L2/L3 пайплайн із grounded-валідатором + advice-correctness gate + escaped user-merge полями.
  7. Тижні 7–8 — Поставити F1 deadline-тригер на Telegram (#1 RPR flow) з pre-approved L3-шаблонами, Fact-Pack source-data QA, Mini App checkout CTA. Pre-approve усі шаблони циклу до липневої хвилі. Налаштувати per-flow контрольне вимірювання + last-touch атрибуцію (якщо зміна payments приземлилась).
  8. Тижні 9–10 — Провіжнити email для 2027: DNS сабдомену news. (DNS-only), Brevo identity + Easy-DKIM-еквівалент, DMARC p=none з обов’язковим RUA-моніторингом, CF Email Routing для postmaster@/abuse@, загартувати one-click unsub POST (CF-challenge exempt, rate-limit, rotatable secret) і протестувати з Gmail/Yahoo/Apple seeds.
  9. Тижні 11–12 — Email list hygiene (ZeroBounce з EU DPA, engagement-recency поріг для catch-all/unknown). Запустити re-permission спершу на згоданих каналах, щоб зменшити cold-залишок; cold-email крихітну ~500 когорту найактивніших впізнавачів, вимагати <0.1% реального ESP complaint за 72 год до будь-якого розширення. Написати complaint/DSAR/erasure + blocklist-remediation runbooks. Притримати решту email warm-up, щоб бути inbox-ready до весни 2027; НЕ ганятися за email-revenue 2026.
  • Legal basis для re-permission-сенду необґрунтований і нерев’юнутий: re-permission-email — це комерційна комунікація, що вимагає ПОПЕРЕДНЬОЇ згоди за УЗ «Про рекламу» ст.14-3 + GDPR/ePrivacy, а аудиторія включає неповнолітніх. Сенд до задокументованого legal-висновку ризикує штрафами (загострено законопроєктом 8153) і назавжди палить cold-домен. Мітигація: ADR-0005 — жодного сенду до письмового sign-off; прибрати транзакційний bypass.
  • Колізія сезонного вікна: revenue циклу 2026 залежить від того, що deadline-trigger flow буде live до кінця липня, але компонентні дизайни гейтили його на Фазу 3 за повним email-стеком, а email warm-up не встигне. Якщо F1 не на Telegram до середини липня, найцінніша когорта втрачена на рік. Мітигація: ADR-0004 + переупорядкування F1 на Фазу 1 на Telegram.
  • Амплифікація shared-RDS SPoF: один необережний bulk-insert engagement-подій у схему marketing (бо prod-lake-house не існує) або вичерпання connection-pool під час сплеску sync+send кладе Abitly API + Strapi + Studsearch разом. Мітигація: fail-closed engagement boundary (drop+count, ніколи RDS), Valkey-кешовані hot-path reads, bounded pool, global free-slot alarm.
  • Revenue побудований на невідомому ARPPU й over-built, mis-sequenced плані: кожна цифра тримається на placeholder ARPPU=300 UAH, а email з’їдає ~80% build за ~15–25% base-case revenue. Якщо реальний ARPPU суттєво нижчий, чотиризначний цикл 1 не виправдовує email build. Мітигація: витягнути реальний ARPPU + intent-розподіл до ще коду; вирізати email/holdout/MJML скоуп для циклу 1.
  • Оманлива-але-grounded AI-порада неповнолітнім: fact-валідатор ловить вигадані факти, але не mis-combined реальні (напр. «твій 169 б’є прохідний 168.5 — ти вступиш»), найгірший EdTech-провал, відправлений unseen через L3 auto-approval. Мітигація: ADR-0009 advice-correctness gate + minors guardrail + sampled human review auto-approved L3 + escaped user-контрольовані merge-поля.
  • Telegram whole-bot spam restriction: unsolicited перший broadcast на ~5k користувачів (деякі давно сплячі) може тригернути report-driven обмеження всього bot-токена за ToS 5.2(b), вбиваючи транзакційні/support і open-day reminders теж — rate-budget ізоляція дає нуль захисту проти цього. Мітигація: value-first хвиля лише на недавно активних, помітний opt-out, 403-surge auto-pause, незалежний kill-switch.

End-to-end send pipeline з safety rails (єдиний chokepoint)

Section titled “End-to-end send pipeline з safety rails (єдиний chokepoint)”
flowchart TD
  Camp[Batch Campaign<br/>frozen snapshot] --> Ren
  Trig[Deadline trigger<br/>T-72h, L3] --> Ren
  Ren[render: grounded merge + AI wrapper<br/>+ ADR-3 validator] --> Q[q.send.email / q.send.telegram]
  Q --> R0{0 KillSwitch}
  R0 -- tripped --> DEF[DEFER: re-queue]
  R0 -- ok --> R1{1 Suppression<br/>Valkey cache}
  R1 -- hit --> DROP[DROP + emit skip event]
  R1 -- ok --> R2{2 Consent<br/>consent_current}
  R2 -- no --> DROP
  R2 -- ok --> R3{3 Frequency cap<br/>email<=2/wk TG<=3/wk}
  R3 -- over --> DROP
  R3 -- ok --> R4{4 Quiet hours<br/>22-08 Kyiv}
  R4 -- inside --> DEF
  R4 -- ok --> R5{5 Send window}
  R5 -- outside --> DEF
  R5 -- ok --> R6{6 Advice correctness L3<br/>slot-provenance + verdict + minors}
  R6 -- fail --> FB[fallback to approved template]
  R6 -- ok --> R7{7 Warmup budget LAST<br/>token bucket}
  R7 -- no token --> DEF
  R7 -- token --> TX[Transport: Brevo / @abitlybot]
  TX --> LH[(engagement event<br/>lake-house FAIL-CLOSED:<br/>drop+count if prod not ready)]
  TX -- 403/hard-bounce --> SUP[write suppression immediately]

Топологія даних та sync (стан → RDS, події fail-closed → lake-house)

Section titled “Топологія даних та sync (стан → RDS, події fail-closed → lake-house)”
flowchart LR
  subgraph RDS[shared RDS db.t4g.small abitly_prod_db SPoF]
    AB[(schema abitly<br/>read-only)]
    PUB[(schema public<br/>Strapi)]
    MK[(schema marketing<br/>bounded pool max=5)]
  end
  Strapi[Strapi facts<br/>HTTP at render] --> Ren2[Context Assembler]
  AB -. watermarked paged SELECT<br/>off-peak, bounded pool .-> Sync[Read-model sync worker]
  Sync --> MK
  Ren2 --> MK
  MK --> Orch[Orchestration BullMQ on Valkey]
  Valkey[(Valkey abitly-prod-cache<br/>BullMQ + suppression/consent cache<br/>+ warmup token bucket)] --- Orch
  Orch --> Brevo[Brevo ESP]
  Orch -->|HTTP control-plane mTLS :3001| Bot[@abitlybot runner<br/>resumable state in telegram schema]
  Brevo -. delivered/open/click .-> ENG{LAKEHOUSE_PROD_READY?}
  Bot -. tg_sent/blocked/click .-> ENG
  ENG -- no --> DROPC[drop + count<br/>NEVER to RDS]
  ENG -- yes --> Fire[Firehose -> S3 Iceberg -> Athena -> dbt -> Metabase]
  Brevo -. hard-bounce/complaint .-> MK
  Bot -. 403/stop .-> MK
  Mono[Monobank webhook<br/>as_sid/as_cid] --> MK