TUTORIALS 14 min read

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.

By EgoistAI ·
Build an AI Telegram Bot with Python and Claude: Complete Tutorial

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:

  1. Voice message support — Use Whisper API to transcribe voice messages, then process with Claude
  2. Document analysis — Accept PDF/DOCX files and answer questions about them
  3. Group chat support — Respond when mentioned in group chats
  4. Persistent storage — Use SQLite to maintain conversation history across restarts
  5. Admin commands — Allow specific users to manage the bot remotely
  6. 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.

Share this article

> Want more like this?

Get the best AI insights delivered weekly.

> Related Articles

Tags

Telegram botPythonClaude APIchatbottutorial

> Stay in the loop

Weekly AI tools & insights.