andrii@volkovskey : ~/notes $ cat 01-ertb-postmortem.md
МОВА
0x01 · ПОСТМОРТЕМ · TELEGRAM · OPS

ЯК НЕ ВБИТИ
TELEGRAM-БОТ
ЗА ШІСТЬ РОКІВ

Постмортем ERTB - пет-проєкту, що зібрав 278 тисяч користувачів без жодної реклами, втратив 90 % активності і навчив мене, що стабільність важить більше за фічі.

- Один розробник, один VPS, один прапор у профілі.

/ META - СТАТТЯ
ШЛЯХ
~/notes/01-ertb-postmortem.md
ДАТА
2026-05-19
ЧИТАННЯ
~17 ХВ · ~3 000 СЛІВ
АВТОР
Андрій Волков · @volkovskey
ТЕГИ
telegram · python · postmortem · ertb
СЕРІЯ
/notes · 01
СТАТУСОПУБЛІКОВАНОСЕРІЯ~/notes · 01ОНОВЛЕНО2026-05-19

ERTB - Exchange Rates Telegram Bot - стартував 9 червня 2020 як проєкт на один вечір. Через 11 місяців нас було 100 000 користувачів. Через 19 місяців - 200 000. На шостий рік існування він обробляє 2 500 конвертацій на день - у десять разів менше, ніж у піку 2024-го. На момент написання я підтримую його сам.

Це стаття про шість років. Про те, що тримало бот живим, і про те, що його повільно вбило. Друге було не зростанням навантаження, не змінами Telegram API, не конкурентами. Друге - це ми самі.

/ FIG
К/ДЕНЬ
  27 ┤          █
  23 ┤        █ █ █
  20 ┤      █ █ █ █
  17 ┤    █ █ █ █ █
  13 ┤  █ █ █ █ █ █ █
  10 ┤█ █ █ █ █ █ █ █
   5 ┤█ █ █ █ █ █ █ █ █ █
   1 ┤█ █ █ █ █ █ █ █ █ █ █ █ █ █
     └─────────────────────────────
      '23     '24     '25     '26

0x01ДЕ ВІН ЗАРАЗ

Цифри на травень 2026 (статистика почала збиратись з березня 2023 року):

  • ≈7 970 000 повідомлень оброблено за весь час
  • ≈278 000 унікальних користувачів
  • ≈14 500 груп, які хоч раз додали бота
  • ≈2 500 конвертацій на день. У піку було 25 000.
  • 13 мов інтерфейсу, 6 - для розпізнавання слів-чисел
  • Хостинг - €15 на місяць плюс €10 на рік за домен. 8 GB RAM, 4 CPU.

Команда - формально я і ще двоє. Реально по ERTB останній рік працюю сам. 5.0 я зробив у соло, у форматі парного програмування з Claude Code. Релізи стали рідшими: раз на квартал, інколи рідше.

Стаття написана у час, коли бот скоріше зменшується, ніж росте. Це не похорон. Це інвентаризація. Поки €15 на місяць - підйомна сума для команди, ERTB буде жити.

0x02ЯК ВОНО ВИРОСЛО · 2020-2022

9 червня 2020 - перший commit. Я писав бот для одного групового чату з друзями з різних країн, де постійно треба було переводити «скільки це в гривнях». 10 червня - перша робоча версія. 13 липня - реліз 1.0.

Стек на той момент: Python, aiogram через long polling, шість валют (UAH, USD, EUR, RUB, BYN, PLN), курси з НБУ, налаштування користувачів - у текстових файлах. Незашифровано. Через screen на дешевому VPS.

Через шість місяців - 37 000 користувачів. Через 11 - сто тисяч. Через 19 - двісті. Нуль реклами. Усе - груп-чати: люди додавали бот в одну групу, хтось писав цю саму групу друзям, додавали туди теж. Сарафанне радіо у середовищі, де є україно- та російськомовна діаспора у Європі, СНД, Канаді.

Жодне з цих чисел не було запланованим. Ніщо в коді 2020 року не було спроєктовано під такий розмір. У грудні 2020 я писав у канал:

Мені соромно зізнаватись, але всі налаштування бота зберігаються у вигляді текстових файлів у незашифрованому вигляді.

24 лютого 2022 о пʼятій ранку РФ почала повномасштабне вторгнення. Бот продовжив працювати. Канал додав реквізити для донатів ЗСУ. На пости стали приходити «ображені реакції» з російських акаунтів - ми перестали на них реагувати. У боті у профілі залишився український прапор. Він там і досі.

0x03ПЕРША КРИЗА · DDoS, ЖОВТ 2020

22 жовтня 2020 хтось знайшов, що бот ніяк не обмежує кількість валют у відповіді. Якщо написати повідомлення, де матчиться 300 валют, бот старанно конвертував усі 300 і відправляв величезне повідомлення. Telegram цього не любить - bot rate limit, потім тимчасове блокування бота.

Спочатку був DoS - один акаунт. Швидко закрили. Через день - DDoS, із десяткою акаунтів. Перший фікс не тримав. Сидів писав заплатку, паралельно у DM спілкувався з людиною, що це робила. Зрештою вона написала, як саме обходила перший варіант захисту. Я дякую їй - без неї ми б це закрили інакше і пізніше.

З того хотфіксу виросла система захисту, що називалась StopDDoS. Через три роки її повністю переписали у Mjolnir. Логіка проста - і це принципово важливо.

LIMIT_OF_CURRENCIES_PER_MESSAGE = 50
ACTIVITY_WINDOW_SECONDS = 1800   # 30 хвилин
COEF_PER_MESSAGE = 20            # триггер на одне повідомлення
COEF_FOR_AVERAGE = 5             # триггер на кумулятивну активність

TIER_LADDER_HOURS = [1, 2, 12, 24, 72, 168, 720, 8760, 100000]
QUIET_PERIOD_DAYS = 30

Ковзне 30-хвилинне вікно тримає deque подій (user_id, count_of_cur, time). Поверх - dict із per-user статистикою (скільки разів використано, скільки валют сумарно).

На кожне повідомлення обчислюються середні - кількість валют на повідомлення і кількість використань на користувача - виключаючи поточного користувача з розрахунку. Це ключовий нюанс: інакше масивний спамер сам спотворює baseline, проти якого його ж і перевіряють.

Триггери:

  1. Hard cap. Повідомлення містить більше 50 валют - миттєвий бан.
  2. Per-message ratio. Кількість валют у повідомленні більша за середню × 20 - миттєвий бан.
  3. Cumulative. За 30 хвилин користувач має і валют × 5 від середнього, і використань × 5 від середнього - бан.

Бан не назавжди. Він ескалюється по сходах: 1 година → 2 → 12 → 24 → 72 (3 доби) → 168 (тиждень) → 720 (місяць) → 8 760 (рік) → 100 000 (≈11 років) → permanent. Між банами стоїть 30-денний quiet period: якщо 30 днів немає активного бану, наступний бан скидається на 1 годину. Це знімає більшість false positive - людина натиснула щось дивне, отримала годину бану, прийшла наступного місяця - наче нічого не було.

Mjolnir не використовує ні ML, ні anomaly detection. Це лічильник з адаптивним середнім, який порівнює тебе з тобою-вчорашнім і з усіма іншими-сьогоднішніми. За пʼять років він автоматично забанив сотні людей і не побанив жодної реальної людини, скарга якої б до нас дійшла.

Захист не потребує ML. Захист потребує лічильника, який ти вмієш пояснити по рядку коду.

0x04REGEX, А НЕ NLP

Серце ERTB - не Telegram-частина. Серце - це парсер: алгоритм, який бере довільне повідомлення з групи і відповідає на питання «чи є тут число з валютою, і яке». Бот без нього - пуста оболонка.

Парсер живе окремо від тіла бота. З червня 2024 він і отримання курсів винесені у власне API на .NET 8 - ExRates Connect. Туди ходить ERTB. Туди ж ходить ERDB (Discord-варіант). Туди ж ходитиме все, що ми колись зробимо.

Pipeline - 8 кроків.

  1. Short-circuit. Якщо вхідний рядок коротший за 2 символи - повертається порожня відповідь негайно. Зменшує load на порядок.
  2. Clean. Lowercase. Видалити @mentions і URL. Нормалізувати роздільники тисяч: 1,000 / 1 000 / 1.000.000 - звести до одного формату. Дробова кома → крапка. Сколапсувати пробіли. \n → " , ". Усі regex компільовані статично.
  3. Belarusian context flag. Якщо повідомлення виглядає білоруським - виставити прапорець, щоб пізніше RUB промотати у BYN. Маленька деталь, без якої білоруси отримували б російські рублі замість своїх.
  4. Split. Ручний токенізатор по межах літера↔не-літера і цифра↔не-цифра. Токени з двома і більше крапками викидаються - це ловить IP-адреси і номери версій, що раніше прилітали у пайплайн.
  5. Convert. MathToNumber виявляє 25*35, 150/3, 69-47 у токенах і нормалізує -/-/÷ у стандартні оператори. WordToNumber: німецькі - через портований Zahlwort2Num; решта мов - Levenshtein fuzzy-match по таблиці w2nTokens з LRU-кешем 32 768. «twinty five» і «двадцять пять» однаково перетворюються на число.
  6. Search. Для кожного числового токена шукаємо валюту у trie у чотирьох напрямках, у такому порядку: forward 1 слово, forward 2, backward 1, backward 2. Беремо найглибший знайдений CurrencyCode. Маршрутизація fiat vs crypto - за приналежністю коду.
  7. Promote RUB → BYN. Якщо крок 3 виставив білоруський прапорець - перебиваємо.
  8. De-dup. Групуємо результати по Pair.Equals(value, code), повертаємо без дублів.

Це ≈300 рядків продакшен-коду на C#. Шість мов підтримки слів-чисел. Працює стабільно під мілісекунду на повідомлення. Логи парсера читаються очима, помилка локалізується по конкретному кроку.

Весною 2026 я провів пару тижнів shadow-логування: на кожне реальне повідомлення паралельно з основним парсером запускалась локальна LLM рівня Gemma 4 E2B, потім Gemma 4 E4B. Завдання - той самий extract: число + валюта.

  • На простих кейсах моделі і парсер сходились у 99 % випадків.
  • На складних кейсах моделі і парсер сходились у 75 % випадків. Моделі давали менше знайденого (24 %) і лише в 1 % давали більше, але і те було помилково.

Висновок: для цієї задачі < 8B LLM нічого не покращує. Можливо потрібні кращі моделі, але compute стає занадто дорогим.

Окрема нота - про мови. Я не лінгвіст і не білорус, не поляк, не португалець, не узбек. Парсер знає шість мов тому, що rotlir додав білоруський, jpzex - португальський (бразильський), рис - узбецький інтерфейс, Abviol - польське розпізнавання. Open source як модель розвитку не зайшов нам - про це далі. Але як спосіб залучити чотирьох спеціалістів, які зробили те, чого ми б самі не зробили, - він спрацював ідеально.

0x05ПРАПОР І СКАМЕРИ

Український прапор у профілі бота стоїть з 24 лютого 2022. Українська - одна з двох мов, що завжди гарантовано підтримується (друга - англійська). Перші повідомлення на каналі після початку війни - реквізити ЗСУ. Одразу - реакції розгніваних російських акаунтів на наші пости.

Це коштувало нам частини аудиторії. Російські групи з нашою аудиторією - використовували бот. Частина з них перестала після того, як побачила прапор. Інша частина перестала після січня 2024.

23 січня 2024 ми оголосили скам-чатам війну. Шість місяців до цього збиралась статистика - які чати масово використовують бота під схеми «переведи мені 500 $ і отримай 50 000 $», «відправ криптою - повернемо фіатом». Ми внесли всі знайдені у внутрішній blocklist. Бот перестав відповідати у цих чатах. Ми не читали повідомлення - просто налаштували тригер на підозрілі фрази і слова, якими користуються скамери.

До січня 2024 репозиторій був публічний. У ньому час від часу зʼявлялись зірки, кілька форків - нічого визначного. Ми думали, що відкритість допоможе залучити допомогу. Не допомогла. Натомість підготувала ґрунт для альтернатив, які брали наш код і додавали туди те, що нам категорично не підходило. У якийсь момент зробили форк нашого бота - з російським прапором і донатами на ЗС РФ замість української армії. Це і була друга причина, чому ми закрили GitHub.

1 січня 2024 ми перестали підтримувати публічний repo. Поточна гілка на Lanasys/exchange-rates-tg-bot-api-powered - це README, релізи, лоцманська інформація, без активного коду парсера і API. Бот лишився на версії 3.0, але навіть таку версію ми прикрили потім, і вона більше не є публічною.

Про прапор я не шкодую ні секунди. Про закриття GitHub - ні секунди. Це коштувало нам користувачів і коштувало «ідеї open source ентузіаста». Я готовий платити цю ціну.

0x06МІГРАЦІЇ

Перші три роки бот зберігав усе у SQLite. До певного моменту це працювало. До моменту, коли під одночасними записами в кілька з'єднань база починала ламатись - інколи неможливо було ні зчитати, ні записати. Корупція індексів, locking timeouts, інколи просто залипання.

18 березня 2024 ми мігрували на MySQL 8.

Питання «чому MySQL, а не Postgres» - у мене є чесна відповідь. Postgres я тоді ще не вмів. MySQL - вмів, бо нас його вчили на лабах в універі. Це і весь стратегічний вибір. Через два роки бачу: MySQL легко тримає поточне навантаження, реплікація налаштована, бекапи їдуть, з пулом зʼєднань через aiomysql ніяких сюрпризів. Postgres був би елегантнішим у деяких місцях. Але це був би вибір, на який я б витратив тижні замість двох днів.

У червні 2024 - друга велика міграція. Парсер і отримання курсів виносяться з тіла бота у власний HTTP-сервіс на .NET 8 - ExRates Connect. Причини три:

  • ERDB (Discord) у березні запустили на тому ж парсері, що й ERTB. Дублювати логіку у двох кодових базах - рецепт повільного розходження.
  • Хочеться писати парсер тією мовою, де мені простіше тримати продакшен - у моєму випадку це C#, а не Python.
  • API дає одне місце для логування, метрик, A/B-тестів парсингу. Включно з shadow-логуванням LLM, яким я скористався у 2026-му.

Тіло бота лишилось на Python 3.10 і aiogram 3.27. Деплой - GitHub Actions: на push у main піднімається worker, тестує (pytest, pytest-asyncio, pytest-benchmark), SSH-ом дойдеплоює на VPS, перезапускає screen-сесію. Тривіально, кілька десятків рядків YAML, нуль Kubernetes.

Boring stack - це не консервативний вибір. Це інвестиція у те, щоб не витрачати уваги на інфраструктуру у момент, коли увага потрібна десь ще.

0x07PRO ТА ІНШІ СПРОБИ

24 серпня 2024 ми запустили ERTB Pro. Підписка через Telegram Stars, 150⭐ на місяць. Що було ексклюзивним: inline-режим, мат-операції, Word-To-Number, відсутність реклами у відповідях бота.

За перший місяць - близько п'яти підписників.

16 вересня знизили ціну до 100⭐. Зробили підписки на 3, 6, 12 місяців. Word-To-Number повернули у безкоштовну версію - стало очевидно, що люди готові міняти бота на конкурента, але не платити за те, що раніше було вільним.

За рік ERTB Pro у будь-якій формі купив десяток людей. За весь час 2 550⭐ виторгу. У Telegram Stars це покриває хіба пару місяців хостингу.

Проблема була не у грошах. Проблема була у тому, що ми ексклюзивізували функції, по яких потім не отримували фідбеку. Inline-режим (топ-фіча по запитах у 2021-2023) перестав давати сигнал, що з ним не так. Багатий тест на стабільність парсера у складних виразах - теж замкнувся за paywall, де його дуже мало користувачів.

У 5.0 я Pro закрив. Усі, хто хоч раз купив підписку, перейшли у наш список донорів - це список у /about. Усі функції доступні всім. Я зрозумів: тут зараз немає платної гілки і не буде.

Паралельно з Pro ми пробували ще дві монетизаційні гілки.

ERDB - Exchange Rates Discord Bot. Запустили відкритий бета-тест у березні 2024. Сарафанне радіо, що працювало у Telegram-групах, у Discord не зайшло. Discord - це або голосовий чат, або тематичний канал, де «бот, який реагує на повідомлення про курси», не має тої ж природньої аудиторії, як Telegram-група діаспори.

Реклама в боті. В адмінському інтерфейсі живе команда /money - три рекламні кампанії: посилання у footer відповіді, кнопка під відповіддю, окреме повідомлення про донати. Працюють опціонально, з whitelist для тих, хто не хоче бачити. Кампанії продавались декілька разів етичним рекламодавцям. Пропозицій було багато - більшість від онлайн-казино, гемблінгу, скам-проєктів. На всі такі відповідь одна: ні, дякую. Ми сидимо у точці, де реклама в боті могла б його окупити, але це була б реклама не та, з якою хочеться засинати спокійно.

0x08ВЕЛИКА ПОМИЛКА

Цей розділ найкоротший. І найважливіший.

Дивлюсь на графік активності за останні три роки. Пік - червень 2024, 25-27 тисяч конвертацій на день. Через шість місяців - обвал. На початку 2025 ми відкатились до 5 тисяч за тиждень. Далі - плавний спад до сьогоднішніх 2.5.

Насправді все дуже просто - спонтанні падіння бота на день-два, протерміновані оновлення курсів, скарги у support-боті, на які ми відповідали раз на три місяці. Я це бачив. Команда це бачила. Ми це знали. І в той же час ми додавали нові фічі. Запускали підписку. Малювали логотип.

Користувачі не зрадили. Вони отримали від нас сигнал, що ми ненадійні. Бот не оновлює курси добу - людина перестає його використовувати. Раз. Двічі. На третій раз вона видаляє його з групи. І бот вже не повертається - навіть якщо ми його через місяць полагодили.

Усі красиві аргументи про конкуренцію, про мобільність груп у Telegram, про Telegram-зміни - це шум. Головна причина - коли треба було гасити, ми будували.

Якщо у вас є проєкт, який досяг піка - це не сигнал додавати фічі. Це сигнал стабілізувати, моніторити, відповідати на support за 24 години, а не за 24 дні. Це нудно. Це неможливо опублікувати у канал як «ми додали…». Це і є те, що тримає продукт.

0x095.0 · ЗМИРИЛИСЬ

14 травня 2026 я випустив ERTB 5.0.

Список новацій вийшов довгий, але якщо чесно дивитись на дугу: це реліз, який підбиває риску. Pro прибрали. Інтерфейс пофарбували. Додали /card з рідкісною колекційною механікою - більше як ностальгічний жанр, ніж функціональна фіча. Додали /privacy як явну заяву «ми не комерційний продукт, ми не торгуємо вашими повідомленнями, нам нічого про вас не цікаво крім того, як швидко повернути курс». Додали 6 нових мов інтерфейсу. Перевели всю роботу з БД на async.

Все це я зробив сам. Без Claude Code у форматі pair programming це б займало місяць-два замість двох тижнів. Це і є моя нова реальність - і вона ще раз підкреслює, чому Pro мав бути закритий: все зводиться до ШІ. Чому бот вистрілив? Бо люди багато часу проводили вдома (ковід-19) і спілкувались, і замість того аби відволікатись на браузер чи окремий застосунок для конвертації валют, отримували відповідь одразу у чаті. А зараз людині легше запитати у ШІ, ніж покладатись на ненадійного бота.

Найважливіша зміна у 5.0 не у списку патчнотів. Вона у тоні. Раніше у канал ми писали «ми ще додамо…». У релізі 5.0 ми написали «усе доступне для всіх». Це не маркетинг. Це позиція.

2020 - 1.0 · 2021 - 2.0 · 2023 - 3.0 + 4.0 · 2024 - 4.4 + Premium · … · 2026 - 5.0

0x0AУРОКИ

Підбиваю короткими рядками те, що написав би молодшому собі 2020-го.

  1. Стабільність важить більше за фічі, коли ти на піку. Це єдиний урок, який варто запамʼятати, навіть якщо забути всі інші. Коли користувачів багато і вони активні - вкладай у те, щоб бот не падав, оновлював курси і відповідав на support. Усе нове - після.
  2. Парсер на вузькій задачі переграє LLM утричі. Дисципліновані правила + lookup + fuzzy-match + LRU тримають мілісекундну latency і нульову операційну поверхню. 8B-модель у shadow-логуванні цього не побила. Коли область чітко обмежена - не витрачай час на нейронки.
  3. Один backend-сервіс на всі боти. Парсер і курси у спільному API економлять тобі переписування при кожній новій платформі. Реалізував один раз - користуйся з усіх ботів Lanasys. Треба було робити це раніше.
  4. Open source - це не модель росту для маленького B2C-бота. Він дає тобі чотирьох якісних перекладачів і нуль contributors-у-core. Він також дає твоїм недоброзичливцям матеріал для форків. Маленький проєкт без чітких комʼюніті-ресурсів не отримує від відкритості того, що очікує.
  5. Donation > Subscription для бота-сервісу. Люди готові скинутися, бо люблять. Люди не готові підписуватись, бо це психологічно інший формат - вони очікують гарантії, документації, SLA. Якщо ти один розробник із VPS - donation-режим чесніший і ефективніший. Навіть якщо вас троє.
  6. Політична позиція коштує дорого. Тримай її лише якщо готовий платити. Прапор у профілі, бан скамерських чатів, відмова від російського ринку - це коштувало нам користувачів. І я б знову зробив усе те саме. Бо дещо не продається.

0x0BЩО Я БУДУВАВ БИ СЬОГОДНІ

Майже те саме, чесно. aiogram 3, замість MySQL був би Postgres, .NET API окремо. Це boring stack, який працює. Я не вірю, що його заміна на щось модне допомогла б ERTB.

Що зробив би інакше:

  • Alerting на курси старші за день - з першого дня. Це найбільший важіль, якого ми не мали п'ять років.
  • Парсер як окремий repo з нуля. Не у тілі бота. Не «ми потім винесемо коли треба». Зразу окремо, бо це ядро продукту, а тіло бота - просто адаптер до Telegram.
  • Не Pro. Direct donation з самого початку, без підписки. Якщо хочеш - заплати, отримай імʼя в /donors. Це все.

Решта - як і було. Один розробник, один VPS, один прапор у профілі. На шостий рік це здається нормальним мірилом success.

/ ВИНОСКИ
[1]Mjolnir названо на честь жарту про банхамер. Попередній StopDDoS - заплатка кінця 2020 року після DDoS-атаки 22 жовтня. Принцип той самий, реалізація переписана з нуля у 2023.
[2]Поточного публічного repo з релізами і README немає. Активний код бота, парсера й ExRates Connect API не публікуються.
[3]Розгорнутий патчноут ERTB і ExRates Connect - lanasys.dev/ertb#patches. Канал розробників - @ertb_channel.
AV
Andrii Volkov · @volkovskey
Software-інженер · Системний розробник · Дніпро / Україна
← НА ГОЛОВНУ
ЗРОБЛЕНО В УКРАЇНІ · НЕ ДЛЯ РОСІЇ · © 2026
VOLKOVSKEY · 2026-06-06 15:50:24