Deploy your first Telegram agent
Your personality runs locally in ethos chat. This tutorial puts it in front of real users on Telegram. The path is: create a bot, paste its token, foreground-test the gateway, then wrap it in a service manager that survives reboots.
Goal
By the end, you have:
- A Telegram bot created through BotFather with a token in
~/.ethos/config.yaml. - The Ethos gateway running in the foreground and replying to messages on Telegram.
- The same gateway running as a daemon under
launchd(macOS) orsystemd(Linux), restarting on crash and after reboot. - An access list so only you (and people you name) can DM the bot — using the pairing-code flow.
- A short troubleshooting cheat sheet for the daemon-vs-foreground failures you will eventually hit.
The bot answers as whatever personality you set in Create your first personality — or as one of the built-ins if you skipped that tutorial.
Prereqs
- Build your first agent finished —
ethos chatreaches the provider locally and replies stream in real time. - A Telegram account.
- A box that stays online: your laptop is fine while you test; a VPS or always-on home server is the production target.
- Network egress to
api.telegram.organd to your LLM provider. Both use plain HTTPS.
1. Create the bot
Telegram bots are created through a conversation with @BotFather inside Telegram itself.
- Open Telegram and search for
@BotFather. Start a chat. - Send
/newbot. - Pick a human-readable name (
My Strategist Bot). - Pick a unique username ending in
bot(my_strategist_bot). - BotFather replies with the HTTP API token — a string like
123456789:ABCdefGhIJklmNopQRstuVwxYZ. Copy it. Anyone with this token can speak as your bot, so treat it like a password.
While you are still in BotFather, two settings are worth confirming for a personal bot:
/setprivacyfor your bot, then Enable. Privacy mode means the bot only sees messages addressed to it directly or sent as a reply. This is what you want for a personal bot; you can turn it off later for group-wide listening./setdescriptionand/setabouttextare cosmetic but appear in Telegram's bot card — set them to something honest.
You can regenerate the token any time with /token in BotFather; the old token is invalidated immediately, so do this if you ever leak the value.
2. Configure the channel adapter
Ethos ships a Telegram adapter inside @ethosagent/cli — no extra install. The ethos gateway setup command writes the token into ~/.ethos/config.yaml for you and validates the token against Telegram's getMe endpoint.
ethos gateway setup
Paste the token at the prompt. Expected output:
Validating token...
✓ Bot validated: @my_strategist_bot
✓ Token saved to ~/.ethos/config.yaml
If validation fails, the token is wrong — re-run /newbot or /token in BotFather and try again.
Open ~/.ethos/config.yaml and verify the line landed:
grep telegramToken ~/.ethos/config.yaml
You should see telegramToken: 123456789:ABC....
If you prefer to hand-edit, the same effect is one line:
telegramToken: "123456789:ABCdefGhIJklmNopQRstuVwxYZ"
The gateway reads exactly that field on startup. Anything before : is the numeric bot id (informational); everything after is the secret. Do not commit this file to a public repo — ~/.ethos/config.yaml belongs in .gitignore for any directory you share.
3. Foreground-test before daemonising
Daemons fail silently. Before wrapping the gateway in a service manager, run it in the foreground from a normal shell and prove the bot replies. This is the most important step in the tutorial — fix every error here, not after.
ethos gateway start
Expected boot sequence:
ethos gateway starting...
Runs in the foreground. For always-on production, see https://ethosagent.ai/docs/using/how-to/run-as-daemon ...
Cron scheduler running (checks every 60s)
✓ Telegram online (412ms)
Listening for messages. Press Ctrl+C to stop.
Now message your bot from Telegram. Search for its username, send Hello. The bot should reply within a few seconds with the active personality's voice.
If it does not:
- No
✓ Telegram online— the token is wrong, or the box cannot reachapi.telegram.org. Test withcurl https://api.telegram.org/bot$YOUR_TOKEN/getMe— a working token returns JSON with"ok":trueand the bot's username. - Boot but no reply — your provider API key is wrong. Foreground errors print to stderr; scroll up. Run
ethos chatseparately to confirm the CLI side still works. No platform configured— the token did not land in the config file. Re-runethos gateway setup.- Bot replies once, then nothing — you have two gateways running against the same token. Kill the duplicate; Telegram drops long-poll requests beyond the first.
- Bot replies with an error tool result — your personality's
toolset.yamlallows a tool whose backing service is misconfigured. Switch toresearcher(default toolset works out of the box) to confirm it is configuration and not infrastructure.
Stop the foreground process with Ctrl+C when the bot is replying. Telegram queues incoming messages for several hours, so a brief downtime during the daemon switch does not drop messages.
4. Pick which personality the bot uses
The gateway runs one personality per bot. It uses the personality field in ~/.ethos/config.yaml by default — whatever you picked during ethos setup. To change it, either edit the file:
personality: strategist
…or use the CLI:
ethos personality set strategist
The bot you ship is the personality you picked. If you want a different personality on Telegram than in the CLI, the cleanest path is ethos personality duplicate strategist strategist-tg and then set personality: strategist-tg. They share an id space but are otherwise independent files — you can tighten Telegram's toolset (no terminal, no write_file) without affecting the CLI version.
Two other places personality routing comes up:
- The personality config's
platform: telegramfield binds the personality to the Telegram ingress. The gateway uses this hint to pick the right personality when multiple are configured; for a single-bot deployment, it is informational. - The pairing-code flow in step 5 carries the personality id on approval, so future messages from that sender route correctly.
5. Restrict who can DM the bot
By default, the bot replies to anyone who messages it. For a personal bot this is wrong. The gateway implements a pairing-code flow: a remote user sends a DM, the gateway generates a one-time code, and you accept the pairing from your CLI before that user's future messages are processed.
Start the gateway in the foreground again (ethos gateway start) and DM the bot from a Telegram account other than yours (or from the same account — both work). Open a separate terminal and run ethos chat. Inside chat:
/communications
You see a pending pairing code, something like:
Pending (1):
telegram 1234567 7H3K-9XQ2 "Hello"
Approve the sender:
/allow 7H3K-9XQ2
The gateway emits a confirmation. The remote user's next message lands as expected.
Revoke later:
/deny telegram 1234567
The pairing list lives in ~/.ethos/communications.json. Hand-editing is supported but discouraged — the /allow and /deny slash commands keep it consistent with the in-memory cache.
If you want to skip the pairing flow entirely (single-user bot for yourself), DM the bot from your own account and /allow your own pairing code once. Subsequent runs of the gateway remember the approval — the JSON file is the source of truth.
For a group chat, add the bot to a Telegram group and approve the group's chat id rather than individual senders. Group chat ids are negative numbers; you can read them out of /communications or by tailing the gateway log when the bot is added.
6. Wrap it in a daemon
Foreground works. Time to keep it running. Pick the manager that matches your platform.
macOS — launchd
launchd ships with macOS. Unit files (plists) live in ~/Library/LaunchAgents/. Find your ethos binary and your home directory first:
which ethos
echo $HOME
Common cases:
- Installed via the recommended script with
nvm—~/.nvm/versions/node/v24.x.x/bin/ethos. - Installed via system
npm—/usr/local/bin/ethosor/opt/homebrew/bin/ethos.
launchd runs without your shell's PATH, so use the absolute path you printed. The same applies to HOME — set it explicitly.
Create ~/Library/LaunchAgents/ai.ethosagent.gateway.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.ethosagent.gateway</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOU/.nvm/versions/node/v24.0.0/bin/ethos</string>
<string>gateway</string>
<string>start</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/Users/YOU/.nvm/versions/node/v24.0.0/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/YOU</string>
</dict>
<key>WorkingDirectory</key>
<string>/Users/YOU</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/YOU/.ethos/logs/gateway.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOU/.ethos/logs/gateway.err.log</string>
</dict>
</plist>
Replace YOU with the output of whoami. Load and start:
launchctl load ~/Library/LaunchAgents/ai.ethosagent.gateway.plist
launchctl start ai.ethosagent.gateway
Tail the logs to confirm it boots:
tail -f ~/.ethos/logs/gateway.out.log
You should see the same ✓ Telegram online line you saw in the foreground. DM the bot to confirm.
To stop, reload after editing, or remove:
launchctl stop ai.ethosagent.gateway
launchctl unload ~/Library/LaunchAgents/ai.ethosagent.gateway.plist
launchctl load ~/Library/LaunchAgents/ai.ethosagent.gateway.plist
RunAtLoad plus the file's location in ~/Library/LaunchAgents/ is enough to start the gateway on login. KeepAlive restarts it on crash.
Linux — systemd user unit
systemd user units live in ~/.config/systemd/user/. They run as your login user, not root — appropriate for a personal bot. Find the binary first:
which ethos
Create ~/.config/systemd/user/ethos-gateway.service:
[Unit]
Description=Ethos gateway (Telegram)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/home/YOU/.nvm/versions/node/v24.0.0/bin/ethos gateway start
Restart=on-failure
RestartSec=5
StandardOutput=append:%h/.ethos/logs/gateway.out.log
StandardError=append:%h/.ethos/logs/gateway.err.log
Environment=NODE_ENV=production
[Install]
WantedBy=default.target
Replace the ExecStart path with the output of which ethos (it must be absolute — systemd does not source your shell rc, so bare ethos does not resolve).
Enable, start, status:
systemctl --user daemon-reload
systemctl --user enable --now ethos-gateway.service
systemctl --user status ethos-gateway
Watch logs live:
journalctl --user -u ethos-gateway -f
For a server you SSH into, user units stop when your login session ends by default. Make them persist:
sudo loginctl enable-linger $USER
Restart, stop, disable:
systemctl --user restart ethos-gateway
systemctl --user stop ethos-gateway
systemctl --user disable ethos-gateway
For more daemon options — pm2, Docker, plain nohup — see Run Ethos as a daemon. launchd and systemd are enough for one bot.
7. Send the first production message
From Telegram, DM your bot. You should see a reply within a few seconds. Three things are happening:
- The Telegram adapter polls Telegram for new updates (long-polling, no webhook required).
- The gateway routes the inbound
InboundMessagetoAgentLoop.run()withsessionKey: telegram:<chatId>andpersonalityId: <your-personality>. - The streamed
AgentEventoutput is reflowed into Telegram messages (one per 4,096-character chunk; later edits update the same message id rather than appending new ones).
The bot's session is independent from your CLI session — each Telegram chat has its own session key. Your CLI conversation history is untouched, and vice versa. They do share MEMORY.md if (and only if) the personality's memoryScope is global.
The outbound deduplication cache is keyed by (sessionId, sha256(content)) with a 30-second TTL. If the agent emits the same message twice in quick succession (a streaming retry, for example), Telegram only sees the first. This is automatic; adapters do not roll their own deduplication. If you ever see the bot ignore a message you sent twice on purpose, that is the same gate firing — wait 30 seconds and try again.
8. Operate it
A few commands worth knowing once the bot is running.
Is it actually running?
launchctl list | grep ai.ethosagent # macOS
systemctl --user is-active ethos-gateway # Linux — exits 0 if active
Where are the logs?
tail -f ~/.ethos/logs/gateway.out.log
Ethos writes structured logs to ~/.ethos/logs/ on top of stdout. The service manager's logs are a superset — launchd writes the file directly per the plist; systemd writes to journalctl and also to the file because of the StandardOutput=append: directive.
Reading recent errors:
ethos errors # last 50 errors with cause and action
The errors command reads ~/.ethos/logs/errors.jsonl — every EthosError envelope produced by the CLI or the gateway lands there.
Updating the bot:
ethos upgrade
launchctl stop ai.ethosagent.gateway && launchctl start ai.ethosagent.gateway # macOS
systemctl --user restart ethos-gateway # Linux
Always restart after upgrade — the running daemon keeps the old binary in memory until restarted.
Reading the bot's session history:
Inside ethos chat, the bot's per-chat sessions are visible through SQLite. The TUI's /sessions modal lists them; from outside, the database file is ~/.ethos/sessions.db (WAL + FTS5).
9. Troubleshooting the common cases
Daemon starts but bot does not reply. Foreground-test the same config: ethos gateway start. If foreground works and daemon does not, it is almost always PATH or HOME. Use absolute paths in the unit file and set HOME explicitly.
ethos: command not found in daemon logs. Service managers do not source your shell rc. If you installed via nvm, ethos lives under ~/.nvm/versions/node/.... Use that absolute path in ProgramArguments / ExecStart. which ethos from a shell where chat works prints exactly the path you want.
Daemon log says Run ethos setup first. The daemon's HOME does not point at your user. systemd user units inherit this; launchd sometimes does not — set HOME in the plist's EnvironmentVariables.
Telegram returns 429 (rate limit). Two gateways are running against the same bot token. Check for a duplicate plist, a forgotten tmux session, or a still-running foreground process. One gateway per token. Telegram's rate limit is 30 messages per second per bot; you only hit this with a real conversation flood, so 429 in normal operation almost always means duplication.
Bot replies are slow. The first turn loads memory and starts the LLM stream — expect 1–2 seconds before the first chunk arrives. Steady-state should be sub-second per chunk. If every reply takes 10+ seconds, the model picked is slow (Opus is heavier than Sonnet) or the provider is throttling.
Memory grows unbounded. Long-running daemons accumulate per-session state. The session store evicts on its own retention TTLs (see retention.messages in config.yaml); if RSS climbs steadily, file an issue with a heap snapshot.
Bot crashes and KeepAlive / Restart=on-failure flaps. Look at the error log first. Repeated boot failures usually mean a misconfiguration the service manager keeps retrying through. Disable the service, fix the config, re-enable. Hot-looping a broken config wastes CPU and floods Telegram with reconnects.
I want webhooks instead of long-polling. Long-polling needs no public URL and works behind any NAT. Webhooks need a domain with TLS. The current adapter only supports long-polling; this is on the roadmap.
Two bots, one box. Run two gateways with two ~/.ethos/ directories. Set HOME to a different path in each unit file. The two configs, two SQLite databases, and two log directories stay separate.
What you learned
- A Telegram bot is two artifacts: a token from BotFather, and the gateway running with that token configured.
- The gateway is a separate process from
ethos chat— both can run in parallel against the same SQLite store. - The Telegram adapter polls Telegram's long-poll endpoint; no domain or TLS is required.
- The pairing-code flow (
/communications,/allow,/deny) is the access list — one approval per sender, kept in~/.ethos/communications.json. - Wrapping the gateway in
launchdorsystemdis what turns "running in a shell" into "running in production." Use absolute paths; setHOMEexplicitly. ethos upgradeplus a service restart is the deploy loop.- Outbound dedup is in the gateway, not the adapter; the same
(sessionId, content)does not get sent twice within 30 seconds.
Next step
You have one Telegram personality replying to messages. Two natural next steps:
- Run Ethos as a daemon — the full reference for
launchd,systemd,pm2, plus tmux and screen for short-lived tests. - Deploy on Telegram — bot mentions, group chats, rate limits, multi-workspace.
- Configure providers — set up fallback chains so a single rate-limited provider does not take the bot offline.
- Glossary —
channel adapter,gateway,session, every term used here.