Неделя 128. Сколько стоит один разговор: как мы снесли биллинг и собрали заново, чтобы научиться считать диалоги

Разбор этой статьи
Эту тему разобрали в подкасте. Слушай параллельно с чтением.
Есть вопрос, который звучит как бухгалтерия, а на деле определяет всю бизнес-модель: что вы продаёте и как считаете проданное. Для нас единицей ценности стал диалог. Клиент платит не за сообщение, не за минуту и не за место в базе, а за разговор бота с его покупателем. И вот на неделе 128 выяснилось, что мы толком не умеем сосчитать, что такое один разговор. Пришлось снести систему тарификации и собрать заново. Семьдесят девять коммитов почти целиком про это.
Я Дмитрий Дьяконов, основатель Botseller AI. Это двенадцатый выпуск серии «Ретроспектива», бортжурнал в прошлое: от настоящего к первому коммиту. Сегодня отматываю на 12-18 января 2026 года. У читателей серии тут есть особое преимущество: вы уже видели баги этой системы в будущем. Двойное списание, которое я разбирал в выпуске про неделю 133, было болезнью именно того движка, что родился на этой неделе. Все те «деньги списываются за диалоги», которые я мимоходом упоминал в каждом выпуске, начинаются здесь. Сегодня расскажу, как мы вообще решили, что такое диалог, и во сколько он обходится.
Контекст: что было к началу недели
Середина января 2026 года, продукту полгода. Боты работают, клиенты платят, но система, которая считает эти деньги, держится на честном слове. Технически она была построена на так называемом материализованном представлении: это когда база данных периодически пересчитывает готовую витрину цифр по расписанию. Звучит удобно, пока не начинаешь задавать вопросы вроде «а почему вот этот диалог посчитан дважды» и «почему транзакция пятидневной давности вдруг приклеилась к сегодняшнему разговору». Ответов система не давала, потому что внутри у неё была не связь, а пересчёт по таймеру. И была ещё одна деталь фона: за пару дней до начала недели у нас случился тяжёлый инцидент с базой, после которого доверие к «магическим» механизмам, работающим втихую, упало до нуля. Про этот инцидент будет отдельный выпуск. Скажу лишь, что он очень располагал к тому, чтобы всё неявное сделать явным.
Главный вопрос недели: что такое один диалог

Начну с вопроса, который кажется детским, пока не попробуешь ответить в коде. Клиент написал боту в понедельник, бот ответил. Клиент вернулся в среду с новым вопросом. Это один диалог или два? А если он пишет каждый день по чуть-чуть неделю подряд? А если замолчал на месяц и вернулся? От ответа зависят живые деньги: посчитаешь широко, клиент недоплатит и мы работаем в минус; посчитаешь узко, клиент переплатит и почувствует себя обманутым.
Мы приняли решение, которое до сих пор считаю верным: один диалог живёт двадцать четыре часа. Первое сообщение открывает окно, все реплики в пределах суток идут в тот же разговор, а сообщение за пределами окна начинает новый. Просто на словах, коварно в коде. Первая наша реализация проверяла окно только с одной стороны, «не старше суток», и из-за этого старые транзакции пятидневной давности умудрялись приклеиваться к сегодняшним разговорам. Пришлось сделать проверку двусторонней: и не раньше, и не позже. Одна эта поправка в девять строк заняла у меня половину вторника, потому что суть была не в строках, а в том, чтобы наконец точно определить границы того, что мы продаём.

Здесь мой первый вывод, ради которого стоит читать этот выпуск. Единица ценности это не данность, а решение. Большинство основателей берут её как очевидную: продаём подписку помесячно, берём за сообщение, тарифицируем за место. А это самое важное продуктовое решение из всех, потому что оно определяет, за что клиент чувствует, что платит, и совпадает ли это с тем, что он ценит. Мы выбрали диалог, потому что клиент ценит именно решённый разговор с покупателем, а не байты и не минуты. Выберите свою единицу ценности осознанно. Это фундамент, на котором стоит всё остальное.
Снести и построить заново

Определив, что такое диалог, мы упёрлись в то, что старая архитектура не умеет его толком считать. И встала развилка, знакомая каждому, кто когда-либо работал с накопившимся техническим решением. Ветка первая: латать. Материализованное представление вроде работает, допилим пересчёт, подкрутим расписание, закроем дыры заплатками. Ветка вторая: снести и построить на прямой связи, когда каждая транзакция честной ссылкой привязана к своему диалогу, без пересчётов по таймеру.
Мы выбрали снести, и в понедельник 12 января легла новая архитектура: прямая связь вместо витрины по расписанию. Решение далось не героизмом, а арифметикой. Латать имеет смысл, пока стоимость заплатки меньше стоимости переделки. Как только вы ловите третий баг подряд из-за самой природы механизма, а не из-за частной ошибки, латать становится дороже, чем перестроить. Материализованное представление давало нам ровно это: баги не из-за кривого кода, а из-за того, что оно по устройству своему не знает, какая транзакция к какому диалогу относится, оно просто пересчитывает всё скопом. Против такого заплатки бессильны.

Отдельно горжусь одной защитой, которую мы туда встроили. Когда клиент пишет боту, теоретически может случиться гонка: два сообщения приходят одновременно и оба пытаются открыть новый диалог. Получилось бы два открытых разговора там, где должен быть один, и двойной счёт. Мы поставили на уровне базы жёсткое правило: на каждую пару «клиент и бот» может существовать только один открытый диалог одновременно. Не два, физически. База сама не даст. Это тот случай, когда правильнее запретить плохое состояние на уровне данных, чем ловить его логикой: логику можно забыть позвать, а ограничение в базе работает всегда.

Во сколько обходится разговор

Определив, что такое диалог, надо было понять, сколько он нам стоит. И тут вылезла ошибка, которая мне очень нравится своей поучительностью. В расчёте себестоимости мы по недосмотру подставляли цену для клиента, ту, что уже с наценкой, вместо реальной стоимости у поставщика модели. То есть система считала, что разговор обходится нам ровно в столько, за сколько мы его продаём. Прибыль в такой картине мира всегда ноль, и вы этого даже не замечаете, потому что цифры выглядят правдоподобно. Починили: себестоимость теперь считается от настоящей стоимости обращения к модели, а наценка накладывается сверху отдельным, видимым множителем.
Была там и тонкость с уровнями моделей. Разные разговоры под капотом обслуживаются моделями разной мощности и разной цены, и определять уровень нужно точно. Наивное сопоставление по названию промахивалось: короткое имя случайно совпадало с началом длинного и относило дорогую модель не к тому уровню. Пришлось сделать сопоставление по самому длинному совпадению, чтобы уровень определялся однозначно. Клиент всего этого не видит и видеть не должен: для него есть простые и понятные уровни тарифа, а вся кухня выбора и подсчёта модели спрятана. Но за этой ширмой должна быть абсолютная точность, потому что ошибка на копейку с диалога на масштабе превращается в дыру в экономике продукта.
Деньги, которые входят и выходят
Тарификация это половина дела. Вторая половина в том, чтобы деньги вообще попадали в систему и было видно, куда они делись. На этой же неделе мы дособрали приём платежей: пополнение баланса через платёжный шлюз, история пополнений отдельным списком, витрина транзакций по диалогам с фильтрами и статистикой по дням. Клиент теперь видит связную картину: сколько он завёл денег, сколько списалось, за какие именно разговоры и по какому тарифу.
Прозрачность биллинга я считаю не удобством, а вопросом доверия. Продукт, который берёт деньги, и не может внятно показать за что, вызывает подозрение даже когда считает честно. А биллинг, где клиент видит каждую копейку и каждый разговор, за который она ушла, снимает половину вопросов в поддержку ещё до того, как их задали. На этой неделе мы, по сути, строили не столько калькулятор, сколько доверие в цифрах.
Кто что видит

Раз речь зашла про то, кто видит цифры, на неделе появилось разграничение доступа в биллинге. Клиент видит свою картину в упрощённом, понятном виде, без служебной внутренней кухни: себестоимости, множителей, технических деталей выбора модели. Это ему не нужно и, честно говоря, может только сбить с толку. А полная развёртка доступна тем, кому она по роли положена.
Тут работает тот же принцип, что и в безопасности: показывать ровно столько, сколько нужно роли, и ни байтом больше. Лишняя информация в интерфейсе это не щедрость, а источник путаницы и утечек. Каждой роли свой срез правды.
И снова изоляция данных

Теперь про то, что читатели серии узнают с полуслова. В новой системе тарификации обнаружилась дыра: зная идентификатор чужого диалога, можно было вытащить его содержимое, имя бота, имя клиента, историю переписки. Знакомый почерк: система верит идентификатору из запроса и не проверяет, твой ли это диалог. Починили обязательным фильтром по владельцу во всех запросах, а заодно сделали тонкую, но важную вещь: при попытке достучаться до чужого диалога система отвечает «не найдено», а не «доступ запрещён». Разница принципиальная. «Доступ запрещён» подтверждает, что диалог существует, и это уже утечка. «Не найдено» не выдаёт ничего.
Да, я помню, что в выпуске про неделю 129 торжественно передатировал «первую трещину» изоляции данных на 23 января. А вот она, ещё раньше, 12 января. Я перестал искать первую. Их семья, и корень у неё один: мой собственный рефлекс сначала делать функциональность, а проверку прав прикручивать потом. Это не история про отдельные баги, это история про привычку, и лечится она не фиксом, а сменой привычки: проверка прав идёт первой строкой, а не последней.
Музей недели: консьюмер, который не мог определиться

Каждую неделю я коллекционирую мелочь с характером. На этой неделе это история из четырёх коммитов подряд с прекрасными в своей честности сообщениями: «схлопнул консьюмер», «вернул очередь на место», «схлопнул консьюмер», «вернул консьюмер». Кто-то, и это был я, два дня подряд то объединял обработчик очереди в один, то возвращал обратно, потому что каждый вариант чинил одно и ломал другое.
Я оставляю такие следы в рассказе намеренно. В причёсанной истории успеха таких коммитов не бывает: там всё сразу получается правильно. В настоящей разработке половина решений это метания туда-сюда, пока не нащупаешь верное. Тот, кто говорит, что сразу знал, как надо, либо забыл, либо привирает. Рядом, кстати, лёг коммит с алгоритмом заполнения пропущенных следов в аналитике: система местами теряла данные о том, что происходило, и мы учились достраивать пробелы. Всё та же сквозная тема серии: мы построили систему быстрее, чем научили её честно о себе рассказывать.
Урок недели: единица ценности определяет всё

Каждый выпуск я стараюсь заканчивать одним уроком, пережившим свою неделю. Урок недели 128 самый, наверное, дорогой из всех, потому что он про деньги, а деньги ошибок не прощают.
Единица, в которой вы считаете ценность, это не бухгалтерская мелочь, а главное решение о продукте. Мы могли брать за сообщения, и тогда бот был бы заинтересован писать многословно. Мы могли брать помесячно, и тогда клиенту было бы всё равно, работает бот или простаивает. Мы выбрали диалог, и это выстроило все стимулы правильно: и нам, и клиенту выгодно, чтобы разговоров с покупателями было больше и каждый вёл к результату. Единица ценности это скрытая ось, вокруг которой крутятся все остальные решения.
И второй, более приземлённый вывод. Считать деньги правильно надо строить до того, как денег стало много, а не после. Мы пересобирали биллинг, когда клиентов были десятки, и это было неприятно, но терпимо. Пересобирать его на тысячах клиентов, с уже накопленными кривыми данными и с людьми, которые заметят каждую копейку расхождения, это уже не рефакторинг, а операция на открытом сердце. Если ваш продукт берёт деньги, сделайте так, чтобы он считал их честно и прозрачно, пока цена ошибки ещё измеряется вечером работы, а не потерянным доверием сотен людей.
Цифры недели

- 79 коммитов в шести проектах за семь дней, почти все про биллинг
- Около 30 тысяч добавленных строк кода
- 24 часа: длительность одного диалога как единицы тарификации
- 1 открытый диалог на пару «клиент и бот»: жёсткое правило на уровне базы
- 14 типов ботов, для которых пересобиралась тарификация разом
- 404 вместо 403: как правильно отказывать в доступе к чужому диалогу
- 4 коммита туда-сюда: обработчик очереди, который не мог определиться
- 1 архитектура снесена и построена заново: с витрины по таймеру на прямую связь
Что было дальше
Дальше была неделя 129: ставка на две CRM сразу, запуск подписок, которые начали прорастать уже здесь, и честный разговор про то, как убеждения приходят из рук. Ещё через неделю случится тихий канун, а за ним большой взрыв с тремя ставками. А движок тарификации, собранный на этой неделе, будет служить дальше верой и правдой, изредка радуя нас багами вроде двойного списания, которые я потом разберу по отдельности. Всякая система, которая считает деньги, обречена всю жизнь доказывать, что считает их правильно.
Все выпуски серии собраны в категории ретроспектива, а текущая хроника разработки продолжается в бортовом журнале.
FAQ
Что такое ретроспектива Botseller?
Серия «Ретроспектива», бортжурнал в прошлое: от настоящего к первому коммиту. Каждый выпуск разбирает одну неделю разработки платформы по реальным git-логам, с фокусом на развилки: почему выбрали именно эту задачу, какие были альтернативы и как решение развернулось спустя месяцы. Все выпуски собраны в категории ретроспектива.
Почему тарификация по диалогам, а не по сообщениям?
Потому что единица оплаты задаёт стимулы. Оплата за сообщение подталкивает бота писать многословно, оплата помесячно делает клиенту безразличным, работает бот или нет. Диалог как единица выстраивает интересы правильно: и продавцу, и клиенту выгодно, чтобы разговоров с покупателями было больше и каждый вёл к результату. Клиент платит за то, что реально ценит, за решённый разговор.
Что считается одним диалогом?
У нас один диалог живёт двадцать четыре часа. Первое сообщение открывает окно, все реплики в пределах суток относятся к тому же разговору, а сообщение за пределами окна начинает новый диалог. Важно проверять границу с двух сторон, и снизу, и сверху, иначе старые сообщения могут ошибочно приклеиваться к текущим разговорам и путать расчёт.
Когда стоит переписывать систему с нуля, а не латать?
Простое правило: латайте, пока стоимость заплатки меньше стоимости переделки. Сигнал к переписыванию, это когда баги идут не из-за частных ошибок, а из-за самой природы механизма. Если вы ловите третью проблему подряд, вызванную устройством системы, а не опечаткой в коде, заплатки уже дороже переделки. Мы так и решили снести старую тарификацию: она по устройству не умела связать транзакцию с диалогом.
Как защитить биллинг от двойного списания?
Двумя уровнями. Первый, ограничение в базе данных, которое физически не позволяет существовать двум открытым расчётным сессиям на одну пару клиента и бота одновременно. Второй, идемпотентность обработчиков, чтобы повторная доставка одного события не создавала вторую транзакцию. Правило в базе надёжнее логики в коде, потому что логику можно забыть вызвать, а ограничение работает всегда. Разбор реального бага двойного списания есть в выпуске про неделю 133.
Что должен видеть клиент в биллинге?
Связную и честную картину: сколько денег он завёл, сколько списалось, за какие именно разговоры и по какому тарифу. Служебную кухню, себестоимость, внутренние множители, детали выбора модели, показывать не нужно, это только запутает. Прозрачность биллинга снимает половину вопросов в поддержку заранее и работает на доверие сильнее любых слов о честности.



