Banksteer: A Private Finance App on Free Irish Bank APIs

Oct 17, 2026 | Build Log

Euro coins and a bank card on a dark desk with glowing data streams flowing into a small home server

For years our household money lived in one Google Sheet: a tab per year going back to 2010 and a “Recurring” tab that planned the months ahead. It worked, in the way a spreadsheet works. Nothing in it ever cleared itself, there was no running balance, it never looked at a bank, and splitting shared bills was mental arithmetic. The sheet knew what we planned to pay. It had no idea what actually happened.

So we replaced it with Banksteer, a private finance app that reads AIB, Bank of Ireland and Revolut through free, public open-banking APIs, ticks bills off as they are paid, and forecasts the months ahead. It runs on our own hardware, it is reachable only at home and privately over Tailscale, and it was built with Claude Code in about ten days. This is how it works, how you can get the same free bank access, and what went wrong on the way.

Why leave a spreadsheet that works?

The sheet failed in five specific ways, and each became a requirement:

  • Bills never cleared themselves. Every paid bill meant a manual edit.
  • No running balance or forecast. “Will we be fine at the end of next month?” meant scrolling and adding.
  • Nothing read the banks. The sheet and reality drifted apart within weeks.
  • Shared bills were manual maths. Who owes what for the streaming, the broadband and the electricity was recalculated by hand.
  • Receipts and finance emails were never captured. They lived in inboxes and camera rolls.

The success test was blunt: the sheet stays closed for a full month, at least 80% of bills clear without a manual edit, and the forecast month-end lands within a small fixed margin of the real one.

We also wanted control. Finance apps exist, but they mean handing every transaction to someone else’s cloud. Banksteer keeps the database, the documents and most of the AI work at home.

The free Irish bank APIs, and how to get them

Under the EU’s second Payment Services Directive (PSD2), banks must give licensed providers read access to your accounts, with your consent. You approve the access in your bank’s own app, and the provider gets balances and transactions. No screen-scraping, no sharing your banking password.

For individuals, the practical route in Ireland today is Enable Banking. Its restricted mode is free for personal, non-commercial use: you register an application, link your own accounts, and the API serves data only for the accounts you linked. Their changelog from January 2026 says it plainly: personal usage, sandbox and evaluation “continue to be free of charge”. Paid production plans exist for businesses and are priced per account.

Step What you do
1 Sign in at enablebanking.com/sign-in with your email; a one-time link creates the account.
2 In the Control Panel, register a Production application with a name and the redirect URL(s) your app will use.
3 Let the browser generate the private key, or create an RSA key pair with OpenSSL and upload only the public key.
4 Link your own accounts in the Control Panel. In restricted mode this step is required, or sessions come back with no accounts.
5 Run the authorisation flow from your code (below) and approve it in your bank’s app.

Enable Banking’s Ireland page lists AIB, Bank of Ireland, Permanent TSB and Revolut, each with a redirect login to the bank’s own app. Banksteer runs live AIB, Bank of Ireland and Revolut feeds this way. The docs worth reading first are the quick start, the Control Panel guide, the page on linked accounts and the FAQ.

What about GoCardless (formerly Nordigen)? It used to be the default free option, and many open-source budgeting tools still mention it. As of 13 October 2026 its sign-up page still reads “New signups for Bank Account Data are currently disabled”, and has since July 2025. Existing users keep access. We did not find another provider that gives individuals free production access: the big names (Tink, TrueLayer, Yapily, Plaid) gate production behind sales or contracts.

Two limits to plan around

Consent expires. Each bank sets a maximum consent length, and Enable Banking says it is 180 days for most banks. The EU raised the re-authentication limit for account access from 90 to 180 days with Delegated Regulation (EU) 2022/2360, which has applied since July 2023. We have not confirmed the exact maximum for each Irish bank, so Banksteer asks for the lower of 180 days and whatever the bank reports.

Background fetches are rationed. Enable Banking notes that many banks allow about four data fetches a day when you are not actively logged in. Banksteer budgets exactly four per account.

The architecture

Diagram: Irish banks feed Enable Banking, a sync worker stores data in Postgres, an engine matches, forecasts and projects, AI runs on a local DGX first, and the app is reached only over the LAN or Tailscale
How Banksteer fits together: banks on the left, home hardware in the middle, the phone on the right. Dashed parts are fallbacks or planned.

Banksteer is a TypeScript monorepo (pnpm, Node 22) with three apps and five packages:

Part Built with Job
API Hono 4, zod-openapi, Drizzle ORM, Postgres 16 The OpenAPI spec is the contract, so a future phone app reuses it. Migrations run at start-up.
Worker pg-boss 10 (queues on Postgres, no Redis) Bank sync 4× a day, nightly auto-clear and categorising, reminders, weekly insights, document extraction, PDF rendering.
Web Next.js 15, React 19, PWA Phone-first with a bottom tab bar, a wider layout on desktop, installable to the home screen.
engine pure TypeScript Schedules, forecast, projection, insights: plain functions with no I/O.
banking pure TypeScript Matching, payee recognition, transfer pairing, statement parsers, the bank client.
tax pure TypeScript Irish PAYE, USC and PRSI from a versioned table of rates per tax year.
inference TypeScript Which AI model reads which document, in which order, with which prompt.
shared zod Types shared by everything.

Three conventions remove whole classes of bugs. Money is stored in integer cents, never floats. Every date is in Europe/Dublin time. Every schedule is an RRULE, the same recurrence format calendars use. Other currencies convert to euro at ECB daily rates, and every automatic action writes an audit row with its evidence and can be undone.

Bills as schedules, payments as occurrences

The heart of Banksteer is the plan item: a name, amount, direction, account, category, schedule, clear mode, split rule and dates. Each item generates occurrences, one per due date, keyed by the item and its original date so a moved payment keeps a stable identity.

Schedules are RFC 5545 RRULEs built with the rrule library. Two cases took real thought:

  • “Monthly on the 31st” has to land on the 30th or the 28th in shorter months. The rule becomes BYMONTHDAY=28..31;BYSETPOS=-1: of the days 28 to 31 that exist this month, take the last.
  • “Last working day” is BYSETPOS=-1 over weekdays, then moved back over Irish public holidays, which the engine computes itself, Easter Monday included.

An occurrence is planned, due or overdue purely from today’s date, and becomes cleared or skipped only through an action, which records how, when and with what evidence. There are three ways to clear one:

Clear mode How it clears Good for
auto-on-date Nightly, on its due day Direct debits that never fail (example: rent on the 1st)
tick You mark it paid Cash, or anything the bank cannot see
bank-match A matching bank transaction clears it Most bills, once the payee is known

Edits ask “this one, this and future, or all”, like a calendar. Budget items (food, fuel) are drawn down by receipts and bank lines. Variable bills such as electricity clear at the real amount within a ±50% band, and the forecast keeps the planned figure until the actual one arrives. Shared bills carry a split rule (fixed amounts, equal shares or fractions such as two thirds and one third), and a PDF statement shows who owes what.

Bank sync, in detail

The Enable Banking client is small and strict:

  • Auth: each request carries an RS256 JWT signed with Node’s built-in crypto, no JWT library. The header kid is the application id; the payload has iss: enablebanking.com and aud: api.enablebanking.com; it lives at most an hour and is cached until 60 seconds before expiry.
  • Consent: GET /aspsps?country=IE lists the banks. POST /auth sends the consent length, a signed state, a whitelisted redirect URL and psu_type: personal. You approve in the bank’s app, and the callback swaps the one-time code at POST /sessions for the session and its accounts. A bank login left unfinished for an hour is marked failed.
  • Data: /accounts/{uid}/balances and /transactions, paged with continuation_key, capped at 50 pages.
  • Retries: a 429 honours Retry-After up to 30 seconds. 5xx and network errors are retried only on idempotent calls, never on POST /sessions, because that would burn the one-time code. Every response is validated with zod.

The sync worker stretches the four daily fetches carefully. The first sync pulls 90 days. Later syncs re-read from the last sync minus ten days, because banks sometimes post transactions late. Scheduled syncs sit five hours apart, manual ones need a three-hour gap, and a restart never triggers a catch-up sync that would spend a slot. Messages warn 14 and 3 days before consent lapses.

Duplicates are removed with a SHA-256 hash of the account and the bank’s transaction id. When a bank gives no id, the fingerprint is the account, date, amount, currency and normalised description, plus a same-day counter so two identical coffees stay two coffees.

Three glowing data streams flowing into a small dark home server
Three banks, one small box at home. Bank data is read-only: nothing in the app can move money.
Gotcha What to do
Restricted mode returns no accounts Link each account in the Control Panel first.
Only about 90 days of history at consent Import older months from CSV or PDF statements.
Some products have no feed Keep them as manual accounts with a “refresh every N days” reminder.
Revolut credit card missing Give consent from inside the Revolut app; it appears only then.
Currency pockets share one IBAN Make the currency part of the account identity, so each pocket links separately.

Credit cards: the gap in open banking

Open banking card feeds carry transactions and balances, and that is all. There is no statement date, due date, statement balance, minimum payment, APR, or interest and fees as such. We confirmed this on the AIB, Bank of Ireland and Revolut card feeds. For anyone who pays a card in full to avoid interest, the due date is the one number that matters, and the API does not have it.

So Banksteer takes a monthly statement upload, a PDF or a screenshot, and reads it in two tiers:

  1. Deterministic parsers first. pdftotext -layout output goes to hand-written layout parsers for the AIB, Revolut and Bank of Ireland statements.
  2. AI only as a fallback. If no layout matches (a scanned PDF, another bank), the inference layer extracts a strict JSON schema: dates, balances, purchases, payments, interest, fees, stamp duty, minimum and limit.
  3. Arithmetic checks. The statement summary is compared with the sum of its printed lines. Confidence is 1 only when they agree.
  4. You confirm before anything is saved.

After a few statements Banksteer learns each card’s statement day, reminds you to upload the next one, and warns 7 and 2 days before “pay in full by”. Any interest or fee on a statement raises an alert. The lesson went straight into the build log: deterministic parsers beat an AI read.

A paper statement being scanned by a fan of orange light into neat glowing rows
Card statements fill the gap open banking leaves: parsers read them, AI is the fallback, you confirm.

Matching a bank line to a bill

When a transaction arrives, a pipeline runs before any scoring: zero-amount lines are ignored, transfers between your own accounts are filed, shop rules for budgets apply, ignore rules apply, and bank fees are filed only by rule or by bank-charge wording, never on an AI guess alone.

Then each candidate pair is scored:

score = 0.6 × payee + 0.25 × amount + 0.15 × date

Setting Default
Date window ±3 days
Amount tolerance the larger of EUR 2 and 5%
Clear automatically score ≥ 0.8
Suggest score ≥ 0.3
Otherwise ask in the inbox

A learned payee alias scores 1, and fuzzy text similarity is capped at 0.6 of that, so only known payees ever clear automatically. Pairs are assigned one-to-one by score. One purchase is one transaction: the receipt photo, the email and the bank line attach to it as evidence, so nothing is counted twice.

Unknown lines go to an inbox that asks once: one-off, monthly, yearly, or split? Each answer teaches an alias. Before asking, Banksteer tries to name the payee itself: your earlier answers first, then the local model, then a web lookup for businesses only. People are never looked up, and only the payee text ever leaves the app.

Transfers between your own accounts

Moving money from one of your accounts to another is neither spending nor income, and a naive app counts it as both. Banksteer pairs a debit and a credit of the same amount on two of your own accounts within three days, but only if the text carries a transfer signal (your own name, top-up or card-repayment wording) or the other side’s IBAN matches one of your own, stored hashed. An amount coincidence alone never pairs: a 20.00 purchase and a 20.00 transfer on the same day (example amounts) are not the same thing.

When we ran this over imported history, a dry run listed about 600 pairs. One of them matched a shop purchase on a credit card, so a card debit without a positive transfer signal now never pairs. The rule we took from it: before applying a bulk change, list every pair where one leg does not look like a transfer.

The AI interview that builds the bill list

Typing in every recurring bill by hand is the step where people give up. Banksteer imports history instead (Revolut CSV, AIB and Bank of Ireland CSV with header detection, AIB statement PDFs), with each import one undoable unit; importing the same file twice adds nothing.

Then it finds the recurring charges itself. It groups by payee pattern, direction and currency, needs at least three charges, and reads the cadence from the median gap between them:

Cadence Median gap
Weekly 5–9 days
Fortnightly 12–17 days
Monthly 25–36 days
Quarterly 80–100 days
Yearly 340–390 days

At least 70% of the gaps must fall in the band, and a charge counts as variable if any of the last six is more than 15% off the median. Then the interview starts: one question per group the plan does not cover yet, the most expensive per year first, each with a proposed item already filled in. You accept it (which also teaches the payee, so the next charge clears itself), say “it’s an existing item”, mark it stopped, or skip it. Free-text answers go to the local model, which returns strict JSON and a one-line “understood” summary, and nothing changes until you confirm. The interview refreshes weekly and after every import, and also asks about bills whose payee has gone quiet.

Forecast and projection: two different questions

The forecast answers “what happens if the plan goes as planned?” It starts from today’s balances, applies every open occurrence day by day, and gives daily and month-end balances, the lowest point and its date, and per month what is “left to pay” and “still coming in”. Scenarios (pause an item, change an amount, add a one-off) are compared with the base line.

The projection answers “what has actually been happening, extended five years?” Per category it uses robust statistics, so one strange month cannot bend the line:

Term Definition
level median of the last 12 months
slope median month-over-month change over the last 48 months
sigma 1.4826 × median absolute deviation over 24 months (a robust standard deviation)
season median month-of-year ratio, used only with 36+ months of data
value (level + slope × t) × season, clamped between 0 and 3 × level

It shows three cases: low (spending one sigma higher), mid, and high (spending one sigma lower). Transfers, savings moves and card repayments are excluded. Two fixes came from real data: projected income now comes only from the plan, because history had counted one-off credits as income; and with less than 49 months of data there is no slope at all, because about a year of bank data had roughly doubled some trends. The note in the log: a trend model tuned on 15 years of spreadsheet data needs re-checking before it reads one year of bank data.

A glowing orange line running through a dark landscape into the distance, with a softer band around it
Forecast and projection: one plays the plan forward, the other extends what has actually been happening.

AI where it helps, and never for the maths

Banksteer uses AI to read things and to phrase things. It never lets AI do arithmetic.

Insights are found by fixed rules over integer-cent statistics: a category running above its rolling four-month average, a price change, a new recurring charge, a subscription that looks unused. The model only rewrites the finding in plain words, with the instruction “never add, change, round or work out a number”. A validator, phrasingOk(), pulls every amount, percentage and multi-digit number out of the model’s text; if a single figure is not in the facts, the phrasing is thrown away and a template sentence is used. Every insight links to the transactions behind it.

Tax works the same way: a deterministic Irish PAYE, USC and PRSI engine with rates in a versioned table per tax year. The AI extracts fields from documents; it never calculates.

The inference layer tries models in order:

  1. Our own NVIDIA DGX Spark at home: a LiteLLM gateway in front of a local vision model served by vLLM.
  2. OpenRouter, restricted to providers with zero data retention.
  3. The Claude Code CLI in the worker container.

runWithFallback() skips models that cannot take the input, returns the first answer with self-reported confidence of at least 0.75, and otherwise returns the best low-confidence answer, flagged for review. It logs the source, model, time and outcome, never the response body, and gives up after 120 seconds. IBANs, card numbers and ID numbers are stripped from prompts.

Receipts, email and a chat box

Capture is anywhere on the page: the phone camera, a file picker, paste with Ctrl+V or ⌘V, or drag and drop. A receipt gives the merchant, total, currency, date and the card’s last four digits, and confirming it against a bank line teaches which card belongs to which account. Finance email arrives through an n8n workflow we already ran: it reads the mailboxes every five minutes and posts receipts, bills and refunds to Banksteer’s ingest endpoint. Banksteer never logs in to email, processes each message once by Message-ID, and never moves or deletes mail.

Notifications started as a plan for Web Push and Telegram. We dropped both for in-app messages (bills due, money not arrived, forecast dips, stale balances, consent expiring, statements to upload, cheaper offers found), and each message retires itself when its condition clears.

On 12 October an “Ask Banksteer…” box went live on every page. It knows which page and item you are looking at, streams its answers, and uses tools from Banksteer’s own MCP server with a fixed allow-list: no shell, files or web. Small changes (mark paid, recategorise, answer an inbox question) apply directly with Undo; anything bigger appears as a Do it / Cancel card. Every write goes through the same API, audit trail and undo as a tap would.

Private by design: deployment and access

  • Where it runs: four containers (API, worker, web, Postgres) inside a container on our Proxmox cluster. The database is on its own internal network and not exposed.
  • Access: the home network and Tailscale only. Phones reach it through a Tailscale subnet route on the router. It is never published with Tailscale Funnel or any public address.
  • HTTPS: a reverse proxy with a certificate from the lab’s own certificate authority, trusted on every device, which also lets the app install as a PWA.
  • Secrets: pulled from a self-hosted password vault at deploy time, never in git.
  • Recovery: the container replicates to a second node with automatic failover, the database is dumped nightly and before any deploy that changes the schema, and a copy goes to the NAS for 90 days. A test restore produced identical row counts in every table.

How Claude Code built it

The product spec was written on 3 October, with ten mock-up screens, and the chat assistant shipped on 12 October. In between, each feature was built the same way: a frozen contract file describing exactly what to build, shared types and the database schema first, then a swarm of Claude Code agents working in parallel on separate files, with one controller owning git, the quality gate (build, type check, lint, tests on real Postgres) and the deploy.

The result is about 100,000 lines of TypeScript and 219 test files. The engine is pure functions tested directly; the bank client, the AI sources and even the chat model (with scripted tool calls) are faked in tests; CI runs on GitHub Actions with Postgres 16 and TZ=Europe/Dublin. Test data is synthetic. Once, a real value written into a spec file got copied straight into a test, and the rule since is: a contract that quotes a real value gets it copied into tests.

Every manual change to live data is logged with its “general lesson”. The human in the loop decided what to build, approved each contract and confirmed every bulk change to live data after a dry run.

What went wrong

  • A split word. pdftotext turned “PAYMENT” into “PAYMEN T” and broke transfer pairing. PDF statements and the API feed also describe the same payment differently; both forms are now recognised.
  • Thirty percent of a CSV vanished. A strict time parser silently dropped rows with one-digit hours like 9:12:33.
  • Fees that were not fees. Foreign-currency fee notes made whole purchases look like bank charges, until the notes were stripped before categorising.
  • Counted twice. An interview answer added a bill that was already planned under another name. Fixed with payee names per item and the “it’s an existing item” answer.
  • An inflated projection. Imported history went into the statistics before its transfers were recognised.
  • Messages that never left, and zero-amount lines waiting in the inbox. Both now clear themselves.
  • “Mass timeouts” that were load. A wall of failing tests turned out to be other jobs on the same machine. The lesson: read a mass timeout as load before reading it as a defect.

The human-in-the-loop rules

Rule How it is enforced
Nothing can move money Bank access is read-only account information; payment initiation is not used at all.
AI reads, a person confirms Statements, receipts, screenshots and interview answers are confirmed before saving.
AI never does the maths Tax, forecast, projection and insight figures come from deterministic code; invented numbers are rejected.
Everything can be undone Auto-clears, transfers, fees, learned payees, imports and chat edits are audited and reversible.
Cheaper bills are suggestions The bill optimiser compares phone, broadband, energy and insurance with 2–3 current Irish offers (bonkers.ie, switcher.ie, provider sites) with the catches listed; nothing is ever switched, cancelled or contacted.

Where it stands, and what is next

Today Banksteer runs live feeds from three banks, clears bills by itself, keeps a forecast and a five-year projection, reads card statements, and has a chat box for questions like “what is still to pay this month?”. Next on the list is an Android app (planned, design approved, not started): a thin Expo shell around the web app with a fingerprint lock, a one-tap receipt camera, “Share to Banksteer” and a home-screen widget. Its push messages are data-only wake-ups: the phone then fetches the actual message over Tailscale, so the push service never sees any finance data.

Glossary

Term Meaning
PSD2 The EU Payment Services Directive that gives licensed providers access to bank accounts with the customer’s consent.
Open banking Using those APIs to read balances and transactions instead of scraping or sharing passwords.
Restricted mode Enable Banking’s free personal mode: only accounts you link yourself.
Consent Your approval in the bank’s app for read access; it expires and must be renewed.
RRULE The standard calendar format for repeating dates (RFC 5545).
Occurrence One instance of a repeating bill, such as this month’s electricity.
PWA Progressive web app: a website you can install to your phone’s home screen.
Tailscale A private, encrypted network between your own devices.
MAD Median absolute deviation: a spread measure that ignores outliers.
Zero data retention AI providers that do not keep the prompts and answers they process.

The same method, applied to your systems

The pattern behind Banksteer (deterministic code for the numbers, AI for reading and phrasing, a human approving anything that matters, and data kept where you control it) is the same one we use for client work. If you have a spreadsheet that has quietly become a system, a free 30-minute call is enough to see what replacing it would take.

FAQ

Is there a free bank API for personal use in Ireland?

Yes. Enable Banking's restricted mode is free for personal, non-commercial use and covers AIB, Bank of Ireland, Permanent TSB and Revolut. You register an app, link your own accounts and approve access in your bank's app.

Is GoCardless (Nordigen) still free?

New sign-ups to GoCardless Bank Account Data have been disabled since July 2025 and were still closed on 13 October 2026. Existing users keep access.

How long does open banking consent last?

Each bank sets its own maximum, and for most it is 180 days. EU rules raised the re-authentication limit for account access from 90 to 180 days in 2023. After that you approve access again in your banking app.

Can open banking show my credit card due date?

No. Card feeds give transactions and balances only, with no statement date, due date, minimum payment or interest. Banksteer reads the monthly statement instead and asks you to confirm it.

Can the app move money?

No. It uses read-only account information access. Payment initiation is not used, and nothing in the app can send a payment.

Why not let the AI calculate the forecast?

Language models are good at reading documents and writing sentences, not at reliable arithmetic. Banksteer does all maths in tested code and rejects any AI text that contains a number not in the facts.

Want this working in your business?

D3X Solutions LABS builds AI agents and automation that pay back fast. Set up a free 30-minute call and we will map your first win together.

Get in touch

Blog

What We Do

Product

Consumer and enterprise software, built with taste and shipped fast.

Infrastructure

AI agents, automation, and orchestration - production systems, not demos.

Advisory

Hands-on engineering and strategy for teams shipping AI into production.