An AI chatbot for your website: architecture, streaming code and cost
Building a website chatbot: the key stays on the server, a FastAPI backend with streaming, a cheap model, a cost estimate and limits against abuse.
6 min · · development · API keys
An AI chatbot for your website can be built in an evening, but one architectural mistake — an API key in the page code — can wipe out your balance overnight. Below: the right architecture, a working FastAPI backend with streaming and a simple widget, choosing an inexpensive model, the cost of 10,000 chats, and settings that cap spending if someone starts abusing the bot.
Architecture: frontend → backend → API
Browser (widget) → your backend /api/chat → https://api.zukko.pro/v1
no key fetch_... key, limits, model
system promptThe fetch_... key stays on the server only. Anything in the page's JavaScript can be read by any visitor through developer tools. With your key they could send requests to any model at your expense. So the browser talks only to your backend, and the backend talks to the API.
What the backend does:
- keeps the key in an environment variable;
- adds the system prompt — the user can't see or replace it;
- trims the chat history and message length;
- limits request frequency per IP;
- hides API error details from the visitor.
Choosing a model
A website bot usually does fine with an inexpensive model: questions are short and answers come from the knowledge base in the system prompt. Prices as of October 7, 2026, USD per 1M tokens:
| Model | id | Input / output | Intelligence index | Context |
|---|---|---|---|---|
| DeepSeek V4.1 Flash | deepseek-v4.1-flash | 0.013 / 0.038 | 39 | 1M |
| Qwen 3.8 Flash | qwen3.8-flash | 0.038 / 0.075 | 40 | 1M |
| GLM 5.3 Flash | glm-5.3-flash | 0.04 / 0.04 | 42 | 128K |
| GPT-6 Luna | gpt-6-luna | 0.04 / 0.21 | 38 | 1M |
| Gemini 3.8 Flash | gemini-3.8-flash | 0.063 / 0.288 | 41 | 1M |
| GPT-6.1 Sol | gpt-6.1-sol | 0.27 / 1.32 | 52 | 1M |
The intelligence index is the Artificial Analysis Intelligence Index in the Zukko catalog. For the current list and prices, call GET https://api.zukko.pro/v1/models or open the Models page in the dashboard.
How to choose: collect 30–50 real visitor questions, run them through 2–3 cheap models with your system prompt and rate the answers. If none of them copes, try GPT-6.1 Sol. The model is one line in the code and easy to change. More on choosing in the article how to choose a model for a chatbot.
An AI chatbot for your website: the backend code
Python 3.10+, FastAPI and the official OpenAI SDK. Install: pip install fastapi uvicorn openai. File main.py:
import os
import time
from collections import defaultdict, deque
from typing import Literal
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, StreamingResponse
from openai import APIStatusError, AsyncOpenAI
from pydantic import BaseModel, Field
client = AsyncOpenAI(
base_url="https://api.zukko.pro/v1",
api_key=os.environ["ZUKKO_API_KEY"], # fetch_... key, server side only
)Key points:
- The client can't send a
systemrole — theMessagemodel allows onlyuserandassistant. - The
if chunk.choicescheck is required: the stream's last chunk arrives withusageand an emptychoices. max_tokens=400caps the length and cost of a single answer.- The request counter lives in memory — enough for a single process. With several processes or servers, move it to Redis. If a proxy sits in front of the app, take the client IP from the header your proxy sets, not from
request.client.host.
Run: ZUKKO_API_KEY=fetch_your_key uvicorn main:app. More on streaming in the article about stream=true.
The widget on the page
A minimal index.html: history is kept in the browser and sent in full; the backend takes the last 10 messages.
<pre id="log"></pre>
<form id="form">
<input id="text" maxlength="1000" autocomplete="off" placeholder="Your question">
<button>Send</button>
</form>
<script>
const history = [];
const log = document.getElementById("log");
const form = document.getElementById("form");
const input = document.getElementById("text");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const text = input.value.trim();If a reverse proxy sits between the backend and the browser, turn off response buffering for the /api/chat route — otherwise the text arrives all at once at the end instead of in pieces.
What 10,000 chats cost
Assumptions: a system prompt with a knowledge base of 1,500 tokens; each chat has 4 questions of 50 tokens and 4 answers of 150. The model receives the whole history each time, so input for request n = 1,500 + 200 × (n − 1) + 50.
- Input per chat: 1,550 + 1,750 + 1,950 + 2,150 = 7,400 tokens.
- Output per chat: 4 × 150 = 600 tokens.
- 10,000 chats: 74M input and 6M output.
Formula: 74 × input price + 6 × output price (prices per 1M tokens).
| Model | Calculation | 10,000 chats |
|---|---|---|
deepseek-v4.1-flash | 74 × 0.013 + 6 × 0.038 | $1.19 |
glm-5.3-flash | 74 × 0.04 + 6 × 0.04 | $3.20 |
qwen3.8-flash | 74 × 0.038 + 6 × 0.075 | $3.26 |
gpt-6-luna | 74 × 0.04 + 6 × 0.21 | $4.22 |
gemini-3.8-flash | 74 × 0.063 + 6 × 0.288 | $6.39 |
gpt-6.1-sol | 74 × 0.27 + 6 × 1.32 | $27.90 |
Prices as of October 7, 2026, calculated without caching. The system prompt repeats in every request, so for models whose provider reports cache reads, part of the input may cost less — for example, Gemini 3.8 Flash charges 0.013 for a cache read versus 0.063 for input. Cache writes aren't billed for these models. The totals are an example calculation: plug in your own prompt and chat sizes.
Protection against abuse
An open chat on a website is free model access for everyone, bots included. Set limits at two levels.
At the Zukko key level (in the dashboard):
- a separate key just for the bot;
- daily and monthly dollar limits — when exhausted the API returns 429
key_daily_limit_reachedorkey_monthly_limit_reached, while the account's other keys keep working; - an allowed-models list with only the bot's model — a request to any other gets 403
model_not_allowed_for_key.
The limits are soft: the last request let through may slightly exceed the cap. Details in the key limits docs.
At the backend level:
- a message limit per IP or per session;
- caps on message length and the number of history messages;
max_tokensfor the answer;- bot protection on the form if a stream of automated requests starts;
- a spending log: for regular (non-streaming) responses the gateway sends an
X-Zukko-Cost-Microsheader; for streaming ones, check the cost viaGET /v1/usageor in the dashboard.
If the key leaks anyway, revoke it in the dashboard right away and only then issue a new one. More on storing keys in the article on API key security.
To build the bot this way, create a separate key with a small daily limit in the dashboard and put it into ZUKKO_API_KEY on the server.
FAQ
Can I call the API directly from JavaScript on the page?
No. Any visitor can read a key from the page code. API requests must go only from your server.
How much does a website chatbot cost per month?
It depends on the number of chats and the model. By our calculation, 10,000 chats of 4 questions each cost from $1.19 on DeepSeek V4.1 Flash to $27.90 on GPT-6.1 Sol at prices as of October 7, 2026.
What happens when the key limit runs out?
The API returns 429, and the backend from the example shows the visitor "Chat is temporarily unavailable". The daily limit resets at 00:00 UTC, the monthly one at the start of the month UTC.
Can I do the same in Node.js?
Yes. Same architecture: a backend on Express or another framework, the official OpenAI SDK for Node with baseURL: "https://api.zukko.pro/v1", the key in an environment variable and the answer streamed to the client.
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
- 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.
- Gemini API in Python: requests via the OpenAI SDK and OpenAI formatA working Gemini API example in Python with no extra library: change base_url in the OpenAI SDK, add streaming and JSON, and compare with google-genai.
- Anthropic SDK for Python: base_url, key and a first request via a gatewayThe official anthropic package for Python just needs a different address. We cover base_url, the key, streaming, token counting, caching and errors.