Build an AI Telegram Bot with Python and Claude: Complete Tutorial
Build a Telegram bot powered by Claude that handles conversations, analyzes images, and remembers context. Full code included — from BotFather setup to deployment.
Telegram bots are one of the most practical AI projects you can build. Unlike a web app that requires frontend development, hosting, and user authentication, a Telegram bot gives you an instant UI, push notifications, and 900 million monthly active users who already know how to use it.
In this tutorial, you’ll build a Telegram bot powered by Claude that:
- Maintains conversation history per user
- Analyzes images sent by users
- Supports custom system prompts
- Handles rate limiting gracefully
- Deploys to a VPS for 24/7 availability
Total build time: about 90 minutes.
Prerequisites
- Python 3.11+
- Telegram account
- Anthropic API key ($5 in credits is enough for testing)
- A VPS for deployment (optional — can run locally for testing)
Step 1: Create Your Telegram Bot
Open Telegram and message @BotFather:
You: /newbot
BotFather: Alright, a new bot. How are we going to call it?
You: My AI Assistant
BotFather: Good. Now pick a username for your bot.
You: my_ai_assistant_bot
BotFather: Done! Your bot token is:
7123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw
Save that token. You’ll need it.
Configure your bot:
/setdescription - Add a description
/setabouttext - Add an about section
/setuserpic - Upload a profile photo
/setcommands - Set command menu:
start - Start a conversation
clear - Clear conversation history
help - Show available commands
mode - Change AI behavior mode
Step 2: Project Setup
# Create project directory
mkdir ai-telegram-bot && cd ai-telegram-bot
# Create virtual environment
python3 -m venv venv
source venv/bin/activate
# Install dependencies
pip install python-telegram-bot anthropic python-dotenv
# Create project structure
mkdir -p src
touch src/__init__.py src/bot.py src/ai_handler.py src/config.py
touch .env .gitignore requirements.txt
Create .env:
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
ANTHROPIC_API_KEY=your_anthropic_api_key_here
MAX_HISTORY_LENGTH=20
MODEL_NAME=claude-sonnet-4-20250514
Create .gitignore:
.env
venv/
__pycache__/
*.pyc
Step 3: Build the AI Handler
# src/ai_handler.py
"""Handles communication with Claude API."""
import anthropic
import base64
from typing import Optional
class AIHandler:
def __init__(self, api_key: str, model: str = "claude-sonnet-4-20250514"):
self.client = anthropic.Anthropic(api_key=api_key)
self.model = model
self.conversations: dict[int, list] = {} # user_id -> message history
self.system_prompts: dict[int, str] = {} # user_id -> custom prompt
self.default_system_prompt = (
"You are a helpful AI assistant in a Telegram chat. "
"Keep responses concise and well-formatted for mobile reading. "
"Use markdown formatting when helpful. "
"If asked to analyze an image, describe what you see in detail."
)
def set_system_prompt(self, user_id: int, prompt: str) -> None:
"""Set a custom system prompt for a user."""
self.system_prompts[user_id] = prompt
def get_system_prompt(self, user_id: int) -> str:
"""Get the system prompt for a user."""
return self.system_prompts.get(user_id, self.default_system_prompt)
def clear_history(self, user_id: int) -> None:
"""Clear conversation history for a user."""
self.conversations.pop(user_id, None)
def _get_history(self, user_id: int) -> list:
"""Get conversation history, creating if needed."""
if user_id not in self.conversations:
self.conversations[user_id] = []
return self.conversations[user_id]
def _trim_history(self, user_id: int, max_length: int = 20) -> None:
"""Keep only the last N messages to manage token usage."""
history = self._get_history(user_id)
if len(history) > max_length:
self.conversations[user_id] = history[-max_length:]
async def chat(
self,
user_id: int,
message: str,
image_data: Optional[bytes] = None,
image_media_type: str = "image/jpeg"
) -> str:
"""Send a message and get a response from Claude."""
history = self._get_history(user_id)
# Build the user message content
content = []
if image_data:
image_b64 = base64.standard_b64encode(image_data).decode("utf-8")
content.append({
"type": "image",
"source": {
"type": "base64",
"media_type": image_media_type,
"data": image_b64,
}
})
content.append({"type": "text", "text": message})
# Add user message to history
history.append({"role": "user", "content": content})
try:
response = self.client.messages.create(
model=self.model,
max_tokens=1024,
system=self.get_system_prompt(user_id),
messages=history,
)
assistant_message = response.content[0].text
# Add assistant response to history
history.append({
"role": "assistant",
"content": assistant_message
})
# Trim history to prevent token overflow
self._trim_history(user_id)
return assistant_message
except anthropic.RateLimitError:
# Remove the user message we just added
history.pop()
return "I'm receiving too many requests right now. Please try again in a moment."
except anthropic.APIError as e:
history.pop()
return f"API error occurred: {str(e)}"
except Exception as e:
history.pop()
return f"An unexpected error occurred: {str(e)}"
Step 4: Build the Bot
# src/bot.py
"""Main Telegram bot implementation."""
import logging
from telegram import Update, BotCommand
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
filters,
ContextTypes,
)
from .ai_handler import AIHandler
from .config import Config
# Set up logging
logging.basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
level=logging.INFO
)
logger = logging.getLogger(__name__)
# Initialize AI handler
config = Config()
ai = AIHandler(api_key=config.anthropic_api_key, model=config.model_name)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /start command."""
user = update.effective_user
await update.message.reply_text(
f"Hey {user.first_name}! I'm an AI assistant powered by Claude.\n\n"
f"Just send me a message and I'll respond. You can also:\n"
f"- Send images for analysis\n"
f"- Use /clear to reset our conversation\n"
f"- Use /mode to change my behavior\n"
f"- Use /help for more info"
)
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /help command."""
await update.message.reply_text(
"*Available Commands:*\n\n"
"/start - Start a new conversation\n"
"/clear - Clear conversation history\n"
"/mode <prompt> - Set custom AI behavior\n"
"/mode reset - Reset to default behavior\n"
"/help - Show this help message\n\n"
"*Tips:*\n"
"- Send images and I'll analyze them\n"
"- I remember our conversation context\n"
"- Long conversations use more API credits\n"
"- Use /clear periodically to save costs",
parse_mode="Markdown"
)
async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /clear command."""
user_id = update.effective_user.id
ai.clear_history(user_id)
await update.message.reply_text("Conversation history cleared. Fresh start!")
async def mode(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle /mode command to set custom system prompt."""
user_id = update.effective_user.id
if not context.args:
current = ai.get_system_prompt(user_id)
await update.message.reply_text(
f"*Current mode:*\n{current}\n\n"
f"To change: /mode <your instructions>\n"
f"To reset: /mode reset",
parse_mode="Markdown"
)
return
new_prompt = " ".join(context.args)
if new_prompt.lower() == "reset":
ai.system_prompts.pop(user_id, None)
ai.clear_history(user_id)
await update.message.reply_text("Mode reset to default. History cleared.")
return
ai.set_system_prompt(user_id, new_prompt)
ai.clear_history(user_id)
await update.message.reply_text(
f"Mode updated! I'll now behave according to:\n\n"
f"_{new_prompt}_\n\n"
f"Conversation history cleared for the new mode.",
parse_mode="Markdown"
)
async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle text messages."""
user_id = update.effective_user.id
message_text = update.message.text
# Show typing indicator
await update.message.chat.send_action("typing")
response = await ai.chat(user_id, message_text)
# Telegram has a 4096 character limit per message
if len(response) > 4096:
# Split into chunks
chunks = [response[i:i+4096] for i in range(0, len(response), 4096)]
for chunk in chunks:
await update.message.reply_text(chunk, parse_mode="Markdown")
else:
try:
await update.message.reply_text(response, parse_mode="Markdown")
except Exception:
# If Markdown parsing fails, send as plain text
await update.message.reply_text(response)
async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Handle photo messages."""
user_id = update.effective_user.id
# Get the highest resolution photo
photo = update.message.photo[-1]
# Download the photo
file = await context.bot.get_file(photo.file_id)
image_bytes = await file.download_as_bytearray()
# Get caption or use default prompt
caption = update.message.caption or "What's in this image? Describe it in detail."
# Show typing indicator
await update.message.chat.send_action("typing")
response = await ai.chat(
user_id,
caption,
image_data=bytes(image_bytes),
image_media_type="image/jpeg"
)
try:
await update.message.reply_text(response, parse_mode="Markdown")
except Exception:
await update.message.reply_text(response)
async def post_init(application: Application) -> None:
"""Set bot commands after initialization."""
commands = [
BotCommand("start", "Start a conversation"),
BotCommand("clear", "Clear conversation history"),
BotCommand("mode", "Change AI behavior mode"),
BotCommand("help", "Show available commands"),
]
await application.bot.set_my_commands(commands)
def main() -> None:
"""Start the bot."""
app = (
Application.builder()
.token(config.telegram_bot_token)
.post_init(post_init)
.build()
)
# Register handlers
app.add_handler(CommandHandler("start", start))
app.add_handler(CommandHandler("help", help_command))
app.add_handler(CommandHandler("clear", clear))
app.add_handler(CommandHandler("mode", mode))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
# Start polling
logger.info("Bot started. Polling for updates...")
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
Step 5: Configuration
# src/config.py
"""Bot configuration from environment variables."""
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
def __init__(self):
self.telegram_bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
self.anthropic_api_key = os.getenv("ANTHROPIC_API_KEY")
self.model_name = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
self.max_history = int(os.getenv("MAX_HISTORY_LENGTH", "20"))
if not self.telegram_bot_token:
raise ValueError("TELEGRAM_BOT_TOKEN not set in .env")
if not self.anthropic_api_key:
raise ValueError("ANTHROPIC_API_KEY not set in .env")
Step 6: Run Locally
# From project root
python -m src.bot
# Or create a run script
echo 'python -m src.bot' > run.sh
chmod +x run.sh
./run.sh
Open Telegram, find your bot, and start chatting. Send text messages, images, try /mode to change behavior, and /clear to reset.
Step 7: Deploy to a VPS
For 24/7 availability, deploy to a cheap VPS ($5-10/month from DigitalOcean, Hetzner, or Vultr):
# On your VPS
sudo apt update && sudo apt install -y python3-pip python3-venv git
# Clone your project
git clone https://github.com/yourusername/ai-telegram-bot.git
cd ai-telegram-bot
# Set up environment
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# Create .env file with your tokens
nano .env
# Create systemd service for auto-start
sudo tee /etc/systemd/system/telegram-bot.service << 'EOF'
[Unit]
Description=AI Telegram Bot
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/ai-telegram-bot
Environment=PATH=/home/ubuntu/ai-telegram-bot/venv/bin
ExecStart=/home/ubuntu/ai-telegram-bot/venv/bin/python -m src.bot
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
# Start the service
sudo systemctl enable telegram-bot
sudo systemctl start telegram-bot
# Check logs
sudo journalctl -u telegram-bot -f
Step 8: Add Cost Controls
To prevent runaway API costs, add rate limiting:
# Add to src/ai_handler.py
import time
from collections import defaultdict
class RateLimiter:
def __init__(self, max_requests: int = 20, window_seconds: int = 3600):
self.max_requests = max_requests
self.window = window_seconds
self.requests: dict[int, list[float]] = defaultdict(list)
def is_allowed(self, user_id: int) -> bool:
now = time.time()
# Clean old requests
self.requests[user_id] = [
t for t in self.requests[user_id]
if now - t < self.window
]
if len(self.requests[user_id]) >= self.max_requests:
return False
self.requests[user_id].append(now)
return True
def remaining(self, user_id: int) -> int:
now = time.time()
active = [t for t in self.requests[user_id] if now - t < self.window]
return max(0, self.max_requests - len(active))
Cost Estimate
Typical usage for a personal bot:
- 50 messages/day average
- ~500 input tokens + ~300 output tokens per message
- Using Claude Sonnet
Daily cost: 50 × (500 × $0.003/1K + 300 × $0.015/1K) = $0.30/day
Monthly cost: ~$9/month
With rate limiting (20 msgs/hour per user):
- Very manageable for personal/small group use
- Add user allowlisting for public bots
Next Steps
Once your basic bot is running, consider adding:
- Voice message support — Use Whisper API to transcribe voice messages, then process with Claude
- Document analysis — Accept PDF/DOCX files and answer questions about them
- Group chat support — Respond when mentioned in group chats
- Persistent storage — Use SQLite to maintain conversation history across restarts
- Admin commands — Allow specific users to manage the bot remotely
- Multi-language support — Auto-detect language and respond accordingly
The full code for this tutorial is about 200 lines of Python. That’s all it takes to build a private AI assistant accessible from any device with Telegram installed. No app store approval, no web hosting, no UI design — just a bot that responds to your messages with the full power of Claude behind it.
Sources
> Want more like this?
Get the best AI insights delivered weekly.
> Related Articles
AI Agent Approval Workflows: Put Humans at the Right Control Points
Human approval can make an agent safer—or merely slower. Design checkpoints around irreversible actions, changing risk, and evidence people can actually review.
LLM Trace Redaction in Production: Debug Without Logging Private Data
LLM traces are debugging gold and privacy dynamite. Capture structure, decisions, and timing while removing secrets and personal data before storage.
Secret Management for AI Agents: Stop Leaking Credentials Into Prompts
An agent needs tools, not a backpack full of API keys. Keep secrets outside model context, issue short-lived capability tokens, and audit every use.
Tags
> Stay in the loop
Weekly AI tools & insights.