Self-hosted · macOS + Cloudflare · free tier

Build a Telegram Bot That Runs Your Mac

A working pattern for controlling a Mac from your phone: one bot on the machine for files, camera, microphone and power; a second bot on Cloudflare that keeps answering after the Mac shuts down. 58 commands, no server bill, no open ports.

Mac must be awake

The local bot

Python on the machine itself. Reaches out to Telegram over long polling, so there are no open ports and no public IP.

45 commands$0 / month
Always on

The cloud bot

JavaScript on a Cloudflare Worker. Woken by webhook when a message lands, and by cron once a minute to fire reminders.

13 commands$0 / month

The split is the whole idea. A bot running on your laptop can do things no cloud service can — read your files, take a webcam photo, quit an app, shut the machine down — but it dies the moment the lid closes for good. A bot running on Cloudflare can answer at 3am from anywhere, but it has no idea what a Mac even is. Run both, on two separate tokens, and you stop having to choose.

What follows is the full command surface of a working setup, then how each half is deployed and why neither costs anything.

System status

Local bot
/ayuda
Prints the full command list inside the chat — /start does the same
/bateria
Reports battery percentage, charging state, and estimated time remaining
/espacio
Shows how much free disk space is left
/sistema
One-shot summary of uptime, CPU load, memory pressure, disk, and battery
/procesos
Lists the twelve processes currently eating the most CPU
/red
Reports the connected Wi-Fi network, the local IP, and the public IP
/estado
Says whether surveillance, recording, or reminders are running right now

Files

Local bot

The three folder commands list their contents when called bare, and send the named file when given one.

/desktop [name]
Lists the Desktop, or sends the named file from it
/descargas [name]
Lists Downloads, or sends the named file from it
/docs [name]
Lists Documents, or sends the named file from it
/buscar <word>
Searches the entire Mac by filename through Spotlight
/descargar <url>
Pulls a file off the internet straight into the Downloads folder
/zip <name>
Compresses a file or an entire folder and sends the archive back
/imprimir <name>
Sends a document to the default printer

Screen and camera

Local bot
/pantalla [doc]
Captures whatever is on the monitor right now — add doc for full quality
/foto
Takes a still photo through the webcam
/video [seconds]
Records a webcam clip with sound, three to sixty seconds long

Controlling the machine

Local bot

Shutdown and restart both require a confirmation word, so a stray tap cannot kill the machine.

/abrir <target>
Opens a website, a file, or an application
/cerrar <app>
Quits a running application by name
/apps
Lists every application currently open
/decir <text>
Makes the Mac read the text aloud through its speakers
/volumen <0-100>
Sets output volume, or silences it with /volumen mute
/musica <action>
Drives Spotify or Apple Music: play, pause, next, previous, now playing
/portapapeles
Sends back whatever text is sitting on the Mac's clipboard
/copiar <text>
Places text onto the Mac's clipboard, ready to paste
/notificacion <text>
Pops a native notification banner on the Mac's screen
/bloquear
Turns the display off, locking the Mac if it asks for a password on wake
/dormir
Puts the Mac to sleep
/apagar si
Shuts the Mac down
/reiniciar si
Restarts the Mac
/sonar
Plays a loud alarm for fifteen seconds, then puts the volume back where it was
/donde
Reports the machine's rough location from its public IP, with a map link

Surveillance mode

Local bot

While active the Mac is held awake, the display is slept, and the volume is muted, so the setup is not obvious to anyone in the room. The original volume comes back when you disarm it.

/fuera [sensitivity]
Watches through the webcam and sends a photo the moment it sees motion
/casa
Turns surveillance off and reports how many alerts fired

Audio and transcription

Local bot

Transcription runs locally through Whisper, so recordings never leave the machine. Anything past 3,500 characters arrives as a text file rather than a wall of message.

send a voice note
Any voice message sent to the bot comes back as transcribed text
/grabar
Starts recording the room through the microphone
/parar
Stops the recording and delivers the result
/acta
Records a meeting; stopping then returns both the audio and a written transcript

Daily life

Local bot

/atajo is the extensible one: anything built in the Shortcuts app becomes remotely triggerable without touching the bot's code.

/recordar <when> <text>
Sets a reminder in minutes, hours, or at a clock time — 20m, 2h, 18:30
/recordatorios
Lists pending reminders, and deletes one by number
/nota <text>
Appends a quick note to a running text file
/notas
Shows recent notes, and can clear them
/clima [city]
Reports current weather conditions
/tarea <text>
Creates a task in Apple Reminders, which syncs to the iPhone through iCloud
/atajo [name]
Lists Apple Shortcuts, or runs the named one
/velocidad
Runs an internet speed test for download, upload, and latency

Sent without being asked

Local bot
on boot
A message arrives when the Mac starts up and the bot comes online
low battery
A warning arrives when the battery drops below 15% while unplugged
on error
Internal failures are reported over Telegram instead of vanishing silently

The cloud bot — the always-on half

Always on

A separate bot on its own token, answering whether or not the Mac is running. The morning digest bundles the date, the weather, the pending reminder count, and the shopping list size into one message.

/ayuda
Prints the full command list inside the chat — /start does the same
/recordar <when> <text>
Sets a reminder that still fires with the Mac shut down
/recordatorios
Lists pending reminders, and deletes one by number
/lista añadir <item>
Adds an item to the shopping list
/lista
Shows the numbered shopping list
/lista quitar <N>
Removes item number N from the list
/lista vaciar
Empties the shopping list
/nota <text>
Saves a timestamped note to cloud storage
/notas
Shows saved notes, and can clear them
/clima [city]
Reports current weather, defaulting to the saved city
/ciudad <name>
Sets the default city used by the weather command and the digest
/cambio <amt> <from> <to>
Converts between currencies at current exchange rates
/diario <HH:MM>
Schedules a morning digest, or cancels it
/hora
Reports the current date and time in the configured timezone
/id
Reports your Telegram user ID, which you need during setup

No command matches that.


How each half is deployed

The local bot: long polling on the machine

A Python script using python-telegram-bot, running on the Mac. It uses long polling: it opens an outbound connection to Telegram and waits. Nothing listens on an open port, so there is no public IP to expose, no port forwarding, and no firewall hole. The machine reaches out; the internet never reaches in. That single design choice removes most of the attack surface a self-hosted bot would otherwise have.

Cost is zero. Telegram's Bot API is free and unmetered at this scale. The only thing consumed is the machine's own electricity.

brew install imagesnap ffmpeg
/usr/bin/python3 -m pip install --user \
  "python-telegram-bot[job-queue]" Pillow openai-whisper

Launching is a double-click on a .command file, which is just a shell script macOS opens in Terminal. Three things are worth building into that launcher:

  1. Locate the script relative to itself with cd "$(dirname "$0")", so the project folder can be moved without breaking anything.
  2. Probe for a working Python rather than trusting python3 from PATH.
  3. Restart on crash, by wrapping the run in a while true loop with a short sleep, so a dropped network connection does not end the day.

The second point is the one that bites people. A Mac with Homebrew typically has at least two Python installs — Homebrew's at /opt/homebrew/bin/python3 and Apple's at /usr/bin/python3 — and your libraries are installed into exactly one of them. PATH usually resolves plain python3 to Homebrew's, which may not be the one holding your dependencies, and the bot dies on ModuleNotFoundError. Probing sidesteps the whole question:

PY=""
for c in /usr/bin/python3 /opt/homebrew/bin/python3 python3; do
    if command -v "$c" >/dev/null 2>&1 && \
       "$c" -c "import telegram" >/dev/null 2>&1; then
        PY="$c"; break
    fi
done

To start it on boot: System Settings → General → Login Items → add the .command file. Have the bot message you on startup, so you know it came back up.

The limitation that justifies a second bot: polling needs a running machine. A sleeping Mac queues commands and runs them on wake, since Telegram holds updates for 24 hours, but a powered-off Mac cannot answer at all. No setting fixes that — the process has to be running somewhere else.

The cloud bot: a Cloudflare Worker

The second bot is a Worker: a small piece of JavaScript that Cloudflare runs on its edge network, on demand, with no server to maintain. It uses webhooks instead of polling — Telegram sends an HTTP request to the Worker whenever a message arrives, and the Worker answers. Between messages nothing runs and nothing is billed.

Two pieces support it:

Free-tier allowanceActual use
100,000 requests / day~1,440
100,000 KV reads / day~1,440
1,000 KV writes / dayonly on change
5 cron triggers1
1 GB KV storagea few KB

A once-a-minute cron costs 1,440 invocations a day against a 100,000 ceiling, so the headroom is enormous. The one real constraint is that writes are capped at 1,000 a day, which is why the scheduled handler should only write back to KV when something actually changed — not on every tick. Cron granularity also bottoms out at one minute, so a reminder can land up to sixty seconds late.

The outside services involved — wttr.in for weather, frankfurter.app for exchange rates — are free and need no API key.

Deploying the Worker

  1. Create a second bot with @BotFather and copy its token. Use a separate bot from the local one, so the two never compete for the same updates.
  2. Create a Worker in the Cloudflare dashboard and paste in the source.
  3. Add TELEGRAM_TOKEN and a self-invented WEBHOOK_SECRET as encrypted secrets, not plain variables.
  4. Create a KV namespace and bind it to the Worker under a name the code expects.
  5. Add a cron trigger of * * * * *.
  6. Register the webhook, passing the same secret:
curl "https://api.telegram.org/bot<TOKEN>/setWebhook\
?url=https://<your-worker>.workers.dev\
&secret_token=<YOUR_WEBHOOK_SECRET>"

Roughly fifteen minutes end to end. The Worker can be served from a subdomain of your own domain instead of the default workers.dev address, though that is cosmetic.

Project layout

telegram-bot/ ├── mac-bot/ │ ├── bot.py the local bot │ └── launch.command launcher, auto-restarts ├── cloud-bot/ │ └── worker.js Cloudflare Worker source └── README.md

Security worth building in from the start

This is remote-control software by design. It reads files, watches through the camera, listens through the microphone, and can power the machine down. That reach is the point, and it also means your Telegram account becomes as sensitive as your login password. Four things are worth doing on day one: