Short answer: For a small shop, a useful website chatbot is one POST endpoint that sends the visitor's question plus your FAQ to a model, with the API key on the server, CORS limited to your site, a per-IP rate limit and a spending cap on the key. Under 100 lines of Python and a small VPS can run it.
Disclosure: the Hostinger and AICredits links are affiliate links. I ran the backend's behaviour tests on 23 Oct 2026 with the model call replaced by a stub, so the real outputs below prove the guard rails, not answer quality. The embed snippet, Docker and Caddy steps are unrun, and I did not evaluate model answers.
If you already run a shop or services business, the questions your visitors ask are repetitive: opening hours, delivery cost, returns. A bot that answers those from a document you control is a reasonable first AI project. It's also a way to burn money if you leave the endpoint open, so most of this post is about the guard rails.
What does the backend look like?
import os
import time
from collections import defaultdict, deque
from pathlib import Path
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from openai import OpenAI
from pydantic import BaseModel, Field
SITE_ORIGIN = os.environ.get("SITE_ORIGIN", "https://www.example.com")
MODEL = os.environ.get("MODEL", "openai/gpt-4o-mini")
FAQ = Path(os.environ.get("FAQ_FILE", "faq.md")).read_text(encoding="utf-8")
LIMIT, WINDOW = 10, 60 # 10 questions per IP per 60 seconds
SYSTEM = (
"You answer questions for a small business using ONLY the FAQ below. "
"If the answer is not in the FAQ, say you don't know and ask the visitor "
"to contact the business. Never invent prices, hours or policies. "
"Keep answers under 80 words.\n\nFAQ:\n" + FAQ
)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[SITE_ORIGIN],
allow_methods=["POST"],
allow_headers=["Content-Type"],
)
hits: dict[str, deque] = defaultdict(deque)
class Ask(BaseModel):
question: str = Field(min_length=1, max_length=500)
def ask_model(question: str) -> str:
client = OpenAI(base_url="https://api.aicredits.in/v1", api_key=os.environ["AICREDITS_API_KEY"])
r = client.chat.completions.create(
model=MODEL,
max_tokens=200,
messages=[{"role": "system", "content": SYSTEM}, {"role": "user", "content": question}],
)
return r.choices[0].message.content
@app.get("/healthz")
def healthz():
return {"ok": True}
@app.post("/chat")
def chat(body: Ask, request: Request):
ip = request.headers.get("x-forwarded-for", request.client.host).split(",")[0].strip()
now = time.time()
q = hits[ip]
while q and now - q[0] > WINDOW:
q.popleft()
if len(q) >= LIMIT:
raise HTTPException(status_code=429, detail="Too many questions. Try again in a minute.")
q.append(now)
return {"answer": ask_model(body.question)}
Your faq.md is plain text you write. Mine for the test had three lines: opening hours, delivery charge and the returns window. Every request sends the whole FAQ, which is fine while it's short. When it outgrows the prompt, move to retrieval with the RAG lesson as your guide.
The design choices that matter:
max_length=500on the question andmax_tokens=200on the answer cap what one request can cost.- The system prompt forbids answers outside the FAQ. That reduces made-up policies; it doesn't remove them. Ask it five questions that are not in your FAQ before launch.
- The model call is isolated in
ask_model, so you can swap the provider or stub it in tests. - The
x-forwarded-forheader is only trustworthy when it comes from your own reverse proxy. Don't expose the app port directly or visitors can spoof it.
Do the guard rails work?
I tested with FastAPI's test client, replacing ask_model with a stub and the site origin set to https://shop.example:
ok origin: 200 https://shop.example {'answer': 'stub answer to: When are you open?'}
bad origin allow header: None
too long: 422
burst codes: [200, 200, 200, 200, 200, 200, 200, 200, 429, 429, 429, 429]
What this shows:
- A request from the allowed origin gets
access-control-allow-originback. - A foreign origin gets no such header, so a browser on that site can't read the answer.
- A 501-character question is rejected with 422.
- In a burst of 12, the first eight succeeded and the rest got 429. Eight, not ten, because the earlier requests in the same test already used two of the ten slots.
The part people miss: the foreign-origin request still ran the handler and counted against the rate limit. CORS controls what browsers expose to pages, not who can call your server. A script using curl ignores it. Your real defences are the rate limit, the length caps and a spending cap on the key. The in-memory limiter also resets when the container restarts and doesn't share state across multiple workers, so it's a floor, not a complete answer.
How do you add it to the website?
A bare-bones snippet (unrun, style it to match your site):
<div id="faq-bot">
<input id="faq-q" placeholder="Ask a question" maxlength="500" />
<button id="faq-send">Ask</button>
<p id="faq-a"></p>
</div>
<script>
document.getElementById("faq-send").onclick = async () => {
const q = document.getElementById("faq-q").value;
const out = document.getElementById("faq-a");
out.textContent = "...";
const r = await fetch("https://bot.example.com/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question: q }),
});
const data = await r.json();
out.textContent = r.ok ? data.answer : (data.detail || "Something went wrong.");
};
</script>
Use textContent, not innerHTML, so a model reply can't inject markup into your page.
How do you deploy it?
Containerise it (unrun):
FROM python:3.13-slim
WORKDIR /app
RUN pip install --no-cache-dir fastapi uvicorn openai
COPY main.py faq.md .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Run it with Compose, publishing 127.0.0.1:8000:8000, an env_file holding AICREDITS_API_KEY and SITE_ORIGIN, and restart: unless-stopped. Put Caddy in front on bot.example.com with reverse_proxy localhost:8000, as in the Streamlit deployment post. The FastAPI Docker guide goes further on production setup.
This workload is light, so a one-core plan is a reasonable start and KVM 2 gives room for the other tools a small business ends up wanting (an uptime monitor, a form handler). See sizing.
For the model bill, AICredits lets you top up in rupees and cap a key's budget. Make a key just for this bot with a limit you're comfortable losing, because the key is the last line of defence. The WhatsApp support bot guardrails post has more on prompt-level limits.
What to check before launch
- Ask it questions that aren't in the FAQ and read what it says.
- Show a visible phone number or email next to the widget.
- Tell visitors it's an AI assistant. Check any disclosure rules that apply to your business and location with a professional.
- Do not put customer personal data into the prompt or logs.



