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.

Step 1 Historical Lookup Exact match against every description you've entered before. Free, instant, zero API calls.
Step 2 LLM classify Azure OpenAI, structured JSON-schema output, only on a genuine cache miss.
Step 3 You confirm Edit or accept. Either way, the cache learns — that's the whole loop.

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.

Real number

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.

Azure SQL · Key Vault · Vercel · Cloudflare₹0 / mo
Azure OpenAI (GPT-5-mini), live entriesa few paisa / entry (well under a cent)
One-time backfill, ~4,300 rows≈ ₹250 (~$3)
Ongoing, steady state₹10–30 / mo (~$0.15–0.35)

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:

Bug 1
Silent import-time crash

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.

Bug 2
TLS off by default

Unlike pyodbc, python-tds doesn't encrypt the connection unless told to — and Azure SQL simply refuses unencrypted connections.

Bug 3
A dependency of a dependency

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

Latest addition

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.
Try the live demo

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_object mode 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.
Launch AQLedger Get in touch