Tiền: The Robot That Reads My Bank's Email So I Don't Have To
Banks won't give me an API, but they can't stop sending email. So a Cloudflare Worker parses every transaction alert, pings Telegram with category buttons, and routes the results to the rest of my money stack.
The API My Banks Won't Ship
Every expense-tracking system eventually collides with the same wall: the data you need lives at the bank, and consumer banks in Vietnam (and mostly everywhere) do not hand out transaction APIs to individuals.
But they do send an email for every single transaction. Instantly, reliably, with the amount, merchant, and card right there in the body. That's an API — it's just wearing an email costume.
Tiền (money) is the Cloudflare Worker that takes the costume off.
The Pipeline
Gmail bank alerts
→ Apps Script forwarder
→ Tiền Worker (parse + dedupe, D1)
→ Telegram (category buttons)
→ routing rules → Chia / Xu (service bindings)
A small Google Apps Script watches Gmail and forwards matching bank notifications to the Worker. Tiền parses, stores in D1, posts an interactive message to Telegram, and then routes the transaction onward according to rules.
Unglamorous detail that carries the whole system: deduplication keys on the Gmail message ID. Early versions tried matching on amount + timestamp to detect re-forwarded duplicates — a fool's errand of float-serial and timezone alignment bugs. The Gmail message_id is globally unique and free. Zero arithmetic, zero duplicates since.
Parsers: the Unreasonable 80%
Each bank formats its alerts differently, so Tiền ships per-bank parsers: VIB (card charges, QR payments, card payment confirmations, batch bills, outgoing transfers), HSBC (card charges, retail notifications), VPBank (balance movements). Plus one fun outlier: a Shopee receipt parser that extracts the actual line items from order confirmations — so the ledger says what I bought, not just "Shopee: 340,000₫."
Writing parsers for bank email is exactly as tedious as it sounds, and it's also where all the value is. HTML tables inside HTML tables, inconsistent number formatting, marketing footers — every parser is a small archaeology project. This is the moat nobody wants to dig.
Telegram as the Review Queue
Each parsed transaction lands in my Telegram chat as a message with inline keyboard buttons for category — Food, Bills, Shopping — one tap to classify. Renaming is even lazier: reply to the alert message with new text and Tiền updates the description.
This is the baby-tracker lesson applied with full force. I never log an expense; I react to one, in an app I already had open, with one thumb. Categorizing the day's spending takes seconds and, weirdly, is almost satisfying — inbox-zero for money.
The Routing Engine
The newest layer makes Tiền the hub of the whole money stack. A forwarding-rules engine evaluates each transaction against configurable matchers (recipient, bank, subject, sender) and pushes matches onward:
- Transfers to/from the household? → Chia receives it as a pending suggestion in its one-click inbox.
- Everything personal? → Xu logs it straight into the ledger.
Inter-Worker calls use Cloudflare Service Bindings rather than HTTP — Workers calling their own public hostnames is a classic Error 1042 loopback trap, and bindings sidestep it while staying entirely on-platform. A small React admin UI manages the rules and lets me audit what got routed where.
The System That Emerged
None of this was planned as a "stack." Chia came first (splitting bills), Xu second (personal ledger), and Tiền grew out of annoyance at feeding them both by hand. Now the shape is clear:
Tiền captures. Chia splits. Xu reports. And the input method for my entire financial life is: receive Telegram message, tap button.
The banks still won't give me an API. At this point, I'd like to formally thank them for the email instead.