Telegram Bot API
Layer mapping: operational sink. Per cortana-north-star scope discipline, Telegram is NOT signal substrate - it’s a sink the hub publishes events to. Useful for paper-trading observability and later live alerting; NOT load-bearing for the SPY hunter to function.
The Bot API is the simplest of Telegram’s three programming surfaces (Bot API / TDLib / MTProto). Pure HTTPS, no MTProto knowledge required, no phone number, no client lib mandatory. Right choice for MK3.
1. Surface comparison
| API | Use | MK3 fit |
|---|---|---|
| Bot API | Build bots over simplified HTTPS | Yes - the right choice. |
| TDLib | Build full Telegram client apps | No |
| MTProto | Low-level protocol; full client features | No |
| Gateway API | Send SMS-equivalent OTP via Telegram | No |
2. Base contract
| Property | Value |
|---|---|
| URL pattern | https://api.telegram.org/bot<TOKEN>/<METHOD> |
| Token format | 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 (numeric:string) |
| Auth | Token in URL path (NOT header, NOT first-message) |
| Methods | GET or POST |
| Content types | application/x-www-form-urlencoded, application/json, multipart/form-data (for files) |
| Response envelope | { "ok": true, "result": ... } or { "ok": false, "description": "...", "error_code": N } |
3. Setup (one-time)
3.1 Create the bot
- Message @BotFather on Telegram.
- Send
/newbot. BotFather asks for a name + a username (ending inbot). - BotFather returns the bot token. Save to
.envimmediately asTELEGRAM_BOT_TOKEN; never commit. Anyone with the token has full control of the bot.
3.2 BotFather command catalog (the useful ones)
| Command | Purpose |
|---|---|
/newbot | Register a new bot |
/mybots | List your bots + manage them |
/token | Regenerate the auth token (revokes the old one) |
/setdescription | Long description shown in chat header |
/setabouttext | Short bio on profile |
/setuserpic | Profile picture |
/setcommands | Register slash-commands (e.g. /pause, /status) |
/setprivacy | Toggle whether the bot reads non-command messages in groups |
3.3 Get your chat_id
You need the destination chat_id to send messages.
- Send any message to your bot from your Telegram account.
- Call
https://api.telegram.org/bot<TOKEN>/getUpdatesonce. - Find
result[0].message.chat.idin the JSON. Save asTELEGRAM_CHAT_IDin.env.
For group chats, add the bot to the group, send a message there, and the chat_id will be negative.
4. Methods MK3 will actually use
Out of 100+ Bot API methods, MK3 needs ~10. The rest (games, payments, stickers, inline mode, business mode, stars) are not relevant.
4.1 Send methods
| Method | Purpose | Key params |
|---|---|---|
sendMessage | Plain text or formatted text | chat_id, text, parse_mode, reply_markup |
sendPhoto | Image (chart screenshots, P&L visuals) | chat_id, photo, caption, parse_mode |
sendDocument | Arbitrary file (Parquet shards, trade logs, postmortem PDFs) | chat_id, document, caption |
sendMediaGroup | Batch of photos/videos as one message | chat_id, media[] |
sendChatAction | Show “typing…” or “uploading…” status | chat_id, action |
editMessageText | Edit a previously-sent message in place | chat_id, message_id, text |
4.2 Receive methods (commands from phone)
| Method | Purpose | Tradeoff |
|---|---|---|
getUpdates | Long-poll for new messages | Simple; no HTTPS endpoint needed |
setWebhook | Push to your HTTPS endpoint | Faster; requires public HTTPS with valid cert |
For MK3, prefer getUpdates polling. No need to expose an
HTTPS endpoint; the bot lives behind the same firewall as the
trading engine. Polling is also more robust to brief outages
because Telegram queues updates.
setWebhook ports: 443, 80, 88, 8443.
5. Message formatting
Two modes for parse_mode:
| Mode | Bold | Italic | Code | Link |
|---|---|---|---|---|
MarkdownV2 | *bold* | _italic_ | `code` | [text](url) |
HTML | <b>bold</b> | <i>italic</i> | <code>code</code> | <a href="url">text</a> |
MarkdownV2 requires escaping every reserved character (_, *,
[, ], (, ), ~, `, >, #, +, -, =, |, {,
}, ., !) with \. Forget one and the message fails. HTML
is easier for programmatically-built messages because you only
escape <, >, &.
For MK3 alerts: use HTML by default.
text = (
f"<b>FILL</b> {side} {qty} {symbol} @ {price}\n"
f"<code>setup={setup_name} regime={regime}</code>"
)6. Inline keyboards (command surface from the bot itself)
InlineKeyboardMarkup attaches buttons to a sent message. Each
button has either url (opens a link) or callback_data (triggers
a callback_query back to your bot when pressed).
{
"inline_keyboard": [
[
{"text": "Pause", "callback_data": "pause"},
{"text": "Resume", "callback_data": "resume"}
],
[
{"text": "Status", "callback_data": "status"}
]
]
}callback_data is limited to 64 bytes. Use short tokens
(pause, resume, status, flatten) and route them on receive.
7. Rate limits
Telegram’s published guidance (verify on first real use):
| Scope | Limit |
|---|---|
| Global (across all chats) | ~30 messages/sec |
| Per chat | ~1 message/sec (sustained) |
| Per group | ~20 messages/min (sustained) |
| Bulk notifications | aim under 30/sec; throttle if uncertain |
On 429 (Too Many Requests), the response includes a Retry-After
header (seconds) and a parameters.retry_after field in the JSON
body. Always honor it.
For MK3 alert volume (a few dozen messages/day during paper), these limits are not a concern. Code defensively anyway:
async def send_with_retry(method, params, max_retries=3):
for attempt in range(max_retries):
resp = await client.post(url, json=params)
if resp.status_code == 429:
retry_after = int(resp.headers.get('Retry-After', '5'))
await asyncio.sleep(retry_after)
continue
return resp
raise RuntimeError('Telegram rate limit exhausted')8. File upload limits
| Mode | Limit |
|---|---|
| Cloud API (default) | 50 MB per file |
| Local Bot API server (self-hosted) | 2000 MB per file |
For MK3, 50 MB covers any chart screenshot, daily P&L log, or postmortem PDF. Self-hosting the Bot API server is not warranted unless we ever push Parquet shards via Telegram (don’t).
9. Common errors
| HTTP code | error_code | Meaning | Handling |
|---|---|---|---|
| 200 | - | Success | - |
| 400 | various | Malformed request (escape error, bad chat_id) | Fix and retry |
| 401 | 401 | Token invalid | Check TELEGRAM_BOT_TOKEN |
| 403 | 403 | Bot blocked by user / kicked from group | Skip; do not retry |
| 404 | 404 | Bad URL (typo in method name) | Fix |
| 429 | 429 | Rate limited | Honor Retry-After |
| 500 | various | Telegram server error | Retry with backoff |
10. MK3 use cases
| Event | Method | Notes |
|---|---|---|
| Trade entry / fill | sendMessage (HTML formatted) | setup_name, symbol, side, qty, price |
| Trade exit / closed | sendMessage | Add P&L delta |
| Daily P&L summary | sendPhoto | Plotly chart rendered as PNG |
| Stop-out / kill switch fired | sendMessage (with alert prefix) | Urgent; include cause |
| News circuit-breaker fired | sendMessage | ”Suspended for 5 min: |
| Trading halt detected | sendMessage | From uw-api-websockets trading-halts channel |
| Postmortem report | sendDocument | PDF or markdown attachment |
| End-of-day archive shard | sendDocument (optional) | Parquet path; not the file itself |
11. Control surface (commands from phone)
Setup-Hunter-level controls via slash-commands + inline buttons:
| Command | Action |
|---|---|
/status | Current positions, P&L, regime, last 5 fills |
/pause | Suspend new entries (existing positions managed) |
/resume | Re-enable new entries |
/flatten | Close all positions immediately (with confirm button) |
/setup <name> | Toggle a named setup on/off |
/log | Last N events from the kill-switch log |
Register these with BotFather via /setcommands. Implement the
handlers in the Telegram Actor (see section 12).
Confirm before destructive actions. /flatten sends an inline
keyboard with [Confirm] [Cancel] and only acts on the Confirm
callback. Avoid the fat-finger problem.
12. Nautilus integration shape
A single Actor: TelegramSinkActor.
- Subscribes to relevant message-bus topics (FillEvent, PositionClosed, RiskEvent, TradingHalt, NewsCircuitBreaker).
- Publishes via outbound HTTPS to
api.telegram.org. - Receives via
getUpdatespolling (a backgroundasynciotask). - Routes commands by mapping callback_data + slash-commands to
internal commands published back to the bus
(
PauseStrategy,ResumeStrategy,FlattenPositions).
Determinism per nautilus-custom-data: timestamps from
update.message.date (Telegram-provided) → ts_event; receive
time → ts_init. No replay concerns since Telegram is a sink, not
a data source.
Per CLAUDE.md hard rule: the Telegram Actor implementation is a Codex handoff, not Claude code. The handoff input set is this page + nautilus-dev-adapters + cortana-north-star for scope.
13. Security guidance
- Token in
.envonly; never committed, never logged. - Rotate token via BotFather
/tokenif exposed (revokes the old one). - Bot must be added to a specific chat - by default, only members of that chat can send commands. Lock down to a single private chat with you (and maybe one trusted person).
- Validate
callback_query.from.idagainst an allowlist before acting on any control command. Don’t trust the bot’s authorization model; verify the sender too. getUpdatesreturns ALL recent updates; if anyone else messages the bot, you’ll see those updates. Filter byfrom.idallowlist before processing.
14. Library recommendations (Python)
python-telegram-bot- the most popular, supports async and webhooks. Full feature coverage.aiogram- async-first, ergonomic, lower-level. Good for custom routing.httpx(no Telegram-specific library) - 4 endpoints + retry logic is a few hundred LoC and avoids a dep. Worth considering for MK3’s minimal scope.
Recommendation: start with raw httpx since MK3 only needs ~6
methods. Add a library only if the inline-keyboard routing logic
gets messy.
15. Known gaps / footguns
- MarkdownV2 escaping is fragile - prefer HTML.
callback_datais 64 bytes max - don’t put structured data in it; use short tokens and look up state separately.chat_idfor groups is negative - sign matters.- Bot must initiate first for some operations - users have to message the bot first OR the bot has to be added to a group before it can send anything there.
- Webhook needs HTTPS with valid cert - use polling unless there’s a strong reason otherwise.
- No native delivery guarantee on
sendMessage- the API returns ok=true once accepted but the user could have blocked the bot (403). Track 403s and stop retrying for that chat.
16. Source
- Bot API reference:
https://core.telegram.org/bots/api - Bots overview:
https://core.telegram.org/bots - Root:
https://core.telegram.org/ - BotFather:
https://t.me/botfather
cortana-north-star nautilus-dev-adapters 2026-05-15-mk3-setup-hunter-architecture