Мне нужна была CRM. Bitrix24 — от 2000₽/мес. AmoCRM — от 1500₽/мес. Для 5–10 лидов в месяц — абсурд.

Потом я посмотрел на свой Telegram. Групповые чаты с топиками — это по сути тикет-система: один топик = один лид, вся переписка в одном месте, поиск, уведомления на телефон. Не хватало только входной точки — бота, который квалифицирует лид и создаёт топик автоматически.

Я написал такого бота на Python, задеплоил на Yandex Cloud Functions — и получил мини-CRM за 0 ₽/мес.

Важный дисклеймер: это не замена Bitrix. Нет воронок, нет дашбордов, нет автоматических follow-up. Это для соло-основателей и маленьких команд, которые обрабатывают до 20 лидов в месяц и не хотят платить за CRM, которую используют на 10%.

Что получится

Схема работы:

Пользователь на сайте
       ↓
t.me/your_bot?start=website
       ↓
Бот: 2 вопроса (inline-кнопки)
       ↓
Yandex Cloud Function
       ↓
Telegram-группа с топиками
  ├── "Иван — E-commerce — AI-агент"
  ├── "Мария — Услуги — Консультация"
  └── ...

Пользователь нажимает CTA на сайте → попадает в бот → отвечает на два вопроса кнопками → бот создаёт топик в админской группе с карточкой лида → дальше вы общаетесь через бота, а вся переписка лежит в топике.

Стоимость:

Ресурс

Стоимость

Cloud Function

0 ₽ (1 млн вызовов/мес бесплатно)

Object Storage

0 ₽ (1 ГБ бесплатно)

DNS + SSL-сертификат

0 ₽

Итого

0 ₽/мес

API Gateway не нужен — allow-unauthenticated-invoke делает функцию доступной по HTTPS напрямую, этого достаточно для Telegram webhook.

Как написать бот для приема заявок в Telegram на Python

Почему без фреймворков

aiogram, python-telegram-bot — отличные библиотеки для сложных ботов. Но для Cloud Functions они избыточны:

  • Cold start. Тяжёлые фреймворки добавляют 1–3 секунды к первому вызову. httpx импортируется мгновенно.

  • Polling не нужен. Cloud Functions работают по webhook — фреймворк с polling просто не запустится.

  • Роутинг не нужен. У нас одна точка входа, два типа апдейтов. if/elif хватает.

Весь API-слой — 57 строк кода. Вот обёртка над Telegram Bot API:

"""telegram_api.py — тонкая обёртка над Bot API."""
import os
import httpx

BOT_TOKEN = os.environ.get("BOT_TOKEN", "")
BASE_URL = f"https://api.telegram.org/bot{BOT_TOKEN}"

def _call(method: str, **kwargs) -> dict:
    resp = httpx.post(f"{BASE_URL}/{method}", json=kwargs, timeout=10)
    return resp.json()

def send_message(chat_id, text, reply_markup=None, message_thread_id=None):
    params = {"chat_id": chat_id, "text": text, "parse_mode": "HTML"}
    if reply_markup:
        params["reply_markup"] = reply_markup
    if message_thread_id:
        params["message_thread_id"] = message_thread_id
    return _call("sendMessage", **params)

def create_forum_topic(chat_id, name):
    return _call("createForumTopic", chat_id=chat_id, name=name)

def copy_message(chat_id, from_chat_id, message_id, message_thread_id=None):
    params = {"chat_id": chat_id, "from_chat_id": from_chat_id, "message_id": message_id}
    if message_thread_id:
        params["message_thread_id"] = message_thread_id
    return _call("copyMessage", **params)

Почему copy_message, а не forward_message? При forward Telegram добавляет заголовок “Переслано от…” — выглядит неаккуратно. copy_message отправляет чистое сообщение без маркера пересылки.

Точка входа: webhook-хендлер

Cloud Function получает HTTP-запрос от Telegram. Первое, что делаем — валидируем секретный токен:

def handler(event, context):
    """Yandex Cloud Function entry point."""
    headers = event.get("headers", {})
    secret = headers.get("X-Telegram-Bot-Api-Secret-Token") or \
             headers.get("x-telegram-bot-api-secret-token", "")
    if secret != WEBHOOK_SECRET:
        return {"statusCode": 403, "body": "Forbidden"}

    body = event.get("body", "{}")
    update = json.loads(body) if isinstance(body, str) else body

    if "callback_query" in update:
        _handle_callback(update["callback_query"])
    elif "message" in update:
        _handle_message(update["message"])

    return {"statusCode": 200, "body": "ok"}

secret_token задаётся при установке webhook — Telegram присылает его в каждом запросе. Без этой проверки кто угодно может слать фейковые апдейты на URL вашей функции.

Квалификация лида: inline-кнопки

По команде /start бот задаёт два вопроса. Первый — про бизнес:

def _handle_start(chat_id, from_user):
    # Защита от повторных заявок
    if storage.get_topic_id_by_user(chat_id):
        tg.send_message(chat_id,
            "Вы уже оставили заявку. Мы свяжемся с вами в ближайшее время.\n\n"
            "Если хотите что-то добавить — просто напишите сообщение здесь.")
        return

    keyboard = {"inline_keyboard": [
        [{"text": "E-commerce / Маркетплейсы", "callback_data": "q1:ecommerce"}],
        [{"text": "Услуги / Сервис",           "callback_data": "q1:services"}],
        [{"text": "Производство",               "callback_data": "q1:production"}],
        [{"text": "IT / Стартап",               "callback_data": "q1:it"}],
        [{"text": "Другое",                     "callback_data": "q1:other"}],
    ]}
    tg.send_message(chat_id, f"Привет, {from_user.get('first_name', '')}! Я бот VexAI.\n\n"
                              "Ответьте на 2 коротких вопроса, и мы свяжемся с вами.")
    tg.send_message(chat_id, "<b>Чем занимается ваш бизнес?</b>", reply_markup=keyboard)

Обратите внимание на защиту от дублей: если пользователь уже оставлял заявку, бот не создаёт второй топик, а предлагает написать в существующий тред. Один топик = один клиент.

Трюк с callback data: stateless-квалификация

Cloud Functions — stateless. Каждый вызов — чистый лист. Чтобы не лезть в хранилище на каждый клик, я кодирую ответ Q1 прямо в callback data Q2:

def _send_q2(chat_id, q1_key):
    keyboard = {"inline_keyboard": [
        [{"text": "Автоматизация продаж",    "callback_data": f"q2:sales:{q1_key}"}],
        [{"text": "Обработка данных",         "callback_data": f"q2:data:{q1_key}"}],
        [{"text": "AI-агент / чат-бот",       "callback_data": f"q2:agent:{q1_key}"}],
        [{"text": "Консультация",             "callback_data": f"q2:consult:{q1_key}"}],
        [{"text": "Другое",                   "callback_data": f"q2:other:{q1_key}"}],
    ]}
    tg.send_message(chat_id, "<b>Какую задачу хотите решить с помощью AI?</b>",
                    reply_markup=keyboard)

Когда пользователь нажимает кнопку Q2, callback приходит в виде q2:sales:ecommerce — оба ответа в одной строке. Ни одного обращения к хранилищу на happy path.

Ограничение: callback data — максимум 64 байта. Для предустановленных вариантов хватает. А для “Другое” (свободный текст) приходится использовать Object Storage — записываем временный state файл state/{chat_id}.json, который удаляется после завершения flow.

Финализация: создание топика

После двух ответов бот создаёт топик в админской группе:

def _finalize(chat_id, from_user, q1_answer, q2_answer):
    tg.send_message(chat_id, "Спасибо! Свяжемся с вами в ближайшее время.")

    full_name = f"{from_user.get('first_name', '')} {from_user.get('last_name', '')}".strip()
    topic_title = f"{full_name} — {q1_answer} — {q2_answer}"
    if len(topic_title) > 128:  # лимит Telegram
        topic_title = topic_title[:125] + "..."

    result = tg.create_forum_topic(ADMIN_GROUP_ID, topic_title)
    topic_id = result["result"]["message_thread_id"]

    summary = (f"<b>Новый лид</b>\n\n"
               f"<b>Имя:</b> {full_name}\n"
               f"<b>Бизнес:</b> {q1_answer}\n"
               f"<b>Задача:</b> {q2_answer}\n"
               f"<b>Источник:</b> website")
    tg.send_message(ADMIN_GROUP_ID, summary, message_thread_id=topic_id)

    # Сохраняем маппинг для relay
    storage.save_topic_mapping(topic_id, chat_id)
    storage.save_user_mapping(chat_id, topic_id)

Двусторонний relay

Самая CRM-подобная фича. Вы отвечаете в топике — бот пересылает сообщение клиенту. Клиент отвечает боту — бот пересылает в топик.

def _handle_message(msg):
    chat = msg.get("chat", {})
    chat_id = chat.get("id")
    chat_type = chat.get("type", "")

    # Админ отвечает в топике группы → пересылаем клиенту
    if chat_type in ("supergroup", "group") and chat_id == ADMIN_GROUP_ID:
        topic_id = msg.get("message_thread_id")
        if not topic_id:
            return
        if msg.get("from", {}).get("id") != ADMIN_USER_ID:
            return
        user_chat_id = storage.get_chat_id_by_topic(topic_id)
        if user_chat_id:
            tg.copy_message(chat_id=user_chat_id, from_chat_id=chat_id,
                           message_id=msg["message_id"])
        return

    # Клиент пишет боту → пересылаем в его топик
    if chat_type == "private":
        topic_id = storage.get_topic_id_by_user(chat_id)
        if topic_id:
            tg.copy_message(chat_id=ADMIN_GROUP_ID, from_chat_id=chat_id,
                           message_id=msg["message_id"], message_thread_id=topic_id)

Клиент видит обычный чат с ботом. Вы видите организованный список лидов в Telegram-группе. Каждый диалог — в своём топике, с историей и поиском.

Бонус: трекинг источника через /start

Параметр /start поддерживает deep link: t.me/your_bot?start=website, ?start=habr, ?start=instagram. Бот получает это в тексте сообщения как /start website. Сейчас у меня источник захардкожен как “website”, но распарсить параметр и включить в карточку лида — 3 строчки кода.

Мини-CRM на базе Telegram: что умеет и чего не умеет

Настройка группы

  1. Создайте supergroup в Telegram

  2. Включите Топики (режим форума) в настройках группы

  3. Добавьте бота как админа с правами: Управление темами + Отправка сообщений

Что вы получаете

  • Один топик = один лид — полная история переписки в одном месте

  • Поиск по всем лидам — Telegram ищет по тексту внутри всех топиков

  • Закреплённые сообщения — закрепите карточку лида в начале топика

  • Уведомления на телефон — никакого отдельного приложения

  • Двусторонний relay — отвечаете в топике, клиент получает в боте. И наоборот.

Чего нет

Функция

Мини-CRM

Bitrix/AmoCRM

Приём заявок

Переписка с клиентом

Воронка продаж

Автоматические follow-up

Аналитика

Командная работа

Стоимость

0 ₽

от 1500 ₽/мес

Для соло-основателей с 0–20 лидами в месяц — хватает. Когда нужно трекать стадии воронки или подключать команду — переезжайте на нормальную CRM.

Claude Code: от идеи до работающего бота за одну сессию

Весь этот бот — от первой строчки до работающего webhook — был написан и задеплоен в одной сессии с Claude Code. Я описал задачу: “Нужен Telegram-бот для квалификации лидов, деплой на Yandex Cloud Functions.” Дальше Claude Code:

  1. Написал код бота (handler, API-обёртку, storage)

  2. Собрал zip-пакет с зависимостями

  3. Задеплоил через yc CLI на Cloud Functions

  4. Настроил webhook с secret token

Claude Code использует кастомный навык (skill) для Yandex Cloud — набор команд yc CLI для работы с Object Storage, Cloud Functions, DNS и сертификатами. Навык open source — можно подключить к своему Claude Code и деплоить на Yandex Cloud голосом.

Как задеплоить Telegram-бот на Yandex Cloud Functions бесплатно

Что нужно заранее

  • Аккаунт Yandex Cloud (free tier)

  • Сервис-аккаунт с ролью storage.editor

  • Статические ключи доступа: yc iam access-key create --service-account-name <sa>

  • Приватный бакет Object Storage для состояния бота

  • Токен бота от @BotFather

  • Chat ID группы и ваш Telegram user ID

Пошагово

1. Собрать пакет:

cd bot
pip3 install -t package httpx boto3
cp *.py package/
cd package && zip -qr ../bot-function.zip . && cd ..
rm -rf package

2. Загрузить в Object Storage:

yc storage s3 cp bot-function.zip s3://my-bot-data/deploy/bot-function.zip

3. Создать функцию:

yc serverless function create --name my-bot

4. Задеплоить версию:

yc serverless function version create \
  --function-name my-bot \
  --runtime python312 \
  --entrypoint handler.handler \
  --memory 128m \
  --execution-timeout 10s \
  --package-bucket-name my-bot-data \
  --package-object-name deploy/bot-function.zip \
  --environment "BOT_TOKEN=<TOKEN>,WEBHOOK_SECRET=<SECRET>,ADMIN_GROUP_ID=<GROUP_ID>,ADMIN_USER_ID=<USER_ID>,S3_BUCKET=my-bot-data,S3_ENDPOINT=https://storage.yandexcloud.net,AWS_ACCESS_KEY_ID=<KEY_ID>,AWS_SECRET_ACCESS_KEY=<KEY>" \
  --service-account-id <sa-id>

5. Открыть публичный доступ:

yc serverless function allow-unauthenticated-invoke --name my-bot

6. Установить webhook:

curl -X POST "https://api.telegram.org/bot<TOKEN>/setWebhook" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://functions.yandexcloud.net/<function-id>","secret_token":"<SECRET>"}'

7. Тест: откройте бота в Telegram, отправьте /start.

Подводные камни: что пошло не так

1. “Not enough rights to create a topic”

Бот добавлен как админ, но топики не создаёт. Причина: нужно явное разрешение “Управление темами” в правах администратора. Добавить как админа недостаточно — проверяйте каждое разрешение отдельно.

2. Privacy mode блокирует relay

По умолчанию боты в группах видят только команды (сообщения с /). Для relay нужно видеть все сообщения. Решение: @BotFather/setprivacyDisable.

3. Лимит 128 символов на название топика

Telegram молча обрезает длинные названия. Если в ответах Q1+Q2 много текста, название топика будет неинформативным. Обрезайте явно в коде:

if len(topic_title) > 128:
    topic_title = topic_title[:125] + "..."

4. Callback data — максимум 64 байта

Паттерн q2:sales:ecommerce влезает. Но если пользователь выбирает “Другое” и пишет свободный текст — callback не подходит. Поэтому “Другое” переключается на Object Storage для хранения промежуточного состояния.

5. Cold start ~2 секунды

Первый вызов после простоя — медленный. httpx импортируется быстро, но boto3 весит ~30 МБ и тормозит. Не критично: Telegram ждёт ответа 60 секунд. Но если хочется быстрее — можно заменить boto3 на raw HTTP к S3 API.

6. Безопасность: секреты в переменных окружения

В статье мы передаём секреты через --environment — это самый простой способ, но давайте разберём реальные риски.

Что хранится в env vars:

Секрет

Что будет, если утечёт

BOT_TOKEN

Полный контроль над ботом: чтение сообщений, отправка от его имени

WEBHOOK_SECRET

Возможность слать фейковые апдейты на функцию

AWS_ACCESS_KEY_ID + SECRET

Доступ к бакету с маппингами (chat_id пользователей)

ADMIN_GROUP_ID, ADMIN_USER_ID

Не секреты — просто числа, без токена бесполезны

Единственный по-настоящему чувствительный секрет — BOT_TOKEN. Остальное без токена бесполезно: webhook secret проверяется только в связке с ботом, а S3-ключи дают доступ к бакету с JSON-файлами, где лежат только chat_id.

Кто может увидеть env vars:

  1. Пользователи Yandex Cloud с ролью functions.editor или admin на вашу папку (folder)

  2. Те, кто имеет доступ к вашей машине — команда попадает в ~/.zsh_history

Если вы единственный пользователь в Yandex Cloud и единственный, кто работает на этой машине — реальный вектор атаки отсутствует. Для соло-проекта с лид-ботом это приемлемый trade-off.

Когда стоит перейти на Yandex Lockbox:

  • В проекте появились другие люди с доступом к Yandex Cloud

  • Бот обрабатывает чувствительные данные (платежи, персональные данные)

  • Вы деплоите через CI/CD, где логи команд могут сохраняться

# Создать секрет в Lockbox
yc lockbox secret create --name bot-secrets \
  --payload '[{"key":"BOT_TOKEN","text_value":"123:ABC..."}]'

# Передать в функцию — секрет инжектится в runtime,
# но не хранится в метаданных функции
yc serverless function version create \
  --function-name my-bot \
  --secret name=bot-secrets,key=BOT_TOKEN,environment-variable=BOT_TOKEN \
  ...

Lockbox бесплатен для первых 3 секретов. Но для соло-бота с 5 лидами в месяц — --environment достаточно.

Итого

Мини-CRM в Telegram: бот для квалификации лидов + группа с топиками для переписки. Ноль рублей в месяц, ноль инфраструктуры, работает с телефона.

Что

Как

Приём лидов

Telegram-бот с inline-кнопками

Хранение

Топики в Telegram-группе

Переписка

Двусторонний relay через бота

Хостинг

Yandex Cloud Functions (бесплатно)

Состояние

Object Storage (бесплатно)

Навыки Claude Code для Yandex Cloud (open source): GitHub


Делаю AI-автоматизации для бизнеса: агенты, интеграции с API, чат-боты с LLM под капотом. Если есть задача — напишите в Telegram.