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.

By Mickarle Wagstaff-Irons - Micky Irons

  • Level 3Building
  • 90 min
  • 6 chapters
  • Online course and PDF workbook
The Loopagents / 03

Read, practise and review

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

Mickai privately records this registration and sends its team a course-start notification. It records your intention to start, not proof of attendance or completion. Our course-start privacy notice explains the details and the 30-day resume cookie.

Follow the Mickai company page on LinkedIn

You can start learning without subscribing or following. Newsletter email confirmation and provider enrolment are recorded separately.

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

01

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.

text · 3 lines
Browser  --message-->  Your function  --request + key-->  Model API
(public)               (private)                          (provider)
Browser  <--reply----  Your function  <--reply-----------  Model API

Scroll sideways to see every column.

The safe architecture: browser, your function, the model · Table 1
WhereWhat belongs thereWhat must never be there
BrowserThe chat window and the conversation the visitor seesKeys, private instructions, other visitors' data
Your functionThe key (from the environment), the system prompt, limits, loggingAnything the visitor can change, such as the system prompt or model name
Model providerThe model, under its own data termsYour other secrets

Try it yourself · Activity 01

10 min

The view-source test

See for yourself how much a browser reveals. Only look; do not change or attack anything.

  1. Open any website and open the browser's developer tools (often F12, or right-click and Inspect).
  2. Open the Network tab and reload the page. Click one request and read its details.
  3. Open the Sources or Debugger tab and find one of the page's scripts.
  4. 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?

Next chapter

Learn in your browser · Chapter 02 of 06

02

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.

Protecting your key and your wallet · Table 2
RiskControl
A stolen keyKeep 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 messagesRate limit per visitor address or session
Huge messages or long historiesValidate size and count on the server
Expensive repliesCap the reply length in the function
Your bot used as a free general assistantA narrow system prompt, refusals for off-topic requests, a limit on messages per session
Runaway total spendA 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 min

Set your limits

Decide the limits for your own chatbot on paper.

  1. Write your limits for messages per minute per visitor, maximum characters per message, maximum messages in a history and maximum reply length.
  2. Write the daily cap, and the alert level at which you want to be told.
  3. 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?

Next chapter

Learn in your browser · Chapter 03 of 06

03

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.

js · 76 lines
// 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.

html · 40 lines
<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.

Next chapter

Learn in your browser · Chapter 04 of 06

04

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.

Privacy, consent and conversations · Table 3
ChoiceWhat it meansWatch out for
Store nothingThe chat lives in the visitor's browser tab. Your function keeps no messages.Simplest to explain and protect, but you cannot investigate complaints
Store brieflyKeep chats for a short, stated period, then delete them automaticallyNeeds a notice, restricted access and a working deletion job
Store long termKeep chats to improve the bot or as a recordNeeds 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 min

Draft your chat notice

Write the short notice that will sit next to your chat box.

  1. Use the list above and write one plain sentence for each point.
  2. Read it aloud. Cut any word that a non-specialist would not use.
  3. 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?

Next chapter

Learn in your browser · Chapter 05 of 06

05

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.

Next chapter

Learn in your browser · Chapter 06 of 06

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 min

The launch dry run

Run the checklist against your real site, or against a site you have designed on paper.

  1. Mark each line yes, no or not sure.
  2. Try the whole chat using only the keyboard.
  3. Switch the kill switch on and check what a visitor sees.
  4. 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.

Assessment options

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.

  1. 01
    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.

    Read here · 1 exercise
  2. 02
    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.

    Read here · 1 exercise
  3. 03
    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.

    Read here · Reading
  4. 04
    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.

    Read here · 1 exercise
  5. 05
    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.

    Read here · Reading
  6. 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.

    Read here · 1 exercise

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.

We privately record your name, email and request IP for this download. Read our privacy notice and retention policy.

Visit Mickai on LinkedIn

Newsletter confirmation does not affect your download. Following Mickai on LinkedIn is optional; this site does not verify followers.

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 questions

Learn 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.

  1. Right to be informedInformation Commissioner's Office (ICO)
  2. Cross-Origin Resource Sharing (CORS)MDN Web Docs
  3. Using server-sent eventsMDN Web Docs
  4. 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