Что показал аудит нашей собственной платёжной системы
Две критические находки, и обе не там, где мы стали бы искать. Открытый отчёт об ошибках, которые с тех пор исправлены.

В августе 2026 года мы проверили нашу платёжную систему с самого основания. Не потому, что что-то случилось, а потому, что незадолго до этого она заработала, и мы хотели знать, что упустили.
Упустили мы немало. В этой статье названы находки. Всё описанное здесь исправлено, и большая часть и так уже была в публичном списке изменений.
Ошибка в модели, из которой выросли обе критические находки
Когда кто-то покупает у нас Premium, у нашего платёжного провайдера создаётся запись клиента. Мы хранили эту запись под ID сервера, ведь Premium в итоге попадает на сервер.
Это ровно та модель, которая неверна, и она стала корнем двух самых серьёзных находок.
Запись клиента принадлежит покупателю. В ней его способ оплаты, история счетов и, как только кто-то платит за два сервера, несколько подписок. Если ключом служит ID сервера, несколько человек указывают на одну и ту же строку.
Находка 1: чужие данные клиента можно было перезаписать, не платя
Покупка открыта любому участнику сервера, а функция, которая находит или создаёт запись клиента, срабатывает до оплаты. Она находила строку по ID сервера и молча переписывала её на нового потенциального покупателя, включая его адрес электронной почты у платёжного провайдера.
На практике это означало: хватало одного клика на сервере, где кто-то когда-то покупал. После этого счета по каждой подписке того первого покупателя уходили бы на адрес постороннего. Без оплаты, без подтверждения.
Исправлено так: перед любым повторным использованием проверяется, действительно ли запись принадлежит этому аккаунту. При несовпадении создаётся отдельная, а другая остаётся нетронутой.
Находка 2: портал оплаты показывал чужие подписки
Та же ошибка в модели, уровнем глубже. Проверка владельца относилась к подписке сервера, но сессия портала открывалась для записи клиента, которая по своему устройству может охватывать несколько серверов.
Так любой, кто законно купил на одном сервере, мог видеть подписки, которые та же запись вела для других серверов, и управлять ими.
Исправлено так: запись клиента определяется по конкретной покупке, а не по строке сервера.
Две находки, где код противоречил нашим собственным условиям
Это категория, которая беспокоила нас больше всего, потому что она не техническая.
Чёрный список отнимал оплаченное время. В наших условиях сказано, что подписка заканчивается в конце текущего срока. Но при блокировке сервера код вызывал немедленную отмену: без возврата денег и без услуги с этого момента за уже оплаченный период. Код делал не то, что мы обещали письменно. Теперь оплаченный период дорабатывает до конца.
Согласие на утрату права отказа от договора нельзя было доказать. Перед покупкой нужно подтвердить, что услуга начинается немедленно. Это подтверждение жило только в браузере, никуда не отправлялось и нигде не сохранялось. Бремя доказывания лежит на нас, а доказательства у нас просто не было. Теперь согласие это обязательное поле маршрута оформления заказа, оно сохраняется с отметкой времени и с версией формулировки, а сама формулировка версионируется: существующий текст никогда не редактируется, всегда создаётся новая версия, иначе старые записи больше нельзя было бы воспроизвести.
Второй раунд оказался хуже первого
После первой 21 находки мы через несколько недель посмотрели ещё раз. Это было верное решение, потому что самая серьёзная находка всплыла только тогда.
Возврат денег на сервере B удалял оплаченную пожизненную лицензию сервера A. Уведомления о возврате и чарджбэке называют только платёж, а не сервер. Мы сопоставляли их через запись клиента. После того как в первом раунде мы правильно установили, что запись принадлежит покупателю на нескольких серверах, именно это сопоставление перестало быть однозначным. Тот, кто заплатил за два сервера и получил возврат за один, мог потерять пожизненную лицензию другого.
Теперь сопоставление идёт через четыре точных шага, а всё, что нельзя сопоставить однозначно, попадает в отчёт, а не угадывается.
Выигранный чарджбэк ничего не восстанавливал. При чарджбэке код удалял строку покупки, единственное доказательство того, что у этого сервера вообще была пожизненная лицензия. Если спор потом выигрывался, это нельзя было отменить. Теперь строка помечается, а не удаляется.
Хранение данных удаляло оплаченные серверы. Когда бот покидает сервер, его данные удаляются через 30 дней. Эта очистка никогда не спрашивала, привязана ли к серверу действующая подписка.
Что мы из этого вынесли
Баги были не в платёжном коде. Ни одна критическая находка не касалась суммы, валюты или расчёта. Все они были в модели за ними: кому принадлежит запись клиента, кому принадлежит платёж, кому принадлежит сервер. Кто проверяет платёжную систему, глядя на суммы, смотрит мимо опасной части.
Аудиту нужен второй раунд. Самая серьёзная находка была прямым следствием исправления из первого. Это не случайность: исправляя модель, ты обесцениваешь допущения, которые в других местах всё ещё считаются верными.
«Так сказано в наших условиях» это проверяемое утверждение. Две находки были расхождениями между тем, что мы обещали письменно, и тем, что делал код. Это можно прочитать, и никто этого не делает, потому что это похоже на право, а не на разработку.
Условия покупки полностью изложены в наших условиях использования, право на отказ от договора в разделе Отказ от договора.
Другие статьи
Почему почти всё в AlphaCloud остаётся бесплатным
Большинство ботов Discord прячут свои самые полезные функции за подпиской. Мы намеренно так не делаем, и щедрости в этом меньше, чем кажется.
За кулисамиЗакон внутри нашего кода
Кнопка расторжения, которая обязана работать без входа. Форма, которая никогда не должна тебе отказывать. Почему юридические обязанности для нас архитектура, а не шаблонный текст.
За кулисамиКак уследить за 4000 серверов Discord: Grafana, PostHog и что они нам показали
Цикл, который незаметно создавал половину нашего трафика, три миллиона бессмысленных запросов в день и атака, закончившаяся через минуту. Что видит наш мониторинг и чего он намеренно не видит.