Skip to main content
Glama

Vaani Pay Assistant

Безопасный, работающий в реальном времени, многопользовательский, двуязычный (английский + хинди) чат-бот поддержки платежей: пользователи регистрируются и входят в систему под своей учётной записью, а затем спрашивают о своих платежах, заказах, возвратах, транзакциях, рисках мошенничества и статистике — со строгой изоляцией данных для каждого пользователя на уровне MCP-инструментов, настоящей базой данных SQLite и живым потоком статусов по WebSocket, показывающим, что делает агент.

Всё начиналось как статическая демонстрационная сборка для хакатона (захардкоженные пользователи, фиксированные токены, только английский) и было обновлено до платформы на базе данных, многопользовательской, безопасной и двуязычной, без изменения рабочих частей, которые уже работали: протокол WebSocket, архитектура MCP-инструментов и базовая логика агента имеют ту же форму, что и раньше, — под ними изменились только источник данных и модель аутентификации.

Архитектура

Browser (chat UI + login/signup/profile)
      │ REST (/auth, /users/me, /transactions)      │ WebSocket (/ws)
      ▼                                              ▼
FastAPI — auth endpoints, profile endpoints    FastAPI — WebSocket handler
      │                                              │
      ▼                                              ▼
app/auth.py  (register / login / sessions)     AI Agent (app/agent.py)
      │                                           NLU (Grok API) → intent + entities
      │                                           Tool selection → MCP tool
      ▼                                              │
app/db.py — SQLite                                   │ MCP (stdio transport)
  users, sessions, chat_history,                      ▼
  payments, orders, refunds, transactions        MCP Server (mcp_server/server.py)
      ▲                                           get_payment_status │ get_order_details
      │                                           get_refund_status │ get_customer_details
      └───────────── same DB, same ownership ──── get_transaction_history │ check_fraud_risk
                      checks on every query        get_payment_statistics
                                                        │
                                                        ▼
                                              mcp_server/data_layer.py
                                              — ownership check on every lookup,
                                                now backed by SQLite instead of JSON

Related MCP server: AlipayPlus MCP Server

1. Аккаунты и конфиденциальность данных

  • Настоящие аккаунты. POST /auth/register создаёт строку пользователя в SQLite с надёжно хешированным паролем (PBKDF2-HMAC-SHA256, случайная соль для каждого пароля, 260 000 итераций — см. app/security.py). Пароли в открытом виде нигде не хранятся и не логируются.

  • Настоящие сессии входа. POST /auth/login проверяет учётные данные и выдаёт непрозрачный, не поддающийся угадыванию токен сессии (generate_token() из app/security.py, 256 бит энтропии), хранящийся в таблице sessions со сроком действия (SESSION_TTL_HOURS в .env, по умолчанию 24 часа). Просроченные или неизвестные токены отклоняются везде, где проверяются.

  • Каждый MCP-инструмент, который ищет конкретный ресурс (get_payment_status, get_order_details, get_refund_status, check_fraud_risk), требует параметр requesting_user_id и проверяет в mcp_server/data_layer.py, что ресурс действительно принадлежит этому пользователю — теперь через параметризованный SQL-запрос с условием WHERE ... AND user_id = ? — прежде чем что-либо вернуть.

  • Если ресурс принадлежит кому-то другому или вообще не существует, в обоих случаях возвращается тот же самый общий ответ: "Access denied. You are not authorized to access this information." Если бы для случаев «не найдено» и «чужие данные» возвращались разные сообщения, пользователь мог бы перебирать валидные ID, наблюдая, какая ошибка приходит, — одинаковый ответ закрывает этот побочный канал.

  • requesting_user_id — это всегда аутентифицированная личность вызывающей стороны (определяется один раз при аутентификации через WebSocket или при REST-запросе — см. app/auth.py), а не значение, извлечённое из сообщения чата, URL-параметра или тела запроса. Схема извлечения в app/nlu.py вообще не содержит поля user_id, поэтому сообщение (даже злонамеренное) не может протащить другую личность в вызов инструмента.

  • get_customer_details, get_transaction_history и get_payment_statistics вообще не принимают ID ресурса — они всегда возвращают данные самого вызывающего пользователя, так что для этих трёх инструментов не существует никакой поверхности для манипуляции ID.

  • Удаление аккаунта (DELETE /users/me) требует повторного ввода текущего пароля в качестве подтверждения, после чего удаляет строку пользователя — внешние ключи ON DELETE CASCADE удаляют вместе с ней все сессии, историю чатов, платежи, заказы, возвраты и транзакции этого пользователя.

Проверьте изоляцию данных напрямую:

python3 test_offline.py

Это выполняет реальные вызовы инструментов (к настоящему MCP-серверу, читая из настоящей базы данных SQLite) от имени двух разных демонстрационных пользователей и проверяет, что попытки межпользовательского доступа отклоняются, что история транзакций каждого пользователя содержит только его собственные данные и что двуязычные ответы отображаются корректно.

2. Регистрация, вход и управление аккаунтом

  • POST /auth/register — имя, email, пароль, необязательный телефон и языковая настройка. Пароль должен быть не короче 8 символов и содержать смесь букв и цифр (app/security.py).

  • POST /auth/login — возвращает токен сессии + профиль пользователя.

  • POST /auth/logout — отзывает текущий токен сессии на стороне сервера.

  • GET /users/me / PUT /users/me — просмотр/обновление профиля (имя, телефон).

  • POST /users/me/change-password — требует текущий пароль; его смена делает недействительными все существующие сессии (вынуждает повторный вход везде), поэтому утёкший старый токен перестаёт работать.

  • GET /users/me/preferences / PUT /users/me/preferences — чтение/обновление языковой настройки (en/hi), сохраняемой в базе данных, так что она переживает выход и повторный вход.

  • DELETE /users/me — окончательное удаление аккаунта (требуются пароль и явное confirm: true).

Всё это также доступно из самого чат-интерфейса через кнопку ⚙️ в шапке (просмотр/редактирование профиля, переключатель языка, смена пароля, выход, удаление аккаунта).

3. Чат-интерфейс

static/index.html — одностраничное приложение:

  • Вкладки входа и регистрации отображаются до того, как станет возможен чат.

  • Панель профиля и настроек (редактирование имени/телефона, смена пароля, переключатель языка, выход, удаление аккаунта с шагом подтверждения).

  • Сворачиваемое меню подсказок над полем ввода, отображаемое из того же словаря переведённых строк, что и остальной интерфейс.

  • Пузыри чата, строка статуса в реальном времени и индикатор статуса в шапке — без изменений относительно исходного дизайна.

4. Связь в реальном времени

Чат по-прежнему работает через один WebSocket (/ws) — форма протокола не изменилась, только теперь вместо статического значения токен аутентификации — это настоящий токен сессии на основе базы данных:

{"type": "auth", "token": "<session token from /auth/login>"}
      ↓
{"type": "auth_success", "user_id": "...", "name": "...", "language": "en"}

Для каждого сообщения чата сервер потоково передаёт события статуса в следующем порядке, а затем финальный (локализованный) ответ:

🔍 Understanding your request...
🔧 Checking payment information...
✓ Payment information retrieved
🤖 Generating response...
<final answer, in the user's selected language>

Каждый ход чата (и сообщения пользователя, и сообщения ассистента) также сохраняется в таблицу chat_history (_persist_chat_turn из app/main.py) с привязкой к аутентифицированному пользователю.

5. Архитектура на основе MCP

mcp_server/server.py предоставляет ровно эти 7 инструментов, разбитых по доменным модулям в mcp_server/tools/:

Tool

File

get_payment_status

payment_tools.py

check_fraud_risk

payment_tools.py

get_order_details

order_tools.py

get_refund_status

refund_tools.py

get_customer_details

customer_tools.py

get_transaction_history

customer_tools.py

get_payment_statistics

analytics_tools.py

get_balance

wallet_tools.py

add_money

wallet_tools.py

get_transactions

wallet_tools.py

validate_recipient

wallet_tools.py

create_transfer

wallet_tools.py

confirm_transfer

wallet_tools.py

cancel_transfer

wallet_tools.py

get_spending_summary

wallet_tools.py

Всё это работает на базе SQLite (mcp_server/data_layer.py → app/db.py) вместо статических JSON-данных. Сигнатуры инструментов, агент и фронтенд не изменились по сравнению с исходным дизайном — поменялся только источник данных под data_layer.py, ровно так, как и предусматривала исходная архитектура.

6. Двуязычная поддержка (английский + хинди)

  • Строки интерфейса: словарь UI_STRINGS из app/i18n.py, отдаваемый через GET /i18n/{lang}. Фронтенд получает его один раз при загрузке и при каждом изменении языка и применяет через атрибуты data-i18n/data-i18n-placeholder — ни одна переведённая строка не захардкожена в HTML/JS.

  • Ответы ИИ-ассистента: AGENT_STRINGS из app/i18n.py (фиксированные сообщения вроде приветствий) и REPLY_TEMPLATES (интерполируемые сообщения вроде статуса платежа). app/agent.py формирует каждый ответ через них — нигде в агенте английский текст не захардкожен напрямую.

  • NLU: промпт в app/nlu.py явно просит модель Grok обрабатывать ввод на хинди/английском или смешанный ввод и всегда переводить его на английский внутри для извлечения намерений и сущностей, так что ассистент понимает вопрос в любом случае и отвечает на предпочитаемом пользователем языке.

  • Хранение: языковая настройка хранится в поле users.language в базе данных (задаётся при регистрации и может быть изменена в любой момент через PUT /users/me/preferences), поэтому она переживает выход и повторный вход.

  • Динамическое переключение: смена языка в настройках немедленно обновляет интерфейс и переподключает WebSocket, так что уже следующий ответ чата приходит на новом языке — без перезагрузки страницы.

7. Требования к безопасности

Требование

Где реализовано

Аутентификация

app/auth.py — хеширование паролей, токены сессий с истечением срока. WebSocket не обработает ни одно сообщение чата, и ни одна REST-конечная точка не вернёт данные, пока токен не будет проверен.

Авторизация

mcp_server/data_layer.py — каждый запрос ресурса фильтруется по user_id прямо в SQL.

Изоляция пользователя/сессии

app/session_store.py — каждое WebSocket-соединение получает собственное состояние разговора в памяти; user_id/language устанавливаются один раз при аутентификации и никогда не перезаписываются из текста чата.

Проверки прав на уровне MCP

Применяются внутри самих MCP-инструментов (mcp_server/tools/*.py → data_layer.py), а не только на границе приложения — см. diagnose_setup.py и test_offline.py, которые вызывают MCP-сервер напрямую и подтверждают отказ.

Проверка входных данных

app/main.py (Pydantic-модели для каждого REST-тела; проверки типа и длины сообщений в WebSocket) и app/auth.py (формат email, политика паролей).

Защита от SQL-инъекций

Каждый запрос в app/db.py / mcp_server/data_layer.py использует параметризованные плейсхолдеры ? — SQL, собираемый из строк, нигде не используется.

Ограничение частоты запросов

app/main.py — скользящее окно ограничения на IP для /auth/register и /auth/login.

Безопасный CORS

app/main.py — явный список разрешённых источников (CORS_ALLOWED_ORIGINS в .env), по умолчанию только localhost; никогда не * с учётными данными.

Безопасное хеширование паролей

app/security.py — PBKDF2-HMAC-SHA256, случайная соль для каждого пароля, 260 000 итераций.

Истечение срока токена

app/auth.py — сессии истекают через SESSION_TTL_HOURS; при смене пароля все существующие сессии становятся недействительными.

Общие сообщения об ошибках аутентификации

app/auth.py — одинаковое сообщение для «такого email нет» и «неверный пароль»; одинаковое сообщение для «не найдено» и «чужой ресурс».

Безопасная обработка ошибок

_safe_error_message() / глобальный обработчик исключений в app/main.py — неожиданные ошибки полностью логируются на стороне сервера, клиент получает только общее сообщение.

Защита от манипуляции идентификаторами

Пользователь может ввести любой payment_id/order_id/refund_id — инструмент всегда возвращает данные только в том случае, если они принадлежат его аутентифицированному аккаунту (обеспечивается SQL).

8. Схема базы данных

users            id, name, email, phone, password_hash, language, created_at, updated_at, last_login
sessions         token, user_id, created_at, expires_at
chat_history     id, user_id, conversation_id, role, message, timestamp
payments         payment_id, user_id, status, amount, method, failure_reason, date
orders           order_id, user_id, status, total, items (JSON), date
refunds          refund_id, user_id, payment_id, amount, status, date
transactions     txn_id, user_id, type, amount, status, date

-- Wallet: the real money-movement system (see section 9 below)
payment_accounts     id, user_id, payment_id, account_number, ifsc, balance, currency, status, created_at
wallet_transactions  id, transaction_id, sender_account_id, receiver_account_id, amount, transaction_type,
                     status, description, sender_name, receiver_name, recipient_account_number,
                     recipient_ifsc, failure_reason, created_at, updated_at
beneficiaries        id, user_id, recipient_name, account_number, ifsc, created_at

Полный DDL с внешними ключами и индексами см. в SCHEMA в app/db.py.

9. Кошелёк: платёжные счета, пополнение и отправка денег

Каждый зарегистрированный пользователь получает настоящий, рабочий кошелёк, а не просто просмотр истории платежей. Это самое большое добавление поверх обновления базы данных и аутентификации, и оно реализовано как отдельный модуль (app/wallet.py), который вызывают и REST API, и AI/MCP-инструменты, так что правила движения денежных средств соблюдаются ровно в одном месте.

Автоматическое создание счёта. POST /auth/register создаёт запись пользователя И запись payment_accounts в той же транзакции базы данных (см. register() в app/auth.py, вызывающий insert_account_row() из app/wallet.py) — пользователь не может существовать без кошелька, а кошелёк никогда не создаётся как отдельный, независимо завершаемый с ошибкой шаг. Каждый счёт получает:

  • уникальный платёжный ID (PAY..., внутренний идентификатор),

  • уникальный 12-значный номер счёта,

  • фиксированный IFSC (VPAY0000001 — Vaani Pay — это виртуальный кошелёк с одним отделением, поэтому у всех счетов один общий IFSC, как это часто бывает у виртуальных счетов настоящих необанков),

  • начальный баланс ₹0.

Ответ регистрации включает подтверждение message: "Your payment account has been successfully created." плюс новые данные счёта, которые сразу показываются пользователю (и в ответе API, и в сообщении подтверждения на экране регистрации).

Пополнение. POST /wallet/add-money (или кнопка «Add Money» на экране кошелька, или просьба к AI-ассистенту «add ₹5,000 to my account») — проверяет сумму (> ₹0, ≤ ₹2,00,000 за транзакцию — MAX_ADD_MONEY в app/wallet.py), затем атомарно обновляет баланс и добавляет запись CREDIT в wallet_transactions. Для хакатона в систему не подключён реальный платёжный шлюз — это явно симулированное пополнение, соответствующее требованию брифa "безопасный симулированный процесс пополнения".

Отправка денег — всегда двухшаговое подтверждение. Ни REST API, ни AI-ассистент никогда не перемещают деньги одним вызовом:

  1. POST /wallet/transfers (initiate_transfer в app/wallet.py) проверяет получателя и баланс отправителя и создаёт запись PENDING в wallet_transactions — баланс пока не меняется. Он возвращает предпросмотр подтверждения (получатель, замаскированный номер счёта, IFSC, сумма, комиссия, общее списание) — именно он отображается на экране «Confirm Transfer».

  2. POST /wallet/transfers/{id}/confirm (confirm_transfer) — это единственный вызов, который реально перемещает деньги. Он повторно проверяет баланс отправителя и статус счёта в момент подтверждения (а не только в момент инициации, на случай если что-то изменилось между шагами — например, две инициации переводов подряд), затем списывает средства с отправителя и, если получатель — реальный счёт Vaani Pay, зачисляет их ему, всё это в одной атомарной транзакции SQLite, защищённой общепроцессной блокировкой. Если что-то ломается на полпути, всё откатывается целиком — перевод никогда не может остаться списанным, но не зачисленным.

  3. POST /wallet/transfers/{id}/cancel отменяет перевод, который всё ещё в статусе PENDING, не затрагивая никакие балансы.

Отправка на номер счёта, которого нет в нашей системе, всё равно проходит успешно (как симулированный внешний перевод — средства списываются у отправителя, но счёта Vaani Pay для зачисления нет), что соответствует требованию брифa: «зачислить средства получателю, если получатель существует в симулированной системе».

Проверка получателя. POST /wallet/validate-recipient (validate_recipient) проверяет: формат номера счёта (9–18 цифр), формат IFSC (^[A-Z]{4}0[A-Z0-9]{6}$), что IFSC совпадает с номером счёта, если это внутренний счёт, и — что критически важно — что отправитель не переводит средства на собственный номер счёта. Если указано только имя получателя (без номера счёта), выполняется поиск имени среди сохранённых получателей самого вызывающего пользователя, и если есть ровно одно совпадение, оно автоматически разрешается.

Сохранённые получатели. После успешного перевода интерфейс предлагает «Save this recipient?» — POST /beneficiaries сохраняет получателя только для аутентифицированного пользователя (никогда не глобально и не для всех), так что в следующий раз пользователь (или AI- ассистент, когда его просят «send ₹2,000 to Rahul») может выполнить перевод по одному имени.

История транзакций и фильтры. GET /wallet/transactions?filter=... (all / add_money / sent / received / failed / pending) — всё вычисляется в реальном времени из wallet_transactions, никогда не захардкожено. Вкладка History на экране кошелька и AI-запросы «show my wallet transactions» / «how much did I spend this month» читают данные из одной и той же функции (get_wallet_transactions / get_spending_summary в app/wallet.py).

Баланс всегда вычисляется производно, никогда не устанавливается напрямую. В кодовой базе намеренно нет функции set_balance() — баланс меняется только как побочный эффект add_money() или confirm_transfer(), и обе эти функции в том же атомарном шаге также добавляют неизменяемую запись в wallet_transactions. Фронтенд всегда отображает только то, что возвращает GET /wallet/account; влиять на баланс он не может.

Безопасность кошелька, в частности

Правило

Как обеспечивается

Пользователь никогда не может напрямую изменить свой собственный баланс

Ни одна публичная функция не устанавливает баланс, кроме как побочный эффект «Пополнения» / confirm_transfer, обе операции проверяют сумму и создают запись аудита

Пользователь никогда не может изменить баланс другого пользователя

Каждая функция кошелька принимает аутентифицированный user_id вызывающего пользователя и ищет payment_accounts через WHERE user_id = ? — никогда через идентификатор счёта, предоставленный клиентом

Пользователь никогда не может подтвердить/отменить чужой перевод

confirm_transfer/cancel_transfer проверяют, что счёт отправителя транзакции PENDING принадлежит вызывающему пользователю, используя один и тот же общий ответ «не найдено», независимо от того, не существует ли транзакция или она принадлежит другому лицу (проверено в test_offline.py и diagnose_setup.py)

Переводы самому себе заблокированы

validate_recipient сравнивает номер счёта получателя с собственным номером счёта отправителя, прежде чем разрешить перевод

Суммы нельзя изменить в процессе

Сумма, фактически используемая для списания/зачисления в момент confirm_transfer, — это сумма, сохранённая в строке PENDING, созданной в момент initiate_transfer, и никогда не считывается повторно из запроса на подтверждение

ИИ не может перемещать деньги без явного подтверждения

MCP-инструменты create_transfer/add_money никогда не списывают и не зачисляют средства сами по себе; конечный автомат диалога в app/agent.py требует явного ответа «да» на показанное подтверждение перед вызовом confirm_transfer

Атомарность

confirm_transfer выполняет проверку баланса + оба обновления баланса + обновление статуса внутри одной транзакции SQLite (tx() из app/db.py), плюс блокировку на уровне процесса — см. docstring модуля app/wallet.py

10. Двуязычный платёжный процесс

Кошелёк полностью двуязычный и использует тот же механизм app/i18n.py, что и остальное приложение — «Пополнение счёта», «Отправка денег» (все три шага), экран подтверждения, статусы транзакций и все ответы ИИ о балансе/переводах отображаются через t()/tpl() без захардкоженного английского где-либо в app/wallet.py, app/agent.py или в UI кошелька в static/index.html. Например, запрос к ИИ "Rahul ko ₹2,000 bhejo" (хинди/хинглиш) проходит через тот же самый процесс «определение → подтверждение → выполнение», что и английская версия, при этом каждое сообщение — включая экран подтверждения — отображается на хинди.

Настройка

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

cp .env.example .env
# Add your Grok API key (get one at https://console.x.ai)

База данных создаётся (и, только если она пуста, заполняется двумя демонстрационными пользователями — см. ниже) автоматически при первом запуске; для локальной разработки отдельный шаг миграции не требуется. Чтобы создать её явно заранее:

python3 -m app.db

Проверьте перед открытием браузера:

python3 diagnose_setup.py

Запуск

uvicorn app.main:app --reload --port 8000

Откройте http://localhost:8000. Зарегистрируйте новую учётную запись или войдите с помощью одной из созданных демонстрационных учётных записей:

Email

Пароль

ramesh@example.com

Demo@1234

priya@example.com

Demo@1234

Затем попробуйте меню подсказок, задавайте вопросы вроде "check payment status pay_1001" или "मेरा भुगतान pay_1001 का स्टेटस क्या है?", переключите язык в настройках или попробуйте войти как один пользователь и спросить о платежах/заказах/возвратах другого пользователя (pay_1003, ord_2002, rfnd_3002 принадлежат Прие), чтобы увидеть ответ об отказе в доступе.

Чтобы попробовать кошелёк: нажмите кнопку 💰 «Кошелёк» в шапке. Обе демо-учётные записи имеют начальный баланс (₹8 500 у Рамеша, ₹8 000 у Прии) и одну демонстрационную транзакцию в истории. Попробуйте «Пополнить» или «Отправить деньги» на номер счёта другой демо-учётной записи (он виден на экране кошелька этой записи) или напрямую спросите ИИ-ассистента: "what's my balance?", "add ₹5,000 to my account", "send ₹2,000 to Priya Stores" (в первый раз он попросит номер её счёта и IFSC, а после успешного перевода предложит сохранить её как получателя — после этого достаточно просто её имени) или "Mera current balance kitna hai?" на хинди.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to interact with Juspay's payment processing APIs and merchant dashboard for managing orders, transactions, refunds, customers, gateways, and reporting through natural language.
    21
    Apache 2.0
  • A
    license
    A
    quality
    D
    maintenance
    Integrates Ant International's AlipayPlus payment APIs, enabling AI assistants to handle payment and refund operations seamlessly.
    6
    8
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI assistants to securely create payment references and requests for Multibanco, MB WAY, Payshop, Credit Card, PinPay, Cofidis Pay, PIX, and retrieve payment reports.
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    Provides AI assistants with read and safe-write access to Alternative Payments customers, invoices, payment requests, transactions, payouts, and webhooks without directly moving money.
    Apache 2.0