A multi-agent Telegram bot for managing recurring family orders — diapers, medications, and refunds — built with Mastra.
Mostro uses a supervisor/delegation architecture: a central supervisor agent receives Telegram messages and routes them to specialized domain agents. Each domain agent orchestrates a workflow with suspend/resume semantics — workflows pause at specific steps until Mostro's own mailbox-polling cycle finds and matches a reply, then notify subscribed users when milestones are reached.
- Supervisor pattern — single entry point that delegates to domain-specific agents based on intent
- Invite-only access — canonical user identity keyed by Google email; unknown Telegram senders are silently ignored, admins invite people via one-time deep links (see docs/identity.md)
- Google SSO for the web — the Mastra server authorizes logins against the same users collection as the bot
- Suspend/resume workflows — long-running order flows that halt at a step until a matching reply is found in Mostro's own mailbox
- Mailbox polling — a scheduled workflow per domain reads Mostro's Gmail inbox every 15 minutes, matches replies to the suspended step of the run they belong to, and resumes it — see Mailbox Polling below
- Outbound email — orders reach suppliers as real emails sent from Mostro's own Gmail account, so replies land in its inbox; a send that fails leaves the order un-placed and retryable rather than silently marked as sent
- Notification subscriptions — users subscribe to order updates and receive Telegram messages when events occur
- Monthly scoping — one shared order per domain per month (deterministic run IDs like
diapers-2025-07) - Ngrok tunneling — automatic tunnel setup for Telegram's webhook delivery
Telegram ──► access gate ──► Mostro Supervisor
├──► Weather Agent ──► Weather Workflow
├──► Diapers Agent ──► Diapers Workflow (3 steps, 1 suspend)
├──► Meds Agent ──► Meds Workflow (6 steps, 3 suspends)
└──► Refunds Agent ──► Refunds Workflow (8 steps, 3 suspends)
▲ │
│ └──► email (Gmail API) ──► suppliers
│
Diapers/Meds/Refunds Poll Workflows (cron, every 15 min)
│
reads Mostro's own Gmail inbox, resumes the matching run
Only known users get past the access gate; identity, invites, and memory ownership are covered in docs/identity.md.
| Agent | Description |
|---|---|
| Mostro Supervisor | Receives all Telegram messages, delegates to domain agents, relays notification signals to subscribers, handles invites |
| Weather Agent | Provides weather details for a location and suggests activities based on the forecast |
| Diapers Agent | Manages the shared diaper order flow — request, check status, subscribe to updates |
| Meds Agent | Manages medication orders based on prescriptions — request, track pharmacy acknowledgements and delivery |
| Refunds Agent | Manages refund requests — submit, track acknowledgement, confirmation, and deposit |
Each domain workflow follows a request → wait → notify pattern with mailbox-polling-driven resume points:
- Diapers:
requested → date_confirmed → notification_sent - Meds:
requested → acknowledged → ack_notified → delivery_confirmed → notification_sent - Refunds:
requested → acknowledged → ack_notified → confirmed → confirmation_notified → deposit_received → deposit_confirmed → notification_sent
There is no inbound webhook for suppliers to call. Instead, one poll workflow per domain
(diapers-poll, meds-poll, refunds-poll) runs on a 15-minute Mastra schedule. Each cycle
orchestrates three independent modules:
inbox-manager(src/mastra/lib/inbox-manager/) — the only module that talks to Gmail. Translates a natural-language query description into Gmail search syntax once, fetches unprocessed replies (anything without anoutcome.*status label), and applies labels.mail-classifier(src/mastra/lib/mail-classifier/) — classifies each mail and extracts structured data via LLM, driven by versioned classification rules stored in MongoDB, read fresh on every cycle — publish a new rules snapshot and the next cycle picks it up, no redeploy.outcome-processor(src/mastra/lib/outcome-processor/) — runs the side effect registered in code for the classified label (resuming the matching suspended workflow run).
Every processed mail ends up with two orthogonal labels: what it is (e.g. diapers.confirmed,
from the MongoDB rules) and how processing went (outcome.completed / outcome.failed /
outcome.review). Mails without a status label are picked up again on the next cycle
(at-least-once semantics).
Classification rules are seeded from JSON files kept outside the repo (they contain sensitive
data): pnpm seed:classifier -- --domain <domain> --file <path> --author <name> --changelog <text>.
See docs/inbox-pipeline.md for the full architecture — module responsibilities, label semantics, MongoDB snapshot versioning, orchestration, and operational notes — and docs/clasificador.md for the rules JSON format.
The three poll workflows still run every 15 minutes each, but on offset minutes
(diapers-poll at 2,17,32,47, meds-poll at 7,22,37,52, refunds-poll at 12,27,42,57) so
the three domains don't hit the Gmail API at the same instant.
When upgrading an already-deployed instance, re-run pnpm run gmail:auth. An existing refresh
token minted before polling only carries the gmail.send scope; the pollers need gmail.modify
to read replies and apply labels. Without the new scope the poller gets a 403 every 15 minutes.
- Mastra — AI agent framework (agents, workflows, tools, memory, observability)
- DeepSeek v4 Flash via OpenRouter — LLM provider
- @chat-adapter/telegram — Telegram bot integration
- MongoDB — workflow state, agent memory, users, invites, and classification rules
- DuckDB — observability and tracing
- ngrok — tunnel for Telegram's webhook delivery
- Zod — schema validation
- Gmail API via
@googleapis/gmail— sends outbound emails
- Node.js >= 22.13.0
- pnpm
- A MongoDB instance
- An OpenRouter API key
- A Telegram Bot token
- An ngrok account with a reserved domain
- A Gmail account for Mostro itself — outbound orders are sent from it, and suppliers reply to it
- A Google Cloud project with the Gmail API enabled, an OAuth client, and the app published to production (see the one-time setup in step 3 below)
- Optional, for web login: a second OAuth client of type "Web application" — the same project can host it
Sending mail and logging in are separate integrations with separate credentials
(GMAIL_MAILER_* and GOOGLE_SSO_*), but they can live in one Google Cloud project.
Users logging in are never asked for Gmail access. Consent is granted per authorization request,
not per project: the login asks for openid email profile, while gmail.send is requested once,
by you, when you authorize the mailer with Mostro's own account.
What the two do share, being one project: the 100-new-user cap Google applies to an app that has shown the "unverified app" screen — which the mailer will, since its scope is sensitive and the app is published but unverified — plus the verification paperwork if you ever need it, and the blast radius of a suspension. None of that binds at family scale. Split the mailer into its own project if you ever open the login to people outside the household.
-
Clone the repository:
git clone https://github.com/alex-bluetrain/mostro.git cd mostro -
Install dependencies:
pnpm install
-
Copy the environment file and fill in your values:
cp .env.example .env
OPENROUTER_API_KEY= TELEGRAM_BOT_USERNAME= TELEGRAM_BOT_TOKEN= TELEGRAM_WEBHOOK_SECRET_TOKEN= MONGODB_URI= MONGODB_DB_NAME= NGROK_AUTHTOKEN= NGROK_DOMAIN= ADMIN_EMAIL= ADMIN_NAME= ADMIN_TELEGRAM_ID=
ADMIN_EMAILseeds the first authorized user on boot — without it nobody can talk to the bot or log into the web. See docs/identity.md for how identity and invites work. Note: optional variables must be absent, not empty — an empty value fails zod validation and aborts the boot.Optional — Google SSO for the web (Studio and future frontends):
GOOGLE_SSO_CLIENT_ID= GOOGLE_SSO_CLIENT_SECRET= GOOGLE_SSO_REDIRECT_URI= GOOGLE_SSO_COOKIE_PASSWORD=
Required — Gmail, for sending outbound emails and for the poll workflows that read replies back from the same inbox:
GMAIL_MAILER_CLIENT_ID= GMAIL_MAILER_CLIENT_SECRET= GMAIL_MAILER_REFRESH_TOKEN= GMAIL_MAILER_SENDER= DIAPERS_EMAIL_TO= MEDS_EMAIL_TO= REFUNDS_EMAIL_TO=
One-time Gmail account setup:
- Create a Google Cloud project — the same one can also host the web login's OAuth client.
- Enable the Gmail API.
- Create an OAuth client of type "Web application", separate from the SSO one, with a
redirect to
http://127.0.0.1:53682/oauth2callback. Google matches redirect URIs literally, so scheme, host, port and trailing slash must be exactly that — in particular127.0.0.1and notlocalhost, which on Windows resolves to IPv6 first while the script listens on IPv4. ("Web application" rather than "Desktop app" because the client secret lives in a server's.envand is treated as confidential.) - Add the
https://www.googleapis.com/auth/gmail.sendandhttps://www.googleapis.com/auth/gmail.modifyscopes. Sending only needsgmail.send;gmail.modifyis what lets the poll workflows read replies and apply the classification and status labels (e.g.diapers.confirmed,outcome.completed). Gmail doesn't offer a scope narrower than "the whole mailbox" — the poller's containment is in code (a per-domain query description, fixed resume functions per outcome), not in the OAuth grant. - Publish the app to production. In Testing mode the refresh token is invalidated after 7 days and sends (and polling) start failing. Authorizing shows the "unverified app" screen, which you accept manually.
- Run
pnpm run gmail:authwith the Mostro account and save the token in.env.
gmail:authruns as its own process and briefly listens on port 53682 — not Mastra's port, which it would collide with whilepnpm run devis up. The callback cannot be a Mastra route either:GMAIL_MAILER_REFRESH_TOKENis required for the server to boot, so you would need the token to start the thing that gives you the token. The port number itself is arbitrary; it only has to match the redirect URI registered on the OAuth client.The consent screen's user type must be External — Internal only exists for Google Workspace organizations, and Mostro's account is a plain
@gmail.comone. Publishing the app is not the same as getting it verified: you can publish without verification, and authorizing then shows the "Google hasn't verified this app" screen, which you accept manually.The refresh token also dies if the Mostro account's password changes (Google invalidates tokens carrying Gmail scopes) or if it goes six months unused. In all of these cases sends fail with
invalid_grant, and the mailer's error message says so — the fix is always to re-runpnpm run gmail:auth. -
Seed the classification rules (once per domain, and again whenever the rules change):
pnpm seed:classifier -- --domain diapers --file <path-to-rules.json> --author "you" --changelog "initial seed"
The rules JSON lives outside the repo (it contains sensitive supplier data). Start from the per-domain templates in docs/classifier-rules/ — labels and extract schemas already match the code — and fill in the
<...>placeholders. Without a seeded snapshot for a domain, that domain's poll cycle fails fast with a clear error. Format: docs/clasificador.md. -
Start the development server:
pnpm run dev
This starts the Mastra dev server with Mastra Studio at
http://localhost:4111.
src/
├── business/
│ ├── models/ Mongoose models (users, invites, classifier snapshots + pointers)
│ └── repositories/ Data access (classifier.repository: getActiveRules, publishSnapshot)
└── mastra/
├── agents/ Domain agents + supervisor + inboxClassifierAgent
├── tools/ 3 tools per domain (request, get-status, subscribe)
├── workflows/ One directory per workflow (not per domain), suspend/resume workflows
│ │ with steps, schemas, and types
│ ├── diapers/ diapers.workflow.ts
│ ├── diapers-poll/ diapers-poll.workflow.ts (schedule, every 15 min) +
│ │ diapers-inbox.config.ts (query) + diapers-outcome-handlers.ts (label → handler)
│ ├── meds/ meds.workflow.ts
│ ├── meds-poll/ meds-poll.workflow.ts + meds-inbox.config.ts + meds-outcome-handlers.ts
│ ├── refunds/ refunds.workflow.ts
│ └── refunds-poll/ refunds-poll.workflow.ts + refunds-inbox.config.ts + refunds-outcome-handlers.ts
├── lib/
│ ├── inbox-manager/ Gmail gateway: translates the query once, fetches replies, applies labels
│ ├── mail-classifier/ Classifies + extracts via LLM against MongoDB-stored rules (ajv-validated)
│ ├── outcome-processor/ Runs the handler registered in code for a classified label
│ ├── *-run.ts Resume functions per domain, guarded by run + suspended + right step
│ └── ... Users, invites, telegram gate, Google auth, subscriber stores
├── config/ Zod-validated environment configuration
└── index.ts Central registration (agents, workflows, storage)
| Script | Description |
|---|---|
pnpm run dev |
Start development server with hot reload |
pnpm run build |
Build for production |
pnpm run start |
Start production server |
pnpm run gmail:auth |
Get the Gmail refresh token (one-time) |
pnpm seed:classifier |
Publish a classification-rules snapshot to MongoDB |
Private
