Case study
angrynirds
The website for a small studio that builds websites, online stores and AI assistants for small businesses. It prices a project from one price book, builds a Stripe Checkout deposit from that price, and runs the AI assistant the studio sells to its clients.
The problem.
A small business owner who wants a website often cannot get a price without booking a sales call first, and many leave at that step.
A chat assistant that does answer can make it worse. It can state a price the business never set, or promise results the business cannot deliver, and the business is then held to words it never wrote.
How it solves it.
- One price book
- Every price on the site comes from one file,
data/pricing.ts. The service pages, the quote wizard and the assistant all read from it, so they cannot disagree. - A quote wizard
- The visitor answers plain questions about the business and the project. The wizard maps the answers to line items, prices them from the price book, and shows one project price plus any monthly cost.
- A deposit through Stripe Checkout
- The visitor can pay the deposit or the full price. The server recomputes the amount from the quote, so the browser never sends an amount.
- An assistant that only quotes tool results
- The assistant answers questions, looks up prices, books calls, opens checkout links and collects project files. It can state a price only if a tool returned that price in the same turn. Any other dollar figure or discount percentage, and any promise about search rankings, traffic or conversions, is replaced with a fixed safe reply, and the case is sent to a person.
- A person when it matters
- The assistant can hand a conversation to the studio, which receives it by email with the end of the conversation attached. When a question touches a policy the business has not decided, the knowledge search returns no excerpts and a plain "not settled yet" line instead, so the assistant has nothing to improvise from.
Who it helps.
- Small service businesses
- A site that quotes a project and takes a deposit without a sales call.
- Buyers
- A real price before they talk to anyone, and an assistant that cannot invent one.
- The studio
- It sets up the same assistant for each client as a separate instance, with that client's prices, documents and on/off switch. The reply guard and the spend limits are shared code and cannot be changed per instance.
Technical details.
- Stack
- TypeScript, Next.js 16 (App Router), React 19, Tailwind CSS v4, the Vercel AI SDK with the Anthropic provider, Vitest and Biome. Hosted on Vercel. Leads and payment records are stored in Vercel Blob, and email goes out through Resend.
- Tools
- The model calls eleven tools, among them
get_price,list_services,search_knowledge_base,create_booking,create_checkout_linkandescalate_to_human. Tool output is cleaned before it is passed to the model, so internal notes in the price book never reach a customer. - Stream guard
- Each streamed text part is held until it ends, then checked together with everything already sent in that turn, so a figure split across parts is still caught. A dollar figure or discount percentage counts only if it appears in a tool result from the same turn. A part that fails is replaced before the customer sees it, and an escalation is recorded.
- Prompt-injection check
- Incoming messages are checked for attempts to override instructions, requests to reveal the system prompt, role changes and demands for a discount. A message with two or more flags is escalated. An oversized message is refused.
- Spend limits
- Checked before any tokens are bought: a request limit per IP over a rolling ten-minute window, a daily request ceiling per server instance, a cap on how much conversation history is accepted, a cap on output tokens per reply, and a cap on model steps per turn. The counters live in server memory, so the real ceiling is the number of running instances times these limits, and the spend limit on the API key is the backstop.
- Three switches
- The assistant answers only when all three are set:
NEXT_PUBLIC_CONCIERGEat build time mounts the chat launcher,CONCIERGE_ENABLEDat request time lets the endpoint answer, andANTHROPIC_API_KEYis the key replies are billed to. Without the last two, the endpoint returns 503 before any spend. A status endpoint reports whether the assistant is answering, so the widget never shows a chat box in front of a closed endpoint. - Stripe Checkout
- The checkout route recomputes the quote with the same pricing function the wizard uses, then creates a Checkout session through the Stripe REST API with a server-built order summary attached. A webhook checks the Stripe signature with a five-minute replay window, saves the payment or refund record under its Stripe id, and emails the studio.
- Per-client config
- An instance is one config object: system prompt, price book, knowledge documents, booking and checkout providers, where escalations go, allowed origins and an on/off variable. One route serves any registered instance, with a CORS allowlist for client sites on other domains. A salon demo site is the worked example.
- Tests
- Vitest tests cover the stream guard, the percentage and guarantee checks, the spend limits, input screening, checkout, the Stripe webhook, the pricing engine and the wizard. Offline evals run in the same suite against a mock model and attack the input screen, the validator, the stream guard and the tool schemas.
- Live evals
- A separate suite sends fixed scenarios to the production model and grades each reply with fixed graders, which are plain string checks tested against known-bad replies, and with an LLM judge. A calibration test gives the judge replies whose verdict is already known, about half of which must fail, so a judge that passes everything is caught. The live suite spends tokens and runs only when asked.
- CI
- GitHub Actions runs lint and format checks, the test suite and a production build on every pull request, and tests and builds each demo client site.
- Source
- The repository is private. The live site is at angrynirds.com.