Кейс · Веб и SaaS, Автоматизация процессов, LLM и агенты
Социальные Медиа реклама в городских пабликах Иркутска
Сервис рекламного агентства из Иркутска: каталог пабликов с открытыми ценами, бронь даты, согласование с менеджером, оплата, маркировка erid, публикация в назначенный час и закрывающие документы — в одном месте. Работать в нём может и ИИ-агент клиента.
Паспорт проекта
- Отрасль
- Реклама
- Срок
- 8 недель
- Команда
- Команда E404 и ИИ-агент
Стек

Задача
Клиент — рекламное агентство из Иркутска. Оно продаёт посты в городских пабликах Иркутска и Братска во ВКонтакте, Telegram и Одноклассниках, делает лендинги-новости, ведёт таргетированные и контекстные кампании, а через ООО «Иркутск Сегодня» — продаёт материалы на новостном сайте IRK.TODAY.
Нужен был не «ещё один сайт», а весь путь заказа в одном месте: посетитель видит площадки, цены и свободные даты без регистрации, бронирует время, загружает пост, менеджер проверяет его по правилам площадки и закону о рекламе, клиент платит — и пост выходит сам, в назначенную минуту, с маркировкой. После — ссылка, статистика и закрывающие документы.
Ограничения задавали закон и устройство самого агентства:
- Маркировка рекламы. Без erid рекламный пост выходить не может. Маркировка через ОРД VK нужна уже в первой версии: без неё запуска нет.
- Два продавца. Размещения продаёт самозанятый — без НДС, чек из «Мой налог», лимит дохода 2,4 млн ₽ в год. Материалы IRK.TODAY — ООО, только по счёту. У каждого свои реквизиты, нумерация счетов и актов.
- Не биржа. Каждый заказ проходит ручную модерацию менеджером до оплаты. Тарифов, подписок и баланса нет — оплата поштучная.
- Персональные данные. Согласие отдельным документом, политика по фактам обработки, удаление учётной записи по запросу — 152-ФЗ.
- Общий сервер с соседними проектами: ни один наш шаг не должен ронять чужие сайты.
Что сделали
Витрина с открытыми ценами
На лендинге — паблики с подписчиками, охватом и ценой за 1, 5 и 10 постов. Заказ начинается прямо там: площадка, тираж, дата — цена видна на каждом шаге, а календарь свободных дат открыт без входа. Вопрос можно задать менеджеру в чате на сайте, тоже без регистрации.


Заказ в кабинете за пять шагов
Кадры кабинета дальше сняты на стенде с тестовыми данными: клиенты, заказы, суммы и просмотры выдуманы — продаж через сервис тогда ещё не было.
Вход — по коду на почту, через VK ID или Яндекс ID, без пароля. Дальше — мастер заказа:
- Площадки — переезжают с лендинга, смета считается сразу.
- Креатив — с предпросмотром поста в нужной сети и пометкой «реклама».
- Расписание — по иркутскому времени.
- Реквизиты плательщика.
- Проверка со сметой — и заказ уходит на согласование.
Время закрепляется за клиентом сразу: черновик держит его сутки, заказ на согласовании — 48 часов.


Модерация и оплата
Менеджер видит очередь заказов — старые сверху: обычно у них ближе дата публикации. В карточке — креатив по каждому паблику, заказчик и плательщик. При согласовании менеджер выбирает код ККТУ для ОРД, а если платит организация, ей тут же уходит счёт. Клиент платит картой через ЮKassa, переводом по номеру телефона с чеком или по счёту — оплату по счёту менеджер отмечает по выписке.


Маркировка и публикация — автоматически
После оплаты креатив регистрируется в ОРД VK и получает erid: один текст — один креатив на все даты и одна плата, 150 ₽. Пост без erid не выходит. В назначенную минуту воркер выпускает пост в нужной сети — у нас адаптеры для ВКонтакте, Telegram, Одноклассников, MAX и сайтов на WordPress, — а через 30 дней снимает его: сам во ВКонтакте и на WordPress, а в остальных сетях — менеджер по списку. Клиент получает ссылку и статистику там, где сеть её отдаёт.

Отчёты и документы
Аналитика показывает расходы по месяцам и сетям, а для каждого паблика — сколько просмотров дала тысяча рублей. Счета и акты двух продавцов выходят в PDF сами, акт — только на вышедшие публикации, с той же суммой, что уходит в отчёт ОРД.


ИИ-агент в кабинете
Клиент может выпустить токен и подключить Claude, Cursor или другого агента: тот смотрит каталог, заказы и документы, а с полным доступом — оформляет заказы и пишет менеджеру. Агент может не больше, чем заказчик: он вызывает тот же API с правами владельца токена, а платит всегда сам заказчик. Для агента — 129 инструментов; ещё 39 действий, вроде смены почты входа, закрыты для него с записанной причиной.




Как устроено

Лендинг и кабинет — на Next.js, API — на FastAPI с PostgreSQL и Redis. Всё, что происходит по часам, вынесено в отдельный воркер: 22 цикла публикуют посты, регистрируют креативы, сверяют оплаты и возвраты с ЮKassa, снимают посты через 30 дней, где сеть это позволяет, и шлют письма. MCP-сервер живёт внутри API и вызывает те же маршруты с токеном агента — второй правды о правах нет.
Основу мы взяли из своих прошлых проектов: ядро — из MAX Bot, чат с клиентом — из WPSupport. Донорский код переносили с исправлениями, а правила, за которыми стоят прошлые ошибки, записали в памятку для всех, кто правит код: цену считает сервер, статусы — не строки, а таблицы переходов, схему базы создаёт только Alembic, местное время переводится в UTC ровно в одном месте.
От чего отказались: от тарифов и баланса — это не биржа; от паролей — вход по коду; от веб-аналитики без согласия посетителя — это риск по 152-ФЗ.
Код проверяет конвейер CI из 19 заданий: больше 1 900 тестов бэкенда с покрытием 94 % при пороге 92 %, сборка образов, сквозные сценарии в браузере — с обоими типами продавца и в двух часовых поясах, поиск секретов в истории git.
Инженерные решения
Иркутск живёт на восемь часов впереди UTC
Слоты публикаций — местное время Иркутска, UTC+8. Сравнить время слота с utcnow() — значит выпустить пост на восемь часов позже, а взять часы браузера — показать клиенту из Москвы время на пять часов раньше. Такие ошибки трудно заметить: неверная дата выглядит как верная.
Мы оставили одно место, где местное время становится моментом в UTC, и запретили сравнения в обход него:
def to_utc(local_date: date, local_time: time | str) -> datetime:
"""Местные дата и время слота -> момент в UTC.
`local_time` принимается и как `time`, и как строка «HH:MM» —
из формы приходит строка, из БД объект.
"""
if isinstance(local_time, str):
hh, _, mm = local_time.partition(":")
local_time = time(int(hh), int(mm or 0))
return datetime.combine(local_date, local_time, tzinfo=agency_tz()).astimezone(UTC)
А сквозные сценарии прогоняем дважды — в поясе Иркутска и в поясе Москвы. Сначала московский прогон ловил ошибку «месяц по часам браузера» только в те часы, когда месяцы двух городов расходятся, — 60 часов в году. Теперь в сценариях часы браузера остановлены на 00:30 первого числа по Иркутску: в Москве в этот момент ещё прошлый месяц, и ошибка видна при любом прогоне.
Запросы с деньгами — вне транзакции
Возврат через ЮKassa сначала был обычным запросом внутри транзакции с блокировкой заказа. Пока ЮKassa отвечала, заказ был заперт: цикл маркировки ждал его, и выходила взаимная блокировка. А выкладка, перезапустившая сервер в эту минуту, теряла запись о деньгах, которые уже ушли клиенту.
Теперь возврат идёт в три шага:
- До запроса сохраняются запись «возврат в работе» и снятие публикаций.
- Сам запрос в ЮKassa — вне транзакции и без блокировок.
- Исход записывается отдельно, а всё оборванное доводит сверка воркера.
Это правило теперь общее: любой внешний запрос с деньгами — вне транзакции.
Запрос агента не должен съедать память сервера
MCP пускает в кабинет запросы, которые составляет не человек, а агент. Ревью показало: один запрос в 16 МБ из мелких значений JSON добавлял серверу 874 МБ памяти при пределе контейнера в 1 ГБ. Разбор JSON случался раньше всех проверок.
Мы перенесли проверку до разбора: большое тело принимается, только если оно большое из-за файла — одной строки content_base64, — и проходит общую очередь больших запросов.
def _outside_file(body: bytes) -> int:
"""Сколько байт тела — не содержимое файла."""
at = body.find(_FILE_KEY)
if at < 0:
return len(body)
value = _FILE_VALUE.match(body, at + len(_FILE_KEY))
return len(body) - (len(value.group(1)) if value else 0)
Результат
Сервис развёрнут и открыт: лендинг — на irk.agency, кабинет — на app.irk.agency; на сервере он с конца сентября 2026 года. У пабликов каталога в сумме больше 430 000 подписчиков; одни и те же люди могут быть подписаны в нескольких сетях. Заказы во ВКонтакте открылись, когда поддержка ВК выдала приложению права на стену и фото.
Боевой запуск ещё впереди: магазин ЮKassa не подключён, и продаж через сервис в октябре 2026 ещё не было. Поэтому цифры в этом кейсе — про сам продукт: скорость, проверки, инструменты для агентов, аудитория пабликов. Цифры по заказам и выручке добавим, когда пойдут продажи.
Лендинг быстрый — Lighthouse 13.5.0, замер в октябре 2026, по одному прогону:
| Замер | Телефон | Компьютер |
|---|---|---|
| Производительность | 100 | 98 |
| Доступность, рекомендации, SEO | 100 | 100 |
| Первая отрисовка (FCP) | 1,0 с | 0,4 с |
| Отрисовка главной (LCP) | 1,2 с | 0,7 с |
| Сдвиг вёрстки (CLS) | 0 | 0 |

Ещё до боевого запуска код за три дня прошёл семь кругов независимого ревью. Каждую находку сверяли с кодом. У каждой правки — тест и мутация: в код возвращали исправленную ошибку и проверяли, что тест её ловит. В журнале записано больше 700 таких мутаций — и все пойманы. Находок больше 250 — от гонок при оплате до текста подсказки на кнопке.
Поделиться записью



Комментарии
Писать можно, войдя через VK ID, через Яндекс ID или по коду из письма. У комментария — имя и фото из профиля VK или Яндекса либо имя, которое вы укажете при входе по почте. Как мы обращаемся с данными — в политике конфиденциальности.