SOMA docs
Surfaces

Telegram bot

Same agent, different surface. Webhook on Vercel, polling for local dev.

Bot: @happ_soma_bot. Webhook registered at https://soma-ai.cc/api/telegram/webhook, HMAC-guarded via TELEGRAM_WEBHOOK_SECRET.

flowchart LR
  U((Telegram user)) -->|message| TG[Telegram API]
  TG -->|POST w/ secret header| Route["/api/telegram/webhook<br/>(Vercel serverless)"]
  Route --> Bot["@soma/bot<br/>grammY instance"]
  Bot -->|text| Agent["somaAgent.stream"]
  Bot -->|voice| Whisper
  Whisper --> Agent
  Agent --> Claude
  Agent -->|tools| PG[(Postgres)]
  Agent -->|reply| Bot
  Bot -->|sendMessage| TG
  TG --> U
  Route -.->|event: conversation.turn| INN[Inngest Cloud]
  INN -.-> Route

Commands

  • /start — intro message + rate-limit notice
  • /reset — clear the per-chat conversational thread (graph stays untouched)

Message handlers

  • message:text — forward to somaAgent.stream, fire conversation.turn for async fact extraction
  • message:voice — Whisper-transcribe (≤5 min duration), then treat as text

Limits

LimitValue
Rate limit10 messages per minute per chat
Agent timeout20 seconds
Voice duration5 minutes max

The rate limit lives in apps/bot/src/rate-limit.ts. The timeout is enforced via Promise.race in the message handler.

Dev vs prod

  • Prod: Telegram calls POST https://soma-ai.cc/api/telegram/webhook with X-Telegram-Bot-Api-Secret-Token header. grammY's webhookCallback('std/http') adapter verifies the secret and dispatches to the bot.
  • Dev: pnpm --filter @soma/bot dev runs polling.ts — long polling, no public URL needed. Used when iterating on bot UX without a deployed webhook.

Why the bot lives in apps/bot

The grammY Bot instance is defined in apps/bot/src/bot.ts and exported as @soma/bot. apps/web/src/app/api/telegram/webhook/route.ts imports it:

import { webhookCallback } from 'grammy';
import { bot } from '@soma/bot';

export const runtime = 'nodejs';
const handle = webhookCallback(bot, 'std/http', {
  secretToken: process.env.TELEGRAM_WEBHOOK_SECRET,
});

export async function POST(req: Request): Promise<Response> {
  return handle(req);
}

The bot package is dual-purpose: library (for the web route) + dev-only polling runnable.