DamienKomala.
All articles
AI & Agentic Development8 min read

The Guardrails Are the Product: A Portfolio Chatbot That Can't Invent a Price

A chat reply passing through a scanner gate; an invented price is stopped in amber while a published one passes through

There's a little chat launcher in the corner of every page on this site. It answers questions about my work, my process and my pricing, using only what the site itself says. Getting it to chat took an afternoon. Getting it to never say something I'd have to apologize for took quite a bit longer.

An assistant that talks about me has plenty of ways to embarrass me. It can make up a price, hand me a certification I've never earned, promise a start date, or invent an email address. And it does all of that at chat speed, to someone thinking about hiring me, in a transcript they get to keep. On a one-person business, the person who pays for those mistakes is me.

So for this project, the guardrails aren't a nice extra on top of the assistant. They're most of what I actually built. The chat window is the easy part.

A prompt is a polite request

The obvious move is to write the rules into the system prompt: don't invent prices, don't make up contact details, and so on. I did that, and I gave every rule its reason. A model told "never invent a price" can sometimes be talked out of it. A model that's told why a made-up number hurts this particular business holds up a lot better.

Still, a rule that lives only in the prompt is a rule that holds most of the time. "Most of the time" is a great standard for a weather app and a terrible one for a price quote.

So there's a second layer. Every reply goes through a scanner before the visitor sees it. If it finds a problem, the agent gets the specific mistake back and tries again. Not a vague "try again," but something like "you stated $1,800, which isn't a published price." If it keeps getting it wrong and runs out of retries, the visitor gets an honest fallback that points them to the contact form, and the bad answer never sees daylight.

The scanner runs six checks:

CheckWhat it catches
PriceA dollar figure that isn't on the pricing page, a real figure attached to the wrong billing period, a fixed price softened into an estimate, and any discount or payment plan.
Fabricated metricA percentage outside the published set, and any claimed certification or degree.
Sole attributionA team result described as mine alone.
Contact detailAny email or phone number other than the two the site publishes, and any street address.
Overclaimed roleCalling me a backend, database, infrastructure or ML engineer, and any guarantee about search rankings.
CommitmentAvailability claims, start dates, delivery dates and held slots.

I copied it from another project, and two rules flipped

I didn't start from scratch. This assistant is ported from a support agent I built for Tyler 4 Data, and I kept the architecture identical on purpose so a fix in one carries over to the other. The rules didn't carry over so neatly. Two of them turned completely inside out, and if you're ever tempted to copy a chatbot from one site to another, this is the part I'd want you to read.

First, prices. Tyler 4 Data doesn't publish any, so its rule is simple: never state a price. This site publishes all of them (hourly rates, a discovery sprint, web packages, UX packages, care plans). So here the rule becomes a whitelist built straight from the pricing file, and anything outside it is off limits.

Flipping the rule opened up two new ways to be wrong that the original never had to worry about. One is hedging. $2,250 is a published fixed price, but "around $2,250" is a different promise. Every price on this site is hours times a rate, and the rounding is the buffer that lets a quote actually hold. Turn a fixed price into an estimate and it's stopped being a fixed price, and guess who finds that out later? The client.

The other is real parts making a false whole. $125 is an hourly rate. $540 is a care plan's monthly fee. "$125 a month" and "$540 an hour" would both sail through a check that only looks at the numbers, and each one invents a product that doesn't exist. I'd love to sell a $540-an-hour plan, but I don't, so the whitelist stores each price together with the period it's sold in.

Second, contact details. Tyler 4 Data publishes none, so its rule is a ban. This site publishes exactly one email and one phone number, so the rule becomes a whitelist. What worries me here isn't some ridiculous invention. It's the believable one. damien@damienkomala.com looks perfectly reasonable, sends a real enquiry into a mailbox nobody reads, and the person who sent it just assumes I ghosted them.

Let the right answer say the forbidden word

Strict checkers have a sneaky failure mode. Often the correct answer has to mention the forbidden thing in order to deny it: "There's no discount." "Damien isn't a backend engineer." "He doesn't hold that certification." All three are true, and a scanner that rejects them sends the model into a retry loop where every attempt gets vaguer. That's worse than having no scanner, because nobody notices the answers getting mushier.

So every one of those rules handles negation. The role check took some extra care, because I do hand-code front ends (300+ sites and counting). A rule that catches "he's a data engineer" can't also catch "he builds the front end," so there's a test whose whole job is making sure that rule never gets greedy.

There's also a test that runs the assistant's entire knowledge base through the scanner. If the knowledge itself tripped a rule, perfectly correct answers would get bounced into the retry loop, and the model would rewrite them into something worse each time. Watching a correct answer get "fixed" into a wrong one is a special kind of frustrating, and that test means I don't have to.

Check the whole answer before streaming any of it

Most chatbots stream their reply word by word because it feels snappy. This one doesn't. The agent writes its full answer, the scanner checks all of it, and only then does the first word reach your browser.

The logic is simple. If a made-up price shows up on screen for two seconds, the damage is done. It doesn't matter that the check would have caught it a moment later, because someone already read it, and maybe took a screenshot. The cost is a slightly longer pause before the reply starts, so I built a proper typing indicator instead of a lazy spinner. I'll take a slightly slower honest answer over a fast one that's occasionally fiction.

Two smaller lessons

The first version had your browser call the assistant's own server directly, on a different domain. It worked everywhere I tested it. Then it failed in a real visitor's browser, which is the most humbling sentence in web development. Content blockers, privacy extensions, corporate network filters and CORS all fail in exactly the same way: "can't reach the backend," while every check I ran came back green. The fix was to route the chat through this site's own server, so your browser only ever talks to the domain it's already on. That killed the whole category of bug. It also let me pass along each visitor's IP properly, so the rate limit applies per person instead of to the entire internet at once.

The second was the launcher button. An unlabeled chat bubble in the corner usually means "a real person is here to help." On a one-person portfolio that's a weird promise, because the real person it implies is the one you're trying to hire, and I'm probably in Figma. So the launcher has a label that says what it is, and nothing pops it open on its own. There's no timer, no scroll trigger and no "wait, don't leave!" popup. There are tests for both, because I don't trust future me around growth hacks.

The catch: two copies of the truth

I'll be upfront about the trade-off. The assistant answers from its own copy of the site's content, and two copies of anything will drift apart eventually. Every section of that copy names the site file it came from, so when something changes, I at least know where to look.

Prices are the sharpest edge. Changing one takes three edits: the pricing file, the assistant's knowledge, and the scanner's whitelist. Forget the third one and the assistant can't say the new price at all, because its own guardrail blocks it. The visitor gets a polite deflection and has no idea why. So the order is written down now, right where the next price change will trip over it.

If you're adding a chatbot to your site

Write down what it must never say before you write what it should say, because those rules are the real spec. Enforce them in code, not just in the prompt, give the model the specific mistake when it retries, and have an honest fallback ready for when it can't get there. If you copy an agent from another project, rethink every rule, since a ban in one place may need to be a whitelist in another. Worry most about the believable inventions, because those are the ones nobody catches. And if being wrong for two seconds actually costs you something, don't stream an answer you haven't checked.

Go ahead and try it. The launcher is in the bottom corner of this page. Ask it what a project costs, then see if you can sweet-talk it into a discount. (Good luck.) If you want something like this on your own site, let's talk.