Legan Studio
Все статьи
~ 13 мин чтения

Анти-фрод и подозрительная активность в боте MAX

Как защитить бота в MAX от мошенников, мульти-аккаунтов, накруток рефералов, бонус-абьюза и автоматических скриптов: фингерпринт, скоринг, правила и эскалация.

  • MAX
  • безопасность
  • анти-фрод
  • разработка

Любой бот в MAX, который раздаёт бонусы, выплачивает за рефералов, проводит платежи, выдаёт промокоды или просто принимает заявки, рано или поздно становится мишенью для мошенников. От накрутки рефералов школьниками с десятком левых аккаунтов до автоматизированных скриптов, которые выкачивают промокоды и перепродают их в обменниках. В этой статье — как выстроить анти-фрод-слой в боте на MAX от VK Tech: какие сигналы собирать, как считать скоринг, как блокировать без паранойи и не терять реальных пользователей.

Кто и зачем атакует ботов

Перед выбором техник полезно понять, против кого вы защищаетесь:

  1. Бонус-хантеры — заводят 5–50 аккаунтов, чтобы получить welcome-бонус, скидку или подарок несколько раз.
  2. Реферал-фермы — школьники и арбитражники, которые регистрируют десятки фейковых пользователей за свою же реферальную ссылку.
  3. Промокод-сборщики — автоматизированные скрипты, парсящие выдачу промокодов через бота.
  4. Кардеры и тестеры карт — пробуют украденные карты на боте, где есть платёжная интеграция, чтобы понять, рабочая ли карта.
  5. Спамеры и флудеры — забивают ваш чат поддержки, рассылают рекламу через инвайты, накручивают активность.
  6. Конкурентная разведка — выкачивают каталог, тарифы, базу знаний, базу FAQ.
  7. DoS-любители — пытаются положить бота шквалом запросов.

Под каждую группу — свой набор сигналов и контрмер. Универсального «фильтра мошенников» нет.

Какие сигналы собирать

Чем больше независимых сигналов вы фиксируете на каждое действие — тем точнее скоринг. Минимум:

СигналИсточникЧто показывает
user_id MAXupdateУникальность аккаунта
Возраст аккаунтаgetMe / профильАккаунт «вчерашний» — подозрительно
Имя/usernameпрофильСлучайные «aabbcc123», копии других
АватарпрофильСтандартная заглушка vs живое фото
Привязка телефонаесли доступноЕсть ли номер на аккаунте
Часовой пояс / языкпрофиль / first messageНесоответствие декларируемому региону
IP при веб-бэкендепрокси / mini appОдин IP для разных user_id
Device fingerprintmini appОдин и тот же браузер для разных аккаунтов
Behavior patterntelemetryСкорость кликов, паузы, опечатки
Реферал-графБДОдин реферер за час дал 30 «инвайтов»
Платёжный сигналбанк / шлюз3-D Secure отказы, разные карты на одного user_id
Geolocation IP vs phonemixТелефон РФ, IP — Восточная Европа

Каждый сигнал кладите в одну табличку events, чтобы потом легко считать по ним агрегаты в SQL.

Архитектура слоя анти-фрода

Стандартная схема — три уровня:

update (MAX Bot API)
  ↓
1) Pre-checks (rate limit, blacklist) — синхронно, отказ за 5 мс
  ↓
2) Scoring (правила + ML) — синхронно, 20–100 мс
  ↓
3) Async review (очередь подозрительных, ручная модерация)

Pre-checks отбрасывают очевидное (заблокированный user_id, превышен лимит). Scoring считает риск 0–100 на каждое действие. Async-слой накапливает «жёлтые» события и отдаёт их человеку.

# pseudo на aiohttp + MAX Bot API
async def on_update(update: Update):
    user = update.message.from_user

    if await is_blacklisted(user.id):
        return  # тихо игнорируем
    if not await rate_limiter.allow(user.id):
        await reply(update, "Слишком много запросов, попробуйте через минуту.")
        return

    score, reasons = await fraud_score(user, update)
    if score >= 80:
        await soft_block(user.id, reasons)
        await notify_moderator(user.id, score, reasons)
        return
    if score >= 50:
        await flag_for_review(user.id, score, reasons)
        # обработку продолжаем, но без выплат и бонусов
        update.context["risk"] = "yellow"

    await dispatch(update)

Rate limit на user_id и на действие

Один token bucket на пользователя — must-have, но недостаточно. На критичные действия (выпуск промокода, начисление бонуса, реферал) ставьте отдельные лимиты с другими порогами.

from aiolimiter import AsyncLimiter

# общий: 60 запросов в минуту
GLOBAL = lambda: AsyncLimiter(max_rate=60, time_period=60)
# выпуск промокода: 1 в 24 часа
PROMO  = lambda: AsyncLimiter(max_rate=1,  time_period=86400)
# реферал: 5 в сутки
REFER  = lambda: AsyncLimiter(max_rate=5,  time_period=86400)

buckets: dict[tuple[int, str], AsyncLimiter] = {}

def bucket_for(user_id: int, action: str) -> AsyncLimiter:
    key = (user_id, action)
    if key not in buckets:
        if action == "promo": buckets[key] = PROMO()
        elif action == "refer": buckets[key] = REFER()
        else: buckets[key] = GLOBAL()
    return buckets[key]

В проде in-memory словарь не подходит — данные сбрасываются при рестарте. Используйте Redis с INCR + TTL или sliding window log.

Скоринг: простые правила сначала

Не надо начинать с XGBoost. На старте достаточно простых правил, которые покрывают 80% случаев. Считайте баллы и складывайте:

async def fraud_score(user, update) -> tuple[int, list[str]]:
    score = 0
    reasons = []

    # Возраст аккаунта (если MAX отдаёт дату регистрации)
    age_days = await account_age_days(user.id)
    if age_days < 1:
        score += 30; reasons.append("account_age<1d")
    elif age_days < 7:
        score += 15; reasons.append("account_age<7d")

    # Заглушка вместо аватара + случайный username
    if not user.has_photo and is_random_username(user.username):
        score += 20; reasons.append("placeholder_profile")

    # Похожий аккаунт уже банили
    if await similar_to_banned(user):
        score += 40; reasons.append("similar_to_banned")

    # 3+ аккаунта с одного IP за сутки (если есть mini app)
    ip = update.context.get("ip")
    if ip and await accounts_per_ip(ip, hours=24) >= 3:
        score += 35; reasons.append("multi_account_ip")

    # Пользователь зарегистрировался по рефералу из подозрительной фермы
    if await referrer_is_suspicious(user.id):
        score += 25; reasons.append("suspicious_referrer")

    return min(score, 100), reasons

Простой скоринг легко аудировать: видно, за что система начислила балл. ML-модель вам этого не скажет.

Защита реферальной программы

Реферальная программа — главный канал фрода в большинстве ботов. Что обычно ломают и как закрывать:

1. Самореферал. Пользователь регистрирует второй аккаунт и забирает бонус за «приглашение себя».

async def can_apply_referral(referrer_id: int, new_user_id: int) -> bool:
    # один телефон / один device fingerprint / один IP в течение 30 дней
    if await same_phone(referrer_id, new_user_id): return False
    if await same_device(referrer_id, new_user_id): return False
    if await same_ip_within(referrer_id, new_user_id, days=30): return False
    return True

2. Реферал-фермы. Один пользователь приводит 50 «друзей» за час.

  • Пороги: не более 5 рефералов в сутки и не более 30 в месяц с одного аккаунта.
  • Бонус начисляется не сразу, а после полезного действия реферала: первой покупки, прохождения onboarding, недели активности.
  • Если у реферера 70%+ рефералов так и не сделали ни одного действия — реферер уходит на ручную проверку.

3. Накрутка через ботнет. Закрывается требованием SMS-подтверждения телефона реферала и/или ручной проверкой подозрительных кластеров.

Защита от бонус-хантеров

Если у вас welcome-бонус — гарантировано появятся желающие получить его пять раз. Контрмеры по нарастающей:

  1. Один бонус на user_id — базовый уровень, но обходится регистрацией нового аккаунта.
  2. Один бонус на телефон — нужно требовать привязку телефона перед выдачей бонуса.
  3. Один бонус на device fingerprint (если бот связан с mini app или сайтом).
  4. Бонус начисляется только после первой оплаты — мошеннику невыгодно потратиться.
  5. Cool-down на регистрацию — например, не более 1 нового аккаунта с одного IP за 6 часов.

Связка «бонус только после оплаты + проверка по телефону» закрывает 90% бонус-фрода.

Защита платежей: фрод по картам

Если бот принимает платежи через ЮKassa, CloudPayments, Robokassa, Tinkoff — кардеры будут пробовать на нём краденые карты. Признаки:

  • 3+ разных карты на одного user_id за час.
  • 2+ user_id с одной картой (через хеш токена карты).
  • Серия отказов 3-D Secure подряд.
  • Платёж сразу после регистрации с минимальной суммы (тест-проба) и попытки большой суммы.
  • Несоответствие BIN страны и предполагаемой геолокации пользователя.

Реакция — мягкая блокировка: «Платёж временно отклонён, обратитесь в поддержку», без явного указания причины. Иначе кардер просто отрегулирует поведение.

async def evaluate_payment(user_id: int, card_token_hash: str, amount: int):
    last_hour = await payment_attempts(user_id, hours=1)
    if last_hour.distinct_cards >= 3:
        return "decline", "many_cards"
    if await users_with_card(card_token_hash) > 1:
        return "decline", "shared_card"
    if last_hour.failed_3ds >= 2:
        return "decline", "3ds_fails"
    return "allow", None

Детекция автоматизированных скриптов

Скрипты отличаются от живых пользователей по поведению:

  • Скорость отклика — скрипт нажимает кнопки за 50–100 мс, человек — за 500–3000 мс.
  • Идеальная регулярность — кнопка нажимается ровно каждые 1.0 с.
  • Отсутствие опечаток и backspace — пользователь, как правило, что-то правит.
  • Один порядок действий 1000 раз подряд — человек случайнее.
  • Сообщения «вне сценария» не приходят — скрипт не задаёт вопросов от себя.

Простой детектор:

async def behavior_score(user_id: int) -> int:
    actions = await last_actions(user_id, n=20)
    if len(actions) < 5: return 0

    deltas = [b.ts - a.ts for a, b in zip(actions, actions[1:])]
    avg = sum(deltas) / len(deltas)
    var = sum((d - avg) ** 2 for d in deltas) / len(deltas)

    score = 0
    if avg < 0.3:           score += 30   # слишком быстро
    if var < 0.05 * avg**2: score += 25   # слишком регулярно
    if all(a.kind == "callback" for a in actions): score += 10
    return score

В критичных местах добавьте «человеческую проверку» — пауза с просьбой нажать конкретную кнопку через 3 секунды или ввести код с картинки.

Кастомная капча для критичных действий

Капчу не нужно показывать каждому. Достаточно показывать её при триггере:

  • Скоринг ≥ 50 при регистрации.
  • 5 неуспешных попыток подряд.
  • Первый раз при выводе бонуса.
  • Срабатывание поведенческого детектора.

Реализации капчи в боте MAX:

  1. Yandex SmartCaptcha через mini app — самая надёжная.
  2. Математическая («сколько будет 7 + 4?») — простая, но обходится скриптом.
  3. Графическая — отправляем картинку с цифрами, просим ввести.
  4. «Нажмите кнопку через 3 секунды» — лёгкая защита от ботов, не раздражает живых.
  5. Slider / drag через mini app — можно фиксировать траекторию мыши.
async def request_captcha(chat_id: int, user_id: int):
    answer = secrets.choice(range(1000, 9999))
    await save_captcha(user_id, answer, ttl=300)
    img = render_captcha_png(str(answer))
    await max_api.send_photo(chat_id, photo=img,
        caption="Введите цифры с картинки, чтобы продолжить")

Кластеризация мульти-аккаунтов

Один человек может вести 30 аккаунтов, и каждый по отдельности будет «чистым». Чтобы их связать, постройте граф связей: общий телефон, IP, device, BIN карты, паттерн имени. Подозрительный кластер — это компонент связности, в котором 5+ узлов с 1+ общим атрибутом.

Простой подход на Postgres:

-- Таблица «общий атрибут — два user_id»
CREATE TABLE user_links (
  a_user_id BIGINT,
  b_user_id BIGINT,
  link_type TEXT,   -- 'phone' | 'ip' | 'device' | 'card_bin'
  weight    REAL,
  PRIMARY KEY (a_user_id, b_user_id, link_type)
);

-- Найти всех «соседей» подозрительного аккаунта в радиусе 2 шагов
WITH RECURSIVE neighbors AS (
  SELECT b_user_id AS uid, 1 AS depth
    FROM user_links WHERE a_user_id = $1
  UNION
  SELECT ul.b_user_id, n.depth + 1
    FROM user_links ul JOIN neighbors n ON ul.a_user_id = n.uid
    WHERE n.depth < 2
)
SELECT DISTINCT uid FROM neighbors;

Если кластер вокруг одного «грязного» аккаунта насчитывает 20+ — есть смысл выгружать всех на ручную проверку.

Honeypots и невидимые ловушки

Полезный приём — спрятать в боте «приманку», которую никогда не покажете живому пользователю. Например:

  • Скрытая команда /admin_promo, при вызове которой автоматически блокируем аккаунт (живой человек её не введёт).
  • Промокод в источниках, который на самом деле триггерит блокировку использовавших.
  • Кнопка с zero-width-символом в тексте — её увидит только парсер.

Это даёт «бесплатный» детектор автоматизированных сканеров.

Мягкая vs жёсткая блокировка

Не блокируйте сразу намертво — это даёт мошеннику обратную связь, и он адаптируется. Уровни реакции:

СкорингРеакцияЧто видит пользователь
0–30пропускаемвсё работает
30–50замедляем (искусственный sleep 2–5 с)«бот думает»
50–70требуем капчу или верификацию телефонапривычная проверка
70–85shadow ban: бот отвечает, но действия не выполняются«заявка отправлена» (не правда)
85–100hard block«Сервис временно недоступен»

Shadow ban — критично важная техника против упорных мошенников. Они не понимают, что их раскрыли, и продолжают тратить время.

Логи, алерты и ручная модерация

Без оператора-модератора анти-фрод-слой быстро теряет точность. Нужен дашборд, где видно:

  • Топ-20 подозрительных user_id за сутки.
  • Кластеры мульти-аккаунтов с >10 узлами.
  • Графики: динамика регистраций, рефералов, выплат, отказов.
  • Средний скоринг по часам — резкие всплески = новая атака.

Алерты в отдельный чат поддержки в MAX (через тот же бот, но в служебный чат):

async def maybe_alert(score: int, reasons: list[str], user_id: int):
    if score >= 80:
        await max_api.send_message(MOD_CHAT_ID,
            f"⚠️ Подозрительная активность\n"
            f"user_id: {user_id}\n"
            f"score: {score}\n"
            f"reasons: {', '.join(reasons)}\n"
            f"history: {ADMIN_URL}/users/{user_id}")

Модератор проверяет за минуту и принимает решение: разбан, постоянный бан, или «оставить под наблюдением».

ML-модели — когда они нужны

Простые правила покрывают первые 6–12 месяцев. ML имеет смысл, когда:

  • У вас 100k+ пользователей и 10k+ событий в день — есть откуда учиться.
  • Мошенники адаптируются быстрее, чем вы переписываете правила.
  • У вас есть размеченный датасет: «эти пользователи — фрод, эти — норма».

Для табличных признаков (возраст, кол-во рефералов, скорость кликов) хорошо работает gradient boosting (CatBoost, XGBoost). Целевая метрика — precision@top-k: насколько часто из 100 «самых подозрительных» по модели реально мошенники.

Не выкатывайте ML-скоринг на «жёсткую блокировку» сразу. Сначала — в режиме тени (shadow scoring), сравниваете с правилами 1–2 месяца, потом переключаете критичные решения.

152-ФЗ и приватность анти-фрода

Сбор поведенческих данных и фингерпринтов — это обработка ПДн. Что нужно:

  • В согласии на обработку ПДн прямо написать: «оператор обрабатывает данные о действиях пользователя в боте, технические идентификаторы, данные об устройстве в целях защиты от мошенничества».
  • В политике конфиденциальности — отдельный пункт «Защита от мошенничества».
  • Хранить логи на серверах в РФ — чувствительные сигналы (телефон, паспорт) не передавать в зарубежные сервисы.
  • При жалобе пользователя — уметь показать, за что именно его заблокировали (право на разъяснение).
  • Срок хранения — разумный (90–180 дней для большинства событий, дольше — только для подтверждённых случаев).

Метрики качества анти-фрод-системы

Чтобы понимать, не сломали ли вы конверсию ради безопасности, замеряйте:

МетрикаЧто показываетЦелевое значение
False positive rate% заблокированных живых пользователей≤ 1%
True positive rate% пойманных мошенников от всех мошенников≥ 70%
Bonus payout / обращениесредняя выплата на пользователястабильна, без всплесков
Retention day-7 рефераловвозврат приведённых пользователей≥ 40%
Время от события до блокировкизадержка детекции≤ 5 минут для жёлтых, мгновенно для красных
Кол-во жалоб на ложный банчерез поддержку≤ 3 в неделю

Если retention рефералов рухнул — у вас фрод. Если жалоб больше десятка в неделю — у вас перебор с блокировками.

Итого

Анти-фрод в боте MAX — это не одна большая фича, а несколько слоёв: rate limit, скоринг по простым правилам, отдельные пороги для критичных действий (рефералы, бонусы, платежи), кластеризация мульти-аккаунтов, мягкая и shadow-блокировка вместо мгновенного бана, дашборд для модератора и алерты в служебный чат. ML добавляйте только когда у вас есть данные и адаптирующийся противник. Параллельно — корректное оформление по 152-ФЗ: упоминание в согласии и политике, хранение в РФ, право пользователя на разъяснение причины. Минимальный анти-фрод-слой реально собрать за 2–3 недели и сэкономить себе сотни тысяч рублей бонусных выплат за первый же квартал.

Частые вопросы

С чего начать построение анти-фрода в боте MAX?

С двух базовых вещей: rate limit на user_id (общий и отдельный для критичных действий — выпуск промокода, начисление бонуса, реферал) и журнал событий с расширенной телеметрией (user_id, timestamp, тип действия, IP при наличии mini app, базовые атрибуты профиля). На этом фундаменте дальше навешивается скоринг по правилам. Не пытайтесь сразу строить ML — на старте 5–10 простых правил поймают 70–80% фрода и их легко аудировать. Через 2–3 месяца у вас накопится реальная статистика, и вы увидите, какие правила работают, а какие надо донастраивать.

Как защитить реферальную программу бота от накруток?

Три базовые меры: бонус начисляется не за регистрацию реферала, а за его полезное действие (покупка, активность через 7 дней, прохождение onboarding); пороги — не более 5 рефералов в сутки и 30 в месяц с одного аккаунта; проверка по «общим атрибутам» — телефон, device fingerprint, IP в течение 30 дней. Дополнительно отслеживайте долю «мёртвых» рефералов (приведённых, но без активности): если у одного реферера 70%+ таких — на ручную проверку. Кластеризация графа связей через user_links поможет находить фермы из 20+ связанных аккаунтов автоматически.

Как поймать автоматизированный скрипт, имитирующий пользователя?

Скрипты отличаются по поведенческим сигналам: скорость отклика 50–100 мс (человек 500–3000 мс), идеальная регулярность (callback каждые ровно 1.0 с), отсутствие опечаток и backspace, отсутствие сообщений «вне сценария», одинаковый порядок действий тысячи раз. Простой детектор считает среднюю и дисперсию интервалов между действиями: если среднее меньше 0.3 с или дисперсия близка к нулю — добавляем баллы к скорингу. В критичных местах подключайте проверки «нажмите кнопку через 3 секунды» или капчу при превышении порога. Это не панацея, но отсекает 90% наивных скриптов.

Что такое shadow ban и зачем он нужен?

Shadow ban — это мягкая блокировка, при которой бот для пользователя выглядит работающим (отвечает, показывает интерфейс, принимает заявки), но реальные действия не выполняются (бонус не начисляется, заявка не уходит в CRM). Мошенник не понимает, что его раскрыли, и продолжает тратить время на бесполезные попытки вместо того, чтобы регистрировать новый аккаунт. Особенно эффективен против упорных бонус-хантеров и кардеров. Применяется обычно при скоринге 70–85: выше — уже жёсткая блокировка с честным сообщением «сервис временно недоступен».

Как защитить платежи в боте от кардеров?

Признаки кардера на платёжке: 3+ разных карты с одного user_id за час, 2+ user_id с одной картой (через хеш токена), серия отказов 3-D Secure подряд, тест-проба на минимальную сумму с последующей попыткой большой, несоответствие BIN страны и геолокации. Реализуется препроцессором перед отправкой в платёжный шлюз: считаете эти агрегаты по последним N часов, при срабатывании отдаёте «платёж временно отклонён, обратитесь в поддержку» без явной причины. Дополнительно — обязательное 3-D Secure, лимит на сумму первого платежа нового пользователя, и алерт в служебный чат при детекции подозрительной серии.

Когда переходить с правил на ML-модель в анти-фроде?

Когда выполнено три условия: 100k+ активных пользователей и 10k+ событий в день (есть от чего учиться); мошенники адаптируются быстрее, чем вы переписываете правила (видно по росту false-negative); у вас есть размеченный датасет — пользователи с подтверждённым фродом и пользователи с подтверждённой нормой. Для табличных признаков (возраст аккаунта, кол-во рефералов, поведенческие метрики) хорошо работает gradient boosting — CatBoost или XGBoost. Сначала ML работает в режиме тени (shadow scoring) и сравнивается с правилами 1–2 месяца. Только потом переключайте критичные решения. Ключевая метрика — precision@top-k: какая доля «самых подозрительных» по модели реально мошенники.

Какие требования 152-ФЗ при сборе анти-фрод-сигналов?

Сбор поведенческих данных, технических идентификаторов и фингерпринтов — это обработка ПДн, и она должна быть отражена в согласии и политике. В согласии прямо пишите: «оператор обрабатывает данные о действиях, технические идентификаторы и данные об устройстве в целях защиты от мошенничества». Логи с чувствительными сигналами (телефон, паспорт, банковские атрибуты) храните только в РФ — без передачи в зарубежные SaaS. Срок хранения разумный: 90–180 дней для большинства событий, дольше — только для подтверждённых случаев. По запросу пользователя нужно уметь объяснить, за что именно его заблокировали — это право на разъяснение. Уведомление в РКН о начале обработки ПДн обязательно вне зависимости от анти-фрода.