For people whose main channel is Telegram and who want to give an agent access to their correspondence without a third-party service and without the risk of it writing something in their name on its own
Blueprint: Telegram + Claude
Read this before you build. A personal Telegram userbot over MTProto is a gray zone of the Terms of Service: Telegram tolerates personal use but does not approve of automation, spam or scraping. The session file = full access to the account: keep it as a secret (chmod 0600, never in git, with a backup) — whoever gets the file gets your whole Telegram. No mass-send, no broadcast, no scraping of chats you are not a member of. Any outbound goes through the approval gate only: the final ✅ is always yours, and there is no “trusted” path around the card.
Status: living. All 4 base phases are implemented and proven in real use (months in production); since August 2026 a fifth layer has grown on top — the HQ track: a bridge from Telegram to the orchestrator (
@hq), production-bot alerts landing in the orchestrator’s inbox, a scoped MCP for a second agent, and a second bot with a persona of its own for other people’s groups. The architecture, the working patterns and the rakes below come from practice, not from a plan. The document grows as the system grows; v2.2 (19.09.2026) adds Phase 5 and fixes the defaults that did not survive daily operation (approval window 5 → 30 minutes).
Telegram accumulates your conversations for years — clients, friends, work chats, channels on your topics. Search in the Telegram UI is weak, the graph of connections gets lost, and when you’re away from the computer you’re away from Claude. And handing Claude access to your Telegram account through a third-party service — no, thank you.
This Blueprint solves three problems with one system:
- Search and analysis over your own Telegram — Claude gets MCP tools for full-text search, reading chats, finding contacts
- Safe outbound — Claude can propose messages in your name, but physically sends only after your ✅ on a Telegram card with a use-once token
- Pocket Claude — a separate bot becomes a chat interface to Claude Code in your vault, reachable from your phone
- HQ track (Phase 5) — the same bot becomes an entrance to your orchestrator (
@hq …goes not to the pocket session but to the live main session of your agent), a group with production-bot alerts flows into its inbox, and a second bot with a persona lives in other people’s groups by whitelist
The data is local. No third-party services with access to your correspondence: the index in SQLite on your machine, the MCP server on stdio, the HTTP API on 127.0.0.1, an approval bot for callbacks through the standard Telegram Bot API. One honest caveat: inference lives wherever your agent lives — for Claude Code that is the Anthropic API, and everything the agent reads from the index enters the model’s context. This is not “a cloud service with access to your messages”, but it is not “nothing leaves the Mac” either; for a bot in someone else’s group (Phase 5) this has to be said out loud to the participants.
What you get
- A local daemon that indexes all incoming messages into SQLite + FTS5 in real time, plus a backfill of the last N days on first launch
- MCP tools for Claude in any MCP client (Claude Code, Cursor, Codex): the read set (
tg_search_messages,tg_search_contacts,tg_get_chat_info,tg_read_chat,tg_list_recent_chats), outbound (tg_propose_send), admin (tg_backfill_chat— pull history beyond the launch window,tg_transcribe_media— voice notes and video circles → text) and a scoped mode of the server (--scope digest: a second agent sees only the channels from the digest rules and thetg_digesttool, not the whole correspondence) - Transcription of voice notes and video circles by a local Whisper (offline) — the recognized text is cached in the DB and becomes searchable alongside ordinary messages
- An approval bot in Telegram — Claude proposes the text, you get a card with
✅ Send / ✏️ Edit / ❌ Cancel, the physical send happens only after approval, the token is use-once and bound to(chat_id, sha256(text)) - A persistent chat session with Claude through the same bot: you write a DM → the daemon spawns a coding agent in your vault via
--resume, the answer comes back as a reply - An audit log of every outbound event + a daily digest
- A kill switch in one command
- A bridge to the orchestrator (Phase 5): a message to the bot prefixed with
@hqbypasses the pocket session and lands straight in the inbox of your main agent session; the “Alerts” group with admin alerts from production bots goes there too - A second bot with a persona for other people’s groups (Phase 5): its own token, its own whitelist, its own tables; it transcribes voice notes for every participant, reads photos, draws, does end-of-day summaries and reminders — and physically has no access to the message index, the vault or the bridge
How to apply it
- Open a coding agent (Claude Code, Cursor, Codex) in a trusted directory where you normally keep personal code and experiments (NOT in the vault — only logs will live there)
- Show it this Blueprint: “build the Telegram + Claude integration for me from this Blueprint”
- The agent will ask the questions from the Questions for adaptation section
- Answer about your OS, your vault, which chats are critical, how you’ll keep it always-on
- The agent creates the project and implements the 4 phases in order — each phase is valuable on its own, you can stop at any of them
- After phase 3 you create a bot in @BotFather and give its token to the agent
Context: one coding-agent session, a separate git repo for the project (NOT inside the vault), a data dir in the OS-standard location. Telegram API credentials come from my.telegram.org (you need api_id + api_hash).
When to use it
- Telegram is your main communication channel, and months or years of correspondence pile up in it
- Search in the Telegram UI annoys you (slow, no boolean, no advanced syntax)
- You want to ask Claude questions about your own correspondence: “what did we decide with X last week?”, “find contacts from Y”, “give me a digest of channel Z”
- You want Claude to be able to write from your account, but without the risk of it blasting out something strange — every message through approval
- You want a Claude session reachable from your phone / on the road — no terminal and no ssh
- You’re ready to keep an always-on daemon on your own machine (desktop / NAS / VPS under your control)
When not to use it
- Your Telegram is minimal (a few contacts, public channels) — there’s no point
- You’re happy with Telegram UI search
- You don’t trust the idea of “your own MTProto userbot” (Telegram tolerates personal use but doesn’t approve — a gray zone)
- You don’t want to keep an always-on process — without the daemon the index falls behind reality
The key idea
The approval gate is the only safety story for outbound. The userbot reads everything (it is your account over MTProto). Outbound goes only through bot-side approval with use-once tokens. No “trusted” path around the card. If the card is broken — nobody writes.
Separation of roles:
The human:
- Creates the Telegram API credentials and the approval bot (one-time setup)
- Receives approval cards in Saved Messages, presses ✅/✏️/❌
- Uses the MCP tools in the coding agent for read operations
- Writes to the same bot to talk to Claude in the vault (Phase 4)
- Decides which working patterns and extensions they need (see “How we work” / Roadmap)
The coding agent (Claude/Codex/etc.):
- Uses the
tg_*MCP tools to search and analyze Telegram - Proposes outbound through
tg_propose_send— NEVER sends directly - In a Phase 4 chat session may call any
tg_*tool including outbound (still through approval)
The daemon (background):
- Indexes incoming messages in real time
- Keeps the approval-bot polling
- Spawns the coding-agent subprocess for chat sessions
- Audit log, rate limits, kill switch — all live in it
Architecture
┌─────────────────────────────────┐
│ Your Telegram account │
│ (private chats, groups, │
│ channels you subscribe to) │
└─────────────────┬───────────────┘
│ MTProto (userbot SDK)
▼
┌─────────────────────────────────────────────────┐
│ Local daemon (always-on background service) │
│ │
│ ┌──────────────┐ ┌────────────────────┐ │
│ │ Indexer │───→│ SQLite + FTS5 │ │
│ │ (New/Edit/ │ │ • messages │ │
│ │ Deleted) │ │ • transcripts │ │
│ │ + Whisper │ │ • chats / users │ │
│ │ transcribe │ │ • audit_log │ │
│ └──────────────┘ │ • approval_reqs │ │
│ └─────────┬──────────┘ │
│ │ │
│ ┌─────────────────────────────▼─────────┐ │
│ │ Local HTTP API (127.0.0.1, X-API-Key)│ │
│ │ /search /chats /outbound /control │ │
│ └─┬───────────────────┬───────────────┬─┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌──────────────┐ ┌──────────┐ │
│ │MCP shim │ │Approval bot │ │ CLI │ │
│ │(stdio, │ │(Bot API, │ │ commands │ │
│ │ 8 tools)│ │ separate │ │ for ops │ │
│ └────┬────┘ │ identity) │ └──────────┘ │
│ │ └──────┬───────┘ │
│ │ │ │
└───────┼────────────────┼────────────────────────┘
│ │
▼ │
Coding agent │
(Claude Code, │
Codex, Cursor...) │
│ │
┌──────▼──────┐ │ ✅ approve / chat
│tg_* tools │ │ ✏️ edit / ❌ cancel
│for read / │ │
│propose_send │ │ ┌────────────────────┐
└─────────────┘ └→│ Telegram (your │
│ Saved Messages or │
Phase 4 chat: │ bot's DM with you) │
Bot DM ───────────┐ └────────────────────┘
│
┌─────────▼──────────────────┐
│ Coding-agent subprocess │
│ (claude --print --resume) │
│ cwd = your vault │
│ persistent session │
└────────────────────────────┘
Five layers
- Read intelligence — userbot indexer + FTS5 store + read API + MCP read tools. Voice notes and video circles are transcribed by a local Whisper and land in the same index. Claude can search and read, but not write. Capability A.
- Outbound with approval — propose-send through the bot with use-once tokens and audit. Claude proposes, you approve. Capability B.
- Inbound chat session — the bot = a persistent Claude conversation with your vault as context. Capability C.
- Cross-cutting — owner-only checks, rate limits, kill switch, daily digest, audit log. Safety on top of all the layers.
- HQ track (Phase 5) — the
@hqbridge to the orchestrator, alerts into its inbox, a scoped MCP for additional agents, a second bot-persona in groups. This is no longer “Telegram for one agent” but Telegram as transport for a team of agents; every identity in the daemon has a gate of its own.
Key principles
-
The approval gate is the entire safety story. There is no trusted path for outbound. Every code path always goes through the card with a use-once token. If someone proposes “an optimization via bypass” — refuse.
-
Owner-only on the bot. The approval bot accepts callbacks and DMs only from one
chat_id(your own Telegram user_id). Anyone else — silent ignore (DM) or aNot authorizedtoast (callbacks). -
Use-once tokens bound to the content. An approval token = (random nonce, chat_id, sha256(text), expires_at, used). On ✅ the daemon validates all four fields and atomically sets
used=true. Substituted content or a repeated send — refused. -
Local-only by default. The HTTP API on
127.0.0.1, MCP over stdio. No cloud services in the data path. Even the approval bot works through the standard Bot API (via Telegram’s servers), but callbacks are received by local polling, not a webhook. -
Append-only audit. Every event (
PROPOSED,APPROVED,EDITED,SENT,SEND_FAILED,RATE_LIMITED,REJECTED,CANCELLED,TIMEOUT) is written to a separate table. INSERT only, never UPDATE/DELETE. Reasoning: forensics after a screw-up. -
Rate limits as defence in depth. Configurable caps: per hour (say 30), per minute (10), per-peer cooldown (3s). A triggered limit → a
RATE_LIMITEDaudit entry + reject, not throttle. -
A visible kill switch. The
/pausecommand via the bot or the CLI instantly stops all outbound./resumeturns it back on. Use it when you suspect a bug or while you sort out an incident. -
Group/channel writes are opt-in. In
config.yamlthe defaults areallow_groups: false,allow_channels: false. Phase 3 refuses groups/channels until you flip the flag. Reasoning: far more sensitive, wider blast radius. (In our instanceallow_groupshas been on since 17.09.2026 — needed for posts into the course cohort’s group; the approval gate did not go anywhere.) -
Vault-as-context (Phase 4). The coding-agent subprocess starts in your knowledge-base directory. It reads your
CLAUDE.md/AGENTS.md, sees your structure, can call the Telegram MCP tools. A persistent session via--resume— the conversation accumulates. -
TOS hygiene. Telegram tolerates personal userbots without spam/automation/scraping. No mass-send, no broadcast, no scraping of chats you’re not a member of, no bypassing the UI. Owner-only + rate limits — that is your protection.
-
Use it, see what hurts. After every phase — a soak period. Real friction points go into the backlog, not planned refactors. Code review before merge — yes; pre-emptive optimization — no.
-
One identity — one gate (Phase 5). Once more than one bot or more than one consuming agent lives in the daemon, each has its own admission operator first in every handler: the approval bot — only the owner’s
chat_id; the bot-persona — only groups from the whitelist plus the owner’s DM; the second agent over MCP — only--scope. The boundaries are guarded by tests (an AST test “gate is the first operator”, a boundary test “the referent doesn’t touchmessages/vault/bridge”), not by discipline.
Phases
Four base phases plus a fifth, the HQ one (it appears when Telegram becomes transport for a team of agents rather than for one). Each phase is valuable on its own. You can stop at any of them. A realistic size: each ~1–2 working days with a coding agent, ~15–25 tasks, ~50–100 unit tests.
Phase 1 — Foundation (read intelligence)
What gets built:
- Project skeleton (a separate git repo, not in the vault)
- Telegram API credentials in config.yaml
- Userbot (MTProto client) — authorization via SMS+2FA, the session file is stored locally. The session file = full access to the account: keep it as a secret (chmod 0600, not in git, make a backup). Don’t run the same session from two places at once — Telegram sees a conflict and may revoke the session or flag the account
- An always-on daemon (see Cross-platform notes below for OS-specific startup)
- SQLite schema:
messages(+ atranscriptcolumn for recognized voice) + an FTS5 virtual table,chats,users,meta, plusaudit_logandapproval_requestsin advance (Phase 3 will use them without migrations) - A real-time indexer on 3 events:
NewMessage,MessageEdited,MessageDeleted - An initial backfill of N days on first launch (say 30), with a persisted flag so it doesn’t repeat. Backfill in batches with pauses, not “everything at once”: mass reading easily catches a
FloodWait(Telegram asks you to wait N seconds) - CLI commands:
setup,status,search,contacts,chat
Capability A: you can search your correspondence via the CLI.
What you have to decide:
- Which MTProto SDK for your language (Python: Telethon / Pyrogram. JS: gramjs. Go: gotd. Rust: grammers)
- Where the data dir goes (see the questions)
- How many days of backfill
- Which FTS5 tokenizers your language/SQLite supports (for Russian + English — usually
unicode61 remove_diacritics 2)
Phase 2 — Local API + MCP integration
What gets built:
- An HTTP server on
127.0.0.1:<port>, X-API-Key auth (an auto-generated random key stored in the data dir with mode 0600) - Endpoints:
POST /search/messages,POST /search/contacts,GET /chats/{id},GET /chats/{id}/messages,GET /chats/recent,GET /health,GET /stats - An MCP shim — a separate binary/script that connects to the coding agent over stdio. Inside, it makes HTTP calls to its own local API
- 5 read MCP tools:
tg_search_messages,tg_search_contacts,tg_get_chat_info,tg_read_chat,tg_list_recent_chats - Admin tools (added as you go, not required for the MVP):
tg_backfill_chat(a history pull beyond the launch window viaiter_messages) andtg_transcribe_media(voice notes / video circles → text through a local Whisper; the result is cached in the DB and indexed in FTS)
Capability A through Claude: Claude Code (or another MCP client) sees the 5 read tools and can use them.
What you have to decide:
- HTTP framework (Python: FastAPI/Starlette. JS: Hono/Express. Go: chi/Echo)
- MCP SDK (there is one for Python, TypeScript, Go)
- How to store the API key (generated once, in the data dir, not in the repo)
Phase 3 — Outbound with approval
What gets built:
- Creating the approval bot via
@BotFather(one-time, your action) - The Bot SDK integrated into the daemon (same process): aiogram (Python), grammY (JS), telegram-bot-api (Go) — whatever fits the language
- Endpoints:
POST /outbound/propose,POST /control/pause,POST /control/resume - Storage primitives:
tokens.py(issue/validate/consume),audit.py(append-only writer),ratelimit.py(in-memory three-cap limiter),proposals.py(CRUD over approval_requests) - The approval card: an HTML message with an inline keyboard
✅ Send / ✏️ Edit / ❌ Cancel, callback_data =<action>:<proposal_id> - The orchestrator (a state machine): propose → creates the row, sends the card, parks a future, waits for a callback or timeout. On ✅ — issues a token → validates → sender (via the userbot) → audit
SENT - MCP tool:
tg_propose_send(chat_id, text, chat_type, ...)— a long-poll at the HTTP level up to the approval window (30 minutes in our case), blocks in the coding agent until the decision - CLI:
tg propose,tg pause,tg resume,tg setup-bot
Capability B: Claude can propose messages, you approve.
What you have to decide:
- The bot’s name (something recognizable:
@MyClaudeApprovalBotetc.) - Where to keep the bot token (config.yaml; data dir; encrypted)
- Default rate limits (recommendation: 30/hr, 10/min, 3s/peer for personal use)
- Approval timeout (recommendation: 30 minutes by default, configurable; we started with 5 — not enough whenever the phone wasn’t in hand: cards expired, the agent hung waiting)
- Groups/channels — enable right away or keep off?
Phase 4 — Inbound chat session (the tg session)
What gets built:
- The
tgsessionpackage:meta.py(k-v session state in the same SQLite),transcript.py(a per-session markdown writer),agent_runner.py(an async wrapper over the coding-agent CLI — see “Agent Runner” below),manager.py(lock + queue + state),formatting.py(Markdown → Telegram-HTML + a length splitter),commands.py(/new /status /help renderers) - In the daemon’s msg_handler: dispatch for
/new/status/help(new) +/pause/resume(existing) + plain text →manager.handle()→ Agent Runner - A per-session markdown transcript in a separate folder inside the vault (e.g.
Claude-tg-sessions/) — it doubles as the context store for strategy B (below) - UX: a ⚡ reaction on your message (in progress), ⏳ if queued, a “typing…” indicator while the agent works, bot menu commands (
/new/status/helpappear in Telegram’s menu button) - Concurrency: lock + queue (max 5), 6+ → reject. Late replies (queued messages) arrive as separate messages in the same chat
- (Optional) A 50% context warning: if the CLI returns usage metadata — once cumulative tokens cross half of
contextWindow, add a one-time prefix⚠️ context 50%+ (consider /new). No usage metadata — just skip it.
Agent Runner — the contract and the strategies (this is what makes Phase 4 portable)
Don’t tie yourself to one CLI. Describe the Agent Runner as a component with a contract and implement it for your agent. The contract:
- (required) accept the message text + working directory = the vault → return a text answer;
- (desirable) keep the conversation context between messages;
- (optional) return metadata:
session_id, token usage.
“Memory between messages” — three strategies in descending order of preference; pick by what your CLI can do:
| Strategy | When | How |
|---|---|---|
| A. Native resume | The CLI has resumable sessions (Claude Code: --resume <id>) |
Store session_id in meta, pass it on every request. The agent keeps the context itself. Cheapest in tokens. |
| B. Transcript replay | The CLI is one-shot without memory (Codex exec, Gemini non-interactive, aider --message) |
You’re writing a markdown transcript anyway — feed it as context on every request (stdin / a context file / --message). Works with any one-shot CLI; more expensive in tokens. |
| C. Stateless one-shot | A minimal implementation / simple questions | Every message is an independent request without history. Degraded UX, but up and running in an hour. |
The same for the other capabilities:
- System-prompt injection (instruct it: answers via Telegram, ≤4096 chars, light Markdown, no tables): a native flag if there is one (
--append-system-prompt), otherwise prefix the instruction to the start of the message / context file. - session_id / usage: parse from structured output if the CLI gives JSON (
--output-format json); otherwise generate your ownsession_id(uuid) on the daemon side and disable the context warning.
Capability C: you write to the bot from a phone/tablet — the agent answers with the context of your vault.
What you have to decide:
- Which coding-agent CLI (
claudefrom Anthropic,codexfrom OpenAI, gemini CLI, aider…) — and whether it has resumable sessions (this is what picks strategy A/B/C above) - Where the transcript folder lives (inside the vault — recommended; for strategy B it is also the context store)
- The default model (if the CLI is multi-model): for coding-style questions — stronger; for chit-chat — faster/cheaper
- The subprocess timeout (recommendation: 10–20 minutes — long sessions with a big context really do hit a small timeout)
Phase 5 — The HQ track (orchestrator, alerts, a second bot)
It appears when you no longer have “one agent with Telegram tools” but a team of agents with a main orchestrator session, and Telegram becomes transport for it. Five independent pieces, each switched on separately:
5a. The bridge to the orchestrator (@hq). A message to the approval bot that starts with @hq … (or @main …) is intercepted in the daemon code before the pocket tg session and delivered to the live main session of the agent: either into a unix socket the session publishes on startup, or (the default, more reliable) as a ## TG <date time> section in an append-only inbox file in the vault, which the main session keeps a monitor on. The bot’s reply in Telegram — “Passed on ✓ (socket|inbox)”. Reasoning: the pocket session is a separate process with no orchestrator context; HQ needs a direct entrance from the phone. Media albums captioned @hq take the old route through the tg session.
5b. Alerts into the inbox. A separate Telegram group, “Alerts”, where the production bots (community, product) duplicate their admin alerts; the userbot sees it as an ordinary chat, the alerts module drops the messages as ## ALERTS sections into the same inbox file. The main session verifies the facts, acts, and sends the human only a summary “what happened → what we did”. The human’s channels stay a duplicate, not a relocation.
5c. A scoped MCP for a second agent. The same MCP server, started with --scope digest, gives the second agent (in our case — Codex, assembling a channel digest) only the channels from the digest rules and the tg_digest tool, not the whole correspondence. One daemon, different access windows per agent.
5d. A second bot with a persona for other people’s groups. A separate Bot API token and a separate Dispatcher in the same process (handle_signals=False on the second one, otherwise aiogram fights over signals). Its own track: its own tables (a sliding 7-day window of the group’s history), its own gate as the first operator — a group from the allowed_chats whitelist or the owner’s DM, everything else stays silent (fail-closed, an empty list = silent everywhere). Privacy mode is off for the bot in BotFather, it reads the whole group and says so honestly on /privacy. It does what the group needs, not what the owner needs: replies on mention/reply in persona, auto-transcription of voice notes and video circles by a local Whisper for every participant, photo analysis, “draw me…”, end-of-day summaries, reminders, jokes in context, a council of personas, a duel of two models. An artifact like “make a deck out of this thread” goes as a ## REFERENT section into the orchestrator’s inbox, and the file comes back into the group through POST /referent/send_file (the chat — only from the tasks table, the path — only from the whitelist of folders). A daily spend cap, a rate limit, a stale filter on updates (the Mac was asleep — old ones go to memory only). Boundaries by construction: the referent never touches messages, the vault, the pocket session, MCP or the bridge — boundary tests guard this.
5e. Cards of other tracks through the same bot. The approval bot turned out to be a convenient “card mailbox” for the whole HQ: the mail watcher sends email cards with buttons through it, the production watcher — alerts. The rule is the same — only the owner’s buttons are accepted.
What you have to decide:
- Where to deliver
@hq: the live session’s socket (instant, but depends on the session being alive) or an inbox file with a monitor (survives restarts). Recommendation: the file as default, the socket optional. - Whether you need a second bot at all. It adds something new only in a group with other people; for the owner the pocket session does all the same. Don’t start one “because you can”.
- What the bot-persona sees and who has been told: the group is someone else’s, the replies go to a model’s API — the participants must know.
How we work (working patterns)
This is the part the system exists for. The four phases above are what is built; below is how you live with it day to day. The patterns are described generically (recognizable scenarios, not tied to specific chats); the lines in italics are illustrations — substitute your own.
Every pattern: when → what the agent does → tools → example.
1. Research over your own correspondence
- When: you need to recall or find something in your own history — “what did we decide with X”, “find contacts from Y”, “give me a digest of channel Z for the week”.
- The agent: searches FTS, reads the relevant chats, summarizes. Returns an extract, not raw message bodies (see the privacy principles).
- Tools:
tg_search_messages,tg_search_contacts,tg_read_chat,tg_get_chat_info,tg_list_recent_chats. - Example: “what did we last agree on with Igor?” → find the contact → read the chat → summarize the key agreements.
2. Restoring history (backfill) before research
- When: the chat/channel you need is older than the indexing window (the daemon accumulates from the moment it started + N days of backfill at startup). Old correspondence is simply missing from the index.
- The agent: pulls the chat’s history on demand, then works with it as with the ordinary index.
- Tools:
tg_backfill_chat→tg_search_messages/tg_read_chat. - Example: “pull up the whole correspondence with the contractor for the last six months and find where the estimate was agreed”.
3. Voice → text before search/summary
- When: the chat has important voice notes or video circles that text search can’t see.
- The agent: transcribes with a local Whisper (offline, cached — every clip once), the text lands in the index, then — ordinary research.
- Tools:
tg_transcribe_media→tg_search_messages/tg_read_chat. - Example: “the call chat was all voice notes — transcribe them and put together the task list”.
4. Assisted outbound in your name
- When: “write to X that …” — the agent phrases a message as you.
- The agent: composes the text with the context of the correspondence →
tg_propose_send→ waits for your ✅ on the card. Never sends directly. - Tools:
tg_propose_send(+ the read tools for context). - Example: “write to Igor that the meeting moves to Thursday” → a card in Saved Messages → ✅ → sent.
5. The agent writes as itself, introducing itself
- When: you want the agent itself to answer, not you through its hands — “answer for me”, “let your agent write to him”. The agent doesn’t pass itself off as you: it introduces itself as your AI agent.
- The agent: reads the context of the correspondence, answers to the point in the first person (“this is so-and-so’s agent”), still goes through approval. The very fact of such an answer is a demonstration that the agent sees the context and has the access (which a chat in a vacuum doesn’t).
- Tools:
tg_read_chat→tg_propose_send. - Example: the other person asks “how is your agent better than an ordinary chatbot?” — you answer “let it explain for itself”, and the agent writes to them directly, proving the difference as it goes.
6. Pocket Claude — working from the phone
- When: you’re not at the computer, but you need to ask or do something in your knowledge base.
- The agent: you DM the bot → the daemon brings up the coding agent in your vault via
--resume→ the answer comes back as a reply. The session is persistent,/newresets the context. - Tools: the tg session (Phase 4) + any
tg_*from inside the session. - Example: from the subway — “what’s on my calendar tomorrow, and draft talking points for the call”.
7. Daily digest
- When: you want to keep outbound activity under control without checking the logs by hand.
- The system: at a set time it sends the approval chat a summary (what was proposed / sent / rejected / timed out over the day).
- Tools: the digest scheduler in the daemon.
- Example: the morning summary “yesterday: 3 proposed, 2 sent, 1 rejected”.
8. HQ from the phone (@hq)
- When: you’re away from the computer and the orchestrator needs an instruction or an answer — not the pocket session, but the one that holds the context and the decisions.
- You: write to the bot
@hq publish yesterday's recording— and get “Passed on ✓”. - The system: the daemon drops a section into the inbox file, the main session’s monitor wakes it, it does the job and answers you through an approval card or in the live dialogue.
- Tools: the bridge in the daemon + the inbox file + a monitor on the agent’s side.
9. A bot in someone else’s group
- When: in a group with friends or colleagues you want a “house” bot: transcribe a voice note for everyone, read a screenshot, do the day’s summary, joke in tune.
- You: add the bot, put the group id on the whitelist, tell the participants that the replies go to a model’s API.
- The system: answers only when addressed, remembers the group’s window, has no access to your correspondence or your vault.
- Tools: Phase 5d.
The invariant over all the patterns: the read patterns (1–3) are fully local — the data doesn’t leave your machine. Any outbound (4–5) goes through the approval gate — the final ✅ is always yours, no exceptions.
Cross-platform notes
macOS
- Always-on: a
launchdplist in~/Library/LaunchAgents/<your-app>.plist. RunAtLoad=true, StartCalendarInterval for cron-like jobs (backup). - Data dir:
~/Library/Application Support/<app>/ - Coding-agent CLI: usually installed via
brew. Path:/opt/homebrew/bin/<cli>(Apple Silicon) or/usr/local/bin/<cli>(Intel). - PATH gotcha: a launchd subprocess does NOT inherit the interactive shell PATH. Hard-code
<absolute path>/cliin the spawn args, or set the env viaEnvironmentVariablesin the plist. - Sleep: with the lid closed the daemon carries on; the MTProto client catches up via
getDifferenceafter wake.
Linux
- Always-on: a
systemduser unit in~/.config/systemd/user/<app>.service(systemctl --user enable --now <app>) — recommended for a desktop/server. Alternatives: supervisord, runit. - Data dir:
~/.local/share/<app>/(XDG spec) or$XDG_DATA_HOME/<app>/. - Coding-agent CLI: install via npm/pip/curl, the path is usually
/usr/local/bin/or~/.local/bin/. - PATH gotcha: systemd user services usually inherit the user environment if
Type=simplewithoutUser=overrides — but an absolute path in the spawn args is still better. - Headless server: simply more convenient — no sleep, no VPN trouble.
Windows
- Always-on: several options, pick by comfort:
- Task Scheduler — built in, can be set up with an “At log on” trigger + “Restart on failure”, launched as a hidden window
- NSSM (Non-Sucking Service Manager) — a wrapper that turns any program into a Windows Service
- WSL2 — if you’re a Linux fan and want systemd, WSL2 supports
systemd(has to be enabled in.wslconfig). Bonus: the same install as on a Linux desktop
- Data dir:
%APPDATA%\<app>\(C:\Users\<you>\AppData\Roaming\<app>\) - Coding-agent CLI: the install depends on the CLI. Claude CLI — via
npm i -g @anthropic-ai/claude-codeor scoop. Codex — npm. The path is in%USERPROFILE%\AppData\Roaming\npm\or scoop’s~\scoop\shims\. - Path separators: use
pathlib.Path(Python) or the equivalent — no"/"in string concatenation - Subprocess gotcha: Windows doesn’t like
asyncio.subprocess.PIPEwithoutasyncio.WindowsProactorEventLoopPolicy. On Python 3.8+ it’s the default, but verify - WSL2 alternative: run the whole daemon inside WSL2 — a simpler mental model, but the coding-agent CLI has to be in WSL2 too so the paths match
Docker (universal)
- One container with the daemon (indexer + HTTP API + approval-bot polling + tg-session subprocess host)
- A volume for the data dir + the vault dir
- The only gotcha: the coding-agent subprocess has to be in the same image. I.e. the container must have the
claude/codexCLI installed - Plus: on a VPS / NAS Docker is the cleanest path
- Minus: on Mac/Windows — the Docker Desktop overhead, and Telethon/grammers are more efficient run directly anyway
Tech stack — the default recommendation
If you have no preferences at all — Python:
- Telethon (MTProto userbot) — mature, active, cross-platform
- aiogram 3.x (Bot API for the approval bot) — async, clean
- aiosqlite (async SQLite) — no separate DB server
- FastAPI or Starlette (HTTP API)
- Anthropic’s MCP SDK for Python (the
mcppackage on PyPI) - pytest + pytest-asyncio
A JS fan → the Telethon analogue gramjs + grammY (Bot API) + better-sqlite3 + Hono + the MCP TypeScript SDK.
Go → gotd/td (MTProto) + go-telegram/bot (Bot API) + a sqlite3 driver + chi/Echo + the MCP Go SDK.
Questions for adaptation
This is the central section of the Blueprint. The coding agent must ask the user these questions before it starts writing code. Without the answers the implementation will break on specifics.
Environment
- OS: macOS / Linux / Windows / a Docker host?
- Always-on mechanism: launchd / systemd / Task Scheduler / Docker / WSL2 / a VPS?
- Where the project code lives: which directory for the repo (NOT inside the vault)?
- Where the vault / knowledge base lives: the path? (needed for Phase 4)
- Where the data dir lives: a preference for a specific path or the OS default?
- Which language / stack: Python / JS / Go / other?
Telegram
- API credentials: do you already have
api_id+api_hashfrom my.telegram.org? If not — create the app first. - Phone number / account: which number will be used for authentication? Recommendation — your main, living account, not a freshly created one: a new account + sudden automation = a higher risk of a flag.
- Backfill depth: how many days back to index on first launch? (recommendation: 30 for active users, 7 for heavy channels)
- Approval bot username: anything in mind? It will be created in @BotFather after Phase 3.
- Your Telegram user_id: for the owner-only check. If you don’t know it — you’ll find out after the Phase 1 launch via
tg statusor a direct DB query. - Groups/channels — outbound from the start or later? Default — off, opt-in via config.
- Voice transcription: do you need it? If yes — which Whisper engine (mlx-whisper for Apple Silicon, faster-whisper / whisper.cpp for CPU/CUDA, or a cloud API for a prototype on non-sensitive data) and which language dominates your chats?
Coding agent
- Which MCP client will call the
tg_*tools: Claude Code / Cursor / Codex / Gemini CLI / other? - The Phase 4 agent CLI:
claude/codex/gemini/ aider / other? Does it have resumable sessions (this picks the memory strategy A/B/C in Phase 4)? Which model by default? - Your timezone (for the daily digest and the logs).
Possible extensions (Roadmap)
Current use is described in “How we work”. Below is what isn’t built yet and needs a design of its own (Phase 5+):
- Monitoring specific channels / chats with keyword alerts (Claude tags an interesting message → a ping in Saved Messages) — partly covered by Phase 5b another way: alerts not by keyword but by a dedicated group
- Background digests by topic (“what was in the AI channels this week”)
- An image OCR pipeline (pictures → text in FTS5; voice is already done, see pattern 3)
- Cross-chat thread tracking (one topic discussed in 5 chats → a consolidated feed)
- Auto-categorization of incoming (work / personal / promo / …)
- Templated outbound (
tg_compose <template> <recipient>for frequently repeated tasks) - Multi-account support (a personal + a work account in one system)
- Voice replies (TTS from Claude’s answer → a voice message in the bot chat)
- Media through the approval card (
tg_propose_send_file: a file/audio with the same ✅) — right now the bot sends only text, media goes by a one-off script on the userbot session - A second wave of the bot-persona: memory across groups through a semantic store, a topic radar, regular summaries — only with the participants’ consent and whitelists
The user brainstorms with the agent which of these are relevant, and which new ones will emerge from their real work.
Nuances and rakes
From real operation — what you trip over, and how not to.
- A 5-minute approval window is too short. We started with 5, cards expired every time the phone wasn’t in hand, and the agent hung in the long-poll for nothing. 30 minutes is the working default; the token is still use-once.
- You have to press the approval card in time. The token lives for a limited window (say 60 s after issue), the proposal itself — until its timeout (minutes). Miss it — status
timeout, the message does not go out, and that’s correct: a silent late send is worse than a miss. Just propose again. (Caught live: the card went out, the person got distracted — the propose had to be repeated.) - The bot sends text only. The approval path is built for text. A file / photo / audio can’t be sent through it. The workaround for a one-off media send is a separate script on the userbot session (
send_file), outside the approval loop. Don’t bolt media onto the card “on the fly”. - Privacy: summarize, don’t dump. This is personal correspondence. By default the agent returns an extract, not raw message bodies into the chat, and never exports the correspondence to external services.
database is locked. Usually two running daemons (an orphan + launchd/systemd) fighting over the DB, or a long write transaction. WAL +busy_timeoutremove 99%; if it persists — kill the duplicate process.- In-flight state doesn’t survive a daemon restart. Pending proposals and active tg sessions live in memory. A restart mid-wait → the HTTP caller gets a timeout, and a “ghost” card with old buttons stays in the chat. Known follow-up: mark hanging pendings as expired on startup.
- FTS5 and special characters. Hyphens, URLs and special characters break the FTS5 syntax if the query isn’t sanitized (phrase escaping) — otherwise search falls over with a 500. At the same time advanced syntax (quotes,
*, AND/OR/NOT, NEAR) has to pass through as is. - Transcription: cache and resilience to broken clips. Recognized once → cached in the DB, never run again. A batch of voice notes must not fail as a whole because of one broken clip — each is processed in isolation.
- launchd/systemd don’t inherit the shell PATH. The coding-agent subprocess won’t find
claude/codexby name. Put the absolute path to the CLI in the spawn args (details in Cross-platform notes). - The session file is the key to the account. Don’t commit it, don’t share it, back it up, permissions 0600. Whoever gets the file gets your whole Telegram. Loss/invalidation = SMS+2FA again.
- One session — one place. The same session file run from two machines/processes reads to Telegram as a conflict → it may revoke the session or flag the account. (This is about the Telegram session — unlike
database is locked, which is about two daemons on one DB.) - FloodWait — respect it, don’t hammer. On backfill / mass reading Telegram answers with a
FloodWaitErrorand a number of seconds. The SDK usually waits on its own, but not always — don’t retry aggressively on top. Backfill in batches with pauses; the first 30-day backfill on an active account is really a lot of requests. - Tests — without a live Telegram. Run those 50–100 tests against a fake MTProto client (mock the SDK interface, not the real network) + an in-memory SQLite. Hitting the real Telegram from tests is both flaky and a FloodWait risk.
- Two bots in one process — the second
Dispatcherwithhandle_signals=False. Otherwise aiogram installs the signal handlers twice and the daemon won’t stop cleanly. - A bot’s privacy mode changes only with a re-add to the group. After
/setprivacyin BotFather the bot has to be removed from the group and added again — otherwise it keeps seeing mentions only. - The gate goes in every handler, not “somewhere at the start”. Once the owner-only check sat in one place, new handlers went around it, and the bot answered strangers’ DMs. Now the gate is the first operator in every handler, and an AST test checks it.
- The Mac slept — the updates went stale. After sleep a bot in a group catches a pile of old messages; answering them is spam. A stale filter by update age: old ones go to memory only, no reply.
- Phase 4 depends on what the CLI can do. Not every agent has a native
--resume. If your CLI has no resumable sessions — don’t emulate it with flags, take strategy B (transcript replay), see “Agent Runner” in Phase 4. - The Agent Runner writes to stdout, not to Telegram — the daemon closes the loop. The most common slip when assembling Phase 4: the agent “answers in the console” rather than in the chat. Two causes. (1) The CLI was launched in interactive mode — run it one-shot / non-interactive (
claude -p/codex exec/ aider--message), otherwise the agent opens a TUI and prints to the terminal, not to the captured stdout. (2) The daemon got the Agent Runner’s return value but didn’t send it back —bot.reply(answer)was skipped, the output settled indaemon.log. Closing the loop “DM → Agent Runner →bot.reply(answer)” is a mandatory explicit step, not something that “just happens”. The check: you DM the bot → the answer arrives in the same chat, not only in the daemon log. (Caught with someone building from the blueprint: everything worked, but the answers went to the console — the closing reply was missing.)
TOS / Safety constraints
A personal Telegram userbot is a gray zone in the Terms of Service. Telegram tolerates personal use but does not approve of automation/spam/scraping. What you must not do:
- Mass-send / broadcast — owner-only check + rate limits + the approval gate. Every message is manual (through ✅).
- Scraping channels you’re not in — the userbot reads only what your account is already a member of.
- Reverse-engineering the Telegram UI / web client — no client hacks, only the official MTProto API.
- Automation that mimics a person — the approval gate means the final ✅ is always yours; the coding agent proposes, you decide.
- Sharing Telegram data with third-party services — a local-only design, MCP over stdio, HTTP on 127.0.0.1.
- Bot identity confusion — the approval bot has an explicit bot name and doesn’t pretend to be a human.
What you can do, and is encouraged:
- Personal search / analytics over your own correspondence
- A local LLM-augmented assistant
- Approval-gated outbound in your own name
- A backup of your own messages (it’s your data)
What is NOT in the Blueprint (out of scope)
- Multi-account — the system is designed for one Telegram account per instance. You can bring up a second instance in another data dir, but cross-account features (a merged search, switching) aren’t provided
- Web UI — no dashboards / browser interfaces. Everything via CLI / MCP / the Telegram bot
- A public bot service — the approval bot is owner-locked, it can’t be turned into a shared service. The bot-persona for groups (Phase 5d) is not an exception to this but a separate identity with a separate gate; it isn’t a public service either, but a bot in your groups by whitelist
- Cloud deployment — the system assumes a machine you control (a desktop / NAS / VPS under your account). Managed cloud services fall outside the local-first design
- Persistent in-flight state — pending proposals / pending chat sessions live in memory. A daemon restart loses them (the HTTP caller gets a timeout, ghost cards in the chat). A known limitation, fixed in a follow-up
- Real-time streaming output — the chat session returns the answer whole once the subprocess finishes. Streaming / a typing buffer is a feature for Phase 5+
Workflow
Setup (one-time, ~30–60 min)
- Get the Telegram API app credentials at
https://my.telegram.org - The coding agent creates the project skeleton and implements Phase 1
tg setup— authorization via SMS + 2FA, the session file is savedtg status— verify everything is connected, the daemon runs through the always-on mechanism you chose- Backfill (30 days by default) — takes a few minutes on an active Telegram
- Phase 2: the HTTP API + the MCP shim, registering the MCP in the coding-agent client
- Verify via MCP:
tg_search_messages("test")returns results - Phase 3: create a bot in @BotFather, save the token in the config, implement the approval flow
- A manual smoke test:
tg propose <self_user_id> "hello" --timeout 60→ a card in Saved Messages → ✅ → message received - Phase 4: implement tgsession, restart the daemon, write the bot its first message
Daily use
- Read / research: in the coding agent (Claude Code/etc.) — “find what we discussed with Igor about project X”, “which AI channels mentioned Y”, and so on. Claude uses the MCP tools.
- Outbound: “write to Igor that the meeting moved” — Claude calls
tg_propose_send, you get the card, you press ✅/✏️/❌ - Pocket Claude (Phase 4): you write to the bot from your phone — “what’s on my calendar today”, “find that recipe I saved in favorites” — Claude answers in the chat through a subprocess in your vault
- Daily digest (if enabled): at 09:00 (or your time) the bot sends a summary of the day’s outbound activity
Maintenance
tg pause/tg resume— the kill switch when something looks strange/newvia the bot — reset the chat session when the context is tired- A daily DB backup via cron / launchd (Phase 1 supplement)
- Monitor
daemon.logfor FloodWait warnings (Telegram’s server-side rate limits — the SDK usually handles them itself, but stay alert)
Readiness checklist
After all 4 phases and the smoke test you should have:
- The daemon runs through your OS’s always-on mechanism
- The DB grows (the msg count increases over time)
- HTTP
/healthanswersok - The MCP tools are visible in the coding-agent client, search returns results
- The approval bot responds to
tg setup-bot - CLI
tg propose <self> "test"produces a card in Saved Messages - The bot reaction (⚡), the typing indicator, the menu commands (
/new/status/help) work - The audit log fills up (sql
SELECT * FROM audit_log ORDER BY ts DESC LIMIT 20) - Group/channel writes are properly rejected (test:
tg propose <group_id> "x" --type supergroup) - The kill switch works (
tg pause→ propose →rejected,tg resume→ propose → ok) - (opt.) Transcription:
tg_transcribe_mediaon a voice note → the recognized text shows up intg_search_messages - (opt.) Backfill:
tg_backfill_chatpulls history older than the launch window
Reference implementation
The reference implementation (Python: Telethon + aiogram + FastAPI + MCP, all five phases, ~360 unit tests, voice transcription by a local Whisper, history backfill, a daemon under launchd) is private; the blueprint describes its architecture and its rakes.
Known follow-ups
The blueprint is living — these aren’t holes but a deliberate backlog.
The system (engineering):
- Persistent in-flight state. On daemon startup, mark hanging
pendingrows astimeoutand, where possible, edit the ghost cards to “expired”. Right now a restart loses in-flight work. - Atomic token store. If the token store moves from memory into SQLite — switch to
UPDATE ... WHERE used = 0+ arowcountcheck instead of a non-atomic check-then-set. - Background-task done-callbacks. If the bot’s polling dies (a revoked token, the network), the daemon is alive but outbound is silently broken. Needs a done-callback that logs loudly and triggers a graceful stop.
- Server-side recipient resolution.
recipient_username/recipient_namecurrently come from the caller. Resolve through the DB (users.get(chat_id)) so the card is always trusted. - Media through approval.
tg_propose_sendis text only; a file/audio goes by a one-off script on a copy of the userbot session outside the approval loop. A mirrorsend_file_after_approval+ a tool is ~50 lines, not needed regularly so far.
Items 1–4 are still open as of 19.09.2026 — in operation they don’t hurt enough to overtake product work.
The blueprint itself (doc):
- Verify the Cross-platform notes on real Linux/Windows (currently based on general knowledge).
- A decision tree for the tech stack — a flowchart “language X + OS Y → this SDK” instead of text recommendations.
- Cost estimates — the daemon’s RAM/CPU/disk, the number of coding-agent API calls in Phase 4.
- A migration path — how to coexist if the user already has Telegram tooling (another userbot, an old bot).