Telegram is the best platform for AI roleplay bots. Bold claim? Here’s why:
- No content restrictions — Telegram doesn’t police bot content
- Free bot API — No costs for the messaging layer
- Rich message support — Text, images, buttons, inline keyboards
- Long messages — Up to 4,096 characters (vs Discord’s 2,000)
- Privacy — End-to-end encryption for secret chats, no real-name requirement
- Global reach — 900M+ monthly active users
This guide walks through building a roleplay bot on Telegram from zero to functional, based on how Suzune is built.
Table of contents
Open Table of contents
The Tech Stack
| Component | Choice | Why |
|---|---|---|
| Language | Python 3.11+ | Best LLM library ecosystem, async support |
| Telegram Library | aiogram v3 | Modern async design, type hints, middleware support |
| LLM Client | OpenAI SDK (for OpenRouter) | Works with any OpenAI-compatible API |
| Database | SQLite (WAL mode) | Zero config, surprisingly fast, perfect for single-server bots |
| LLM Provider | OpenRouter | One API key for all models (DeepSeek, Claude, Gemini, etc.) |
Why Not Discord?
Discord works too, but Telegram has advantages for RP bots:
| Telegram | Discord | |
|---|---|---|
| Message length | 4,096 chars | 2,000 chars |
| Content policy | Permissive | Stricter (NSFW channels exist but policy changes) |
| Bot cost | Free | Free |
| Image sending | Easy | Easy |
| Bot per character | One bot token per character | One bot, multiple personalities |
Telegram’s “one bot per character” model is surprisingly useful — each character gets its own chat window, profile picture, and name. It feels like texting a real person.
Step 1: Create the Bot
Talk to @BotFather on Telegram:
/newbot
→ Name: Sakura
→ Username: sakura_rp_bot
→ Receive bot token: 123456:ABC-DEF...
Save the token. You’ll need it in your config.
Bot Settings (via BotFather)
/setdescription → "AI roleplay character. Send any message to start."
/setuserpic → Upload a character portrait
/setcommands → leave empty (commands are optional for RP bots)
Step 2: The Minimal Bot
Install dependencies:
pip install aiogram openai pyyaml
The simplest working RP bot:
# bot.py
import asyncio
from aiogram import Bot, Dispatcher, types
from openai import AsyncOpenAI
BOT_TOKEN = "your-telegram-bot-token"
OPENROUTER_KEY = "your-openrouter-key"
SYSTEM_PROMPT = """You are Sakura, a 24-year-old freelance writer.
You're sharp-tongued and sarcastic, but caring underneath.
Stay in character at all times. Use *asterisks* for actions.
Never acknowledge being an AI."""
bot = Bot(token=BOT_TOKEN)
dp = Dispatcher()
llm = AsyncOpenAI(
api_key=OPENROUTER_KEY,
base_url="https://openrouter.ai/api/v1"
)
# Simple in-memory history
history: dict[int, list] = {}
@dp.message()
async def handle_message(message: types.Message):
user_id = message.from_user.id
if user_id not in history:
history[user_id] = []
# Add user message
history[user_id].append({
"role": "user",
"content": message.text
})
# Keep last 20 messages
history[user_id] = history[user_id][-20:]
# Show typing indicator
await bot.send_chat_action(message.chat.id, "typing")
# Call LLM
response = await llm.chat.completions.create(
model="deepseek/deepseek-v3.2",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
*history[user_id]
],
max_tokens=1024,
)
reply = response.choices[0].message.content
# Save assistant response
history[user_id].append({
"role": "assistant",
"content": reply
})
await message.answer(reply)
async def main():
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Run it:
python bot.py
That’s a working RP bot. ~50 lines. It handles messages, maintains conversation history, shows a typing indicator, and generates responses via DeepSeek through OpenRouter.
Total cost to run: the OpenRouter API calls (~$0.25/million input tokens for DeepSeek V3.2).
Step 3: Add Persistence (SQLite)
In-memory history disappears when the bot restarts. Let’s add SQLite:
import aiosqlite
from datetime import datetime
DB_PATH = "data/bot.db"
async def init_db():
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("""
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
await db.commit()
async def save_message(user_id: int, role: str, content: str):
async with aiosqlite.connect(DB_PATH) as db:
await db.execute(
"INSERT INTO messages (user_id, role, content) VALUES (?, ?, ?)",
(user_id, role, content)
)
await db.commit()
async def get_history(user_id: int, limit: int = 20):
async with aiosqlite.connect(DB_PATH) as db:
cursor = await db.execute(
"SELECT role, content FROM messages WHERE user_id = ? ORDER BY id DESC LIMIT ?",
(user_id, limit)
)
rows = await cursor.fetchall()
return [{"role": r, "content": c} for r, c in reversed(rows)]
Why SQLite?
For a single-server bot, SQLite is perfect:
- Zero configuration (no database server to run)
- WAL mode enables concurrent reads during writes
- Fast enough for thousands of messages
- The entire database is one file — easy to backup
Suzune stores all conversation history in a single SQLite database. It handles thousands of messages across multiple characters without breaking a sweat.
# Enable WAL mode for better concurrency
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA journal_mode=WAL")
Step 4: Character Configuration (YAML)
Move the character definition out of code and into YAML:
# characters/sakura.yaml
name: sakura
display_name: Sakura
telegram:
bot_token: ${SAKURA_BOT_TOKEN}
system_prompt: |
You are Sakura, a 24-year-old freelance writer.
You're sharp-tongued and sarcastic, but caring underneath.
## Speech Rules
- Use short, punchy sentences
- Express actions with *asterisks*
- Never acknowledge being an AI
- Drop casual slang when comfortable
## Example Dialogue
User: How was your day?
Sakura: *sighs and stretches* Don't ask. My editor called
three times about the deadline. *pauses* ...The café near
the station had good matcha though. So not all bad.
Load it:
import yaml
def load_character(path: str) -> dict:
with open(path) as f:
return yaml.safe_load(f)
character = load_character("characters/sakura.yaml")
Now you can add characters by adding YAML files — no code changes needed. See our detailed guide: How to Design AI Personalities with YAML.
Step 5: Typing Indicator and Message Splitting
Two small details that make a big difference:
Continuous Typing Indicator
Telegram’s “typing…” indicator expires after 5 seconds. For LLM calls that take 10-30 seconds, you need to keep refreshing it:
async def send_with_typing(chat_id: int, generate_fn):
"""Send typing indicator while waiting for LLM response."""
typing_task = asyncio.create_task(
keep_typing(chat_id)
)
try:
response = await generate_fn()
return response
finally:
typing_task.cancel()
async def keep_typing(chat_id: int):
while True:
await bot.send_chat_action(chat_id, "typing")
await asyncio.sleep(4)
Message Splitting
Telegram has a 4,096 character limit. Long RP responses need splitting:
def split_message(text: str, max_length: int = 4096) -> list[str]:
if len(text) <= max_length:
return [text]
chunks = []
while text:
if len(text) <= max_length:
chunks.append(text)
break
# Split at last paragraph or sentence break
split_point = text.rfind("\n\n", 0, max_length)
if split_point == -1:
split_point = text.rfind(". ", 0, max_length)
if split_point == -1:
split_point = max_length
chunks.append(text[:split_point])
text = text[split_point:].lstrip()
return chunks
Step 6: Where to Go From Here
The bot we’ve built so far handles the basics: message receiving, LLM calls, history, persistence, and character config. Here’s what to add next, in priority order:
| Feature | Impact | Complexity | Guide |
|---|---|---|---|
| Dynamic system prompts | High | Medium | Prompt Engineering |
| Multi-model routing | High | Medium | DeepSeek vs Claude vs Gemini |
| Image generation | High | High | Dynamic Character Visuals |
| Affection system | High | Medium | Coming soon |
| Lorebooks | Medium | Medium | Coming soon |
| Context compression | Medium | High | Coming soon |
Each of these is covered (or will be covered) in dedicated articles on WaifuStack.
Deployment
Where to Host
For a Telegram bot, you need a server that runs 24/7. Options:
| Provider | Monthly Cost | Best For |
|---|---|---|
| Oracle Cloud Free Tier | $0 | Testing (limited but free forever) |
| Hetzner VPS | ~€4/mo | Budget production |
| DigitalOcean Droplet | $6/mo | Easy setup |
| Contabo VPS | ~€5/mo | Best value for specs |
The bot itself uses minimal resources — a $5/month VPS can handle it. Your costs are dominated by LLM API calls, not compute.
Running as a Service
Use systemd to keep the bot running:
# /etc/systemd/system/rp-bot.service
[Unit]
Description=AI Roleplay Bot
After=network.target
[Service]
User=bot
WorkingDirectory=/opt/rp-bot
ExecStart=/opt/rp-bot/.venv/bin/python main.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl enable rp-bot
sudo systemctl start rp-bot
Cost Breakdown
Running a basic RP bot:
| Item | Monthly Cost |
|---|---|
| VPS hosting | $5–10 |
| DeepSeek V3.2 API | $5–25 (depends on usage) |
| Domain (optional) | ~$1 |
| Total | $10–35/month |
That’s a fully functional AI roleplay bot with persistent memory, character configuration, and uncensored content — for less than a Netflix subscription.
Not Ready to Build?
If coding isn’t your thing but you want the AI roleplay experience:
- Candy AI — Best all-around platform, no coding needed
- FantasyGF — AI girlfriend with photo generation
- Kupid AI — Curated high-quality characters
See our full comparison: Best NSFW AI Chatbot Platforms 2026
This guide covers the foundation. Each feature we add to Suzune gets its own deep-dive article on WaifuStack. Follow us on X to catch them as they publish.