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 -.-> RouteCommands
/start— intro message + rate-limit notice/reset— clear the per-chat conversational thread (graph stays untouched)
Message handlers
message:text— forward tosomaAgent.stream, fireconversation.turnfor async fact extractionmessage:voice— Whisper-transcribe (≤5 min duration), then treat as text
Limits
| Limit | Value |
|---|---|
| Rate limit | 10 messages per minute per chat |
| Agent timeout | 20 seconds |
| Voice duration | 5 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/webhookwithX-Telegram-Bot-Api-Secret-Tokenheader. grammY'swebhookCallback('std/http')adapter verifies the secret and dispatches to the bot. - Dev:
pnpm --filter @soma/bot devrunspolling.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.