1. Architecture: the key lives on the backend only
The rule everything else follows: the secret key lives on your server and nowhere else. The Telegram client sends text to your backend (webhook or polling), the backend attaches the key and calls NormaHub, the answer goes to the user. The client, the mini app, and logs never see the key.
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["NORMAHUB_API_KEY"], # backend only!
base_url="https://api.normahub.cc/v1",
)
def answer(text: str) -> str:
result = client.chat.completions.create(
model="MODEL_ID",
messages=[
{"role": "system", "content": "Answer briefly."},
{"role": "user", "content": text[:2000]},
],
max_tokens=256,
)
return result.choices[0].message.content or ""Trim the input (text[:2000]) and cap the output (max_tokens=256): without these two lines, a single “tell me a long joke” from a user costs as much as a hundred ordinary replies. Create the key in the dashboard separately for the bot, with its own spend limit. Russian version of this guide: Telegram-бот на AI API.
2. Dialog history
The model remembers no past messages: memory is your job. Keep a sliding window of the last 5–6 messages per user and send it in messages together with the system prompt. Never send the whole log: every message would cost as much as the entire conversation so far; the math is in the token cost guide.
# Per-user history: last 6 messages, never the whole log
# Storage: in-memory dict for MVP, Redis/Postgres for production
history: dict[int, list] = {}
def push(user_id: int, role: str, text: str) -> None:
history.setdefault(user_id, []).append({"role": role, "content": text})
history[user_id] = history[user_id][-6:] # sliding windowAn in-memory dict is enough for an MVP; production wants Redis or Postgres with TTL. The system prompt sets language, length, and tone: the sharper the format, the fewer tokens spent on clarifications.
3. Protecting the budget
A public bot is an open wallet — cover it with three layers. First: a rate limit in code (N messages per minute per user, captcha or allowlist for expensive commands). Second: a spend limit on the API key in the dashboard — the emergency brake under attack. Third: max_tokens and input trimming on every call. Watch charges daily during the first week: anomalies show immediately.
Split models by command: a cheap one answers in DMs, a strong one only on an explicit command or for admins. Every model's input and output prices are in the catalog.
4. Errors and UX
Never show users a traceback: map errors to human answers. 402 — “service balance refilling, come back later” plus an alert to you; 429/503 — “model overloaded, retry in a minute” with a single retry button; 401/403 — to your log only, users cannot fix those. Write each call's request ID to the log next to user_id — support will find the operation without your key.
Handle slow answers in stages: an instant “Thinking…” first, then editMessageText with the result. The full code table and retry policies are in the errors guide. The architecture overview is on the Telegram bot solutions page.