How to Build a Claude-Powered Personal Finance Assistant
What you're actually building
A Claude-powered personal finance assistant is a chat or voice interface that lets a user ask questions like "how much did I spend on subscriptions last month?" or "can I afford a €1,200 trip in June?" and get an answer grounded in their real transaction data — not a generic budgeting tip. The hard part isn't the LLM call. It's wiring Claude to live financial data through tool use, keeping that data secure, and formatting numeric answers so they're trustworthy rather than hallucinated.
This guide covers the architecture end to end: data sources, system prompt design, tool calling for transaction lookups, and how to ship the API layer without building your own billing and key management infrastructure from scratch.
Core architecture
A working assistant has four layers:
- Data layer — a read-only connection to transaction data, typically via an aggregator (Plaid, TrueLayer, GoCardless, or your own bank-sync pipeline) that normalizes transactions into categories, merchants, dates, and amounts.
- Tool layer — functions Claude can call to query that data:
get_transactions,get_balance,get_category_spend,get_recurring_charges. - Reasoning layer — Claude itself, given a system prompt that constrains it to answer only from tool results, never to invent numbers.
- API layer — the endpoint your app actually talks to, handling auth, streaming, and usage tracking.
The key architectural decision: Claude should never see raw bank credentials or do arithmetic from memory. It should call a tool, get structured JSON back, and reason over that. This keeps numbers accurate and keeps sensitive credentials out of the model context entirely.
Designing the tools
Keep tool schemas narrow and typed. A finance assistant needs fewer, more precise tools rather than one giant "query database" function — this reduces the chance Claude picks the wrong tool or misinterprets a date range.
{
"name": "get_category_spend",
"description": "Returns total spend per category for a given date range.",
"input_schema": {
"type": "object",
"properties": {
"start_date": { "type": "string", "format": "date" },
"end_date": { "type": "string", "format": "date" },
"category": { "type": "string", "description": "Optional category filter" }
},
"required": ["start_date", "end_date"]
}
}
On the backend, this function hits your transactions database (already synced from the aggregator) and returns clean totals. Claude never touches the database directly — it only sees the function's output.
For a full walkthrough of defining and handling tools, see the tool use tutorial.
System prompt: constrain, don't just instruct
Finance is one domain where vague system prompts cause real damage — a wrong balance estimate can lead to an overdraft. Be explicit:
You are a personal finance assistant. You only answer using data
returned by tool calls. Never estimate or round numbers without
calling a tool first. If a tool call fails or returns no data,
say so explicitly instead of guessing. Always state the date range
you used. Flag any transaction over €500 as "large" in your summary.
Test this with edge cases: ask about a date range with no transactions, ask about a category that doesn't exist, ask for a prediction. The assistant should decline to fabricate and instead ask a clarifying question or call the right tool.
Handling the conversation flow
A typical turn looks like this:
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
system: SYSTEM_PROMPT,
tools: [getCategorySpendTool, getTransactionsTool, getBalanceTool],
messages: [
{ role: "user", content: "How much did I spend on dining out in May?" }
]
})
});
Claude responds with a tool_use block requesting get_category_spend with category: "dining" and the May date range. Your backend executes the query, returns the result as a tool_result, and Claude produces the final natural-language answer. The Messages API reference covers the exact request/response shapes if you're implementing this from scratch.
For chat UIs, stream the final response so the answer appears token by token instead of after a multi-second delay — this matters more in finance apps than you'd think, since users are often checking balances on mobile with poor connectivity. The streaming guide covers the SSE event format.
Privacy and security are not optional here
A few non-negotiables when building this:
- Never pass full account numbers or raw credentials into the model context. Pass only what's needed to answer the question — categorized, anonymized transaction summaries are usually enough.
- Log tool calls, not raw financial data, in your analytics. You want to debug "did the assistant call the right function," not store a copy of someone's bank statement in your logs.
- Rate-limit per user, not just per API key, so one account can't flood your backend with queries that each trigger a database read.
- Encrypt data at rest in your transactions store regardless of what the aggregator already does — assume you'll be asked about this in a security review.
Where SubToAPI fits
Once the tool-calling logic works, the remaining work is infrastructure: issuing scoped API keys per environment, tracking token usage per customer if you're billing for the assistant as a feature, and giving your team visibility into what's actually being called. SubToAPI wraps Claude access into a standard HTTPS API with sub_live_ keys, streaming, tool use, and usage metadata built in, so you're not maintaining auth and billing plumbing alongside the actual product. Plans start with a free trial at signup, and pricing scales from Solo to Team to Scale seats — see pricing for details. If you're starting from zero, the quickstart gets a working request running in a few minutes.
Testing before you ship
Run a fixed set of transactions through the assistant and verify:
- Totals match a manual sum for at least three different date ranges.
- The assistant refuses to answer when asked about future spending without being told it's an estimate.
- Category edge cases (uncategorized transactions, refunds, transfers between own accounts) don't get double-counted.
- Currency formatting matches the user's locale, not the model's default.
Treat this like testing a calculator, not a chatbot — correctness matters more than personality here.
Questions
Does Claude need direct access to my bank data? No. Claude should only interact with financial data through tool calls that return structured results from your own backend. It should never receive raw credentials or query a database directly.
How do I prevent Claude from hallucinating financial numbers? Force every numeric answer through a tool call in the system prompt, and explicitly instruct the model to decline rather than guess when a tool returns no data or fails.
What's the fastest way to add Claude access without building my own API infrastructure? Use a service like SubToAPI, which exposes Claude through a standard API with keys, streaming, and usage tracking already built, so you focus on the finance logic instead of auth and billing.