Любой бот в MAX, который раздаёт бонусы, выплачивает за рефералов, проводит платежи, выдаёт промокоды или просто принимает заявки, рано или поздно становится мишенью для мошенников. От накрутки рефералов школьниками с десятком левых аккаунтов до автоматизированных скриптов, которые выкачивают промокоды и перепродают их в обменниках. В этой статье — как выстроить анти-фрод-слой в боте на MAX от VK Tech: какие сигналы собирать, как считать скоринг, как блокировать без паранойи и не терять реальных пользователей.
Кто и зачем атакует ботов
Перед выбором техник полезно понять, против кого вы защищаетесь:
- Бонус-хантеры — заводят 5–50 аккаунтов, чтобы получить welcome-бонус, скидку или подарок несколько раз.
- Реферал-фермы — школьники и арбитражники, которые регистрируют десятки фейковых пользователей за свою же реферальную ссылку.
- Промокод-сборщики — автоматизированные скрипты, парсящие выдачу промокодов через бота.
- Кардеры и тестеры карт — пробуют украденные карты на боте, где есть платёжная интеграция, чтобы понять, рабочая ли карта.
- Спамеры и флудеры — забивают ваш чат поддержки, рассылают рекламу через инвайты, накручивают активность.
- Конкурентная разведка — выкачивают каталог, тарифы, базу знаний, базу FAQ.
- DoS-любители — пытаются положить бота шквалом запросов.
Под каждую группу — свой набор сигналов и контрмер. Универсального «фильтра мошенников» нет.
Какие сигналы собирать
Чем больше независимых сигналов вы фиксируете на каждое действие — тем точнее скоринг. Минимум:
| Сигнал | Источник | Что показывает |
|---|---|---|
| user_id MAX | update | Уникальность аккаунта |
| Возраст аккаунта | getMe / профиль | Аккаунт «вчерашний» — подозрительно |
| Имя/username | профиль | Случайные «aabbcc123», копии других |
| Аватар | профиль | Стандартная заглушка vs живое фото |
| Привязка телефона | если доступно | Есть ли номер на аккаунте |
| Часовой пояс / язык | профиль / first message | Несоответствие декларируемому региону |
| IP при веб-бэкенде | прокси / mini app | Один IP для разных user_id |
| Device fingerprint | mini app | Один и тот же браузер для разных аккаунтов |
| Behavior pattern | telemetry | Скорость кликов, паузы, опечатки |
| Реферал-граф | БД | Один реферер за час дал 30 «инвайтов» |
| Платёжный сигнал | банк / шлюз | 3-D Secure отказы, разные карты на одного user_id |
| Geolocation IP vs phone | mix | Телефон РФ, 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-бонус — гарантировано появятся желающие получить его пять раз. Контрмеры по нарастающей:
- Один бонус на user_id — базовый уровень, но обходится регистрацией нового аккаунта.
- Один бонус на телефон — нужно требовать привязку телефона перед выдачей бонуса.
- Один бонус на device fingerprint (если бот связан с mini app или сайтом).
- Бонус начисляется только после первой оплаты — мошеннику невыгодно потратиться.
- 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:
- Yandex SmartCaptcha через mini app — самая надёжная.
- Математическая («сколько будет 7 + 4?») — простая, но обходится скриптом.
- Графическая — отправляем картинку с цифрами, просим ввести.
- «Нажмите кнопку через 3 секунды» — лёгкая защита от ботов, не раздражает живых.
- 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–85 | shadow ban: бот отвечает, но действия не выполняются | «заявка отправлена» (не правда) |
| 85–100 | hard 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 дней для большинства событий, дольше — только для подтверждённых случаев. По запросу пользователя нужно уметь объяснить, за что именно его заблокировали — это право на разъяснение. Уведомление в РКН о начале обработки ПДн обязательно вне зависимости от анти-фрода.