Blog
AIMCPFlutterFirebase

Why the AI in Money Chat AI only writes slash commands

In Money Chat AI the model never touches the database. It replies with a slash command, and the app checks it like anything you type yourself.

I made Money Chat AI in July 2025. It's a finance tracker where you just chat with it. You can type something like "please add expense burger 10000", send a photo of a receipt, or ask for a recap of the month. It's a Flutter web app on Firebase, and it uses gpt-4o-mini through a Cloud Function.

There's one design decision in this app that I think is worth writing about, which is how the AI is connected to your data. Actually it's not connected to it at all. The AI can only reply with text, and the app decides what to do with that text.

Slash commands

The app has a few slash commands that you can type yourself:

/addex 10000 burger food
/addin 150000 freelance
/edit ex000001 amount 30000
/del ex000001
/recap 01 2025

When you type a command directly, the app parses it and runs it. No AI involved, no API call.

When you type a normal sentence, the app sends it to the model together with a system prompt. The prompt lists all the commands with their arguments, the categories you have, and a list of examples like this:

- User: "please add expense burger 10000"
  GPT: /addex 10000 burger food

So the model's main job is translation, from your sentence into one command. Then the app takes the model's reply and runs it through the same parser it uses for the commands you type:

final result = CommandParser.parseCommand(res.content ?? '');

The model never writes to Firestore and it never gets a function that can. If what it returns isn't a valid command, nothing is saved. If it's a normal answer, like a money tip or an explanation of a feature, it's just shown as a chat message.

The parser

For /addex the parser checks that there are at least four parts, that the amount is a number, and takes the last word as the category id. Everything in between is the description.

final amount = double.tryParse(parts[1]);
if (amount == null) {
  return CommandResult(
    type: CommandType.invalid,
    error: '❌ Invalid amount: ${parts[1]}',
  );
}

final description = parts.sublist(2, parts.length - 1).join(' ');
final categoryId = parts.last;

It's very simple code. The model can still misunderstand you, for example take the wrong number from a long sentence. But whatever it produces has to pass the same checks as a command you typed by hand. The worst case is a bad command and an error message, not a model doing something with your data that no command allows.

This matters more for receipts. The text from a receipt photo goes into the prompt, and a receipt is text written by someone else. Keeping the model limited to "suggest a command" means there's not much a weird receipt can make it do.

Receipts use two calls

Reading a receipt is split into two requests.

The first one only gets the photo. The prompt asks the model to decide if it's actually a receipt, and if it's not, or it's too blurry to read, to answer with exactly invalid receipt. If it is readable, it returns only the raw text.

The second request gets that text plus your category list, and has to return JSON in a fixed shape: merchant, date, discount, amount, categoryId and the items with name, qty and price.

I split it like this so a bad photo fails early with a clear message. When one prompt does everything at once and it goes wrong, you get JSON that looks right but has a total from nowhere, and it's hard to tell if the model misread the photo or misunderstood the text.

The Cloud Function

All requests go through one Cloud Function, so the OpenAI key is on the server and not in the web app. The function also counts how many requests each user made today. The limit is a number in Firestore, in the config/gpt document, called userMaxRequests. The counter resets when the date changes:

const currentDailyRequests = lastRequestDate === todayDateString ? dailyRequestCount : 0;

The prompts are in Firestore too, in the config collection, and the app loads them from there. So when a prompt needs fixing I edit a document, no new deploy needed. Sample versions of all three prompts are in the repo under prompts/.

What about MCP

People ask about MCP a lot now, so to be clear, Money Chat AI doesn't use MCP. But the idea behind the commands is close to it. With MCP, an app exposes tools with typed inputs, the AI calls them, and the app still decides what's allowed. The slash commands are basically that, just written as plain text:

Command As an MCP tool
/addex add_expense(amount, description, category)
/addin add_income(amount, description)
/edit edit_transaction(id, field, value)
/del delete_transaction(id)
/recap monthly_recap(month, year)

If I add an MCP server to it someday, the commands map almost one to one, and the checks that the parser does now would run on the server. Then the chat inside the app wouldn't be the only way in. Any assistant that supports MCP could add an expense or ask for a recap.

Code

Money Chat AI is open source under MIT on GitHub. The README explains the Firebase setup and the config collection. You can also try it at money-chat-ai.web.app.