Skip to content
[ fig. my-expenses-bot ]

My expenses bot: set it up once, stop logging purchases

I used to think tracking expenses required a habit. Pay for something, remember to open an app, enter the amount, choose a category, repeat. The problem was not that I lacked a sufficiently beautiful spreadsheet. The problem was that every purchase created another tiny task, and the system only worked if I kept doing those tasks.

Now a card payment sends a bank text to my iPhone. A Shortcut forwards it to my server. A small program turns it into an expense in Notion, with a category and a month attached. I did the setup once. When I want to know where the money went, I open the tracker or ask Hermes, my assistant, to read it. There is no daily expense-entry ritual.

That is the important distinction: I still review my spending. I no longer have to record it. A system that captures purchases without asking for my attention is a system I might actually use.

  • An illustration made for this piece: a phone under a bank message bubble, with one ink line running out of the phone and away to the right.

First, give the phone a job

The bank already does the first part. Each card transaction produces a message with an amount, usually a date, and a merchant. The message is not a neat database row, but it contains enough information to make one. My iPhone is the only device that sees the incoming SMS, so the automation has to start there. A server cannot react to a text it never receives.

In the Shortcuts app, I created a personal automation with the Message trigger. I selected the bank as the sender and used Message Contains to narrow it to transaction alerts rather than every bank message. I set the automation to run immediately, without asking for confirmation. The action is Get Contents of URL: POST to my server's `/expense` endpoint, send a JSON body whose `text` field is the received message, and include an authorization token in the request header. Apple's Shortcuts guide documents the sender and message-contains filters; its API request guide covers the POST action and JSON body.

That is the only part I need the phone to do. It does not need to know my Notion database, decide whether a merchant is groceries or transport, or wait for a web search. It passes along the original text. I tested the first real transaction by checking the new Notion row, not by trusting the Shortcut's success banner.

[ fig. The whole path as one chain: a bank message, the phone that hands it on, the server that reads it, and one row that arrives at the other end. ]

What Hermes actually orchestrated

Hermes helped me put the pieces together: the Shortcut's handoff, the Python endpoint on a server that stays on, the connection to Notion, and the rules that turn unfamiliar bank prose into structured data. When a merchant landed in the wrong bucket, I could say what it should be; Hermes could update the rule for future purchases and correct the existing rows. When I want a spending answer, Hermes can query the live Notion data rather than rely on what I remember entering.

There is an important limit to that AI story. The bot does not call a language model for every purchase. Its categorization is automatic, but the running path uses merchant overrides and keyword rules, followed by a web lookup for an unknown merchant. The AI was useful in designing and maintaining the system, applying corrections, and answering questions over the resulting records. Pretending a model guesses every category would make the story sound more fashionable and the actual system less intelligible.

I prefer this division. A repeat merchant should not cost a model call every time I buy lunch. A rule is cheap and repeatable. If it gets Careem Food wrong, I can fix the specific rule; it will not invent a different answer next Tuesday.

The server does the boring part

The Shortcut sends a JSON request to `POST /expense`. The endpoint requires a bearer token; a request without it is rejected. `expense_bot.py` runs on the server under `expense-bot.service`, with systemd set to restart it if the process exits. I keep the token out of this article for the obvious reason. The endpoint is reachable from my phone, so anyone reproducing this needs to think about transport security as well as authentication. My current direct HTTP setup is a personal implementation, not a security template to copy unchanged; I would put TLS in front of it before recommending it to somebody else.

The server answers the phone immediately, then handles the expense in the background. iOS Shortcuts will not wait indefinitely for a merchant lookup and a Notion round trip. The quick response prevents a timeout, but it has a cost: “processing” does not prove the row was saved. A failed Notion write is visible in the server log, not on the iPhone. I check the tracker and the service when something looks missing. This is not a perfect zero-maintenance appliance.

The parser extracts the first positive two-decimal amount, reads a date if the SMS contains one, and takes the merchant after the bank's `at` wording. If a date is absent, it falls back to the server's current UTC date. If the bank changes its message format, those assumptions need another look. An amount in a bank message is not magically unambiguous just because a regular expression found it.

The category comes next. Specific exceptions run before broad rules: Careem Quik and Noon Minutes are groceries, Careem Food is dining out, Careem Plus is a subscription. Then familiar merchant and category keywords handle most of the rest. For an unknown merchant, the program searches the business name with Dubai context and looks for clues such as restaurant, supermarket, pharmacy or petrol station. If it still cannot tell, it uses a fallback category rather than stopping the capture. This is automated categorization, not infallible categorization. The real win is that I do not have to choose a category for every transaction myself.

[ fig. A rule table with one entry wrong in it, and the repair is one line: a mistake that repeats identically instead of changing shape. ]

Notion becomes the record, not the input form

The bot creates a row in my Expenses database with the merchant and amount in the title, an AED amount as a number, a transaction date, and a time field. It links the row to a page in Budget for the category and to a page in Month Classification for its month. The Budget relation is set when the row is created; the month relation is added immediately afterwards. Those links are what turn isolated messages into a tracker I can actually read.

This required one-time Notion setup: an Expenses database with those fields, Budget pages for the categories I use, Month Classification pages, and an integration with access to the relevant databases. The bot needs a valid Notion token. The month page also needs to exist; if it does not, the bot logs a warning and leaves that relation unset. A missing category match has a fallback, but a missing month page does not create itself.

In Notion, the Budget pages have a monthly budget and a rollup of this month's linked expenses, plus a utilization formula. The Month Classification pages roll up expenses by month and sit alongside income and monthly net fields. So the reporting is not a separate export or a Sunday session of copying figures into another sheet. I can inspect a category against its budget, open a month, or ask Hermes a question about recent spending using the records already there. I still have to decide whether a purchase was worth it. That is the part I do not want automated.

What “set it up once” really means

It means the entry work disappears. My bank texts, my phone forwards, the server parses and categorizes, and Notion stores the result without me touching an expense form. It does not mean never checking whether the automation is alive. A phone without network access, a revoked Notion token, a changed SMS format, or a wrong merchant rule can break the chain. I look at the tracker when I need an answer and occasionally compare it against what actually happened, especially after changing a rule.

  • An illustration made for this piece: a magnifying glass resting on a ruled page, the one habit that stays.

The promise of AI here is not that it knows my finances better than I do. It is that it helped build a small, maintainable bridge between the data I already receive and the questions I want to ask later. The category is assigned without another tap. The amount is saved without another app opening. My attention goes to reviewing the spend, not transcribing it.

That is enough automation for me: set up the path once, let the routine work happen, and keep the judgement.