Why it exists
Most visitors leave with their question still in their head.
You land on a portfolio with a specific problem — “can this person actually help me ship an AI feature without it going sideways?”— and the site makes you hunt for the answer. Read the about page, skim a few projects, guess, maybe fill out a form and wait two days. Most people just close the tab.
Buddy exists to close that gap. Ask it anything in plain language and it answers in two lines, points you to the work that matches what you’retrying to do — the practice, the person, or the principles — and, if there’s a fit, hands the conversation to Ray already warm and summarized. No forms, no waiting, no digging.
For the visitor, that’s an instant, low-pressure way to find out if there’s a fit. For the practice, it’s a lead that arrives with context instead of a cold “hello” — the same conversation, started five steps ahead. That’sthe thing worth protecting — which is where this story actually begins.
A bot whose only job is to get out of the way.
Buddy is the assistant on this site. It isn’t a novelty and it doesn’t try to be Ray. Its job is narrow and commercial: understand what a visitor is working on, recognize a real potential client, and hand a warm, summarized lead to the inbox. Email-first practice, email-first bot.
A lead-capture bot is a small thing with a large attack surface. It spends money on every message, it can be talked into saying things, and it touches the one asset a one-person practice can’t afford to lose — the lead. So it was built like infrastructure, not like a toy.
Building the layers is not the same as the layers working.
The break: a guard that quietly disabled itself.
Every protection that needed shared state — the rate limits, the daily spend kill-switch, the anti-spam lead caps — ran on a small Redis database. At some point that database went away. Its hostname stopped resolving. Nothing crashed.
By the numbers
17
documented security layers
8/8
injection attacks held in testing
13s→5s
request latency after the fix
0
rate limiting — until a routine audit caught it
That’s because the code was written to fail open: if the limiter couldn’t reach the database, it let the request through rather than block a real user over an infrastructure hiccup. A reasonable instinct. Applied without a floor, it meant the chatbot spent weeks with no rate limiting at all — and because it still answered every visitor politely, nothing looked wrong.
A security pass caught it the only way it could be caught: by attacking the live endpoint. Twenty-five rapid requests in a row. All twenty-five returned a cheerful 200. Each one also hung for thirteen seconds, paying for a database that wasn’t there.

The model was never the risk. The system underneath it was. And the system was failing in the one mode no one was watching.
The fix: four principles, not a patch.
Restarting the database would have made the symptom disappear and left the real defect in place — the next outage would land the same way. The fix was to change how the system behaves when a dependency dies.
Fail-safe, not fail-open
When the rate-limit store is unreachable, the limiter now degrades to a bounded in-memory window instead of waving everyone through. A guard that disables itself under pressure was never a guard.
Degrade fast
Every call to the shared store is capped at 1.5 seconds. A dead dependency now costs a blink, not a thirteen-second hang on every message.
Make failure loud
The degraded path is audited, so the next outage shows up on a dashboard instead of hiding behind a still-friendly chatbot.
Protect the irreplaceable thing
The model can be wrong and the bill can be capped — but a lost lead is gone. Lead delivery was split across channels and backed by a durable record so the one thing that matters most survives.
And then we went further
Where does the user experience actually end?
We gave the site a second affordance: highlight any text on any page and a small “Ask Buddy about this” button appears. Click it and the chat opens with your selection already in hand. It looks like a one-afternoon feature. It wasn’t — and what it surfaced is the whole reason I build the way I do.
The catch: context is king, and a highlight rarely has enough of it.
Highlight a single word and ask “what does this mean?” and a naive bot will cheerfully invent an answer — or worse, decide your word is a typo and correct it. The selection is the visitor’s focus; it is almost never enough context. So Buddy now reads around the highlight: it captures the surrounding passage and the page it lives on, and it’s instructed to ask a short clarifying question rather than guess when a highlight is too thin to answer honestly. Intelligence isn’t answering everything — it’s knowing when you don’t have enough to answer well.
The second catch: the feature collided with the security.
This very case study contains the phrase “ignore all previous instructions.” The moment we feed highlighted page text to the bot, the injection filter that protects Buddy would flag a visitor’s perfectly innocent question — on the page where the feature matters most — and refuse it. The security layer would sabotage the experience. So we split the two: the question you type is the instruction, and it alone is screened for attacks; the text you highlighted travels separately as untrusted reference material, capped and quarantined, never read as a command. The guard still guards. The feature still works. Neither one breaks the other.
Why sitewide, not just the pages that “need” it.
The case studies are where this earns its keep; the short marketing pages barely use it. The tidy answer is to ship it only where it pays off. We didn’t — and that was a UX decision, not a technical one. A feature that exists on some pages and silently vanishes on others teaches the visitor that the product is unreliable: they try it where it isn’t, nothing happens, and now they don’t trust it anywhere. Consistency of behavior isthe experience. The trade-off is real and we took it on deliberately: sitewide means we changed how the entire site behaves — a new thing the visitor can do everywhere — so we had to understand the behavior we introduced everywhere, not just admire it on the pages we cared about.
Here’s the part most people won’t follow me on: security is user experience. So is latency, so is a lost lead, so is a bot that bluffs instead of asking. The experience isn’t the screen — it’s everything the product does, including what they never see.
One more leash
Even Buddy’s links are on a leash.
The most recent addition: Buddy can hand you buttons straight to the right page — one tap to the work that matches what you asked. Useful. It’s also a brand-new attack surface, and the kind most teams ship without thinking twice.
A bot that can suggest a link can be talked into suggesting the wrong one — a look-alike domain, a page that doesn’t exist, a “click here to verify your account.” So Buddy can’t write links at all. It can only choose from a fixed allowlist of real pages on this site, re-checked on the server before anything reaches your screen; anything off the list is dropped silently. The bot proposes, the allowlist disposes.
Same rule as everything else here: give the model one job, then build the walls so it can’t do anything else. Built like infrastructure, not like a toy.
What Buddy actually does.
Underneath the friendly two-line replies is a deliberately boring, defensive system.
Lead qualification, not chit-chat
Buddy is built on Gemini with a single tool — capture_lead. It understands the visitor first, then captures name, email, and a written summary only once it has real context. Every argument is re-validated server-side, so even a coerced model can’t submit junk.
A layered defense stack
Origin checks, burst and global rate limits, a daily budget kill-switch, input caps, sanitization, an injection pre-filter, output sanitization, disposable-email blocking, per-IP lead caps, and dedup — each an independent layer, most of which hold even when the LLM doesn’t.
Prompt-injection resistance
Eight attack classes — direct overrides, novel phrasings, roleplay persona breaks, multi-turn history poisoning, instruction smuggling — tested live. Eight held. The regex filter kills low-effort attacks at zero model cost; the system prompt treats every visitor message as untrusted data.
Privacy by default
Conversations are never stored server-side. The only durable records are anonymized metadata, captured leads, and a short brand-safety trail of what the bot said — enough to protect the business, little enough to stay low-liability.
Leads that can’t be lost
Every capture is delivered on email and Slack independently and written to a recoverable audit record. If all channels fail, the lead is still retrievable — a real prospect is never dropped to a transient outage.
Email-first hand-off
Buddy qualifies and gets out of the way. It makes no commitments on Ray’s behalf — no rates, no timelines — and hands a warm, summarized lead straight to the inbox.
Highlight-to-ask, sitewide
Select any text on any page and an “Ask Buddy about this” button opens the chat with your selection in hand. Buddy reads the surrounding passage + page for context and asks rather than guesses when a highlight is thin — and the quoted text is quarantined as untrusted reference so the feature never trips the injection filter.

