Build an AI fund assistant with tool calling over structured data
A language model can explain a fund, compare two funds or summarise a portfolio, but only if the facts it works from are current and structured. This page shows how to expose FundFacts API as a tool the model calls, how to keep it inside guardrails, and what the AI companion kit adds when the assistant is built with Cursor, Claude Code or Codex.The problem
Ask a general-purpose model about a fund by ISIN and you get one of two things: a refusal, or a confident paragraph assembled from training data that may be years old and may describe a different share class. Neither is acceptable in a product that clients read.
The fix is well understood: give the model a tool that returns the current facts, and instruct it to answer only from tool output. The remaining questions are practical. What does the tool return, how large is the payload, and how do you stop the model from drifting into advice?
What you need from the data
A fund-assistant tool should return a compact, structured document the model can quote from:
- Name, ISIN, share class, currency, distribution policy
- Investment objective in the fund house's words
- Risk rating 1–7 and profile labels (kind, category, region focus, sector tilt)
- TER, AUM, volatility and other headline metrics
- Top holdings and sector or country exposure
- Calendar and cumulative returns
dataAsOf, so the model can state how current the facts are
Structured is the point. A model asked to compare fees does better with headlineMetrics.ter as a number than with a factsheet PDF.
How it works with FundFacts API
Define one tool, get_fund, whose implementation calls the API and returns a trimmed data object. The example uses a generic tool-calling shape that maps onto any major LLM SDK.
typescriptexport const getFundTool = {name: "get_fund",description:"Return current structured facts for a fund or ETF share class by ISIN. " +"Use it before answering any question about a specific fund.",parameters: {type: "object",properties: { isin: { type: "string", pattern: "^[A-Z]{2}[A-Z0-9]{9}[0-9]$" } },required: ["isin"],},async execute({ isin }: { isin: string }) {const res = await fetch(`https://fundfactsapi.com/api/v1/funds/${isin}`, {headers: { Authorization: `Bearer ${process.env.FUNDFACTS_API_KEY}` },});if (res.status === 404) return { error: "fund_not_found", isin };if (res.status === 429) return { error: "rate_limited", resetAt: res.headers.get("X-RateLimit-Reset") };if (!res.ok) return { error: `http_${res.status}` };const { name, data, generatedAt } = await res.json();return {isin,name,dataAsOf: data.dataAsOf,generatedAt,objective: data.investmentObjective,riskRating: data.riskRating,profile: data.profile,keyFacts: data.keyFacts,headlineMetrics: data.headlineMetrics,topHoldings: (data.topHoldings ?? []).slice(0, 10),sector: data.sector,geography: data.geography,cumulativePerformance: data.cumulativePerformance,};},};
Pair the tool with a system prompt along these lines: answer only from get_fund output; always state dataAsOf; never recommend buying, selling or holding; if the tool returns an error, say so rather than guessing. Returning errors as data instead of throwing lets the model explain the situation to the user.
Design notes
Guardrails. Three layers work well. First, the system prompt rules above. Second, an output check that blocks phrases such as "you should buy" or "this fund is better". Third, a footer appended by your code rather than the model: "Facts read from documents published by the fund house, as of {dataAsOf}. Not investment advice."
Payload size. The full data object can be several kilobytes. Trim what the model sees, ten holdings rather than fifty and no indexed NAV series unless the user asks for a chart, to keep context small and answers focused.
Caching. The API refreshes every 24 hours; cache tool results by ISIN until expiresAt so a conversation that mentions the same fund five times costs one request. A cold ISIN can take one to three minutes on first load, so tell the user the fund is being loaded rather than timing out silently.
Quota. Tool calls are user-driven and bursty. Read X-RateLimit-Remaining and, when low, have the tool return rate_limited so the model can say the service is busy. Do not let a chat loop retry on its own; a 502 upstream_error should likewise be reported once, not retried in a loop.
The AI companion kit. Pro and Scale plans include an AGENTS.md, a Cursor rule, a Claude Code skill, the OpenAPI spec and TypeScript types. Drop them into a repository and a coding agent can scaffold this assistant, from tool definition and types to caching and the disclaimer footer, from a single prompt. The walkthrough is in build a fund app with an AI coding agent.
Disclaimers. The assistant explains facts; it does not advise. Keep "not investment advice" in the UI, not only in the model's output, and show dataAsOf for every fund mentioned. For the identifier the tool expects, see ISIN; for the data fields, fund data from an ISIN.