A minimal Python Telegram bot running on PythonAnywhere (free tier) with persistent conversation memory in SQLite and AI powered by Cerebras (defaults to gpt-oss-120b — strong reasoning at Cerebras speed; qwen-3-235b-a22b-instruct-2507 is also available).
Stack: Python · Flask · pyTelegramBotAPI · OpenAI SDK · SQLite · PythonAnywhere
All services used are free. No credit card required.
Deployment smoke test: 2026-07-02.
| Service | Purpose | Needed for | Free tier |
|---|---|---|---|
| Telegram | The bot platform | Everything | Always free |
| Cerebras | AI API — gpt-oss-120b (default), qwen-3-235b-a22b-instruct-2507, and more |
Everything | 1M tokens/day, 30 req/min |
| GitHub | Source code | Everything | Always free |
| PythonAnywhere | Hosting the bot | Deployment | 1 web app, 512MB disk, monthly renewal click required |
Age requirements (check before signing up). Each of the services above has a minimum age in its Terms of Service. As a rule of thumb: Telegram, Cerebras, GitHub, PythonAnywhere, Hugging Face are 13+ globally (16+ in the EU/UK for some, due to GDPR). If you're under 13, or in a region where the minimum is 16+, the safest path is to walk through the signup steps with a parent or teacher — they create the accounts and share the API keys with you. You can still do all of the coding, testing, and deployment work yourself.
You can have the bot replying to your messages on Telegram in about 10 minutes without touching PythonAnywhere or any deployment. Perfect for getting started and iterating on changes.
- Open Telegram and search for @BotFather
- Send
/newbot - Choose a name (e.g.
My AI Bot) and a username ending inbot(e.g.myai_bot) - BotFather will reply with a bot token that looks like
7123456789:AAF... - Save this token — you will need it in Step 4
- Go to cloud.cerebras.ai and sign up (free, no credit card)
- Verify your email and log in
- Click your profile icon (top right) → API Keys
- Click Create new API key, give it a name
- Copy the key (looks like
csk-...) - Save it — you will need it in Step 4
Using a different provider? Any OpenAI-compatible API works. Set
AI_API_KEYto your provider's key,AI_BASE_URLto their base URL, andAI_MODELto the model name.
- Create a GitHub account if you don't have one
- Go to the template repo and click Fork (top right) to copy it to your account
- Clone your fork to your computer:
git clone https://github.com/<your-username>/telegram-pythonanywhere-bot.git
cd telegram-pythonanywhere-botCreate the virtualenv and install Python dependencies:
make installThen copy the template and fill in the values you saved in Steps 1 and 2:
cp .env.example .envOpen .env in your editor and set these two lines:
TELEGRAM_BOT_TOKEN=<paste your BotFather token here>
AI_API_KEY=<paste your Cerebras API key here>
Leave everything else as-is for now. SQLite memory is optional — without it the bot runs in stateless mode (no conversation memory, no rate limit), which is fine for initial testing.
make runYou should see something like:
Storage not configured — running in stateless mode (no memory, no rate limit).
Bot @your_bot_username starting in polling mode.
Send your bot a message on Telegram to try it out.
Press Ctrl+C to stop.
Open Telegram, find your bot, and send it a message. You'll see each exchange logged in your terminal:
[14:32:15] @alice → @your_bot: hello, who are you?
[14:32:17] @your_bot → @alice: Hi! I'm an AI assistant powered by Cerebras.
This is the same bot code you'll eventually deploy to PythonAnywhere — the only difference is how Telegram delivers messages. Locally we poll; in production Telegram pushes to a webhook. Edit any file in bot/, Ctrl+C the bot, rerun make run, and you'll see your changes immediately.
Once the bot works locally, the next step is to put it on PythonAnywhere so it keeps running when your laptop is closed. PythonAnywhere (PA) runs the same Flask app via a long-lived WSGI worker. The free tier supports everything this template needs.
PA free-tier note. PA restricts outbound HTTPS on the free plan to a whitelist of domains. The services this template uses (Telegram, Cerebras, Hugging Face) are all whitelisted, so no extra setup is needed. Persistent state lives in SQLite on PA's disk — no external Redis or database is required.
If you just want to ship and don't care to learn what each step does, you can do the whole PA setup from your laptop with a single command. You still have to do Step 6 (sign up + email verify) in the browser — PA has no API for account creation.
After signing up:
- Grab a PA API token: https://www.pythonanywhere.com/account/#api_token → Create new API token → copy the value
- Add two lines to your local
.env:
PA_USERNAME=<your PA username>
PA_API_TOKEN=<the token you just copied>
- Run:
make deploy-paThe script (scripts/pa_deploy.sh) creates the PA web app, opens a bash console, clones your repo, creates the virtualenv, installs deps, uploads a PA-flavoured .env (with SQLITE_PATH + WEBHOOK_URL filled in), uploads the WSGI shim, configures the web app, and reloads. It pauses once and asks you to open one URL in your browser — PA requires that each new bash console be visited once before its API will accept commands. After that it's hands-off.
The script is idempotent — re-running heals partial state (e.g. if you closed the terminal while pip was still installing), or pushes an updated .env. For ongoing code updates, Step 14 below (GitHub Actions auto-deploy on push) is still the smoothest path; this script is most useful for the first deploy and for recovery.
If you'd rather understand what the script is doing, do Steps 7–12 manually instead — they're the same work, step by step.
- Sign up at pythonanywhere.com (free Beginner tier — no card)
- Verify your email and log in
- Your bot will be hosted at
https://<your-pa-username>.pythonanywhere.com
Open a Bash console from the PA dashboard (Dashboard → New console → Bash) and run:
git clone https://github.com/<your-github-username>/telegram-pythonanywhere-bot.gitStill in the PA Bash console:
python3.13 -m venv ~/.virtualenvs/telegram-bot
~/.virtualenvs/telegram-bot/bin/pip install -r ~/telegram-pythonanywhere-bot/requirements.txtThis takes ~1–2 minutes. The virtualenv path /home/<your-pa-username>/.virtualenvs/telegram-bot is what you'll point the web app at in Step 10.
The PA WSGI shim (pythonanywhere_wsgi.py in this repo) reads .env from the project root, the same way make run does locally:
cd ~/telegram-pythonanywhere-bot
nano .envPaste in:
TELEGRAM_BOT_TOKEN=<your BotFather token>
AI_API_KEY=<your Cerebras API key>
AI_BASE_URL=https://api.cerebras.ai/v1
AI_MODEL=gpt-oss-120b
SQLITE_PATH=/home/<your-pa-username>/bot.db
WEBHOOK_URL=https://<your-pa-username>.pythonanywhere.com/api/webhook
SQLITE_PATH enables persistent memory + rate limit + dedupe. The file is created on first use; nothing to set up. If you skip it, the bot runs in stateless mode (no memory between messages).
WEBHOOK_URL enables auto-registration: every time the PA worker boots, the bot calls Telegram's setWebhook against this URL. No manual curl setWebhook needed in production (Step 12 below becomes optional).
Save with Ctrl+O, Enter, then exit with Ctrl+X. .env is in .gitignore, so it never gets committed even though you edited it inside a checked-out repo.
- In the PA dashboard, go to the Web tab → Add a new web app
- Click Next to accept the default domain (
<your-pa-username>.pythonanywhere.com) - Choose Manual configuration (not the Flask wizard — that scaffolds a different layout)
- Pick Python 3.13 to match the virtualenv
- After the app is created, scroll down on the Web tab and configure:
- Source code:
/home/<your-pa-username>/telegram-pythonanywhere-bot - Working directory:
/home/<your-pa-username>/telegram-pythonanywhere-bot - Virtualenv:
/home/<your-pa-username>/.virtualenvs/telegram-bot
- Source code:
Still in the Web tab, click WSGI configuration file (the link looks like /var/www/<your-pa-username>_pythonanywhere_com_wsgi.py). Delete everything in the editor and replace it with:
import sys
project_home = "/home/<your-pa-username>/telegram-pythonanywhere-bot"
if project_home not in sys.path:
sys.path.insert(0, project_home)
from pythonanywhere_wsgi import application # noqa: F401Substitute your actual PA username on the project_home line. Save the file, then go back to the Web tab and click the green Reload button.
Test that the worker booted by visiting https://<your-pa-username>.pythonanywhere.com/api/health in a browser — it should return OK followed by the short commit ID the bot is running (e.g. OK 4ea0ce2). That commit ID is how you can always tell exactly which version of your code is live.
If you set WEBHOOK_URL in Step 9, the bot auto-registers the webhook the first time the PA worker boots. Visit https://<your-pa-username>.pythonanywhere.com/api/health in a browser to force the worker to start, then open Telegram, find your bot, and send a message. Replies will come from PythonAnywhere.
If you'd prefer to register the webhook manually (or skipped WEBHOOK_URL), run this from PA's Bash console (it reads .webhook_secret, which is auto-generated on first worker boot):
cd ~/telegram-pythonanywhere-bot
curl -X POST "https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/setWebhook" \
--data-urlencode "url=https://<your-pa-username>.pythonanywhere.com/api/webhook" \
--data-urlencode "secret_token=$(cat .webhook_secret)" \
--data-urlencode "max_connections=1"The secret_token matches the bot's auto-generated WEBHOOK_SECRET — without it, every update gets rejected with 403. You should see {"ok":true,...} in the response.
PA free-tier web apps must be renewed every month by clicking a button in the dashboard — otherwise they auto-disable. PA emails you a week before the expiry date. To renew manually:
- Go to the Web tab
- Find the "Run until N days from today" button near the top
- Click it — your bot gets another month
If you ever need to update the bot by hand after pushing new code to GitHub, run this in a PA Bash console:
cd ~/telegram-pythonanywhere-bot && git fetch origin && git reset --hard origin/main && touch /var/www/<your-pa-username>_pythonanywhere_com_wsgi.pyThis is the same convergence the auto-deploy endpoint performs: it puts the checkout exactly at your latest pushed commit (discarding any local edits to tracked files — a plain git pull would refuse and get stuck instead), and the touch forces PA to reload the worker without needing to click Reload in the dashboard.
The bot ships with a /api/deploy endpoint and a GitHub Actions workflow that work together to redeploy the bot every time you push to main — no more manual git pull.
- Generate a random secret:
openssl rand -hex 32- Add it to your PA
.env:
DEPLOY_SECRET=<the secret you just generated>
-
Reload your PA web app (Web tab → green Reload button) so the new env var is picked up.
-
On GitHub, go to your fork → Settings → Secrets and variables → Actions → New repository secret, and add two secrets:
| Name | Value |
|---|---|
DEPLOY_SECRET |
the same value you put in PA's .env |
PA_DEPLOY_URL |
https://<your-pa-username>.pythonanywhere.com/api/deploy |
- Push any change to
main. TheDeploy to PythonAnywhereGitHub Action triggers automatically, hits/api/deploywith the secret header, and PA syncs the checkout to your pushed commit and reloads the worker. The workflow then polls/api/healthuntil it reports your new commit's ID — so a green run means your code is actually live, not just "the server answered". End-to-end takes ~30 seconds.
You can also trigger a deploy manually from GitHub: Actions tab → Deploy to PythonAnywhere → Run workflow.
If the secrets aren't set, the workflow skips with a warning instead of failing — so this is fully optional, the rest of the repo keeps working without it.
Two things worth knowing about how deploys work:
- GitHub is the source of truth. Each deploy runs
git fetch+git reset --hardon PA, so the server always ends up exactly at the pushed commit — even after force-pushes, rebases, or if a file on PA was edited by hand. The flip side: don't edit the bot's code files directly on PA (via the Files tab or a console) and expect the edits to survive — the next deploy overwrites them. Your.env,.webhook_secret, and database are untracked files and always survive deploys. - If you change
requirements.txt, the deploy installs the new dependencies into the virtualenv automatically before reloading.
Already automated. On first boot, the bot generates a 64-hex-character random secret, stores it in .webhook_secret (gitignored, mode 0600), and registers it with Telegram via setWebhook so every incoming request must present a matching X-Telegram-Bot-Api-Secret-Token header. Forged updates are rejected with 403.
You don't need to do anything for this to work. The first PA worker boot prints:
Generated webhook secret at /home/<your-pa-username>/telegram-pythonanywhere-bot/.webhook_secret (auto-bootstrap)
Webhook registered: https://<your-pa-username>.pythonanywhere.com/api/webhook
The secret persists across deploys (file lives on PA's disk, outside the git worktree's tracked files), so the value the bot verifies against stays stable.
To override (optional): set WEBHOOK_SECRET=<your value> explicitly in .env. The env var wins over the auto-bootstrapped file. Useful if you want to share a known secret across environments.
To rotate the secret: in PA's Bash console, rm ~/telegram-pythonanywhere-bot/.webhook_secret and reload the web app. Boot generates a new one and re-registers with Telegram automatically.
If you set HF_SPACE_ID in your .env, the bot registers a /model command that lets users switch between the default provider (main) and a Hugging Face Gradio Space (hf). Useful for demoing multiple models in the same bot.
HF_SPACE_ID=username/space-name
HF_TOKEN=your_hf_token_here # only for private/gated Spaces
Users can now run /model main or /model hf to switch per-user.
By default the bot replies to anyone on Telegram. To restrict it to a private allow-list, set ALLOWED_USERS in .env to a comma-separated list of usernames (with or without @) or numeric user IDs:
ALLOWED_USERS=@alice,bob,123456789
When the variable is set, everyone outside the list gets silence — no rejection message, no /start response, nothing. This is deliberate: any reply would confirm to a scanner that the bot exists. Whitelisted users see normal behavior.
To find your numeric user ID, message @userinfobot on Telegram — it replies with your ID. Useful when you have no public username, or want to whitelist by an identifier that can't change later.
Reload (or push) for the change to take effect: the list is read at worker boot.
| What to change | How |
|---|---|
| Bot personality / instructions | Edit SYSTEM_PROMPT in bot/config.py |
| AI model | Set AI_MODEL env var (free-tier tested: gpt-oss-120b (default), qwen-3-235b-a22b-instruct-2507) |
| AI provider | Set AI_BASE_URL env var (any OpenAI-compatible endpoint) |
| Secure the webhook | Auto-generated on first boot — see "Secure the webhook" above |
| Restrict who can use the bot | Set ALLOWED_USERS env var |
| Daily message limit | Set RATE_LIMIT env var (default 250) |
| Add a second provider | Set HF_SPACE_ID (and optionally HF_TOKEN) — enables /model command |
| Conversation memory length | Edit MAX_HISTORY in bot/config.py |
Hosting label shown by /about |
Set HOSTING_LABEL env var |
| Add a new command | Add a handler in bot/handlers.py |
telegram-pythonanywhere-bot/
├── api/
│ └── index.py # Entry point — Flask app, webhook route, /api/health, secret verification
├── bot/
│ ├── config.py # All env vars and constants
│ ├── clients.py # bot, ai, store instances (store is optional)
│ ├── store.py # SqliteStore — KV with TTL, backed by sqlite3
│ ├── ai.py # ask_ai orchestration — history, AI dispatch
│ ├── providers.py # Provider dispatch: OpenAI-compatible (with retry) or HF Gradio space
│ ├── preferences.py # Per-user provider preference (via store)
│ ├── history.py # Conversation memory (via store, graceful degradation)
│ ├── rate_limit.py # Per-user rate limiting (via store, graceful degradation)
│ ├── dedupe.py # Drops repeated update_ids when Telegram retries
│ ├── helpers.py # Utilities (send_reply, keep_typing, should_respond)
│ └── handlers.py # Telegram commands — add new commands here
├── tests/
│ ├── conftest.py # Mocks for running tests without real API keys
│ ├── test_ai.py
│ ├── test_providers.py
│ ├── test_preferences.py
│ ├── test_handlers.py
│ ├── test_helpers.py
│ ├── test_history.py
│ ├── test_rate_limit.py
│ ├── test_dedupe.py
│ ├── test_store.py
│ └── test_webhook.py
├── .github/
│ └── workflows/
│ ├── ci.yml # Runs tests on every push and pull request
│ └── deploy.yml # Triggers PA auto-deploy via /api/deploy on push to main
├── .env.example # Copy to .env for local dev (never commit .env)
├── .gitignore
├── Makefile # install / run / test shortcuts
├── run_local.py # Local polling entry point (used by `make run`)
├── pythonanywhere_wsgi.py # WSGI entry point for PythonAnywhere
├── requirements.txt
├── CLAUDE.md # Agent-readable project guide
└── README.md
make install # set up virtual environment and install dependencies
make run # run the bot locally via polling (no PA needed, reads .env)
make test # run all tests
make deploy-pa # one-command PythonAnywhere deploy (see "Fast path" in Part 2)| Command | Description |
|---|---|
/start |
Welcome message |
/help |
List all commands |
/reset |
Clear your conversation history |
/about |
Show model, storage, and hosting info |
/sha |
Show the live git commit SHA |
/model |
Switch AI provider (only available when HF_SPACE_ID is set) |
make testTests run offline against mocked Telegram and OpenAI clients — no real API keys or network access required. The same suite runs automatically via GitHub Actions on every push and pull request.