agents · Level 3
Host a chatbot on your website, safely
Browser to your own server function to the model: keys, limits, cost control, privacy, accessibility, fallbacks and monitoring.

Your next step
Start this course online
Register your start, then work through the lessons and exercises at your own pace. You can download the PDF separately. Where a scored exam is available, you will find it after the lessons.
Register or resume your course
Start with the essentials
The short answer
To host a chatbot safely, the browser must never call the model provider directly. It sends messages to your own server function, which holds the API key, validates input, applies rate and cost limits, adds the system prompt and calls the model. Add a privacy notice, accessibility, a fallback and human handover, and monitoring before launch.
What you will learn
- You will be able to explain why API keys must never appear in front-end code and draw the safe architecture.
- You will be able to set input validation, rate limits and cost caps for a public chat endpoint.
- You will understand what CORS does and does not protect, and what streaming adds.
- You will be able to read a small serverless function and the page that calls it, and adapt them.
- You will be able to draft a plain-English chat privacy notice and decide whether to store conversations.
- You will have a pre-launch checklist covering accessibility, fallbacks, human handover and monitoring.
Who it is for
Anyone who has a working chatbot, from the previous workbook or elsewhere, and wants to put it on a real website. You should be able to read short JavaScript and HTML examples.
Before you start
- A chatbot with a system prompt and a test set (see Build your first chatbot). Basic familiarity with web pages helps.
Learn in your browser · Chapter 01 of 06
The safe architecture: browser, your function, the model
The most important rule of hosting a chatbot is that the browser never talks to the model provider directly. Everything else in this workbook follows from that rule.
Anything that reaches a visitor's browser is public. The page's code, the requests it makes and any key inside it can be read by anyone who opens the browser's developer tools. So a key placed in front-end code is not a secret. Someone can copy it and spend your money.
The safe pattern puts a small piece of your own code in the middle. The browser sends the visitor's message to your server function. Your function holds the key, checks the request, adds the system prompt, calls the model and returns the reply. A serverless function is a small piece of server code that a hosting platform runs on demand, so you do not manage a server.
Browser --message--> Your function --request + key--> Model API
(public) (private) (provider)
Browser <--reply---- Your function <--reply----------- Model APIScroll sideways to see every column.
| Where | What belongs there | What must never be there |
|---|---|---|
| Browser | The chat window and the conversation the visitor sees | Keys, private instructions, other visitors' data |
| Your function | The key (from the environment), the system prompt, limits, logging | Anything the visitor can change, such as the system prompt or model name |
| Model provider | The model, under its own data terms | Your other secrets |
Try it yourself · Activity 01
10 minThe view-source test
See for yourself how much a browser reveals. Only look; do not change or attack anything.
- Open any website and open the browser's developer tools (often F12, or right-click and Inspect).
- Open the Network tab and reload the page. Click one request and read its details.
- Open the Sources or Debugger tab and find one of the page's scripts.
- Write down three things you could see that a visitor was never shown on screen.
Would you be happy for an API key to appear in that list? What does that tell you about where the key must live?
Learn in your browser · Chapter 02 of 06
Protecting your key and your wallet
A public chat box will be found by people who want free answers and by scripts that try everything. Plan for abuse before launch.
Scroll sideways to see every column.
| Risk | Control |
|---|---|
| A stolen key | Keep it only in the function's environment. Revoke and replace it if it leaks. Use a separate key for each project. |
| One visitor sending endless messages | Rate limit per visitor address or session |
| Huge messages or long histories | Validate size and count on the server |
| Expensive replies | Cap the reply length in the function |
| Your bot used as a free general assistant | A narrow system prompt, refusals for off-topic requests, a limit on messages per session |
| Runaway total spend | A daily cap in code, a spending limit and alerts at the provider if it offers them, and a kill switch |
Input validation means never trusting what arrives. Check the method, the shape of the data, the number of messages, the length of each and the roles allowed. Accept only user and assistant messages from the browser. Never accept a system message or a model name from it.
Rate limiting counts requests per visitor over a time window and refuses the excess with a friendly message. Use the visitor address your host supplies, because headers a client can set are easy to fake. A counter held in one function instance is not shared and resets, so real deployments use a shared store or a platform feature.
CORS (cross-origin resource sharing) is a browser rule: a page from one origin may not read replies from another origin unless that server allows it. If your page and function share an origin, you need no CORS settings. If they differ, allow only your own site's origin rather than a wildcard.
Cost control works in layers: a cap on each reply, a cap on messages per visitor, a daily cap for the whole site that switches the bot to its fallback message, alerts when spend rises, and a kill switch you can flip quickly.
Try it yourself · Activity 02
15 minSet your limits
Decide the limits for your own chatbot on paper.
- Write your limits for messages per minute per visitor, maximum characters per message, maximum messages in a history and maximum reply length.
- Write the daily cap, and the alert level at which you want to be told.
- Write the exact message a visitor sees when a limit is hit.
Using your provider's current price list, what is the most a determined script could cost you in an hour under these limits? Is that acceptable?
Learn in your browser · Chapter 03 of 06
A small serverless function, and the page that calls it
Here is a complete, small example. Both parts are illustrative: adapt names, headers and response shapes to your provider and your host.
// Illustrative serverless function using standard Request and Response objects.
// MODEL_API_URL, MODEL_API_KEY and ALLOWED_ORIGIN are environment variables.
const SYSTEM_PROMPT = "You are the help assistant for an imaginary bakery. ...";
const LIMITS = { messages: 20, chars: 1000, replyChars: 4000, perMinute: 10 };
const hits = new Map(); // per instance only: use a shared store, which also holds a daily cap
const cors = {
"access-control-allow-origin": process.env.ALLOWED_ORIGIN,
"access-control-allow-methods": "POST",
"access-control-allow-headers": "content-type",
"vary": "Origin",
};
const send = (status, body) =>
new Response(body ? JSON.stringify(body) : null, {
status,
headers: { ...cors, "content-type": "application/json" },
});
function rateLimited(ip) {
const now = Date.now();
const entry = hits.get(ip);
if (!entry || now > entry.resetAt) {
hits.set(ip, { count: 1, resetAt: now + 60000 });
return false;
}
entry.count += 1;
return entry.count > LIMITS.perMinute;
}
function validMessages(list) {
return Array.isArray(list) && list.length > 0 && list.length <= LIMITS.messages &&
list.every(m => m && ["user", "assistant"].includes(m.role) &&
typeof m.content === "string" && m.content.length > 0 &&
m.content.length <= (m.role === "user" ? LIMITS.chars : LIMITS.replyChars)) &&
list[list.length - 1].role === "user";
}
export default async function handler(request) {
if (request.method === "OPTIONS") return send(204);
if (request.method !== "POST") return send(405, { error: "Method not allowed" });
if (process.env.BOT_ENABLED === "false") return send(503, { error: "The assistant is switched off." });
if (request.headers.get("origin") !== process.env.ALLOWED_ORIGIN)
return send(403, { error: "Forbidden" });
// Use the visitor address your host documents. Some hosts overwrite x-forwarded-for;
// if yours appends to it instead, the first entry can be faked by the client.
const ip = (request.headers.get("x-forwarded-for") || "unknown").split(",")[0].trim();
if (rateLimited(ip)) return send(429, { error: "Too many messages. Please wait a minute." });
let body;
try { body = await request.json(); } catch { return send(400, { error: "Bad request" }); }
if (!validMessages(body?.messages)) return send(400, { error: "Bad request" });
try {
const upstream = await fetch(process.env.MODEL_API_URL, {
method: "POST",
headers: {
"content-type": "application/json",
"authorization": "Bearer " + process.env.MODEL_API_KEY,
},
body: JSON.stringify({
messages: [{ role: "system", content: SYSTEM_PROMPT },
...body.messages.map(({ role, content }) => ({ role, content }))],
max_output_tokens: 300,
}),
signal: AbortSignal.timeout(15000),
});
if (!upstream.ok) throw new Error("upstream status " + upstream.status);
const data = await upstream.json();
const reply = typeof data.text === "string" ? data.text.trim().slice(0, LIMITS.replyChars) : "";
if (!reply) throw new Error("empty reply");
return send(200, { reply });
} catch {
return send(502, { error: "The assistant is unavailable." });
}
}Read it for the pattern. The function answers the browser's preflight check, refuses anything but a POST whose Origin header is your own site (a script can fake that header, so it is a filter, not a lock), honours a kill switch, limits the rate, validates every message and passes on only its role and text, adds the system prompt itself, caps the reply, times out slow calls and turns any failure into one friendly message. It never passes provider error text to the visitor, and it logs no message content.
<div id="log" role="log" aria-live="polite"></div>
<form id="chat">
<label for="msg">Your message</label>
<input id="msg" maxlength="1000" autocomplete="off" required>
<button type="submit">Send</button>
</form>
<script>
const log = document.getElementById("log");
const turns = [];
function add(who, text) {
const p = document.createElement("p");
p.textContent = who + ": " + text; // textContent, never innerHTML
log.appendChild(p);
}
document.getElementById("chat").addEventListener("submit", async (event) => {
event.preventDefault();
const box = document.getElementById("msg");
const text = box.value.trim();
if (!text) return;
box.value = "";
add("You", text);
turns.push({ role: "user", content: text });
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ messages: turns.slice(-19) }),
});
if (!res.ok) throw new Error("bad status");
const data = await res.json();
turns.push({ role: "assistant", content: data.reply });
add("Assistant", data.reply);
} catch {
turns.pop();
add("Assistant", "Sorry, I cannot answer right now. Please email the team.");
}
});
</script>Two details matter. The reply is added with textContent, never innerHTML, so any markup in a reply is shown as text and cannot run as code. And the log region has a role and aria-live setting, so screen readers announce new messages.
Streaming lets the model send its reply in pieces and your function pass them on, so words appear as they are written. The bot feels faster. A browser can read such a stream using server-sent events (see Sources) or by reading a streamed response. Streaming adds work: a dropped connection can leave half a reply, you cannot check the whole reply before showing it, and cost accounting must handle partial replies. Get the plain version working first.
Learn in your browser · Chapter 04 of 06
Privacy, consent and conversations
This chapter is general information in plain terms, not legal advice. If a visitor types anything about themselves, and in a free text box they will, UK data protection law is likely to apply to what you do with it.
The Information Commissioner's Office (ICO) explains that people have a right to be told how their data is used (see Sources). In practice, show a short notice next to the chat box that says:
- who you are and how to contact you
- that the visitor is talking to an automated assistant, not a person
- what you do with what they type, and why
- who else receives it, which includes the model provider your function calls
- how long you keep it
- where to find your full privacy notice, and a request not to type sensitive details
Collect the least you need. Not every chat needs consent as its legal basis, and cookies and similar technologies fall under separate rules, so check the ICO's guidance for your situation. If the bot may handle children's data, health details or other sensitive information, get proper advice before launch.
Scroll sideways to see every column.
| Choice | What it means | Watch out for |
|---|---|---|
| Store nothing | The chat lives in the visitor's browser tab. Your function keeps no messages. | Simplest to explain and protect, but you cannot investigate complaints |
| Store briefly | Keep chats for a short, stated period, then delete them automatically | Needs a notice, restricted access and a working deletion job |
| Store long term | Keep chats to improve the bot or as a record | Needs a clear purpose, stronger safeguards and a way to answer access and deletion requests |
Whichever you choose, check your model provider's data settings. Messages you send them may be kept or used under their terms, and you should tell visitors the truth about what happens.
Try it yourself · Activity 03
15 minDraft your chat notice
Write the short notice that will sit next to your chat box.
- Use the list above and write one plain sentence for each point.
- Read it aloud. Cut any word that a non-specialist would not use.
- Ask someone who does not work in technology what they think happens to their messages.
Did their answer match what actually happens? If not, what needs to change, the notice or the system?
Learn in your browser · Chapter 05 of 06
Accessibility, fallbacks, handover and monitoring
A chatbot that some people cannot use, or that fails silently, is a poor front door. These four habits make yours dependable.
Accessibility. The Web Content Accessibility Guidelines (WCAG 2.2, see Sources) describe what good looks like.
- Give the input a real label, and make every control reachable and usable by keyboard, with a visible focus outline.
- Announce new replies to screen readers with a live region. WCAG has a success criterion on status messages for this idea.
- Do not trap keyboard focus in the widget. Let people close it with the Escape key.
- Keep good colour contrast, let text resize, and respect the reduced-motion setting for animations such as typing dots.
- Give errors in words, not only colour, and always offer a route that is not a chat, such as email or a form.
Fallbacks and human handover. The bot will sometimes be unavailable: the provider is down, a limit is reached, the kill switch is on. The chat window should say so and offer another route. Keep a way to reach a person visible at all times. Do not trap visitors in a loop with a bot, be honest about response times, and offer to send a transcript to your team only with the visitor's permission.
Monitoring. Watch a small set of numbers: requests per hour, error rate, response time, spend per day, rate-limit hits, how often the bot declines or hands over, and visitor feedback. Set alerts for spend and error spikes. Write down the leaked-key procedure before you need it: revoke the key at the provider straight away, create a new one, update the function's environment and check recent usage.
Learn in your browser · Chapter 06 of 06
The pre-launch checklist
Run this list against the live site, not just your own computer, and only launch when every line is a yes.
- The key exists only in the function's environment, not in the page, the repository or the logs.
- The function refuses the wrong method, the wrong origin, oversize input and malformed input.
- The system prompt, the model and the reply cap are set on the server.
- A rate limit, a daily cap, a provider spending limit and an alert are in place.
- The kill switch works, and the fallback message and human route show when it is on.
- Model output is displayed as text, never as HTML.
- The privacy notice sits next to the chat, retention is decided and provider data settings are checked.
- A keyboard-only run and a screen-reader run both pass.
- Your test set from the previous workbook passes against the live site.
- Monitoring and alerts are on, and the leaked-key steps are written down.
Try it yourself · Activity 04
30 minThe launch dry run
Run the checklist against your real site, or against a site you have designed on paper.
- Mark each line yes, no or not sure.
- Try the whole chat using only the keyboard.
- Switch the kill switch on and check what a visitor sees.
- Write the three most important fixes, in order.
Which line were you least sure about? Who could check it for you?
Launch quietly to a small group first, watch the numbers, then widen it. Your chatbot is only as trustworthy as the least careful thing around it. To let it answer from your own documents with sources, read RAG and knowledge bases next.
Keep learning
The complete workbook
This workbook shows how to put a chatbot on a website without leaking a key or running up a bill. You will learn the browser, function and model architecture, rate limiting, input validation, CORS and cost control, then read a small serverless function and page. It also covers privacy notices, storing conversations, accessibility, fallbacks and a pre-launch checklist.
- 01The safe architecture: browser, your function, the modelRead here · 1 exercise
The most important rule of hosting a chatbot is that the browser never talks to the model provider directly. Everything else in this workbook follows from that rule.
- 02Protecting your key and your walletRead here · 1 exercise
A public chat box will be found by people who want free answers and by scripts that try everything. Plan for abuse before launch.
- 03A small serverless function, and the page that calls itRead here · Reading
Here is a complete, small example. Both parts are illustrative: adapt names, headers and response shapes to your provider and your host.
- 04Privacy, consent and conversationsRead here · 1 exercise
This chapter is general information in plain terms, not legal advice. If a visitor types anything about themselves, and in a free text box they will, UK data protection law is likely to apply to what you do with it.
- 05Accessibility, fallbacks, handover and monitoringRead here · Reading
A chatbot that some people cannot use, or that fails silently, is a poor front door. These four habits make yours dependable.
- 06The pre-launch checklistRead here · 1 exercise
Run this list against the live site, not just your own computer, and only launch when every line is a yes.
Also inside: a 10-point checklist, a glossary of 12 terms and 10 questions and answers to test yourself. 4 hands-on exercises, each with a worked answer at the back where the workbook gives one.
The PDF is free. We ask for your name and email before downloading. Read the privacy notice at the download form. Free to download and use for personal learning, study groups and inside your own team. Please do not resell the workbooks or republish them as your own. Link people to trust-agent.ai instead.
Get your free PDF workbook
Enter your name and email, then download the complete workbook. This form also works without JavaScript.
Test yourself
Questions and answers
Why can't I call the model directly from my web page?
Because anything in a web page is public. Your API key would be visible to every visitor, who could copy it and spend your money. A server function of your own holds the key, checks each request and calls the model for you.
What is a serverless function?
It is a small piece of server code that a hosting platform runs when a request arrives, so you do not maintain a server. It is a good home for a chatbot endpoint because it can hold secrets and apply limits.
How do I stop people running up my bill?
Layer your defences: validate input, cap reply length, rate limit each visitor, set a daily cap for the whole site, use provider spending limits and alerts if offered, and keep a kill switch. Check current prices to size the limits.
What does CORS do?
It is a browser rule that stops a page reading responses from another origin unless that server allows it. It does not stop scripts or command-line tools, so it is not a substitute for rate limits and validation in the function.
Should the system prompt be sent from the browser?
No. Your function should add it. If the browser can send the system prompt, a visitor can replace it. The browser should send only the visitor's messages and earlier turns, and the function should reject any system message.
Do I need consent to run a chatbot?
Not necessarily: consent is only one lawful basis for processing personal data under UK GDPR, and legitimate interests or another basis can apply instead, depending on your purpose. Whichever basis you rely on, you must still tell people plainly what happens to what they type. This is general information, not legal advice, so check the ICO's guidance for your situation.
Should I store conversations?
Only if you have a clear reason. Storing nothing is simplest. If you store, keep them briefly, restrict access, delete automatically and tell visitors. Also check what your model provider keeps under its own terms.
What is streaming and do I need it?
Streaming sends the reply in pieces so words appear as they are written, which feels faster. You do not need it at first. It adds handling for dropped connections and partial replies, so get the plain version working first.
How do I make a chat widget accessible?
Label the input, make everything work by keyboard with visible focus, announce new replies with a live region, avoid trapping focus, keep good contrast, respect reduced motion and always offer a non-chat route such as email.
What if my API key leaks?
Revoke it at the provider immediately, create a new one, update your function's environment and review recent usage for anything unexpected. Write these steps down beforehand so that you can act in minutes.
Write as you go
My notes
Use this scratchpad for your own notes, working or answers as you read. Notes are saved in this browser on this device. Downloading makes a local copy; it does not send your notes to Mickai.
Consolidate your learning
Review your work
Work through the exercises and questions, then compare your reasoning with the worked answers. A scored exam and verifiable certificate are not yet available for this course.
Review the questionsLearn the language
Key terms
- Serverless function
- A small piece of server code that a hosting platform runs on demand, so you do not manage a server yourself.
- Environment variable
- A named setting supplied to your code by its host, commonly used to hold keys so that they are not written into the code.
- Rate limiting
- Refusing requests from a visitor beyond an allowed number in a time window.
- Input validation
- Checking that incoming data has the expected shape, size and values before using it.
- CORS
- A browser rule, relaxed by server headers, that decides whether a page from one origin may read responses from another.
- Origin
- The combination of scheme, host and port that a web page is served from.
6 of the workbook's 12 terms. The complete glossary is in the workbook.
Follow the evidence
Sources and checks
Facts last checked: .
These workbooks use AI assistance. See how the workbooks are made.
- Right to be informedInformation Commissioner's Office (ICO)
- Cross-Origin Resource Sharing (CORS)MDN Web Docs
- Using server-sent eventsMDN Web Docs
- Web Content Accessibility Guidelines (WCAG) 2.2W3C
Created by Mickarle Wagstaff-Irons - Micky Irons with the Mickai team. Published by Mickai LTD. Last updated 25 September 2026.
NextKeep going
Where to go next
Recommended for you
RAG and knowledge bases, step by step
Build a knowledge base a model can answer from: collect, clean, chunk, embed, retrieve, cite and evaluate, with security basics and a hands-on exercise using five short documents.
Recommended for you
The loop harness: how AI agents actually run
How an agent harness runs a model in a loop: context, tools, permissions, budgets, stop conditions, logging and testing, with a minimal Python loop and a paper design exercise.
Recommended for you
Build and launch a website with AI
Plan a simple site, have AI write the HTML and CSS, understand and check the code, then publish it free with HTTPS and sound search, accessibility and privacy basics.