[08]
BANKING TRACKER
█ TIER A · YEAR 2026 · STATUS: LIVE · LANGUAGES: TYPESCRIPT
Regulated banking API integration, receipt OCR and self-learning rules
[FIG. 1] MISSION
I integrate regulated banking APIs (Open Banking, PSD2, Enable Banking) in read-only mode and build on them a product that actually runs household finances: transactions and balances from the bank (Pekao) flow in on their own, and new entries land in an inbox to be 'signed' with one tap. I design categorization as a human → rules → model escalation path: every manual correction of mine creates a rule, so the AI takes over only once it has learned from my decisions, and leaves me the exceptions. I split receipt photos with a vision-model OCR: the photo lands in a private Blob and the model breaks it into priced line items against a closed list of 13 product groups. I match the model to the task by cost, not prestige, and can justify that call — across thousands of receipts the per-call price decides whether the feature makes sense at all. I deliver it as a PWA installed on the phone bypassing the app store, with authentication through my own PIN plus JWT (I deliberately swapped out an already-wired Clerk — for a single-user app a custom solution is simpler and safer to maintain). Bank tokens are AES-256-GCM encrypted and never sit in plaintext, and on top sit budgets, goals, subscriptions and settling up with people. It runs in production, verified end-to-end on ~478 real transactions; underneath: Next.js 16, Drizzle + Neon Postgres, 20 tables and 40 Server Actions.
[FIG. 2] ARCHITECTURE
hover a block to see its description
[FIG. 3] CHALLENGES
[+][CH-01]
The Enable Banking documentation lied three times, and each lie was only exposed by the live bank. First: the API takes lowercase snake_case (valid_until, redirect_url) where the docs suggested camelCase — and the /auth response returns a url field, not the documented authorizationUri, so the client reads all variants defensively. Second: with continuation_key pagination the bank returns 422 unless you repeat the SAME date_from parameters — the first version sent the parameters alternately and subsequent pages failed. Third: Pekao rejects a request for 90 days of history with a 400 — solved with adaptive descent through 90/60/30/14/7 days until the bank accepts the range. The regulatory cherry on top: PSD2 lets banks cap unattended data pulls at 4 per day, which auto-sync exhausted by morning — passing the user's real IP and User-Agent as PSU headers legally reclassifies the request as customer-initiated and lifts the limit, with the IP regex-validated before it ever reaches the bank.
[+][CH-02]
The first PIN login had a 'lockout after 4 attempts' and looked sensible — until I ran a formal security audit on my own code (20 findings across four severity levels). The audit exposed two serious defects at once: the attempt counter was non-atomic — SELECT, +1 in JavaScript, UPDATE — so N parallel requests read the same state and wrote back 1, letting a race bypass the lockout and brute-force the whole 10,000-PIN space in batches; and the 'correct' permanent lockout turned out worse than the attack, because anyone sending 4 wrong PINs turns the app into a brick requiring a redeploy (self-DoS). The final model: the counter incremented atomically in the database, constant-time PIN comparison (timingSafeEqual), and a growing 5 s → 5 min backoff instead of a lockout — an attacker waits exponentially, the owner briefly after a typo. Every audit finding has an id cited in the fixes, so the finding→fix trail is fully auditable.
[+][CH-03]
Automatic categorization from day one would have been either dumb or expensive — instead I designed a 'human → rules → AI' path. At first I sign transactions by hand, but every correction creates a rule ('Żabka → Daily'), so known shops quickly clear themselves; AI enters last and signs using the learned rules, with only exceptions reaching me. The system earns trust gradually — at exactly the pace it deserves it.
[+][CH-04]
An account balance lies: it ignores the rent leaving next week, unpaid subscriptions, and money earmarked for goals. I split money into three layers — real accounts (bank balances), budget envelopes (spending control) and goals/reserves (untouchable) — and 'truly available' is balances minus obligations and earmarked funds. The one number I can trust at the checkout turned out to be the hardest data model in the whole app.
[FIG. 4] AI LAYER
AI goes where it hurts: Claude Haiku 4.5 (vision, via Vercel AI Gateway) reads receipt photos into priced line items, and transaction categorization moves along a 'human → rules → AI' path — the model takes over sign-off using rules learned from my corrections. Haiku is a deliberate choice: receipt OCR is a simple, frequent task, so per-call cost matters more than maximum model intelligence.
[FIG. 2A] RECEIPT → LINE ITEMS
| MILK 2% | 4.99 |
| RYE BREAD | 7.50 |
| BUTTER | 8.99 |
| APPLES 1KG | 5.49 |
RECEIPT AWAITS SCAN
- ŻABKA −23.50░
- ORLEN −180.00░
- NETFLIX −43.00░
→ you sign every transaction by hand — the inbox keeps growing