Большинство 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

? Огонь

5104841245755180586

? Лайк

5107584321108051014

? Дизлайк

5104858069142078462

❤️ Сердце

5044134455711629726

? Конфетти

5046509860389126442

? Какашка

5046589136895476101

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. Пять минут кода, а разница заметна сразу. Потом добавляй прогресс-бар и персонализацию — это уже влияет на удержание.

Комментарии (0)