Most personal-finance trackers make you choose between two bad options: a spreadsheet that remembers nothing, or a bank-linked app that wants read access to every account you own. AQLedger takes a third path — you type what happened in plain language, and it learns.
Every transaction gets a category, sub-category, merchant, and person/project tag suggested automatically. The first time you buy something, it asks an LLM. The next ninety-nine times, it already knows — instantly, for free — because it remembers what it decided last time.
The cascade
The categorization pipeline is intentionally boring in the best way: cheapest, fastest option first, falling back only when it has to.
That last step is the entire trick. Confirming a transaction —
whether you accepted the AI's guess or corrected it — writes back
to a description_patterns table keyed on the
normalized text. It's a cache in the literal, computer-science
sense, and it's fully rebuildable from the transaction history at
any time, so it's never a second source of truth.
Across roughly 4,300 backfilled transactions, the historical-lookup cache now resolves the large majority of new entries without touching the LLM at all — the self-learning loop compounds the longer you use it.
What it costs to run
The entire backend runs on free or near-free tiers, deliberately. Azure SQL, Key Vault, Vercel, and Cloudflare all sit at ₹0/month on their free allowances. The only real line item is Azure OpenAI, and even that stays negligible because the cascade above means most entries never reach it.
Staying on free tiers isn't free of consequences, though — it
means living with their limits. The database is Azure SQL
Serverless on the Free Database Offer: 100,000
vCore-seconds a month, auto-pausing hard once that's gone. During
one heavy dev stretch, that quota ran out mid-month and login
just hung — no clean error, just a spinner. The fix wasn't
guessing; it was pulling the actual
free_amount_remaining metric from Azure Monitor and
watching it hit zero at the exact second the outage started.
Confirmed, not assumed.
Three bugs, one deploy
The most instructive part of building this wasn't a feature — it
was migrating the database driver after discovering, on a live
deploy, that Vercel's Python serverless runtime has no ODBC
driver manager installed and no way to add one.
pyodbc was dead on arrival.
The pure-Python replacement, python-tds, doesn't
support the Entra ID auth the project had been using either — so
the driver swap also meant standing up a dedicated SQL-native
login. And that was just the first layer:
python-tds needs a separate SQLAlchemy dialect plugin to resolve mssql+pytds:// at all — and because the engine is built at module import time, the missing dialect took down the entire serverless function on every cold start, not just DB routes.
Unlike pyodbc, python-tds doesn't encrypt the connection unless told to — and Azure SQL simply refuses unencrypted connections.
Turning TLS on exposed a host-validation code path relying on a pyOpenSSL API that newer pyOpenSSL releases had already removed — fixed only by pinning to a specific older version.
None of the three were visible from a clean pip
install. Each only surfaced once the previous one was
fixed and a real connection to Azure SQL was actually attempted —
the kind of debugging that only heavy manual verification, not
intuition, gets you through.
Try it — no account needed
A five-minute demo session, fully isolated from real data
Recruiters and reviewers shouldn't have to trust a screenshot, but they also shouldn't be handed a login to real financial records. So the newest feature is a one-click, no-password "Try Demo" session:
- Writes go to a structurally separate set of tables — not a filtered view of real data, an entirely distinct table pair — so there is no query to get wrong and no row to leak.
- Every entry is classified live by the same LLM cascade above; the demo just never reads or writes the real learning cache.
- Hard 5-minute expiry, enforced from both ends: the session itself times out, and its data is deleted proactively the moment it ends — not on some later cleanup cycle.
The interesting engineering problem here wasn't the demo login itself — it was cleanup. The obvious answer, a scheduled cron job sweeping expired sessions, doesn't exist on Vercel's free tier at anything finer than a daily cadence, which is useless against a 5-minute window. The actual fix: the client calls an end-session endpoint just before its own countdown hits zero, and that one endpoint deliberately accepts an already-expired token — since its entire job is cleaning up a session that, by definition, just expired.
Why it's built this way
A few decisions that shaped the project, and would again:
- Free-text tags, not lookup tables. Category, merchant, and project are plain strings, not foreign keys into a rigid taxonomy — cheap to keep flexible, with a generalized bulk-rename tool doing the job a strict schema would otherwise force.
- Structured outputs over loose JSON. An early backfill using loose
json_objectmode produced inconsistent values —"Debit","debit","DEBIT"as three different answers to the same question. Strict JSON-schema output fixed it permanently, not case-by-case. - Real financial rules, enforced in code. Type a negative amount — a bank-statement habit for "money coming back" — and the system reads that as intent: it sets the type to Refund, the cash-flow to Inflow, and stores the amount as positive. Every stored amount is non-negative; direction always comes from that explicit cash-flow field, never inferred from a sign. Small rules, but they're what makes totals addable across the whole table without special-casing, and what makes the numbers trustworthy enough to build a dashboard on top of.