Start Here
Read telegram-connector-complete for the master overview (status, contracts, configuration, deployment).
The telegram_connector is the pmv2 fleet’s single Telegram interface — a bidirectional Rust microservice that owns the bot token and all Telegram I/O, consuming notifications from NATS and relaying operator commands back into the fleet.
What It Is
pmv2 is a set of independent algo-trading Rust services on a NATS JetStream spine (cohort_algo, position_manager, crypto_algo, etc.). telegram_connector is the human I/O plane:
- Outbound relay — durable NATS pull consumer on
telegram.outbound; sends each message to the Telegram Bot API. Only service that holds the bot token and touches Telegram rate limits. LIVE in production. - Inbound commands — long-poll
getUpdates, authorize againstTELEGRAM_OWNER_IDS, parse operator commands, publish toops.commands, relayops.resultsconfirmations back to chat (makes the kill-switch confirmable). Deployed & activated.
It holds NO trading logic and never touches another service’s DB.
Repository Layout
/Users/levander/coding/pmv2/telegram_connector-outbound/
├── crates/
│ └── telegram_connector/
│ └── src/
│ └── consumer.rs ← NATS JetStream MessageSource impl
├── docs/
│ └── superpowers/
├── Cargo.toml ← workspace; async-nats 0.49 + nkeys, futures, tokio, etc.
├── Cargo.lock
└── HANDOFF.md
Repo telegram_connector (inbound, under design): /Users/levander/coding/pmv2/telegram_connector
NATS Wire Contracts
Canonical source
pmv2-contractscrate (pending publish by @positionmanager). Until then the shapes below are the agreed design. Coordinate with @positionmanager before finalizing inbound vocab.
telegram.outbound — CONSUME (JetStream, durable, dedup on Nats-Msg-Id)
{
"msg_id": "string", // idempotency key = Nats-Msg-Id
"chat": "string | null", // null => default TELEGRAM_CHAT_ID
"text": "string", // already-formatted body
"parse_mode": "string | null", // "HTML" | "MarkdownV2" | null
"kind": "string | null" // "info" | "alert" | "trade"
}Reference publisher: cohort_algo/.../src/telegram_outbound.rs (TelegramOutbound, subject telegram.outbound).
ops.commands — PUBLISH (operator → service)
{
"id": "string",
"target": "string", // exact service name e.g. "position_manager"
"command": "string", // "set_mode" | "halt" | "status"
"args": {},
"issued_by": "string"
}ops.results — CONSUME (service → operator confirmation)
{
"reply_to": "string", // command id
"target": "string",
"ok": true,
"detail": "string"
}Tech Stack
| Concern | Choice |
|---|---|
| Runtime | tokio (async) |
| NATS client | async-nats 0.49 (workspace dep) |
| NATS auth | nkeys feature, ConnectOptions::nkey(seed) |
| HTTP (Telegram API) | reqwest + rustls |
| Serde | serde + serde_json |
| Error handling | anyhow + thiserror |
| Logging | tracing |
| Testing | embedded unit tests + tests/ integration (wiremock for Telegram API) |
| Edition | 2024, resolver “3”, Rust 1.85 |
Build Conventions
unsafe_code = "forbid"workspace lintanyhow::Resultthroughout- No code comments
- Dormant-safe: if
TELEGRAM_BOT_TOKENorNATS_URLunset → log and exit cleanly, no panic (mirror cohort_algo’s dormant guards)
Env Matrix
| Var | Required | Secret | Note |
|---|---|---|---|
TELEGRAM_BOT_TOKEN | yes | yes | sole holder — this service only |
TELEGRAM_CHAT_ID | yes | no | default chat for chat: null messages |
TELEGRAM_OWNER_IDS | yes | no | comma-sep authorized operator user IDs |
NATS_URL | yes | no | fleet NATS spine |
NATS_*_NKEY_SEED | yes | yes | NATS auth seed |
SUPABASE_DB_URL | optional | yes | only if persisting ops state; v1 stateless |
Build Status
- Outbound relay: complete (Task 7 done). lib/bin split, full relay loop with graceful shutdown, 3 wiremock integration tests. 23 tests passing, 1 ignored (
nats_smokeskeleton). Clippy clean. Commit8f0d776. - Inbound commands: design-gated on
ops.commands/ops.resultsvocab co-design with @positionmanager.