Overview
mianGPT is a multi-model AI chat product: users pick from a catalog of LLMs synced live from OpenRouter, chat through a streaming web interface or a Telegram bot, and pay per token from a wallet billed in Toman. The system serves two very different clients — a Next.js PWA and a Telegram bot — from one Django backend, which shaped most of the interesting engineering decisions.
One pipeline, two clients
The chat logic — build the message payload, call OpenRouter, stream the response, persist the assistant message, deduct credit — is written once as a shared async generator and reused by both the web SSE endpoint and the bot endpoint. The alternative, duplicating that logic per client, would have meant two billing implementations quietly drifting apart. Instead the two surfaces differ only where they have to: the web app authenticates with JWT and speaks REST plus SSE, while the bot authenticates with a static API key and gets endpoints shaped to match the schema of the Telegram bot's original SQLite database, so migrating existing bot users onto the new backend didn't require touching the bot's own logic.
Billing on live exchange rates
Wallet balances are held in Toman, but every model's cost from OpenRouter arrives in USD per token. Billing pulls the USD-to-Toman rate live from the Bitpin API on a short cache window, with a database-configurable fallback rate for when that call fails, so a pricing-provider outage degrades to a stale rate instead of breaking checkout. Per-model prices can be pinned by an admin independently of what OpenRouter reports, which matters because "pass through the raw provider cost" and "the price we want to charge" are not always the same policy, and the system needed room for both.
Frontend
The web client is a Next.js PWA — a real one, with a generated service worker,
offline caching, and an install prompt, not just a manifest file bolted onto a
normal site. Chat responses stream token-by-token through streamdown,
rendering Markdown, LaTeX math, code blocks, and Mermaid diagrams as they
arrive rather than waiting for the full response. State is split the same way
across this codebase as elsewhere: TanStack Query owns server state, Zustand
owns the client-only state, and forms run through React Hook Form with Zod
schemas shared with the API layer.
Delivery
Both services are containerized and run behind Nginx, with Redis backing OTP
state and API throttling and PostgreSQL as the system of record. Attachment
uploads go through MinIO/S3-compatible storage, and the backend enforces that
an upload has actually finished — non-zero size, 201 returned — before it
can be attached to a message, closing a race where a message sent mid-upload
would silently reference an empty file.