I built a currency chat where the answers are charts, not paragraphs A developer built Currency Copilot, a streaming chat application in which the model never states an exchange rate itself but instead calls four typed server-side tools that fetch European Central Bank reference rates via the Frankfurter API, with each result rendered as its own React component rather than prose. The stack is Next.js 16 with the App Router, React 19, TypeScript, the Vercel AI SDK 6, Zod, Recharts, Drizzle ORM and Neon Postgres, and the system prompt instructs the model to "Use the tools for every rate. Never state a rate from memory." Because tool inputs and outputs are typed end to end with Zod schemas, a renamed field becomes a compile error in the client component instead of a blank card in production, and the tool descriptions explicitly tell the model what the UI will display so its reply stays to one sentence of context. Ask a chat model what the dollar is worth in reais and it will answer, confidently, with a number from its training data. That number is months old. Ask it whether today is a good day to exchange, and it will tell you a story about momentum and inflation that no data backs. And even when the numbers are right, five rates inside a paragraph are hard to compare. So I built Currency Copilot : a streaming chat for currency questions where the model never states a rate itself. It calls typed tools that fetch European Central Bank rates, and each result renders as its own React component instead of text. The design goals: The stack is Next.js 16 with the App Router, React 19, TypeScript, the Vercel AI SDK 6, Zod, Recharts, Drizzle ORM and Neon Postgres. The model has four tools, each with a Zod input schema, and they run on the server: The data comes from Frankfurter, a free API over the ECB reference rates, with no key. A tool is a description, a schema and a function: js export const currencyCode = z .string .regex /^ A-Za-z {3}$/ .transform code = code.toUpperCase .describe 'ISO 4217 code, e.g. USD, BRL, EUR' export const getRate = tool { description: 'The latest exchange rate between two currencies ECB reference rate, updated each working day . ' + 'The UI shows the result as a rate card, so reply with one short sentence, not a repeat of the numbers.', inputSchema: z.object { base: currencyCode, quote: currencyCode } , execute: { base, quote } = getLatestRate base, quote ,} The system prompt says “Use the tools for every rate. Never state a rate from memory.” It also says when to use each tool: “this month” means 30 days of history, and an amount in several currencies is one convert call, not three. Why the description talks about the UI: without it, the model fetched the rate and then repeated it in a sentence, a bullet list, and sometimes a table, right under the card that already showed it. Telling the model what the user will see is what keeps its reply to one sentence of context. On the client, useChat streams messages made of typed parts. A tool call arrives as a part named after the tool, and it moves through states: input streaming, input available, output available, or output error. The chat renders each state: case 'tool-getRate': switch part.state { case 'output-available': return