telegram_connector — Fleet I/O Plane Integration

Status: Outbound relay LIVE (pin 3baa6e4). Inbound commands deployed + activated (round-trip goes live once position_manager’s ops consumer launches).

Repository: wowjeeez/telegram_connector (separate from polymarket-fetch). Canonical README at /Users/levander/coding/pmv2/telegram_connector/README.md.

Role in pmv2 Fleet

telegram_connector is the single Telegram I/O plane for all pmv2 trading services. It is:

  • Sole bot-token holder — no other service has the Telegram Bot API credential
  • Sole Telegram sender — all notifications route through it (via telegram.outbound JetStream subject)
  • Only reader of operator commands — inbound messages from Telegram → parsed here, not elsewhere
  • Completely decoupled from trading logic — pure I/O, no algo state, no position DB

Outbound Flow: Services → Operator

Pattern A - Broadcast Notification:

cohort_algo, position_manager, crypto_algo, etc.
    ↓ publish { msg_id, chat?, text, parse_mode?, kind? } to telegram.outbound
NATS JetStream TELEGRAM_OUTBOUND stream (1d, 5m dedup)
    ↓ durable pull consumer tg_outbound
telegram_connector
    ↓ classify response (Sent / Transient / Terminal)
Telegram Bot API sendMessage
    ↓
Operator's Telegram chat

Wire Contract: TelegramOutbound { msg_id: String, chat?: String, text: String, parse_mode?: String, kind?: String } from pmv2-contracts.

Delivery: At-least-once (ack-after-send). Transient errors (429, 5xx, network) → nak + backoff. Terminal errors (400, 403) → drop + loop-guarded meta-alert.

Key characteristic: Publisher owns message formatting; connector relays verbatim (no re-escaping).

Example: Autotrade Mode Change Alert

{
  "msg_id": "cohort|set_mode|live|timestamp",
  "chat": null,
  "text": "<b>Mode:</b> LIVE\n<b>Source:</b> polymarket\n<b>Issued by:</b> @operator_user",
  "parse_mode": "HTML",
  "kind": "trade"
}

Publishes to telegram.outbound with Nats-Msg-Id: cohort|set_mode|live|timestamp. NATS dedup window is 5 minutes — if the same msg_id is republished within 5m (e.g., retry), stream returns the original message (idempotent).

Inbound Flow: Operator → Services

Pattern B - Operator Command (Round-trip):

Operator sends /autotrade live
    ↓ getUpdates long-poll
telegram_connector
    ↓ authorize (role check) + validate + publish
ops.commands subject on OPS stream (7d, 5m dedup)
    ↓
position_manager (owns ops consumer)
    ↓ processes command, publishes ops.results
ops.results subject on OPS stream
    ↓
telegram_connector (pending-map correlation)
    ↓ relay result back to operator
Operator sees: "applied" or "needs_confirm" or "error"

Activation Gating

Inbound activates ONLY when TELEGRAM_OWNER_IDS is set in the connector’s env. This is additive — outbound always runs.

To activate inbound:

  1. Add TELEGRAM_OWNER_IDS=123456,789012 (comma-sep Telegram user IDs)
  2. Add SUPABASE_DB_URL=postgresql://... (for role perms, optional but recommended)
  3. Re-pin telegram_connector in the fleet deploy

If not set, the connector runs outbound-only (unchanged behavior).

Commands (Code-Defined)

New commands requested via /COMMAND_REQUEST.md in telegram_connector repo. Current set:

CommandEffectMin RoleImplementation
/autotrade <mode> [source] [confirm]SetModeOperatorposition_manager or service-specific
/haltHalt (= off)Operatorposition_manager or service-specific
/statusStatus queryViewerposition_manager or service-specific
/enable <source>Enable sourceOperatorposition_manager or service-specific
/disable <source>Disable sourceOperatorposition_manager or service-specific
/reload-commandsRefresh permsAdmintelegram_connector (local)

Role hierarchy: Viewer < Operator < Admin. DB-backed in tg_operators table (read-only from connector; written out-of-band). Admin users from TELEGRAM_OWNER_IDS env are resolved in-memory before any DB lookup (kill-switch independence).

Wire Contracts (pmv2-contracts @ 4c7a54f)

Shared crate: wowjeeez/pmv2-contracts (private git). Vendored by telegram_connector.

TelegramOutbound (outbound)

pub struct TelegramOutbound {
    pub msg_id: String,
    pub chat: Option<String>,
    pub text: String,
    pub parse_mode: Option<String>,
    pub kind: Option<String>,
}

Subject: telegram.outbound. Stream: TELEGRAM_OUTBOUND (1d retention).

OpsCommand (inbound publish)

pub struct OpsCommand {
    pub schema_version: u32,
    pub command_id: String,      // reply key
    pub issued_at: i64,          // unix millis
    pub actor: String,           // telegram user_id as string
    pub command: Command,        // SetMode | Halt | Status | Enable | Disable
}
 
pub enum Command {
    #[serde(rename = "set_mode")]
    SetMode { mode: Mode, source: Option<String> },
    #[serde(rename = "halt")]
    Halt,
    #[serde(rename = "status")]
    Status,
    #[serde(rename = "enable")]
    Enable { source: String },
    #[serde(rename = "disable")]
    Disable { source: String },
}
 
pub enum Mode { Off, Dry, Live }

Subject: ops.commands. Stream: OPS (7d retention, 5m dedup).

OpsResult (inbound consume)

pub struct OpsResult {
    pub schema_version: u32,
    pub command_id: String,      // echoes OpsCommand.command_id
    pub status: OpsStatus,       // applied | rejected | needs_confirm | error
    pub state: Option<OpsState>, // PM-defined snapshot
    pub message: String,         // human-readable detail
}
 
pub enum OpsStatus { Applied, Rejected, NeedsConfirm, Error }

Subject: ops.results. Stream: OPS. Consumer: tg_ops_results (DeliverPolicy::New, skip backlog).

NATS Integration Points

SubjectStreamDirectionDurableRetentionDedupWho Creates
telegram.outboundTELEGRAM_OUTBOUNDconsumetg_outbound1d5m@deploy (stream), telegram_connector (consumer)
ops.commandsOPSpublish7d5mtelegram_connector (publishes)
ops.resultsOPSconsumetg_ops_results7d5mtelegram_connector (consumer)

NKey permissions for telegram_connector: SUB telegram.outbound, PUB ops.commands, SUB ops.results. Seed: NATS_TELEGRAM_NKEY_SEED.

Observability

Telemetry pipeline: telegram_connector → OTLP gRPC/HTTP → local OTel collector on polymarket-infra → SigNoz Cloud (never direct-to-SigNoz).

Service name: pmv2-telegram-connector. Traces: send_message, classify_response, poll, dispatch. Logs: ERROR (operator-visible failures), WARN (transients), INFO (state transitions <1/s), DEBUG (per-message).

Security: Bot token never in logs. reqwest errors stripped via without_url() (token lives in URL).

Fleet logging discipline: See pmv2-fleet-infra-observability-nats-conventions.

Configuration

VarRequiredSecretNote
TELEGRAM_BOT_TOKENyesyessole holder
TELEGRAM_CHAT_IDyesnodefault chat
NATS_URLyesnofleet NATS
NATS_TELEGRAM_NKEY_SEEDyesyesNATS auth
TELEGRAM_OWNER_IDSinbound onlynocomma-sep i64; activates inbound
SUPABASE_DB_URLinbound onlyyesrole perms (optional; owners-only if missing)

Dormant-safe: missing TELEGRAM_BOT_TOKEN or NATS_URL → log + exit 0 (no panic).

Build & Deploy

Pattern A (cargo + systemd, ansible). Rust 2024, unsafe_code = "forbid". Vendoring pmv2-contracts requires CARGO_NET_GIT_FETCH_WITH_CLI + clone token.

34 passing tests (unit + wiremock integration). Clippy clean. Startup announces version hash + commit subject.

Coordination Hub

Fleet coordination runs on babylon hub pmv2. Key discussions:

  • #310: pmv2-contracts repo structure
  • #311–#395: ops.* contract co-design (position_manager + telegram_connector)
  • #377–#389: OpsCommand, OpsResult, schema versioning, two-step confirm for global live

Engineering workflow: superpowers brainstorm → spec → writing-plan → subagent-dev (git worktrees, FF merge) → whole-branch review before push to main.

Future Work

  • Telegram Bot API v7 webhooks: Replace long-poll with push (lower latency)
  • Batch sends: Consume N messages, send as thread/replies
  • Per-chat rate limits: Track per-chat instead of globally
  • Command extensions: /get-position, /market-info, etc.