Building a Telegram-Gated AI Approval System for Autonomous Agents
When you build an AI agent that operates autonomously — trading, posting, modifying systems — you quickly run into a trust problem. How much do you let it do on its own? When does it need to ask permission?
The answer I landed on: a Telegram-gated approval system with risk tiers. Low-risk actions execute automatically. Medium-risk actions require a tap of a button in Telegram. High-risk actions require explicit confirmation plus a simulation run. Critical actions are blocked entirely until you manually intervene.
This article walks through the architecture and how to build it from scratch.
Why Telegram?
You could build a web dashboard for this. I chose Telegram for three reasons:
- It's on your phone already. No login, no URL to remember, instant push notifications.
- Bots are trivially easy to create. BotFather gives you a token in 60 seconds. The API is well-documented and Claude knows it cold.
- Inline keyboards. Telegram supports interactive buttons in messages — approve, reject, explain — without any frontend work on your end.
The Risk Tier Model
Before writing a line of code, define your tiers. Here's what I use for my trading system:
- LOW: Execute automatically. Log to database. Example: reading market data, sending a status update.
- MEDIUM: Send a Telegram message with Approve / Reject / Explain buttons. Wait for response before acting. Example: placing a paper trade, updating a config value.
- HIGH: Run a simulation first, send the simulated result to Telegram with full context, require explicit approval. Example: modifying risk parameters, rolling a futures contract.
- CRITICAL: Block. Send an alert. Require manual intervention via direct command. Example: emergency pause, live fund withdrawal.
The tier system is the most important design decision. Get it wrong and you'll either be approving everything constantly (alert fatigue) or letting risky actions slip through automatically. Start conservative and loosen over time.
Setting Up the Telegram Bot
- Open Telegram, search for
@BotFather - Send
/newbot, follow the prompts, get your token - Message your new bot once to activate the chat
- Get your chat ID by visiting:
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
Store both values as environment variables — never hardcode them.
TELEGRAM_BOT_TOKEN=your_token_here
TELEGRAM_CHAT_ID=your_chat_id_here
The Core Python Structure
The governance bot is a single Python script running as a persistent agent. Here's the skeleton:
import asyncio
import os
from telegram import Bot, InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import Application, CallbackQueryHandler
BOT_TOKEN = os.environ["TELEGRAM_BOT_TOKEN"]
CHAT_ID = os.environ["TELEGRAM_CHAT_ID"]
async def send_approval_request(action_id, description, risk_tier, simulation_result=None):
bot = Bot(token=BOT_TOKEN)
text = f"⚠️ *Approval Required*\n\n"
text += f"*Action:* {description}\n"
text += f"*Risk:* {risk_tier}\n"
if simulation_result:
text += f"\n*Simulation result:*\n{simulation_result}"
keyboard = InlineKeyboardMarkup([
[
InlineKeyboardButton("✅ Approve", callback_data=f"approve:{action_id}"),
InlineKeyboardButton("❌ Reject", callback_data=f"reject:{action_id}"),
InlineKeyboardButton("💬 Explain", callback_data=f"explain:{action_id}")
]
])
await bot.send_message(
chat_id=CHAT_ID,
text=text,
parse_mode="Markdown",
reply_markup=keyboard
)
Handling Responses
The approval handler listens for button presses and routes accordingly:
async def handle_callback(update, context):
query = update.callback_query
await query.answer()
action, action_id = query.data.split(":", 1)
if action == "approve":
execute_action(action_id)
await query.edit_message_text(f"✅ Approved and executed: {action_id}")
elif action == "reject":
cancel_action(action_id)
await query.edit_message_text(f"❌ Rejected: {action_id}")
elif action == "explain":
explanation = get_ai_explanation(action_id)
await query.edit_message_text(f"💬 {explanation}\n\nOriginal action still pending.")
Wiring It Into Your Agent
The governance layer sits between your AI agent's decision and its execution. Your agent proposes an action, the governance layer classifies the risk tier, and routes accordingly:
def govern(action):
tier = classify_risk(action)
if tier == "LOW":
execute(action)
log(action, "auto-executed")
elif tier == "MEDIUM":
pending_actions[action.id] = action
asyncio.run(send_approval_request(action.id, action.description, "MEDIUM"))
# Execution waits for callback
elif tier == "HIGH":
sim_result = simulate(action)
pending_actions[action.id] = action
asyncio.run(send_approval_request(action.id, action.description, "HIGH", sim_result))
elif tier == "CRITICAL":
send_alert(f"🚨 CRITICAL action blocked: {action.description}")
log(action, "blocked")
Running It as a Background Service
On macOS, the cleanest way to run the governance bot persistently is a launchd agent — a plist file that keeps the process alive and restarts it if it crashes.
On Linux (Railway, a VPS), use a systemd service or just a supervised process manager like supervisor.
The bot should run independently from your main agent. They communicate via a shared database (SQLite works fine for single-machine setups) or a lightweight queue.
What This Unlocks
Once this is running, you have genuine autonomy with a human-in-the-loop fallback. Your agent can operate 24/7 while you sleep. Risky decisions wait for you. Critical failures alert you immediately.
The goal isn't to automate everything. It's to automate everything that doesn't need you — and to make the things that do need you frictionless to handle from anywhere.
A Telegram message with two buttons is about as frictionless as it gets.
This architecture is what I use in my live trading system across four instruments. It's also what convinced me the governance pattern is worth packaging as a standalone product — but that's a story for a future article.