Перейти к основному содержимому

ТЗ: Telegram onboarding-бот Magnet — регистрационная и маркетинговая воронка

Метаданные

ПараметрЗначение
Дата создания2026-06-29
Дата последнего изменения2026-06-29
Статус апрува⏳ На рассмотрении
Дата апрува

Новый сервис apps/onboarding-bot — Telegram-бот, который работает как промежуточная воронка между первым знакомством человека с Magnet и полноценной регистрацией в кабинете через подключение кошелька. Лидер приглашает человека в бота (а не сразу на сложную Web3-регистрацию); бот сохраняет лида, закрепляет за пригласившим лидером, прогревает, выдаёт персональную ссылку на регистрацию, напоминает о следующем шаге и продолжает сопровождение после входа в экосистему.

Решаемые проблемы:

  1. Потеря лидов на входе в Web3. Прямая отправка на подключение кошелька даёт высокий отвал. Бот добавляет прогрев и снижает порог входа.
  2. Нет CRM по пред-регистрационным лидам. Сейчас лид «невидим» до момента регистрации в кабинете. Бот фиксирует контакт и стадию воронки ещё до регистрации.
  3. Лидер не видит, где застрял партнёр. Бот даёт лидеру реф-кабинет со статистикой по этапам и уведомления о действиях партнёров.
  4. Нет персонального канала сопровождения после регистрации. Бот становится каналом срочных уведомлений, дайджестов и маркетинговых рассылок.

Важно (контекст): значительная часть инфраструктуры уже существует в apps/backend и переиспользуется (см. 3.2): привязка users.telegram_id, одноразовые connect-ссылки (generateTelegramConnectWalletLink), BotAuthGuard/BOT_TO_BACKEND_SECRET, реферальная модель (referral_link + users.link + users.parentId), таблица notifications, sessions.timeActive. Новый бот — это отдельный, более крупный funnel-бот, не путать с существующим «TransferBot» в apps/eoa-backend.


2.1. Пользовательские сценарии

Лидер

  • Привязка Telegram к аккаунту Magnet. Доступно, только если у пользователя ещё нет users.telegram_id. Лидер входит в бота → «Подключить Magnet» → бэкенд выдаёт одноразовую ссылку → лидер на сайте подключает кошелёк, авторизуется и подтверждает привязку → в users.telegram_id записывается его Telegram. Если telegram_id уже привязан — бот сразу доступен и связан.
  • Получение персональной ссылки на бота. После привязки лидер в боте видит https://t.me/<OnboardingBot>?start=<users.link>, копирует и рассылает партнёрам (с QR).
  • Реф-кабинет в боте. Лидер видит метрики: переходы в бота, всего лидов, посмотрели презентацию, нажали регистрацию, зарегистрировались; список «застрявших на этапе X».
  • Уведомления лидеру. Новый партнёр в боте; посмотрел презентацию; нажал регистрацию; зарегистрировался; ушёл к другому лидеру; запросил помощь. (Фаза 1 шлёт push только «новый партнёр» / «зарегистрировался» / «ушёл к лидеру»; push про «посмотрел/нажал» — Фаза 2; в Фазе 1 эти сигналы видны через метрики кабинета.)

Лид (приглашённый)

  • Вход по ссылке лидера. Открывает ?start=<leaderLink> → бот определяет лидера, закрепляет лида за ним (first-touch), запускает воронку.
  • Прогрев. Приветствие → «Что такое Magnet» → выбор интереса → ключевые продукты (контент — моки, настраивается позже).
  • Регистрация. Получает кнопку «Зарегистрироваться в Magnet» с персональной ссылкой, содержащей реф-код того же лидера (формат сайта ?link=) и одноразовый ?telegramConnect= для связки Telegram.
  • Подтверждение кошелька. После регистрации бот в личке спрашивает «это ваш кошелёк 0x…?»; при подтверждении Telegram привязывается к аккаунту.

Кейс смены рефки на фронте

Лид вошёл по ссылке лидера A, но на форме регистрации поставил рефку лидера B. Тогда партнёр в Magnet уходит под B; B получает «новый партнёр», A получает «ваш партнёр ушёл к лидеру B». Дальнейшие пострег-уведомления по этому партнёру идут лидеру B (Magnet-реферер).

Администратор (Фаза 2)

  • Сегментирует лидов по этапу / неактивности / уровню NFT.
  • Запускает рассылки по сегментам, ручные напоминания.
  • Смотрит воронку конверсии по этапам.

2.2. Бизнес-логика

Реферальная цепочка Telegram → Magnet

  1. Бот-ссылка лидера: https://t.me/<bot>?start=<leader.link>, где leader.link — значение users.link (8 hex-символов, валидно для Telegram start payload).
  2. Лид открывает ссылку → бот резолвит лидера через GET /bot/leader/resolve?code= → создаёт запись bot_users, закрепляя leader_link/leader_uid (first-touch lock: повторные /start с другим кодом игнорируются — анти-увод).
  3. На этапе регистрации бот формирует URL: ${FRONT}/?link=<leader.link>&telegramConnect=<uuid> — реф того же лидера в формате сайта + одноразовый connect-токен лида.

Единый идентификатор рефки = users.link (не refUid/hashedUid). Резолв лидера идёт через таблицу referral_link (там link UNIQUE) чистым lookup без сайд-эффектов.

Привязка telegram_id (модель безопасности)

Связка Telegram лида с новым Magnet-аккаунтом строится на существующем одноразовом механизме (а не на самоподписанном токене):

  1. Бот при выдаче кнопки регистрации вызывает generateTelegramConnectWalletLink( inviteeTelegramId) → одноразовый uuid + Redis 10 мин TTL + обратная карта telegram-connect-<uuid> → telegramId + пред-проверка уникальности telegram_id. URL содержит ?telegramConnect=<uuid>.
  2. Мост login → событие (durable). На регистрации login читает uuid из loginDto.telegramLink (соответствие: URL-параметр ?telegramConnect= маппится в DTO-поле telegramLink), резолвит обратную карту telegram-connect-<uuid> → telegramId и персистит pending-привязку в users.previewTelegramId = telegramId (существующая колонка, «непод­тверждённый» Telegram). Это новый код — текущий закомментированный блок лишь удалял кэш и telegram_id не присваивал.
  3. Корреляция событий. wallet_connected (из findOrCreate, синхронно в login) несёт telegram_id (= previewTelegramId) + magnet_uid + addr; registered (из sync-хендлера) читает users.previewTelegramId и несёт тот же telegram_id + magnet_uid. Оба события несут консистентный ключ telegram_id — бот коррелирует с bot_users по нему (бот уже знает telegram_id лида с момента /start).
  4. Подтверждение и привязка. Получив registered, бот в личке показывает «это ваш кошелёк 0x…?» (адрес у бота уже есть из wallet_connected). При согласии бот зовёт POST /bot/telegram/bind { magnet_uid, telegramId } под BotAuthGuard → backend промоутит previewTelegramId → telegram_id через updateUserTelegramId(user.id, telegramId) напрямую + пред-проверка уникальности. При отказе аккаунт остаётся непривязанным.

Почему так: публичный bearer-токен telegram_id в URL небезопасен (утечка ссылки → привязка чужого Telegram к своему кошельку; telegram_id UNIQUE → блокировка жертвы). In-bot подтверждение кошелька закрывает даже остаточный риск утечки connect-ссылки в окне TTL — настоящий владелец Telegram получает DM и может отклонить чужой кошелёк.

Этапы воронки (bot_users.funnel_stage, TINYINT UNSIGNED, монотонно)

ЭтапКодИсточник
1started_botбот: /start
2started_introбот: «Начать знакомство»
3viewed_presentationбот: просмотр презентации
4chose_interestбот: выбор интереса (→ bot_users.interest)
5offered_registrationбот: показана кнопка регистрации
6clicked_linkбот: клик по кнопке (register_clicks++)
7wallet_connectedoutbox wallet_connected (из findOrCreate)
8registeredoutbox registered (из sync-хендлера on-chain реги)
9nft_activatedoutbox status_nft_activatedполная активация

Решение продукта: покупка статусного NFT = активация Apex и Sandbox одновременно. Поэтому исходные этапы ТЗ 9/10/11/12 (NFT/Apex/Sandbox/active) схлопнуты в один этап 9 nft_activated = «полноценный активный участник». Раздельные lp3/apex/ sandbox для воронки не нужны. Уровень NFT для админ-сегмента хранится отдельно в bot_users.nft_level.

lp3 ≠ этап 9. lp3 безусловно ставится в 1 при on-chain регистрации (sync.service.processRegistrationInProgramsEvent), т.е. lp3 >= 1 соответствует этапу 8 (registered), а не этапу 9. Поэтому источник этапа 9 — событие status_nft_activated из handleProductBought (покупка статусного NFT), а не уровни программ.

Продвижение этапа — строго монотонное: UPDATE … SET funnel_stage = GREATEST(funnel_stage, :new) (два писателя: бот для 1–6, outbox-консьюмер для 7–9).

Точки эмиссии событий (backend → outbox)

СобытиеГде эмитится
wallet_connectedfindOrCreate create-ветка (покрывает все пути коннекта кошелька)
registeredsync.service.processRegistrationInProgramsEvent (есть uid, directReferrerId, isRegistrationConfirmed)
new_partnerвместе с registered (для лидера-реферера)
status_nft_activatedNFT-хендлер покупки статусного NFT (handleProductBought)
reactivationхук покупки/апгрейда после неактивности (Фаза 2, см. техдолг)
struct_overtakeисточник StructPartnerUpReward (Фаза 2, сейчас закомментирован)

Payload событий: wallet_connected{ telegram_id, magnet_uid, addr }; registered{ telegram_id, magnet_uid, submitted_link, actual_parent_uid, actual_parent_name }; status_nft_activated{ magnet_uid, nft_level }. new_partnerединственный источник уведомления «новый партнёр» лидеру-рефереру; блок детекта ухода шлёт только «партнёр ушёл к B» (без дублирования «нового партнёра»).

registered обязан примениться раньше status_nft_activated — отсюда требование к порядку обработки per-user (см. ниже). Эмитить registered из findOrCreate нельзя: create-ветка срабатывает в момент коннекта кошелька (этап 7), а uid/реальный реферер появляются позже в on-chain хендлере.

Надёжность доставки (outbox + поллинг)

  • Бэкенд владеет bot_event_outbox; бот читает через GET /bot/events/pending + POST /bot/events/ack по cron (long-polling, 1 реплика).
  • At-least-once без дублей: дедуп-леджер bot_processed_events(outbox_event_id UNIQUE)INSERT IGNORE по event_id до side-effect; запись доставки до ack; ack идемпотентный; при dedup-hit (0 строк) всё равно делаем ack, чтобы голова очереди двигалась.
  • Порядок per-user: выдача pending с ORDER BY (magnet identity, id), обработка строго последовательно по юзеру, стоп на первой ошибке внутри юзера, ack только непрерывного обработанного префикса.
  • Single-flight: RedlockService.acquire из @magnetmlm/common-backend.
  • Dead-letter: при max_attempts для обычного события ставится processed_at (снимает head-of-line блокировку per-user, очередь юзера двигается дальше). Исключение: если терминальным становится сам registeredprocessed_at НЕ ставится: очередь этого юзера намеренно остаётся заблокированной (иначе status_nft_activated уйдёт не тому лидеру) + алерт оператору на ручное разрешение. Head-of-line блокировка per-user в этом случае — ожидаемое поведение.

Детект ухода к другому лидеру

Сравниваем bot_users.leader_link с сырым submitted_link из формы (loginDto.link, в payload события registered), не с разрезолвленным tree-родителем (ротация смартлинка легитимно меняет родителя и давала бы ложные срабатывания). Если submitted_link != leader_link: notify B «новый партнёр» (DM или пассивно), notify A «партнёр ушёл к B» (DM), magnet_parent_uid = B, reassigned = true. В Фазе 1 детект ограничен wallet-флоу (?link=); email/social-флоу (?ref=registerSocialWallet) — после унификации параметра.

Таргетинг уведомлений

Цель резолвится в bot_users с подтверждённой бот-сессией (по magnet_uid, иначе telegram_id). Если адресат недостижим (лидер B без бот-сессии — бот не может писать тому, кто не нажал Start, 403) → фоллбэк на пассивную таблицу notifications (веб-кабинет) через новый BotAuthGuard-эндпоинт записи; событие ack как «доставлено фоллбэком», без вечных ретраев. Лидер A (давал ссылку) всегда в боте → DM доходит.

Сегменты (админ, Фаза 2)

  • По этапу: funnel_stage (range-фильтры).
  • По неактивности: пред-рег лиды — bot_users.last_activity_at; пост-регsessions.timeActive (последний веб-логин) через /bot/user/state. Это разные определения, не смешивать.
  • По уровню NFT: bot_users.nft_level (обновляется из status_nft_activated/апгрейдов).

2.3. UI/UX требования

Бот (контент — моки в bot_content_blocks, настраивается позже)

  • Роутинг /start: check-account-by-telegram → лидер (кабинет) | лид. Для лида: payload=<link> → resolve + first-touch → воронка; без payload (organic) → лид без лидера (root), URL регистрации без link.
  • Воронка 1–6: приветствие (опц. интро-видео) → «Что такое Magnet» (текст+видео) → презентация → выбор интереса (5 кнопок) → ключевые продукты → кнопка «Зарегистрироваться в Magnet». Рендер блока: text + media_url + buttons json (callback_data → target_stage/action); маппинг interest → product_*; fallback языка ru.
  • Лидерский кабинет (Фаза 1): «Моя ссылка» + QR; метрики этапов 1–8 (счётчики «посмотрели/нажали регистрацию» считаются по дискретным фактам bot_funnel_events, а не по funnel_stage — монотонный funnel_stage допускает скачки и завысил бы промежуточные этапы; funnel_stage — только текущее положение / «застрял на X»); список застрявших. Метрика «активировал NFT» и «нужна помощь» — Фаза 2.
  • Админ (Фаза 2): сегменты, рассылки, воронка конверсии, ручной запуск напоминаний.

Frontend (apps/frontend)

  • Точка входа «Получить ссылку на бота / Подключить Telegram» в кабинете лидера (поверх существующего generateTelegramConnectWalletLink / ?telegramConnect=).
  • Wallet-флоу регистрации уже читает ?link= и ?telegramConnect= — доработка для этого пути не требуется.
  • Рендер новых типов InfoEvent (PartnerLeftToLeader, NewPartnerViaBot) в списке уведомлений.

3.1. Архитектура

┌────────────┐  start=<leader.link>   ┌─────────────────────────────┐
│ Telegram │ ─────────────────────▶ │ apps/onboarding-bot (NEW) │
│ лидеры/лиды │ ◀───────────────────── │ nestjs-telegraf (NEW dep) │
└────────────┘ воронка/кабинет/админ │ long-polling, 1 реплика │
│ свои funnel-таблицы (MySQL) │
│ cron: напоминания + поллинг │
└──────────┬──────────────────┘
│ HTTP (BotAuthGuard,
│ BOT_TO_BACKEND_SECRET,
│ timingSafeEqual)
┌──────────────┴───────────────┐
│ /bot/* сервис-эндпоинты │
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ apps/backend (main)│ │ apps/frontend │
│ users, referral, │ │ кнопка бот-ссылки │
│ sync, nft, │ │ + рендер InfoEvent │
│ bot_event_outbox │ └────────────────────┘
└────────────────────┘

Принципы:

  • Изоляция доступа к данным: бот не читает чужие таблицы напрямую; users/рефералку/ уровни/сессии получает только через сервис-HTTP /bot/* под BotAuthGuard. Свои funnel-таблицы — в той же MySQL, но через отдельный TypeORM-коннекшен.
  • Транзакционный outbox для доменных событий, рождающихся в БД backend (registered и т.д.), которых нет в on-chain EventsService.
  • Long-polling, строго 1 реплика (getUpdates один на токен; outbox-поллинг single-flight через redlock). Миграция на webhook — опционально, позже, конфигом nestjs-telegraf.

Стек нового сервиса: NestJS 9, новые зависимости nestjs-telegraf + @telegraf/session + ioredis-session-store (отдельный Redis-namespace от cache-manager backend); TypeORM (свои entity, synchronize=false); @nestjs/schedule; точечно из @magnetmlm/common-backend (AllExceptionsFilter, parseAllowedOrigins, RedlockService) — без AuthModule. Порт 3200 (свободен). Шаблон — apps/vip-backend / apps/eoa-backend (main.ts + Joi + Sentry).


3.2. Описание технической реализации

A. Новый сервис apps/onboarding-bot

Структура по конвенциям репо (kebab-case файлы, camelCase код, JSDoc на сервисах):

src/
main.ts # bootstrap, Joi, Sentry, AllExceptionsFilter
app.module.ts # TypeORM (свой коннекшен), TelegrafModule, Schedule
config/ # env + Joi schema
bot/ # telegraf: scenes/wizard воронки, /start роутинг
funnel/ # сервис этапов, content-rendering
leader-cabinet/ # метрики/ссылка/QR лидера
admin/ # сегменты/рассылки/конверсия (Фаза 2)
outbox-consumer/ # cron поллинг /bot/events + обработка
magnet-api/ # HTTP-клиент к /bot/* (BotAuthGuard token)
models/ # entity (явный массив, без autoLoadEntities)

Конфиг TypeORM-коннекшена бота: name: 'onboarding', synchronize: false, entities: [BotUser, BotFunnelEvent, BotContentBlock, BotBroadcast, BotReminderSent, BotProcessedEvent, BotBonusGrant], extra: { supportBigNumbers: true, bigNumberStrings: false, timezone: 'Z' }, charset: utf8mb4, migrations, migrationsTableName. DDL — идемпотентные миграции (CREATE TABLE IF NOT EXISTS, только новые таблицы; backend-таблиц не касаться). В репо инфраструктуры миграций нет — заводим явный DataSource; решение по migrationsRun в прод vs CLI зафиксировать (см. вопросы).

B. Таблицы бота (отдельный коннекшен, общая MySQL)

ТаблицаКлючевые поля / индексы
bot_userstelegram_id (BIGINT UNIQUE, сериализуется как строка на всех /bot/*), username, first_name, lang, leader_link, leader_uid, magnet_uid (nullable), magnet_parent_uid (nullable), reassigned, addr (nullable, из wallet_connected — для confirm-промпта), funnel_stage (TINYINT UNSIGNED), interest (nullable), register_clicks, help_requested, nft_level (nullable), last_activity_at, opt-in колонки 4 категорий, created_at. Индексы: (leader_uid), (magnet_uid), (magnet_parent_uid), (funnel_stage, last_activity_at), (nft_level). First-touch lock — INSERT IGNORE по UNIQUE telegram_id (MySQL).
bot_funnel_eventsbot_user_id, type, payload json, created_at. Индекс (bot_user_id, created_at).
bot_content_blockskey, lang, text, media_url, buttons json. (моки)
bot_broadcastssegment json, text, media, status, scheduled_at, stats.
bot_reminders_sentbot_user_id, reminder_key, sent_at. UNIQUE(bot_user_id, reminder_key).
bot_processed_eventsoutbox_event_id UNIQUE, processed_at (+ prune-политика).
bot_bonus_grants (Фаза 2)bot_user_id, bonus_type, status, granted_at, idempotency_key.

C. Изменения в apps/backend

Новая таблица (владелец — backend):

bot_event_outbox: id, event_type (registered/wallet_connected/new_partner/ status_nft_activated/reactivation/struct_overtake), telegram_id (nullable, строкой на выходе — см. п.5), magnet_uid, payload json (состав по типам — см. таблицу payload в §2.2 «Точки эмиссии»), created_at, processed_at, attempts, claimed_at/locked_until (lease), last_error, last_attempt_at, next_retry_at. Индекс (processed_at, id) + покрывающий для предиката claim.

Новые сервис-эндпоинты (BotAuthGuard, ужесточить crypto.timingSafeEqual):

  • GET /bot/leader/resolve?code= — чистый lookup referralLink.findOne({where:{link}, relations:{user}}){uid, link, name} (без сайд-эффектов; не getParentByLink).
  • GET /bot/events/pending?limit= / POST /bot/events/ack — outbox.
  • GET /bot/user/state?telegramId=isRegistrationConfirmed, lp1/lp2, addr, sessions.timeActive, ownLink (своя реф-ссылка лидера для кабинета).
  • POST /bot/telegram/bind — тело { magnetUid | address, telegramId }; промоутит users.previewTelegramId → telegram_id через updateUserTelegramId(user.id, telegramId) напрямую + пред-проверка уникальности.
  • POST /bot/notification — запись пассивного уведомления (addNotification) для фоллбэка недостижимого адресата.

Правки существующего кода (scope подтверждён — включаем всё):

  1. referral_link во всех путях создания. Сейчас строку referral_link пишет только findOrCreate create-ветка. registerSocialWallet кладёт ссылку реферера в users.link, а updateSocialWallet генерирует свежий keccak-хеш в users.link — но ни один из них не пишет строку в таблицу referral_link (дефект именно в этом, а не в «копировании»; не нужно «чинить» несуществующее копирование в updateSocialWallet). Исправить: писать referral_link во всех путях создания/обновления + одноразовая backfill-миграция для существующих social/email-юзеров. После этого чистый резолв по referral_link корректен (users.link не UNIQUE, поэтому резолв именно по referral_link).
  2. Реализовать потребление telegramConnect в auth.service.login (не просто раскомментировать блок ~359–376): читать обратную карту telegram-connect-<uuid> → telegramId по loginDto.telegramLink (= URL ?telegramConnect=) и писать users.previewTelegramId. Без этого wallet_connected/registered не узнают telegram_id лида.
  3. Эмиссия событий: wallet_connected из findOrCreate create-ветки; registered + new_partner из sync.service.processRegistrationInProgramsEvent; status_nft_activated из NFT-хендлера. Общий хелпер записи в outbox.
  4. Безопасность bearer-эндпоинтов: POST /auth/service/telegramId — закрыть под UserRole.Service (сейчас IDOR: magnetId из body под обычным JWT). POST /auth/telegramId/:id уже использует req.user.id (своё id) — отдельной ownership-правки не требует. updateUserTelegramId — добавить app-level пред-проверку уникальности.
  5. telegram_id как строка на границе /bot/*: глобальный bigNumberStrings:false не трогаем (влияет на финансовые bigint) — применяем string-transformer на bot_users.telegram_id и сериализуем telegram_id строкой во всех /bot/* DTO; LinkTelegramAccountDto.telegramId → string.

D. Outbox-консьюмер (бот)

Cron (long-polling, single-flight redlock): GET /bot/events/pending → для каждого события INSERT IGNORE bot_processed_events → обработка (продвижение funnel_stage монотонно, DM/фоллбэк, детект ухода) → POST /bot/events/ack непрерывного префикса. Этапы 7–9 проставляются из событий; уровни/состояние сверяются снапшотом /bot/user/state (идемпотентно).

E. Frontend

Точка входа бот-ссылки в кабинете лидера; рендер новых InfoEvent. Wallet-флоу уже прокидывает ?link=/?telegramConnect=.

F. Машина состояний воронки (Фаза 1, логика — не контент)

Форма bot_content_blocks.buttons (json): массив кнопок { text, action, target_stage?, interest?, url_template? }, где action:

actionПоведение
advanceпродвинуть funnel_stage до target_stage (монотонно) и показать блок этапа
set_interestзаписать bot_users.interest = interest, продвинуть до chose_interest (4)
open_registerсформировать URL url_template (${FRONT}/?link=<leader.link>&telegramConnect=<uuid>), register_clicks++, продвинуть до clicked_link (6)

Пример блока what_is_magnet: { "buttons": [{ "text": "Дальше", "action": "advance", "target_stage": 4 }] }.

Закрытый список ключей bot_content_blocks для этапов 1–6 (сидятся моками): welcome (1), what_is_magnet (2), presentation (3), interest_prompt (4, кнопки set_interest), product_overview (5), register_cta (6, кнопка open_register).

Enum интереса (5 значений) и слот контента (цель маппинга — мок, но enum и слоты фиксированы): team_buildingproduct_team; incomeproduct_income; defiproduct_defi; web3_introproduct_web3; passiveproduct_passive. После set_interest бот показывает соответствующий product_* блок, затем register_cta.

G. Контракты DTO /bot/* (для Swagger §8)

telegram_id/telegramId — везде строка (см. п.C.5).

  • GET /bot/leader/resolve?code={ uid, link, name }.
  • GET /bot/events/pending?limit=[{ id, event_type, telegram_id, magnet_uid, payload, attempts }] (упорядочено по (identity, id)).
  • POST /bot/events/ack → тело { ids: number[] } (бот шлёт id обработанного непрерывного префикса per-user; ack идемпотентный).
  • POST /bot/telegram/bind → тело { magnetUid?: number, address?: string, telegramId }204.
  • POST /bot/notification → тело { recipientMagnetUid, type, payload }204.
  • GET /bot/user/state?telegramId={ isRegistrationConfirmed, lp1, lp2, addr, timeActive, ownLink }.

4. Проблемы и компромиссы

4.1. Известные ограничения

  1. Long-polling = строго 1 реплика.
    • Несколько инстансов на один токен → 409 Conflict; outbox-поллинг должен быть single-flight.
    • Влияние: горизонтальное масштабирование — только после миграции на webhook.
  2. Бот не может писать без бот-сессии (Telegram 403).
    • Недостижимые адресаты (лидер B без сессии) обслуживаются только пассивным фоллбэком в веб-кабинет.
  3. sessions.timeActive ≠ вовлечённость пред-рег лида.
    • Это последний веб-логин зарегистрированного юзера; для пред-рег лидов используем bot_users.last_activity_at.
  4. Контент воронки — моки.
    • Реальные тексты/видео и маппинг interest → product задаются позже через bot_content_blocks.

4.2. Технический долг

  • Реализовать потребление telegramConnect в auth.service.login (читать обратную карту telegram-connect-<uuid> → telegramId по loginDto.telegramLink, писать users.previewTelegramId) — не просто раскомментировать блок; без этого wallet_connected/registered не узнают telegram_id лида.
  • Backfill referral_link для исторических social/email-юзеров + прекратить копирование ссылки родителя.
  • Завести инфраструктуру миграций (в репо её нет) — идемпотентный initial migration.
  • struct_overtake: включить закомментированный источник StructPartnerUpReward (distributionsStats.job.ts) — Фаза 2.
  • reactivation: определить именованный хук эмиссии + baseline неактивности — Фаза 2.
  • Бонусы: определить механизм выдачи (on-chain / backend-cron / ручной админ) — сейчас только леджер bot_bonus_grants.

4.3. Риски

  • Утечка одноразовой telegramConnect-ссылки в окне TTL. Митигация: single-use + 10 мин TTL + in-bot подтверждение кошелька; не логировать полный URL; разрешить ре-минт после отклонения.
  • Рассинхрон BIGINT telegram_id (number vs string) между сервисами. Митигация: строковая сериализация на всех /bot/*, одинаковые extra коннекшена.
  • Потеря порядка registered < status_nft_activated. Митигация: per-user последовательная обработка + ack префикса + dead-letter с алертом на registered.
  • Авто-альтер общей схемы вторым сервисом. Митигация: synchronize=false + явные entity + только идемпотентные миграции новых таблиц.

5. Вопросы на дополнительное обсуждение

  • Прод-применение миграций: migrationsRun=true на старте сервиса или ручной CLI-прогон в CI?
    • Кому адресовано: DevOps / тимлид backend.
  • Механизм выдачи стартовых бонусов (UNIT/лотерея/SecretBox/бейдж) — on-chain, backend-cron или ручной админ?
    • Кому адресовано: Продукт.
  • Доступ админа в боте: список telegram_id из конфига и/или Magnet-роль через role-access?
    • Кому адресовано: Продукт / backend.
  • Реальные тексты/видео воронки и маппинг interest → product — когда и кем наполняются.
    • Кому адресовано: Маркетинг.

6. План реализации

6.1. Этапы разработки

Фаза 1 (MVP):

  • Этап 1: Scaffold apps/onboarding-bot (telegraf long-polling, ioredis-session, synchronize=false + миграции, порт 3200, Joi/Sentry).
  • Этап 2: Таблицы бота + bot_event_outbox (+ миграции, backfill referral_link).
  • Этап 3: Backend /bot/* эндпоинты + харден BotAuthGuard + пред-проверка уникальности + включение telegramConnect + эмиссия wallet_connected/registered/ new_partner/status_nft_activated + закрытие IDOR.
  • Этап 4: Бот — /start роутинг, реф-цепочка + first-touch, воронка 1–6 (моки), URL регистрации, in-bot подтверждение → привязка.
  • Этап 5: Outbox-консьюмер (redlock, порядок per-user, дедуп, dead-letter), этапы 7–9, детект ухода (wallet-флоу), фоллбэк уведомлений.
  • Этап 6: Базовый лидерский кабинет (метрики этапов 1–8, своя ссылка/QR).
  • Этап 7: Frontend — точка входа бот-ссылки, рендер новых InfoEvent.
  • Этап 8: Тестирование (юнит + e2e воронки + сценарий ухода) и деплой 1 реплики.

Фаза 2:

  • Напоминания (cron + bot_reminders_sent) + бонусы-леджер.
  • Уведомления участникам (4 категории, дайджест/отчёт + GET /bot/leader/digest + cron, opt-in).
  • Горячие лиды + флоу «нужна помощь» + метрика активации NFT в кабинете.
  • Админка (сегменты по этапу/неактивности/уровню NFT, рассылки, воронка конверсии).
  • struct_overtake + reactivation события.
  • Опционально: миграция на webhook.

6.2. Критические зависимости

  • Доступ к общей MySQL и Redis для нового сервиса (отдельный коннекшен/namespace).
  • Отдельный ONBOARDING_BOT_TOKEN (не TELEGRAM_BOT_TOKEN/TRANSFER_BOT_TOKEN).
  • Согласование правок в apps/backend (auth/sync/user/nft) с командой backend.
  • Настройка деплоя 1 реплики в GitLab CI / docker.

6.3. Критерии приёмки Фазы 1 (definition of done)

  • First-touch lock: повторный /start с другим кодом НЕ перезаписывает leader_link/leader_uid существующего bot_users.
  • URL регистрации: кнопка формирует ровно ${FRONT}/?link=<leader.link>&telegramConnect=<uuid>; register_clicks инкрементится.
  • Монотонность воронки: funnel_stage продвигается до >= 8 при потреблении wallet_connectedregistered и НЕ регрессирует при повторной/непорядковой доставке.
  • Привязка: confirm → telegram_id привязан (промоут previewTelegramId); отказ → аккаунт остаётся непривязанным; повтор привязки занятого telegram_id отклоняется.
  • Идемпотентность: повторная доставка того же outbox_event_id не шлёт второй DM (дедуп bot_processed_events).
  • Уход к B: submitted_link != leader_link → A получает «ушёл к B», B — «новый партнёр», reassigned = true, magnet_parent_uid = B.
  • Кабинет: отдаёт счётчики по этапам 1–8 (из bot_funnel_events) и свою ссылку/QR (ownLink).

8. Документация

  • API документация (Swagger) для /bot/* эндпоинтов.
  • Engineering docs: apps/docs/docs/engineering/Сервисы/onboarding-bot/.
  • Описание outbox-контракта (типы событий, payload, семантика at-least-once).
  • Обновление .env.example (новый сервис + новые переменные backend).
  • Инструкция лидеру (как привязать Magnet и получить ссылку на бота).

9. Ссылки

  • Исходное ТЗ (бизнес-описание воронки) — внутренний документ.
  • apps/backend/src/user/user.service.tsfindOrCreate, getUserByLink, generateTelegramConnectWalletLink, updateUserTelegramId.
  • apps/backend/src/shared/guards/bot.guard.tsBotAuthGuard (ужесточить crypto.timingSafeEqual).
  • apps/backend/src/auth/auth.controller.tsattach-wallet-to-telegram, check-account-by-telegram; auth.service.ts — потребление telegramConnect.
  • apps/backend/src/sync/processRegistrationInProgramsEvent (эмиссия registered).
  • apps/backend/src/telegram-soft-connector/ — паттерн сервис-эндпоинтов для бота.
  • apps/eoa-backend/src/telegram/ — пример telegraf (TransferBot) и шаблон сервиса.
  • apps/docs/docs/engineering/Стандарты/Code-style.md — конвенции именования.