Кейс · Разработка
Как принять оплату в Telegram-боте: ЮKassa, aiogram 3, webhook и Claude Code
За вечер: каркас бота, приём оплаты через webhook, деплой на systemd. И отдельно история о том, как AI-видео чуть не выдало вымышленный продукт за настоящий.
Недавно разбирал воронку одного известного курса по вайбкодингу: красивый сайт, обещания "$1.5M за год", десятки платных модулей. Стало интересно: а что если сделать честную версию того же самого, без придуманных цифр, и показать процесс как есть, включая места, где что-то ломается.
Ниже: как за вечер получается рабочий Telegram-бот на aiogram 3 с приёмом оплаты через ЮKassa, какие решения оказались неочевидными, и отдельно история о том, как сгенерированное ИИ демо-видео чуть не выдало вымышленный продукт за настоящий.
Лендинг проекта: результат того же вечера работы
Что должно получиться к концу вечера
- Telegram-бот на aiogram 3, отвечает на /start
- Меню с кнопками, тексты вынесены в отдельный файл
- Приём оплаты через ЮKassa (webhook, не polling статуса)
- Автоматическая выдача товара после подтверждения оплаты
- Бот на сервере, переживает перезапуск
Пишу с помощью 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)
Реальный код из проекта, не иллюстрация
Важный момент: телу 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
Сервер: почему 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 →