🤖 Настройка кнопок Telegram-бота EastPay
Дата: 2026-02-12
Связанный промт:5-version-promt.md
Платформа: n8n + NextBot + Supabase
📐 1. Архитектура кнопочного меню
1.1. Обзор структуры
flowchart TD
START["/start"] --> WELCOME["👋 Приветственное сообщение"]
WELCOME --> MENU["📋 Главное меню (3 кнопки)"]
MENU --> BTN1["💱 Узнать курс"]
MENU --> BTN2["📝 Заказать обмен"]
MENU --> BTN3["📦 Статус обмена"]
BTN1 --> RATES_SUB["Подменю валютных пар"]
RATES_SUB --> R1["🇹🇭 THB/RUB"]
RATES_SUB --> R2["🇺🇸 USD/RUB"]
RATES_SUB --> R3["🇨🇳 CNY/RUB"]
RATES_SUB --> R4["🇦🇪 AED/RUB"]
RATES_SUB --> R5["💎 USDT/RUB"]
RATES_SUB --> R6["📊 Все курсы"]
R1 --> SB_RATES[("Supabase: daily_rates")]
R6 --> SB_RATES
BTN2 --> CITY_SELECT["Выбор города"]
CITY_SELECT --> C1["🏝️ Bangkok"]
CITY_SELECT --> C2["🏝️ Phuket"]
CITY_SELECT --> C3["🏜️ Dubai"]
CITY_SELECT --> C4["🕌 Istanbul"]
CITY_SELECT --> C5["🏙️ Moscow"]
C1 --> PAIR_SELECT["Выбор валюты"]
PAIR_SELECT --> AMOUNT_INPUT["Ввод суммы (текст)"]
AMOUNT_INPUT --> SB_CALC[("Supabase: calculate_deal()")]
SB_CALC --> CONFIRM["✅ Подтвердить / ❌ Отменить"]
CONFIRM --> SB_ORDER[("Supabase: bot_create_order()")]
BTN3 --> STATUS_CHECK["Проверка статуса"]
STATUS_CHECK --> SB_ORDERS[("Supabase: orders")]
SB_ORDERS --> STATUS_DISPLAY["Статус заявки #ID"]
1.2. Типы кнопок в Telegram
| Тип кнопки | Назначение | Когда использовать |
|---|---|---|
| ReplyKeyboardMarkup | Постоянные кнопки снизу чата (клавиатура) | Главное меню «Узнать курс / Заказать обмен / Статус» |
| InlineKeyboardMarkup | Кнопки под сообщением бота | Подменю (выбор валюты, города, подтверждение) |
| ReplyKeyboardRemove | Убрать клавиатуру | После завершения сценария |
🔘 2. Главное меню (Persistent Keyboard)
2.1. Конфигурация Reply Keyboard
После /start или команды показа меню бот отправляет сообщение с ReplyKeyboardMarkup:
{
"reply_markup": {
"keyboard": [
[
{ "text": "💱 Узнать курс" },
{ "text": "📝 Заказать обмен" }
],
[
{ "text": "📦 Статус обмена" }
]
],
"resize_keyboard": true,
"one_time_keyboard": false,
"is_persistent": true,
"input_field_placeholder": "Выберите действие или задайте вопрос..."
}
}
Параметры:
resize_keyboard: true— кнопки компактные, не занимают пол-экрана.one_time_keyboard: false— меню не исчезает после нажатия.is_persistent: true— меню всегда доступно, даже после перезапуска бота.
2.2. Настройка в n8n
В n8n ноде Send Message (Telegram):
- Text: Приветственное сообщение.
- Reply Markup → Reply Keyboard:
- Добавьте 2 ряда кнопок:
- Ряд 1:
💱 Узнать курс,📝 Заказать обмен - Ряд 2:
📦 Статус обмена
- Ряд 1:
- Установите
Resize Keyboard = true,One-Time Keyboard = false.
- Добавьте 2 ряда кнопок:
2.3. Настройка в NextBot
В NextBot используйте Сценарий → Событие «Начало диалога»:
- Действие: «Отправить сообщение»
- Тип: «Кнопки (Reply Keyboard)»
- Добавьте 3 кнопки с текстом:
- 💱 Узнать курс
- 📝 Заказать обмен
- 📦 Статус обмена
💱 3. Кнопка «Узнать курс» — Подробная логика
3.1. Триггер
Пользователь нажимает кнопку → Telegram отправляет текст 💱 Узнать курс → n8n Switch ловит это как текстовое сообщение.
3.2. Обработка в n8n Switch
В n8n ноде Switch добавьте новую ветку:
Условие: {{ $json.message.text }} contains "Узнать курс"
3.3. Inline-кнопки с выбором валюты
{
"text": "📊 Выберите валютную пару:",
"reply_markup": {
"inline_keyboard": [
[
{ "text": "🇹🇭 THB/RUB", "callback_data": "rate_THB/RUB" },
{ "text": "🇺🇸 USD/RUB", "callback_data": "rate_USD/RUB" }
],
[
{ "text": "🇨🇳 CNY/RUB", "callback_data": "rate_CNY/RUB" },
{ "text": "🇦🇪 AED/RUB", "callback_data": "rate_AED/RUB" }
],
[
{ "text": "💎 USDT/RUB", "callback_data": "rate_USDT/RUB" },
{ "text": "🇪🇺 EUR/RUB", "callback_data": "rate_EUR/RUB" }
],
[
{ "text": "📊 Все курсы сразу", "callback_data": "rate_ALL" }
]
]
}
}
3.4. Supabase-запрос по callback
При выборе конкретной валюты (rate_THB/RUB):
-- n8n Supabase Node: Read Row from daily_rates
SELECT symbol, rate, updated_at
FROM daily_rates
WHERE symbol = 'THB/RUB' AND is_active = true;
При выборе «Все курсы» (rate_ALL):
-- n8n Supabase Node: Read All from daily_rates
SELECT symbol, rate, updated_at
FROM daily_rates
WHERE is_active = true
ORDER BY symbol;
3.5. Формат ответа
Один курс:
📊 Курс THB/RUB
💱 1 THB = 2.85 RUB
📅 Обновлено: 12.02.2026, 16:00 (ICT)
Рассчитать сумму обмена? 👇
- Inline-кнопка:
[💱 Рассчитать обмен] callback_data: "calc_THB/RUB"
Все курсы:
📊 Актуальные курсы EastPay
| Пара | Курс |
|------|------|
| 🇹🇭 THB/RUB | 2.85 |
| 🇺🇸 USD/RUB | 96.00 |
| 🇨🇳 CNY/RUB | 13.40 |
| 🇦🇪 AED/RUB | 26.80 |
| 💎 USDT/RUB | 98.50 |
| 🇪🇺 EUR/RUB | 105.00 |
| 🇹🇷 TRY/RUB | 3.10 |
📅 Обновлено: 12.02.2026
3.6. Альтернатива: AI-агент обрабатывает кнопку
Вместо отдельной ветки Switch можно направить текст 💱 Узнать курс прямо в AI-агента — он сам вызовет get_rates_tool и ответит. Это предпочтительный подход, если AI-агент уже настроен и работает стабильно.
📝 4. Кнопка «Заказать обмен» — Подробная логика
4.1. Триггер
Пользователь нажимает 📝 Заказать обмен → текст уходит в n8n.
4.2. Пошаговый сценарий (Step-by-step)
sequenceDiagram
participant U as 👤 Клиент
participant BOT as 🤖 Бот
participant AI as 🧠 AI Agent
participant SB as 🗄️ Supabase
U->>BOT: 📝 Заказать обмен
BOT->>U: 🏙️ Выберите город (Inline Keyboard)
U->>BOT: Bangkok (callback)
BOT->>U: 💱 Выберите валюту (Inline Keyboard)
U->>BOT: THB/RUB (callback)
BOT->>U: 💰 Введите сумму в рублях
U->>BOT: 100000
BOT->>SB: RPC: calculate_deal('Bangkok', 100000, 'THB/RUB')
SB-->>BOT: {rate: 2.85, amount_get: 35088, currency_get: THB}
BOT->>U: 📊 Расчёт + [✅ Подтвердить] [❌ Отменить]
U->>BOT: ✅ Подтвердить (callback)
BOT->>SB: RPC: bot_create_order(tg_id, 'Bangkok', 100000, 'THB/RUB')
SB-->>BOT: {order_id: 42, status: 'new'}
BOT->>U: ✅ Заявка #42 создана! Менеджер свяжется...
4.3. Шаг 1: Выбор города (Inline Keyboard)
{
"text": "🏙️ В каком городе хотите совершить обмен?",
"reply_markup": {
"inline_keyboard": [
[
{ "text": "🏝️ Bangkok", "callback_data": "city_Bangkok" },
{ "text": "🏝️ Phuket", "callback_data": "city_Phuket" }
],
[
{ "text": "🏜️ Dubai", "callback_data": "city_Dubai" },
{ "text": "🕌 Istanbul", "callback_data": "city_Istanbul" }
],
[
{ "text": "🏙️ Москва", "callback_data": "city_Москва" }
]
]
}
}
Supabase-запрос для динамических городов:
SELECT DISTINCT name, country
FROM locations
WHERE is_active = true
ORDER BY name;
⚡ Лучшая практика: Вместо хардкода — загружай список городов из Supabase (
get_locations_toolили прямой запрос) и генерируй inline-кнопки динамически в n8n Code Node.
4.4. Шаг 2: Выбор валютной пары
После выбора города бот показывает доступные валютные пары:
{
"text": "💱 Какую валюту хотите обменять?\n\n📍 Город: Bangkok (🇹🇭 Таиланд)",
"reply_markup": {
"inline_keyboard": [
[
{ "text": "RUB → THB", "callback_data": "pair_THB/RUB" },
{ "text": "USDT → THB", "callback_data": "pair_THB/USDT" }
],
[
{ "text": "RUB → USD", "callback_data": "pair_USD/RUB" },
{ "text": "USDT → RUB", "callback_data": "pair_USDT/RUB" }
],
[
{ "text": "⬅️ Назад к городам", "callback_data": "back_cities" }
]
]
}
}
Фильтрация валют по городу из Supabase:
SELECT i.name, dr.symbol, dr.rate
FROM market_rates mr
JOIN instruments i ON i.id = mr.instrument_id
JOIN daily_rates dr ON dr.symbol LIKE '%' || i.base_currency || '%'
WHERE mr.location_id = (SELECT id FROM locations WHERE name = 'Bangkok' LIMIT 1)
AND i.is_active = true
AND dr.is_active = true;
4.5. Шаг 3: Ввод суммы
💰 Введите сумму, которую хотите обменять (в рублях):
📍 Bangkok | 💱 RUB → THB
Примеры: 50000, 100000, 500000
Ожидание: Бот ждёт текстовое сообщение с числом. Валидация:
- Число > 0
- Число — это число (не текст)
- Если введено неправильно → «Пожалуйста, введите сумму числом. Например: 100000»
4.6. Шаг 4: Расчёт + Подтверждение
Supabase RPC вызов:
SELECT * FROM calculate_deal('Bangkok', 100000, 'THB/RUB');
Ответ бота:
📊 Расчёт обмена:
📍 Город: Bangkok (Таиланд)
💱 Направление: RUB → THB
💰 Отдаёте: 100 000 ₽
💎 Получаете: ~35 088 ฿
📈 Курс: 2.8512
Подтверждаете обмен?
{
"reply_markup": {
"inline_keyboard": [
[
{ "text": "✅ Подтвердить", "callback_data": "confirm_order" },
{ "text": "❌ Отменить", "callback_data": "cancel_order" }
],
[
{ "text": "🔄 Пересчитать", "callback_data": "recalc_order" }
]
]
}
}
4.7. Шаг 5: Создание заявки
При нажатии ✅ Подтвердить:
Supabase RPC вызов:
SELECT * FROM bot_create_order(
'123456789', -- p_tg_id (telegram id клиента)
'Bangkok', -- p_city_name
100000, -- p_amount
'THB/RUB' -- p_currency_pair
);
Ответ бота:
✅ Заявка #42 создана!
📋 Детали:
• Город: Bangkok
• Направление: RUB → THB
• Сумма: 100 000 ₽ → ~35 088 ฿
• Статус: Новая
Менеджер свяжется с вами в ближайшее время. Спасибо! 🙏
Что-то ещё? 👇
4.8. Альтернатива: AI-агент ведёт сценарий
Вместо жёстких inline-кнопок AI-агент может вести весь диалог заказа текстом:
- Клиент: «Заказать обмен» → AI: «В каком городе?» → Клиент: «Бгк» → AI (понимает «Бангкок»): «Какую сумму и валюту?» → и т.д.
Преимущество AI-подхода: Обрабатывает свободный ввод («хочу 100к рублей на баты в бангке» → сразу всё понял).
Рекомендация: Гибридный подход — предлагать кнопки, но также принимать свободный текст через AI-агента.
📦 5. Кнопка «Статус обмена» — Подробная логика
5.1. Триггер
Пользователь нажимает 📦 Статус обмена → текст уходит в n8n.
5.2. Логика проверки
flowchart TD
BTN["📦 Статус обмена"] --> LOOKUP["Поиск заявок по telegram_id"]
LOOKUP --> SB[("Supabase: orders + clients")]
SB --> CHECK{{"Есть заявки?"}}
CHECK -->|"Да (1 заявка)"| SHOW_ONE["Показать статус"]
CHECK -->|"Да (несколько)"| SHOW_LIST["Список заявок (Inline)"]
CHECK -->|"Нет"| NO_ORDERS["«У вас пока нет заявок»"]
SHOW_LIST --> SELECT["Клиент выбирает #ID"]
SELECT --> SHOW_DETAIL["Детали заявки"]
5.3. Supabase-запрос для поиска заявок
-- Найти все активные заявки клиента по telegram_id
SELECT
o.id,
o.status,
o.amount_give,
o.currency_give,
o.amount_get,
o.currency_get,
o.exchange_rate,
l.name as city,
o.created_at,
o.updated_at
FROM orders o
JOIN clients c ON c.id = o.client_id
LEFT JOIN locations l ON l.id = o.location_id
WHERE c.telegram_id = :telegram_id
AND o.status NOT IN ('done', 'cancelled')
ORDER BY o.created_at DESC
LIMIT 5;
5.4. Новая RPC-функция для Supabase (рекомендация)
CREATE OR REPLACE FUNCTION bot_get_client_orders(p_tg_id TEXT)
RETURNS TABLE (
order_id BIGINT,
status order_status,
amount_give NUMERIC,
currency_give TEXT,
amount_get NUMERIC,
currency_get TEXT,
exchange_rate NUMERIC,
city TEXT,
created_at TIMESTAMPTZ
)
LANGUAGE plpgsql SECURITY DEFINER
AS $$
BEGIN
RETURN QUERY
SELECT
o.id,
o.status,
o.amount_give,
o.currency_give,
o.amount_get,
o.currency_get,
o.exchange_rate,
l.name,
o.created_at
FROM orders o
JOIN clients c ON c.id = o.client_id
LEFT JOIN locations l ON l.id = o.location_id
WHERE c.telegram_id = p_tg_id
ORDER BY o.created_at DESC
LIMIT 10;
END;
$$;
GRANT EXECUTE ON FUNCTION bot_get_client_orders TO service_role;
5.5. n8n Tool для AI-агента
Добавьте в n8n новый инструмент get_order_status_tool:
| Параметр | Значение |
|---|---|
| Имя: | get_order_status_tool |
| Описание: | Получить статус заявок клиента по telegram_id |
| Тип ноды: | Supabase RPC |
| RPC Function: | bot_get_client_orders |
| Параметры: | p_tg_id = {{ $json.telegram_id }} |
5.6. Маппинг статусов на русский
| Статус (DB) | На русском | Emoji |
|---|---|---|
new |
Новая | 🆕 |
calculated |
Рассчитана | 📊 |
payment_pending |
Ожидание оплаты | ⏳ |
processing |
В обработке | 🔄 |
delivery |
Доставка | 🚚 |
office |
В офисе | 🏢 |
done |
Завершена | ✅ |
cancelled |
Отменена | ❌ |
dispute |
Спор | ⚠️ |
5.7. Формат ответа — одна заявка
📦 Статус заявки #42
• 🆕 Статус: Новая
• 📍 Город: Bangkok
• 💱 Направление: RUB → THB
• 💰 Сумма: 100 000 ₽ → ~35 088 ฿
• 📅 Создана: 12.02.2026
Менеджер скоро возьмёт вашу заявку в работу.
Есть вопросы? Пишите! 😊
5.8. Формат ответа — несколько заявок
📦 Ваши активные заявки:
{
"reply_markup": {
"inline_keyboard": [
[{ "text": "#42 — RUB→THB — 🆕 Новая", "callback_data": "order_42" }],
[{ "text": "#38 — USDT→RUB — 🔄 В обработке", "callback_data": "order_38" }],
[{ "text": "#35 — RUB→AED — ⏳ Ожидание оплаты", "callback_data": "order_35" }]
]
}
}
5.9. Формат ответа — нет заявок
📦 У вас пока нет активных заявок.
Хотите создать новую? 💱
{
"reply_markup": {
"inline_keyboard": [
[{ "text": "📝 Заказать обмен", "callback_data": "start_order" }],
[{ "text": "💱 Узнать курс", "callback_data": "show_rates" }]
]
}
}
⚙️ 6. Реализация в n8n Workflow
6.1. Обновлённая архитектура Switch
flowchart LR
TG["📱 Telegram Trigger"] --> SW{{"Switch"}}
SW -->|"/start"| WELCOME["👋 Welcome + Menu"]
SW -->|"💱 Узнать курс"| RATES_FLOW["Rates Flow"]
SW -->|"📝 Заказать обмен"| ORDER_FLOW["Order Flow"]
SW -->|"📦 Статус обмена"| STATUS_FLOW["Status Flow"]
SW -->|"callback_query"| CALLBACK_HANDLER["Callback Router"]
SW -->|"voice"| AUDIO_FLOW["Audio → Transcribe"]
SW -->|"photo"| PHOTO_FLOW["Photo → Analyze"]
SW -->|"text (other)"| AI_AGENT["🤖 AI Agent"]
RATES_FLOW --> INLINE_RATES["Inline: Выбор валюты"]
ORDER_FLOW --> INLINE_CITIES["Inline: Выбор города"]
STATUS_FLOW --> SB_QUERY["Supabase: bot_get_client_orders"]
CALLBACK_HANDLER --> CB_SWITCH{{"Callback Switch"}}
CB_SWITCH -->|"rate_*"| SB_RATES["Supabase: daily_rates"]
CB_SWITCH -->|"city_*"| SHOW_PAIRS["Inline: Валютные пары"]
CB_SWITCH -->|"pair_*"| ASK_AMOUNT["Запросить сумму"]
CB_SWITCH -->|"confirm_order"| SB_CREATE["Supabase: bot_create_order"]
CB_SWITCH -->|"order_*"| SB_ORDER_DETAIL["Supabase: Детали заявки"]
6.2. Обработка Callback Query в n8n
Telegram Trigger должен слушать не только message, но и callback_query:
- В ноде Telegram Trigger → Updates: добавьте
callback_query. - В Switch ноде добавьте ветку:
Условие: {{ $json.callback_query }} is not empty - Далее Callback Router (ещё один Switch) разбирает
callback_query.data:"rate_*" → Запрос к daily_rates "city_*" → Показ валютных пар "pair_*" → Запрос суммы "confirm_order" → Создание заявки "cancel_order" → Отмена "order_*" → Детали заявки "back_*" → Возврат на предыдущий шаг
6.3. Answer Callback Query
⚠️ Обязательно! После каждого callback нужно отправить
answerCallbackQuery, иначе на кнопке будет бесконечный индикатор загрузки.
HTTP Request:
POST https://api.telegram.org/bot{TOKEN}/answerCallbackQuery
Body: { "callback_query_id": "{{ $json.callback_query.id }}" }
6.4. Хранение state между шагами
Между шагами (город → валюта → сумма → подтверждение) нужно хранить промежуточное состояние.
Варианты:
| Способ | Плюсы | Минусы |
|---|---|---|
| Callback data | Просто. city_Bangkok_pair_THB/RUB |
Ограничение 64 байта |
| n8n Static Data | Быстро, в памяти n8n | Теряется при перезапуске |
| Supabase sessions | Надёжно, персистентно | Доп. таблица |
| AI Agent контекст | Естественно, без кода | Зависит от memory |
Рекомендация: Для кнопочного флоу — кодируй состояние в callback_data:
callback_data: "ord_Bangkok_THBRUB" → город + пара
callback_data: "conf_Bangkok_THBRUB_100000" → все данные для заявки
🔗 7. Связь с Supabase — Сводная таблица
| Кнопка | Supabase таблица/RPC | Действие | Параметры |
|---|---|---|---|
| 💱 Узнать курс | daily_rates (SELECT) |
Получить курс | symbol |
| 💱 Все курсы | daily_rates (SELECT ALL) |
Все курсы | is_active = true |
| 📝 Выбор города | locations (SELECT) |
Список городов | is_active = true |
| 📝 Расчёт | calculate_deal() (RPC) |
Калькулятор | city, amount, pair |
| 📝 Создать заявку | bot_create_order() (RPC) |
Новая заявка | tg_id, city, amount, pair |
| 📦 Статус | bot_get_client_orders() (RPC) * |
Заявки клиента | tg_id |
| 📦 Детали заявки | orders (SELECT) |
Одна заявка | order_id |
* — Нужно создать новую RPC
bot_get_client_orders()(см. раздел 5.4).
📋 8. Чеклист реализации
Фаза 1: Базовые кнопки (🔴 Высокий приоритет)
- n8n: Обновить Telegram Trigger — добавить
callback_query - n8n: Обновить Switch — добавить ветки для 3 кнопок
- n8n: Добавить ноду отправки Welcome + Reply Keyboard при
/start - n8n: Добавить Callback Router (Switch для callback_data)
- n8n: Добавить
answerCallbackQueryHTTP Request после каждого callback - Supabase: Создать RPC
bot_get_client_orders()для статуса заявок
Фаза 2: Динамические данные (🟡 Средний приоритет)
- n8n: Code Node для динамической генерации Inline Keyboard из Supabase данных
- n8n: Валидация ввода суммы (проверка что число > 0)
- n8n: Кодирование state в callback_data (
ord_city_pair) - n8n: Обработка кнопки «Назад» для навигации
Фаза 3: AI-гибрид (🟢 Низкий приоритет)
- n8n: Добавить
get_order_status_toolв AI Agent - n8n: Fallback: если кнопочный флоу сломался — передать в AI Agent
- NextBot: Настроить дожим через «Отложенную отправку» (24 ч)
- NextBot: Настроить ретеншн-сценарии (D+7, D+14, D+30)
🏗️ 9. Архитектура NextBot + n8n (если используем оба)
flowchart LR
TG["📱 Telegram"] --> NB["NextBot\n(Кнопки, Сценарии,\nFollow-up, CRM)"]
TG --> N8N["n8n\n(AI Agent, Tools,\nMedia Processing)"]
NB --> SB[("🗄️ Supabase\n(Единый источник правды)")]
N8N --> SB
NB -.->|"Webhook/API"| N8N
N8N -.->|"Webhook/API"| NB
Роли:
- NextBot: Кнопочные сценарии, Follow-up, Рассылки, Human-in-the-loop (оператор), дожим.
- n8n: AI-агент (GPT-4o-mini), обработка медиа (Gemini), Tools (Supabase RPC), RAG.
- Supabase: Единый источник правды — daily_rates, orders, clients, locations, documents (RAG).
Варианты стыковки:
- NextBot → n8n: При сложном вопросе NextBot вызывает n8n Webhook для обработки AI-агентом.
- n8n → NextBot: После создания заявки n8n вызывает NextBot API для запуска дожим-сценария.
- Оба → Supabase: Обе платформы читают/пишут в одну БД.