How to Build a Telegram Bot with the Claude API in 2026 (Step-by-Step) ⏱️ 10 min read

A Telegram bot backed by Claude is one of the most useful personal tools you can ship in an afternoon. Unlike a web app, it requires no frontend, no auth system, and no hosting complexity — just a Python script, a webhook, and two API keys. I’ve been running a personal Claude bot for daily research tasks for six months, and the setup I’ll walk through here is the exact stack I use in production.

What You’ll Build

By the end of this guide, you’ll have a Telegram bot that:

  • Responds to any message with a Claude-powered reply
  • Maintains per-user conversation history (multi-turn memory)
  • Handles photo uploads with Claude’s vision capabilities
  • Runs on a free Railway or Fly.io instance (or your own VPS)

Stack: Python 3.11, python-telegram-bot 21.x, Anthropic Python SDK, and SQLite for conversation history.

Step 1: Create Your Telegram Bot

Open Telegram and search for @BotFather. Send /newbot, follow the prompts to name your bot, and copy the token it gives you. That token is your TELEGRAM_TOKEN — treat it like a password. Set a description with /setdescription and a profile photo with /setuserpic so the bot looks intentional.

Step 2: Set Up Your Project

mkdir claude-telegram-bot && cd claude-telegram-bot
python3 -m venv venv && source venv/bin/activate
pip install python-telegram-bot anthropic python-dotenv

Create a .env file:

TELEGRAM_TOKEN=your_botfather_token_here
ANTHROPIC_API_KEY=your_claude_api_key_here
ALLOWED_USER_IDS=123456789,987654321

Get your Telegram user ID by messaging @userinfobot. The ALLOWED_USER_IDS restriction is critical — without it, anyone who finds your bot can run up your Anthropic bill.

Step 3: The Core Bot Code

import os
from dotenv import load_dotenv
from telegram import Update
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
import anthropic

load_dotenv()

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
ALLOWED_USERS = set(int(uid) for uid in os.environ.get("ALLOWED_USER_IDS", "").split(",") if uid)
conversation_history: dict[int, list] = {}

SYSTEM_PROMPT = "You are a concise personal assistant in Telegram. Use bullet points for lists."

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text("Claude is ready. Send me anything.")

async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE):
    conversation_history[update.effective_user.id] = []
    await update.message.reply_text("History cleared.")

async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    if ALLOWED_USERS and user_id not in ALLOWED_USERS:
        await update.message.reply_text("Access denied.")
        return

    history = conversation_history.setdefault(user_id, [])
    history.append({"role": "user", "content": update.message.text})
    if len(history) > 20:
        history = history[-20:]
        conversation_history[user_id] = history

    await context.bot.send_chat_action(chat_id=update.effective_chat.id, action="typing")

    response = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=1024,
        system=SYSTEM_PROMPT,
        messages=history
    )
    reply = response.content[0].text
    history.append({"role": "assistant", "content": reply})
    await update.message.reply_text(reply)

def main():
    app = Application.builder().token(os.environ["TELEGRAM_TOKEN"]).build()
    app.add_handler(CommandHandler("start", start))
    app.add_handler(CommandHandler("clear", clear))
    app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
    app.run_polling(allowed_updates=Update.ALL_TYPES)

if __name__ == "__main__":
    main()

Run locally with python bot.py and send your bot a message. You’ll get a Claude response within 2-3 seconds.

Step 4: Add Vision Support for Photo Uploads

Add a photo handler alongside the text one:

async def handle_photo(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_id = update.effective_user.id
    if ALLOWED_USERS and user_id not in ALLOWED_USERS:
        return

    import base64
    photo = update.message.photo[-1]  # highest resolution
    photo_file = await context.bot.get_file(photo.file_id)
    photo_bytes = await photo_file.download_as_bytearray()
    image_b64 = base64.standard_b64encode(photo_bytes).decode("utf-8")

    caption = update.message.caption or "What is in this image?"
    history = conversation_history.setdefault(user_id, [])
    history.append({
        "role": "user",
        "content": [
            {"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}},
            {"type": "text", "text": caption}
        ]
    })

    await context.bot.send_chat_action(chat_id=update.effective_chat.id, action="typing")
    response = client.messages.create(model="claude-sonnet-4-5", max_tokens=1024,
        system=SYSTEM_PROMPT, messages=history)
    reply = response.content[0].text
    history.append({"role": "assistant", "content": reply})
    await update.message.reply_text(reply)

# In main(), add: app.add_handler(MessageHandler(filters.PHOTO, handle_photo))

Step 5: Deploy to Production

For a personal bot, Railway is the simplest path. Create a Procfile:

worker: python bot.py

Push to GitHub, connect to Railway, add your environment variables in the dashboard, and deploy. The free tier gives $5/month in credits — enough for a personal bot at low volume. For a VPS, run the bot inside a systemd service. The polling architecture requires no inbound port or webhook URL — the bot long-polls Telegram’s servers directly.

Cost Reality Check

A personal Claude bot used 20-30 times per day with typical 500-token exchanges costs roughly $0.50-$1.50/month using Claude Haiku, or $3-8/month with Claude Sonnet. That’s less than a coffee for a personal AI assistant that knows your full conversation history.

The 20-message history cap is intentional — uncapped history pushes input tokens into expensive territory. For longer memory, implement summarization: when history exceeds 20 messages, call Claude once to produce a 200-token summary, replace the full history with the summary as a system note, and continue. Effective memory stays intact at a fraction of the cost.

What to Build Next

This foundation handles the hard parts. Natural extensions: a /remind command backed by APScheduler, tool use so Claude can search the web or query your Notion database, or a group bot that only responds when mentioned. The full working code is under 100 lines — fork it, customize the system prompt to your use case, and you’ll have a more useful assistant than most $20/month subscriptions. Because this one knows your context and remembers yesterday’s conversation.

Similar Posts