Your own Telegram bot on Claude or GPT: a minimal example
Building a minimal Telegram bot with aiogram that calls Claude or GPT through the gateway. Full code, plus what to do about balance and rate-limit errors right in the chat.
3 min · · Updated · development
Below is a minimal but working Telegram bot in aiogram that forwards a user's message to Claude or GPT through the gateway and replies with text. Your own bot in Telegram is a separate story from the bonus for subscribing to the @zukkopro channel: there you link your Telegram to your zukko.pro account in the dashboard and get $0.50 once, while this bot you write yourself, and it uses your own API key to generate replies.
Setup and the bot token
pip install aiogram httpxThe Telegram bot token comes from @BotFather in Telegram — a separate entity from the gateway's fetch_... key, and the two shouldn't be confused: one authorizes the bot with Telegram, the other authorizes the request to a model.
Bot code for Claude
import asyncio
import httpx
from aiogram import Bot, Dispatcher
from aiogram.types import Message
TELEGRAM_TOKEN = "your_botfather_token"
ZUKKO_API_KEY = "fetch_your_key"
bot = Bot(token=TELEGRAM_TOKEN)
dp = Dispatcher()
async def ask_claude(text: str) -> str:
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(Same bot, but for GPT/GLM
Only the request function changes — a different endpoint and a different path to the reply text:
async def ask_openai_compatible(text: str, model: str = "gpt-5.6-sol") -> str:
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(
"https://api.zukko.pro/v1/chat/completions",
headers={
"Authorization": f"Bearer {ZUKKO_API_KEY}",
"Content-Type": "application/json",
},
json={
"model": model,
"messages": [{"role": "user", "content": text}],
},
)
data = resp.json()
if resp.status_code != 200:
return f"Error: {data.get('error', {}).get('message', 'unknown')}"
return data["choices"][0]["message"]["content"]Call it with whichever model you need: gpt-5.6-sol, gpt-5.6-luna, glm-5.3, glm-5.3-flash (Claude can be called this way too — the gateway converts the format) — the whole lineup goes through the same /v1/chat/completions path; current list: GET https://api.zukko.pro/v1/models.
Handling rate limits right in the conversation
If the bot is public and used by more than one person, it's worth catching 429 separately and saying so in the chat instead of staying silent:
if resp.status_code == 429:
code = data.get("error", {}).get("code", "")
if code == "key_daily_limit_reached":
return "The key's daily limit is used up, try again after 00:00 UTC"
if code == "key_monthly_limit_reached":
return "The key's monthly limit is used up"
# other 429s come from the model provider — the gateway itself has no rate limit
return "Too many requests, wait a bit"A key's daily and monthly spending cap is set in the dashboard on the key's page (/app/api-keys) — for a public bot, set a sane ceiling so one persistent user can't burn the whole balance in an evening.
Idempotency against Telegram duplicates
Telegram sometimes resends the same update twice if your server's connection is flaky — if that matters (say, the bot spends money on a long generation), add an Idempotency-Key header to that specific request, built from message.message_id and chat.id — it's unique per message and identical for its duplicate, so the duplicate doesn't get charged again:
headers["Idempotency-Key"] = f"{message.chat.id}-{message.message_id}"Generating images from the bot
Image generation through Zukko is temporarily unavailable — this section is kept for reference. Current model list:
GET https://api.zukko.pro/v1/models.
For a command like /image a cat in a spacesuit, call the separate endpoint with its own per-item billing, not token-based:
async def generate_image(prompt: str) -> bytes:
async with httpx.AsyncClient(timeout=120) as client:
resp = await client.post(
"https://api.zukko.pro/v1/images/generations",
headers={"Authorization": f"Bearer {ZUKKO_API_KEY}"},
json={
"model": "nano-banana-2-text-to-image",
"prompt": prompt,
},
)
return resp.json()Checking the balance before a session
A useful habit for a bot running unattended is to check the balance hourly and alert yourself when it gets low:
curl https://api.zukko.pro/v1/balance -H "Authorization: Bearer fetch_your_key"The key is created at https://zukko.pro/app/api-keys and shown once — keep it in the environment variables of the server running the bot, not in code. There's no starting balance; for a test project, subscribing to @zukkopro and linking Telegram in the dashboard gives a one-time $0.50 for your first calls.
An admin-only /stats command
It's handy to add a hidden command that returns spend history — the /v1/usage endpoint returns history for the whole account (all keys together, no per-key filter) and supports cursor pagination, so the latest entries are enough for a daily digest:
@dp.message(Command("stats"))
async def stats(message: Message):
if message.from_user.id != ADMIN_ID:
return
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://api.zukko.pro/v1/usage",
headers={"Authorization": f"Bearer {ZUKKO_API_KEY}"},
params={"limit": 20},
)
await message.answer(str(resp.json()))Checking service health before starting polling
If the bot fails with unclear timeouts while the key and balance look fine, check the service itself before your own code:
curl https://api.zukko.pro/api/statusThis is a public JSON with model status, no key needed — a quick way to tell a temporary gateway outage (in which case models answer with 503 and provider_unavailable) apart from a bug in the bot.
A referral link for the bot's readers
If the bot is public and used by channel subscribers, it's worth telling them that subscribing to @zukkopro earns $0.50, and that the referral program pays 10% of their future top-ups to whoever invited them — these are two different mechanics, and the bot should explain them separately so the subscription bonus doesn't get confused with the referral percentage.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Prompt Engineering Examples: 12 Ready-Made Templates for WorkTwelve prompt templates for everyday tasks: what each one says, why it is built that way, and what a single API call costs.