Большинство Telegram-ботов выглядят одинаково. /start — стена текста — кнопки. Пользователь тыкает, получает ответ, закрывает. Никакого ощущения что за ботом стоит что-то живое. Конверсия падает, люди не возвращаются, и ты не понимаешь почему — ведь функционал вроде работает.
Проблема обычно не в функционале. Проблема в деталях. Бот отвечает мгновенно как машина, не помнит кто ты, не даёт ощущения прогресса, не реагирует на действия. Пользователь это чувствует — даже если не осознаёт. И просто уходит.
В этой статье я собрал 7 конкретных фич с кодом на aiogram 3.x которые это исправляют. Некоторые внедряются за пять минут, некоторые требуют больше времени — но каждая влияет либо на удержание, либо на монетизацию, либо на рост аудитории. Без воды, сразу к делу.
1. Рассылка за Telegram Stars: монетизация контента прямо в мессенджере
Telegram Stars — внутренняя валюта Telegram. Ты как разработчик получаешь звёзды и выводишь их в деньги через Fragment. Метод send_paid_media позволяет продавать контент прямо внутри бота — без подключения платёжных шлюзов, без форм, без головной боли с эквайрингом.
Как это выглядит для пользователя: приходит сообщение с описанием и размытым превью. Нажимает «Оплатить», подтверждает списание Stars — медиа мгновенно открывается. Весь процесс — секунд десять.
Где применяют:
Авторский контент: фотографы, художники, дизайнеры продают работы напрямую
Обучение: уроки и разборы в формате видео
Эксклюзивные рассылки: пользователь видит интригующий анонс и платит за полную версию
Закрытые материалы каналов и сообществ
from aiogram import Bot, Router from aiogram.types import Message, InputPaidMediaPhoto, FSInputFile from aiogram.filters import Command router = Router() @router.message(Command("buy")) async def send_paid_content(message: Message, bot: Bot): await bot.send_paid_media( chat_id=message.chat.id, star_count=50, # цена в Stars media=[InputPaidMediaPhoto(media=FSInputFile("exclusive.jpg"))], caption="? Эксклюзивный контент — только для тех, кто платит" ) # Фиксируем покупку в базе данных @router.message(F.successful_payment) async def on_successful_payment(message: Message): user_id = message.from_user.id # await db.save_purchase(user_id) # сохраняем в БД await message.answer("✅ Оплата прошла! Приятного просмотра.")
Главное преимущество — Telegram сам показывает медиа размытым, сам принимает оплату, сам открывает контент после списания Stars. Твоя задача — только зафиксировать покупку в базе.
Ограничения:
Stars пока нельзя вывести в некоторых странах — проверяй на Fragment
Пользователь должен сначала купить Stars — не все к этому готовы
Курс конвертации устанавливает Telegram
2. Обязательная подписка на канал: быстрый рост аудитории
Один из самых эффективных приёмов для роста — пользователь не может пользоваться ботом пока не подпишется на канал. Звучит агрессивно, но работает. Особенно если бот полезный.
Схема: пользователь запускает бота → видит сообщение с просьбой подписаться → подписывается → нажимает кнопку «Проверить» → бот открывает полный доступ.
Совет: добавляй в это сообщение гифку или фото — так оно выглядит живее и не как стена текста.
from aiogram import Bot, Router, F from aiogram.types import ( Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton, FSInputFile ) from aiogram.filters import CommandStart router = Router() CHANNEL_ID = "@your_channel" # или числовой ID канала async def check_subscription(bot: Bot, user_id: int) -> bool: member = await bot.get_chat_member( chat_id=CHANNEL_ID, user_id=user_id ) return member.status not in ["left", "kicked"] def subscribe_keyboard() -> InlineKeyboardMarkup: return InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton( text="? Подписаться на канал", url=f"https://t.me/your_channel" )], [InlineKeyboardButton( text="✅ Проверить подписку", callback_data="check_sub" )] ]) @router.message(CommandStart()) async def start(message: Message, bot: Bot): is_subscribed = await check_subscription(bot, message.from_user.id) if not is_subscribed: await message.answer_animation( animation=FSInputFile("welcome.gif"), caption=( "? Привет! Чтобы пользоваться ботом,\n" "сначала подпишись на наш канал ?" ), reply_markup=subscribe_keyboard() ) return await message.answer("Добро пожаловать! ?") @router.callback_query(F.data == "check_sub") async def check_sub_callback(callback: CallbackQuery, bot: Bot): is_subscribed = await check_subscription(bot, callback.from_user.id) if not is_subscribed: await callback.answer( "❌ Ты ещё не подписался!", show_alert=True ) return await callback.message.delete() await callback.message.answer("✅ Подписка подтверждена! Добро пожаловать.") await callback.answer()
Плюсы подхода:
Быстрый рост подписчиков канала
Канал можно дополнительно монетизировать через рекламу и платные посты
Пользователи уже «тёплые» — знакомы с брендом до того как начали пользоваться ботом
3. Эффекты сообщений: бот с настроением
Мало кто знает, что Telegram позволяет отправлять сообщения с анимационными эффектами — конфетти, огонь, сердечки. Задаётся через параметр message_effect_id.
Доступные эффекты:
Эффект |
ID |
|---|---|
? Огонь |
|
? Лайк |
|
? Дизлайк |
|
❤️ Сердце |
|
? Конфетти |
|
? Какашка |
|
from aiogram import Router from aiogram.types import Message from aiogram.filters import Command router = Router() # ID эффектов EFFECT_FIRE = "5104841245755180586" EFFECT_CONFETTI = "5046509860389126442" EFFECT_HEART = "5044134455711629726" @router.message(Command("buy_success")) async def payment_success(message: Message): await message.answer( "? Оплата прошла успешно!", message_effect_id=EFFECT_CONFETTI ) @router.message(Command("like")) async def send_like(message: Message): await message.answer( "Спасибо за отзыв! ❤️", message_effect_id=EFFECT_HEART ) @router.message(Command("hot_deal")) async def hot_deal(message: Message): await message.answer( "? Горячее предложение — только сегодня!", message_effect_id=EFFECT_FIRE )
Где использовать:
Успешная оплата → конфетти
Приветствие нового пользователя → сердечко
Срочная акция → огонь
Ошибка → дизлайк (да, и такое бывает уместно)
Эффекты — мелочь, но именно из таких мелочей складывается ощущение «живого» бота.
4. Стриминг ответа: текст появляется в реальном времени
Вместо того чтобы ждать пока бот обработает запрос и пришлёт сообщение целиком — текст появляется прямо по мере генерации. Так работает ChatGPT, так работают все современные AI-боты.
Механика простая: отправляем пустое сообщение, потом редактируем его каждые N миллисекунд добавляя новые куски текста.
import asyncio from aiogram import Router from aiogram.types import Message from aiogram.filters import Command from typing import AsyncGenerator router = Router() # Симуляция стриминга — замени на реальный AI (OpenAI, etc.) async def fake_stream(text: str) -> AsyncGenerator[str, None]: for word in text.split(): yield word + " " await asyncio.sleep(0.1) async def stream_reply(message: Message, generator: AsyncGenerator) -> None: # Отправляем сообщение с курсором sent = await message.answer("▌") full_text = "" last_edit = asyncio.get_event_loop().time() async for chunk in generator: full_text += chunk now = asyncio.get_event_loop().time() # Редактируем не чаще чем раз в 0.5 сек — иначе flood limit if now - last_edit >= 0.5: await sent.edit_text(full_text + "▌") last_edit = now # Финальное сообщение без курсора await sent.edit_text(full_text) @router.message(Command("ask")) async def handle_ask(message: Message) -> None: response_text = "Это пример стриминга в Telegram боте. Текст появляется постепенно прямо как в ChatGPT." await stream_reply(message, fake_stream(response_text))
С реальным OpenAI стримингом:
from openai import AsyncOpenAI client = AsyncOpenAI(api_key="your_key") async def openai_stream(user_message: str) -> AsyncGenerator[str, None]: stream = await client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": user_message}], stream=True ) async for chunk in stream: delta = chunk.choices[0].delta.content if delta: yield delta @router.message(Command("gpt")) async def handle_gpt(message: Message) -> None: await stream_reply(message, openai_stream(message.text))
Главное про flood limit: Telegram не даёт редактировать сообщение чаще чем раз в секунду. Поэтому копим чанки и редактируем с задержкой 0.5s — так и плавно и без бана.
Если хочешь попробовать как это ощущается на практике, то у меня есть бесплатный телеграм-бот. Это бот с четырьмя моделями внутри: ChatGPT, Gemini, Grok и DeepSeek. Отвечает в режиме реального времени — текст появляется прямо по мере генерации.
5. Онбординг с прогресс-баром: снижаем drop-off при регистрации
Когда пользователь регистрируется в боте и не видит прогресса — он бросает на середине. Прогресс-бар [■■■□□] решает это. Человек видит что осталось 2 шага из 5 и продолжает.
from aiogram import Router, F from aiogram.types import Message, ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove from aiogram.filters import CommandStart from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup router = Router() class Registration(StatesGroup): name = State() age = State() city = State() phone = State() def progress_bar(current: int, total: int) -> str: filled = "■" * current empty = "□" * (total - current) return f"[{filled}{empty}] Шаг {current} из {total}" @router.message(CommandStart()) async def start_registration(message: Message, state: FSMContext): await state.set_state(Registration.name) await message.answer( f"{progress_bar(1, 4)}\n\n" f"? Привет! Давай познакомимся.\n" f"Как тебя зовут?", reply_markup=ReplyKeyboardRemove() ) @router.message(Registration.name) async def get_name(message: Message, state: FSMContext): await state.update_data(name=message.text) await state.set_state(Registration.age) await message.answer( f"{progress_bar(2, 4)}\n\n" f"Отлично, {message.text}! Сколько тебе лет?" ) @router.message(Registration.age) async def get_age(message: Message, state: FSMContext): await state.update_data(age=message.text) await state.set_state(Registration.city) skip_keyboard = ReplyKeyboardMarkup( keyboard=[[KeyboardButton(text="Пропустить")]], resize_keyboard=True ) await message.answer( f"{progress_bar(3, 4)}\n\n" f"Из какого ты города?", reply_markup=skip_keyboard ) @router.message(Registration.city) async def get_city(message: Message, state: FSMContext): city = None if message.text == "Пропустить" else message.text await state.update_data(city=city) await state.set_state(Registration.phone) phone_keyboard = ReplyKeyboardMarkup( keyboard=[[KeyboardButton(text="? Поделиться номером", request_contact=True)]], resize_keyboard=True ) await message.answer( f"{progress_bar(4, 4)}\n\n" f"Последний шаг — поделись номером телефона:", reply_markup=phone_keyboard ) @router.message(Registration.phone, F.contact) async def get_phone(message: Message, state: FSMContext): data = await state.get_data() await state.clear() await message.answer( f"✅ Готово! Регистрация завершена.\n\n" f"Имя: {data['name']}\n" f"Возраст: {data['age']}\n" f"Город: {data.get('city', 'не указан')}", reply_markup=ReplyKeyboardRemove() )
Прогресс-бар — психологический трюк. Люди не любят бросать незаконченное дело. Особенно когда видят что осталось чуть-чуть.
6. Персонализация: бот который тебя помнит
Персонализация — это не просто «Привет, Иван». Это когда бот помнит что ты делал, предлагает то что тебе актуально и не заставляет вводить одно и то же по второму кругу.
from aiogram import Router, F from aiogram.types import Message from aiogram.filters import CommandStart from datetime import datetime router = Router() # Простой in-memory стор (в продакшене заменяй на БД) user_storage: dict = {} def get_user(user_id: int) -> dict: if user_id not in user_storage: user_storage[user_id] = { "name": None, "visits": 0, "last_visit": None, "last_action": None } return user_storage[user_id] @router.message(CommandStart()) async def start(message: Message): user_id = message.from_user.id first_name = message.from_user.first_name user = get_user(user_id) user["visits"] += 1 user["name"] = first_name last_visit = user["last_visit"] user["last_visit"] = datetime.now() # Первый визит if user["visits"] == 1: await message.answer( f"? Привет, {first_name}! Рад познакомиться.\n" f"Вот что я умею: ..." ) # Возвращающийся пользователь elif user["last_action"]: await message.answer( f"С возвращением, {first_name}! ?\n" f"В прошлый раз ты {user['last_action']}.\n" f"Продолжим?" ) # Постоянный пользователь else: await message.answer( f"О, {first_name}! Уже {user['visits']}-й визит ?\n" f"Рад видеть постоянного гостя!" ) @router.message(F.text == "? Поиск") async def search(message: Message): user = get_user(message.from_user.id) user["last_action"] = "использовал поиск" # ... логика поиска
Что ещё можно персонализировать:
Язык интерфейса по
language_codeизmessage.from_userРекомендации на основе истории действий
Напоминания с упоминанием имени
Разные онбординги для разных сегментов пользователей
7. Кастомный плейсхолдер: подсказка прямо в строке ввода
Когда пользователь открывает бота, в строке ввода написано «Сообщение». Нейтрально, но не информативно. Параметр input_field_placeholder позволяет заменить этот текст на свой.
Обычный плейсхолдер:

Кастомный плейсхолдер:

from aiogram import Router from aiogram.types import Message, ReplyKeyboardMarkup, KeyboardButton from aiogram.filters import CommandStart router = Router() def main_keyboard() -> ReplyKeyboardMarkup: return ReplyKeyboardMarkup( keyboard=[[KeyboardButton(text="? Главное меню")]], resize_keyboard=True, is_persistent=True, # клавиатура не скрывается после нажатия input_field_placeholder="Введите название города...", ) @router.message(CommandStart()) async def start(message: Message): await message.answer( text="Привет! Введи город и я покажу погоду.", reply_markup=main_keyboard(), )
Параметр is_persistent=True нужен чтобы клавиатура не скрывалась после нажатия — иначе плейсхолдер пропадёт вместе с ней.
Примеры под разные боты:
# Бот погоды input_field_placeholder="Введите название города..." # Бот поддержки input_field_placeholder="Опишите вашу проблему..." # Финансовый бот input_field_placeholder="Введите сумму в рублях..." # Поиск по каталогу input_field_placeholder="Начните вводить название товара..."
Ограничения:
Максимум 64 символа
Поддерживает эмодзи
Работает только с
ReplyKeyboardMarkup— для инлайн-клавиатур недоступноЧисто визуальная штука, на логику бота не влияет
Если после этой статьи захотелось сделать своего бота — попробуй @GenerateYourAIBot. Пишешь промпт с описанием что должен делать бот, он генерирует готовую архитектуру: структуру файлов, хэндлеры, FSM, логику. Не заменяет руки, но сильно ускоряет старт — особенно когда проект нестандартный и не хочется думать с чего начать.
Итого
Вот что у нас получилось:
Фича |
Сложность |
Что даёт |
|---|---|---|
Stars / платный контент |
⭐⭐⭐ |
Монетизация без эквайринга |
Обязательная подписка |
⭐⭐ |
Быстрый рост канала |
Эффекты сообщений |
⭐ |
Ощущение живого бота |
Typing imitation |
⭐ |
Естественный диалог |
Прогресс-бар онбординга |
⭐⭐ |
Снижение drop-off |
Персонализация |
⭐⭐ |
Лояльность пользователей |
Кастомный плейсхолдер |
⭐ |
Чище UX без лишнего текста |
Если времени мало — начни с эффектов сообщений и typing imitation. Пять минут кода, а разница заметна сразу. Потом добавляй прогресс-бар и персонализацию — это уже влияет на удержание.