How do I add a fund lookup tool to a Vercel AI SDK chatbot?
Define a tool with zod that calls the fund API by ISIN inside a Next.js route using the Vercel AI SDK, so your chatbot answers with current TER, holdings, exposures and returns.Updated 12 September 2026 · by FundFacts APIShort answer
In your route handler, pass a tool defined with zod ({ isin: z.string().length(12) }) whose execute function calls GET https://fundfactsapi.com/api/v1/funds/{isin} with the server-side key (or the @fundfactsapi/sdk client) and returns the fields the model should see. streamText or generateText then lets the model call it for any ISIN a user mentions, and you render the result in the chat. The key stays on the server.
Route handler
typescript// app/api/chat/route.ts — npm install ai @ai-sdk/openai @fundfactsapi/sdk zodimport { openai } from "@ai-sdk/openai";import { streamText, tool } from "ai";import { FundFacts } from "@fundfactsapi/sdk";import { z } from "zod";const ff = new FundFacts({ apiKey: process.env.FUNDFACTS_API_KEY! }); // server onlyexport async function POST(req: Request) {const { messages } = await req.json();const result = streamText({model: openai("gpt-4.1"),system: "You answer questions about investment funds using the tools. Always mention asOf with figures; say 'not disclosed' for empty fields.",messages,tools: {fundFacts: tool({description: "Current factsheet of a fund or ETF by 12-character ISIN.",inputSchema: z.object({ isin: z.string().length(12) }),execute: async ({ isin }) => {const f = await ff.getFund(isin); // cached in memory until expiresAtconst d = f.data;return {name: f.name, category: d.profile.category, risk: d.riskRating, ter: d.headlineMetrics.ter, aum: d.keyFacts.aum,topHoldings: d.topHoldings.slice(0, 10), sectors: d.sector.slice(0, 8), countries: d.geography.slice(0, 8),calendarReturns: d.calendarReturns, asOf: d.dataAsOf,};},}),searchFunds: tool({description: "Find ISINs by fund name or issuer. Free.",inputSchema: z.object({ query: z.string().min(2) }),execute: async ({ query }) => (await ff.search(query)).results.slice(0, 10).map((r) => ({ isin: r.isin, name: r.name, issuer: r.issuer })),}),},});return result.toUIMessageStreamResponse();}
The tool result can be rendered as a card on the client (holdings table, sector donut) with @fundfactsapi/widgets, the same React components that render this site's factsheets.
Or connect the MCP server
The AI SDK can consume MCP tools directly:
typescriptimport { experimental_createMCPClient as createMCPClient } from "ai";const mcp = await createMCPClient({transport: { type: "http", url: "https://fundfactsapi.com/api/mcp", headers: { Authorization: `Bearer ${process.env.FUNDFACTS_API_KEY}` } },});const tools = await mcp.tools(); // get_fund, search_funds, compare_funds, analyze_portfolio, fund_overlap, get_scpi
The MCP server exposes six tools, each counted like the REST endpoint it wraps: get_fund (ISIN → the full factsheet payload), search_funds (name → ISINs, free), compare_funds (side-by-side table for several ISINs), analyze_portfolio (weighted look-through of a list of positions, Pro and above), fund_overlap (shared holdings between funds, Pro and above) and get_scpi (French SCPI data by name or ISIN). Authentication is the same bearer API key as the REST API, sent as an Authorization header.
Notes
- A cold ISIN can take minutes on first load; set
maxDurationon the route accordingly or pre-warm the funds your users ask about. - One request per ISIN answered; the SDK client caches until
expiresAtso repeated questions in a session cost nothing extra. - Never call the API from the browser; the key belongs in the route handler.
Verify it yourself
The demo endpoint returns the live payload for a fund that is already in the store, without a key. Everything on this page can be checked against it.bashcurl -s https://fundfactsapi.com/api/v1/demo/funds/IE00B4L5Y983 | jq '{name, asOf: .data.dataAsOf, ter: .data.headlineMetrics.ter, risk: .data.riskRating, top: .data.topHoldings[:3]}'