A free messaging app turns into the remote control for your whole operation: your scripts text you when something breaks, and you text back to run commands, read files, or talk to an AI that's standing inside your actual project. Here's how Telegram bots really work, two ways I wired them, every scar, and a working bridge you can run tonight.
You already have Telegram on your phone. A bot is just a second account your code controls — it can read messages you send and send messages back. That's the whole primitive, and it's enough to build:
/cmd pm2 status from a restaurant and your droplet runs it and texts the output back.No app to publish, no UI to build, no app-store review. The interface already lives on every phone you own.
Every bot starts with @BotFather — Telegram's official bot for making bots. You message it like a person:
/newbot → it asks for a name and a username (must end in bot).8554034779:AAF.... That token is the bot. Anyone holding it controls the bot, so treat it like a password (more on that under harden it).That's it. With a token and your id, the rest is two HTTP endpoints on api.telegram.org: one to receive messages, one to send them.
There are exactly two ways messages get from Telegram to your code. Pick by whether your machine has a public URL.
Your code asks Telegram "anything new?" on a loop. No public URL needed — works behind your home router, on your laptop, anywhere. Slightly higher latency, one long-poll connection open. Best for local + dev. This is how the bot on my Windows PC works.
Telegram pushes to you the instant a message arrives, by POSTing to a URL you register. Needs a public HTTPS URL + a secret token to prove it's really Telegram. Lower latency, scales better. Best for production. This is how the bot on my droplet works (thetower.one/telegram-webhook).
Same bot logic either way — only the delivery differs. The build below uses polling because it runs anywhere with zero setup; the factory section shows the webhook version.
A "bridge" bot is four moving parts, and every Telegram bot I've built is a variation on these:
update_id so you never reprocess one).if (from.id !== ALLOWED_USER_ID) reject. The first line of every handler. Without it, anyone who finds your bot can run your commands./start, /clear, /cmd …, or free text → an action.The action in the middle is the only part that changes. Echo the text, run a shell command, call an LLM, hit an API — the bridge around it stays identical. The working code at the bottom is exactly this skeleton with the action left as a one-line reply() you swap out.
The bridge gets interesting when the action in the middle is your actual workspace. Two real examples from my setup:
The bot on my Windows machine hands the model seven tools — list_directory, read_file, write_file, edit_file, run_command, plus two for reading past AI session transcripts — and runs an agentic loop: the model calls a tool, gets the result, calls another, until it has an answer. From my phone I can ask "what changed in the tax-scanner repo today?" and it reads the files and tells me. The loop is capped at maxLoops = 20 so a confused model can't spin forever.
The bot on my DigitalOcean droplet is repo-aware: /repos lists every project, /repo <name> switches context, and it injects that repo's file list into the system prompt before calling Claude. It runs as an HTTP server behind thetower.one, registers its webhook on boot, and processes messages through a serialized queue (one at a time) so two fast messages can't interleave and corrupt a conversation.
The droplet bot describes changes rather than applying them ("change line 42 of app.py to…") — read-mostly by design. The local bot can write and run. Same pattern, two trust levels. Decide how much hand the agent gets before you wire it, not after.
My Day generates a 6-digit code in the web app. The user sends it to the bot; the bot POSTs { token, chat_id, bot_secret } to the app's /api/telegram/verify; the app ties that chat to that user. Now the app can push reminders straight to their phone. A whole notification channel in ~15 lines.
A Cloudflare Worker receives Sentry's webhook and forwards a formatted alert to a Telegram channel. Production breaks → I know before a user emails. (The honest caveat: a forwarder can drop events if the source goes quiet — verify it end-to-end, don't assume silence means "all good.")
The Tower posts to a public channel — same sendMessage call, a channel id instead of a person. Ingestion in, broadcast out: the bot is a universal pipe.
I once set allowed_updates to ["callback_query"] to catch button taps. Telegram then silently dropped every text message for two days — and the messages sent during that window were gone for good, not queued. Fix: allowed_updates: [] (an empty array means "send me everything"). Only narrow it when you truly mean to.
When an API call failed mid-turn, I'd already pushed the user's message into the conversation history. The next call sent a malformed history and returned 400 forever — one failure bricked the whole chat. Fix: on error, history.pop() the message back off before replying with the error.
A bot's token IS the bot. I have a snapshot file that hardcoded one — and my own production script carries the rule in a comment: "NEVER hardcode tokens; it is committed to git." Read the token from a mode-600 .env, never from source. (For webhooks, also set a secret_token so a random POST can't impersonate Telegram.)
unhandledRejection + uncaughtException handlers, and back off on network blips — a bridge that dies silently is worse than one that errors loudly.Codex (OpenAI's coding agent) can be driven remotely — you hand it a task from away from the keyboard and it works against your repo, the same way the Telegram bots act on the machine. It's the "agent, not a chat box" pattern: you describe the outcome, it does the steps. (OpenAI's connectors to GitHub and Notion are the strongest in the set — see the Ledger.)
I reach my AntiGravity workspace by two independent paths, and having both means one is always up:
The polling bot on the PC — text it from anywhere, it acts on the local workspace and replies. Works through any network, no VPN.
A private mesh VPN puts my PC on a tailnet at a fixed address. The AntiGravity web server listens on that interface, so from my phone's browser I open http://<tailnet-ip>:3333 and get the full UI — encrypted, no ports opened to the public internet.
Telegram = the lightweight command line; Tailscale = the full desktop, privately. Pick by whether you need a quick command or the whole interface.
You don't have to write any of this. The whole point of vibe coding is that you describe the outcome and let the AI build the steps. Open Claude Code (or any coding agent) in an empty folder and paste the brief below — it'll produce the bridge, then walk you through getting a token and running it. This is the same bridge as the code section; one of you types it, the other talks it into existence.
A brief like this is more useful to a non-coder than the finished file — because the day you want it to do something different (run a command, call an LLM, ping an API), you change one sentence and ask again. You're steering, not maintaining.
This is the real skeleton, sanitized: long-polls Telegram, authenticates one user, chunks replies, never crashes, and carries the allowed_updates: [] lesson in a comment. The action is a one-line reply() — swap its body to run a command, call an LLM, or hit any API. Token comes from the environment, never the file.
Run it: get a token from @BotFather (/newbot), your id from @userinfobot, thenTELEGRAM_BOT_TOKEN=<token> ALLOWED_USER_ID=<your-id> node telegram-bridge.js — message your bot, it replies. That's the whole loop.