DEV Community

Oluchi Okwuosa
Oluchi Okwuosa

Posted on

Adding an AI Chat Feature to My Expense Manager

I already had a working expense tracker built with React, Express and SQLite. For this task I had to pick an AI feature that actually made sense for it, not just bolt something on for the sake of it. So I went with a chat feature where you can ask questions about your own spending in plain English, like "how much did I spend on food this month" or "what's my biggest category", and it answers based on your real data.

Why this feature

The app already had a summary section showing totals and category breakdowns, but that only tells you so much. If you want anything more specific, like comparing two categories or checking a particular time period, you'd have to scroll through the list and do the math yourself. A chat feature solves that. You just ask the question and get an answer. It felt like the most natural extension of data I already had sitting in the database, and it's also the feature that actually forced me to think hard about prompts and streaming instead of just calling an API once and printing the result.

I did consider two other options first. Auto categorizing expenses as you type felt too small, more of a classifier than an AI feature. A one shot spending summary was decent but static, you'd generate it once and that's it. Chat felt like the one that actually used the AI SDK the way it's meant to be used.

Technical decisions

Provider and model. I used Google AI Studio for the API key since it's free and doesn't need a card. I started with gemini-2.5-flash because that's what most tutorials use, but it turned out that model isn't available to new API keys anymore, so I switched to gemini-flash-latest instead, which just points at whatever Google's current default flash model is. At one point I got hit with a 503 "high demand" error when testing, which was annoying since it made me think something was broken on my end. I left the model as is and tried again after a few hours and it just worked, so it really was Google's servers being temporarily overloaded like the error message said, not a bug in my code.

Streaming. This one was an easy call. It's a chat interface, so it streams. Waiting for the whole response to load before showing anything would feel broken compared to literally every other chat app people use. If this had been a one shot "summarize my month" button instead of a back and forth chat, I probably wouldn't have bothered with streaming.

Messages and prompt structure. On the frontend, useChat keeps track of the conversation and sends the full message history to my backend route on every new message. On the backend, before calling the model, I query the expenses table fresh every single time, pull the total, the category breakdown, and the most recent expenses, and drop all of that into the system instructions along with a rule that says only answer from this data, don't make numbers up. So the model isn't relying on memory of a past conversation for the actual numbers, it's getting current data injected right before it answers.

How I approached the integration

I started by getting the API key, installing the ai and @ai-sdk/google packages, and writing the backend route first before touching any frontend code. My plan was to test it with curl before building any UI on top of it, which turned out to be a really good decision because I hit four separate bugs before it worked at all.

First one, I had named the environment variable wrong in my .env file. I used something like APIKEY instead of the exact name the SDK expects, GOOGLE_GENERATIVE_AI_API_KEY, so the provider just couldn't find a key at all.

Second one, I was passing convertToModelMessages(messages) straight into streamText without awaiting it. Turns out that function became async in this version of the SDK, so I was actually passing a Promise object into the messages field instead of an array, which threw a weird error about messages.some not being a function. Took a bit of digging through the stack trace to realize the fix was just adding await.

Third one, that model version thing I mentioned above, gemini-2.5-flash returning a 404 saying it's no longer available to new users.

Fourth one, after switching models I got hit with a 503 saying the model was experiencing high demand. I left the code as is and tried again a few hours later and it worked fine, so that one really was just Google's servers being temporarily overloaded, not something I needed to fix on my end.

Worth mentioning too, once I actually had the app deployed and was testing it live, I hit a different error from the 503, a 429 saying I'd exceeded my quota, specifically the free tier's limit of 20 requests per minute. That one wasn't Google's servers being overloaded, it was me sending too many test questions back to back while checking that everything worked. Different failure mode, same fix though, just wait a bit and it clears itself.

Once the backend was actually working, wiring up the frontend with useChat was pretty smooth in comparison. I also ran into a proxy issue where the dev server couldn't reach my Express server since I hadn't set one up yet, so I added a proxy in vite.config.js pointing /api requests at localhost:5000.

One more thing I noticed while testing, I asked "how much have I spent on food this month" and it gave me a number, but all my actual expenses were logged in June, not July. Turns out the model had no idea what today's date even was, so "this month" meant nothing to it and it just summed everything. I fixed this by explicitly passing today's date into the system instructions and telling it to filter by date before answering, which helped a lot but wasn't a perfect fix. Asking "how much have I spent this month" after that gave me an answer that mentioned June instead of clearly saying there's nothing for July, so there's still some inconsistency there.

What I'd do differently

The biggest one, I currently send raw expense rows into the prompt and let the model do date filtering and math over plain text. It mostly works but it's not reliable, as I found out with the date filtering bug above. A better approach would be doing that filtering in SQL or JavaScript before it ever reaches the model, so the model is just explaining numbers I already calculated correctly instead of being trusted to calculate them itself.

I'd also want to look into giving the model actual tools it can call to query the database directly instead of getting a static dump of everything on every message. Right now it gets the same big block of context whether the question needs it or not, which is wasteful and will get worse as the expense table grows.

Top comments (0)