Кейс · Разработка

Как принять оплату в Telegram-боте: ЮKassa, aiogram 3, webhook и Claude Code

За вечер: каркас бота, приём оплаты через webhook, деплой на systemd. И отдельно история о том, как AI-видео чуть не выдало вымышленный продукт за настоящий.

Недавно разбирал воронку одного известного курса по вайбкодингу: красивый сайт, обещания "$1.5M за год", десятки платных модулей. Стало интересно: а что если сделать честную версию того же самого, без придуманных цифр, и показать процесс как есть, включая места, где что-то ломается.

Ниже: как за вечер получается рабочий Telegram-бот на aiogram 3 с приёмом оплаты через ЮKassa, какие решения оказались неочевидными, и отдельно история о том, как сгенерированное ИИ демо-видео чуть не выдало вымышленный продукт за настоящий.

Скриншот лендинга проекта

Лендинг проекта: результат того же вечера работы

Что должно получиться к концу вечера

Пишу с помощью Claude Code: не копирую готовый код с гитхаба, а формулирую задачу текстом и проверяю результат на каждом шаге.

Демо (реальный код, реальная цена, реальный бот, без ИИ-выдумок):

youtu.be/2s10A_JQmlQ

Каркас

Первый запрос выглядит так:

Помоги мне создать каркас Telegram-бота на Python.

Требования:
- Библиотека aiogram версии 3.x
- Структура: bot.py, handlers/, .env, requirements.txt
- Токен из переменной окружения BOT_TOKEN через python-dotenv
- Логирование в консоль (INFO)
- Обработчик /start с ответом "Бот на связи"

20 минут, включая получение токена у @BotFather. Дальше меню с двумя кнопками ("Что это" и "Купить"), тексты сразу выносим в texts.py, чтобы менять формулировки, не трогая логику обработчиков.

Диалог о том, зачем вообще такой бот

Постановка задачи текстом, а не готовым кодом

Оплата: почему webhook, а не проверка по кнопке

Здесь первая развилка, которая не очевидна новичку. Можно после нажатия "Купить" просто спрашивать у API ЮKassa "оплатил ли он" по кругу, но это ненадёжно: пользователь может закрыть вкладку в момент оплаты, а бот так и не узнает об этом.

Правильный путь: ЮKassa сама шлёт POST-запрос на ваш сервер, когда статус платежа меняется.

Configuration.account_id = YOOKASSA_SHOP_ID
Configuration.secret_key = YOOKASSA_SECRET_KEY

payment = Payment.create({
    "amount": {"value": f"{PRICE_RUB}.00", "currency": "RUB"},
    "confirmation": {"type": "redirect", "return_url": "https://t.me/ваш_бот"},
    "capture": True,
    "description": "Ваш продукт",
}, idempotence_key)
Реальный кусок кода из handlers/payment.py

Реальный код из проекта, не иллюстрация

Важный момент: телу webhook-запроса нельзя доверять вслепую. При получении уведомления мы заново запрашиваем статус платежа напрямую у ЮKassa через Payment.find_one(payment_id) своим секретным ключом, и только если API подтверждает succeeded, отправляем товар. Иначе кто угодно может прислать поддельный POST на ваш webhook-адрес с "успешной оплатой".

Локальная разработка без домена

На этапе разработки у вас ещё нет сервера с HTTPS, а ЮKassa отправляет webhook только на настоящий адрес. Проверяли так: подняли лёгкий aiohttp-сервер на отдельном порту, сделали тестовый платёж тестовой картой (ЮKassa даёт тестовый режим, test_... ключ, деньги не списываются по-настоящему), а затем вручную сформировали такой же POST-запрос через curl с реальным payment_id, чтобы проверить логику доставки без ожидания настоящего вебхука:

curl -X POST http://localhost:8081/yookassa-webhook \
  -H "Content-Type: application/json" \
  --data '{"event":"payment.succeeded","object":{"id":"<реальный id платежа>"}}'

Это не заменяет реальный webhook на проде, но позволяет проверить всю цепочку "оплата → доставка" ещё до деплоя на сервер.

Реальный вид бота после нажатия кнопки VIP

Реальный вид бота после нажатия кнопки VIP

Сервер: почему systemd, а не Docker

Для одного небольшого бота Docker избыточен: лишний слой абстракции, который на маленьком VPS не даёт ощутимой пользы. Достаточно systemd-юнита:

[Unit]
Description=Telegram bot
After=network.target

[Service]
ExecStart=/opt/bot/venv/bin/python bot.py
WorkingDirectory=/opt/bot
Restart=always
User=bot

[Install]
WantedBy=multi-user.target

Restart=always переживает падения, WantedBy=multi-user.target переживает перезагрузку сервера. Спереди nginx как reverse proxy с HTTPS через certbot: без него ЮKassa не будет слать webhook.

История про честность демо-видео

Отдельно расскажу вещь, которая нас саму удивила. Мы сгенерировали демо-видео через ИИ-видеосервис (дали общее текстовое описание процесса, без точного сценария по кадрам). Результат выглядел красиво: анимация кода, интерфейс Telegram-бота, оплата.

Проблема вскрылась, когда стали разбирать кадры покадрово перед тем, как пустить видео в промо: в сгенерированном ролике бот продавал не наш реальный продукт за 990₽, а вымышленный "билет на конференцию" за 5000₽, а показанный код был на английском и не соответствовал нашему реальному bot.py. ИИ-сервис не срисовывал наш продукт, а сочинил правдоподобный, но полностью придуманный пример.

Для проекта, который весь построен на позиции "никаких придуманных цифр", использовать такое видео было бы прямым нарушением собственного принципа. Решение: пересобрать демо-сцену вручную из HTML/CSS с настоящими текстами из texts.py, настоящей ценой и настоящим фрагментом кода, записать через Playwright, наложить настоящую озвучку. Дольше, чем взять готовый ИИ-ролик, зато на кадрах то, что есть на самом деле.

Честное видео уже встроено на сайт

Честное видео уже встроено на сайт

Итог

Весь путь (каркас → меню → оплата → выдача → сервер) занимает вечер, если не считать время на подтверждение магазина в ЮKassa (обычно 1-2 дня для ИП, самозанятым иногда быстрее).

Если кто-то повторит путь и упрётся в шаг, где не сходится, пишите в комментарии, разберём.

Хотите тот же результат?

Пак с точными запросами для Claude Code под каждый шаг выложен бесплатно.

Забрать пак на mivdvoem.ru →