Tillbaka

Den här texten finns bara på engelska.

How I built Vad svarar AI? with TanStack Start and TanStack AI 1.0

Kenny Damgren

Vad svarar AI? ("What does AI answer?") is a small tool for Swedish business owners. You type your company's name, trade and town. ChatGPT, Gemini, Claude and Perplexity each get two customer questions with web search on, and the eight answers stream in at the same time. A ninth call reads them and writes a summary. It runs on TanStack Start on Cloudflare Workers, with TanStack AI 1.0 talking to every model through OpenRouter. The code is on GitHub.

Eight streams on one page

Each answer is its own useChat hook with its own threadId, so the page runs eight hooks in a fixed order. They all post to one server route, /api/svar, with forwardedProps saying which service and which question. The browser never sends the question text. The server reads the run from D1 and builds the two questions itself, so the endpoint can only ask what the tool is for.

The models are cheap ones: GPT-6 Luna, Gemini 3.1 Flash Lite, Claude Haiku 5.5 and Perplexity Sonar. Swapping one is a single line. Sonar searches on its own, so it is the only model that gets no web search tool. A full run costs about 0.18 dollars.

Where the sources went

The answers mean little without their sources, and my first version showed none. The OpenRouter SDK parses the stream and drops the annotations field, which is where the URL citations live. I pinned the SDK and gave the adapter a custom HTTP client that tees the raw server-sent events. One copy goes to the SDK as usual and the other is read for citations. When an answer is done, its sources reach the browser as a custom event just before the run finishes.

A summary that streams

The summary uses outputSchema with a Zod schema. TanStack AI streams the partial object, so the verdict for each service fills in while the model writes it. The model judges each answer. My code counts the mentions, shows a quote only if it really appears in the answer and saves the result in D1. Opening the run again replays the saved summary without a new model call.

Keeping the bill small

I pay for every question, so a good part of the work went into limits. The button first runs Turnstile. Then it looks for the same question from the last day and sends you to that saved result. After that comes a daily budget of 100 kronor, summed from the cost OpenRouter reports, and three runs per IP address and day. Each run may make at most 16 model calls during its first 15 minutes. They are counted in one UPDATE … RETURNING, so parallel requests can't slip past the limit, and answers that are already saved are replayed.

Small things that bit

React StrictMode mounts components twice in development, and a ref guard still sent every question twice. The hooks now send from a setTimeout that the effect cleanup clears. crypto.randomUUID only exists in secure contexts, and I followed the dev server on my phone over plain HTTP through Tailscale, so IDs are built from getRandomValues instead. The share image is drawn with workers-og, which needs Inter as woff because Satori can't read woff2.