One screen for every bank balance: a treasury dashboard built for a client
A client ran cash across 20 to 25 bank accounts and saw it one portal at a time. Here is the dashboard we built to put it on one screen, with a snapshot written every morning.
A commercial real estate developer runs a lot of bank accounts. Property entities, construction jobs, and operating companies each tend to get their own. This firm had somewhere between 20 and 25 of them across several banks, and the question leadership asked most often was the simplest one: how much cash do we have right now?
Answering it meant logging into each bank portal, writing down the balance, and adding it up. Nobody kept the history, so "how much did we have last Tuesday" meant a second round of logins. The number existed. Getting to it was a chore that only happened when someone needed it badly enough.
We built an internal treasury dashboard to make that question a glance instead of an errand.
What we built
One screen. Total cash across every connected account at the top, the account list underneath, and a chart of cash over time. Bank connections run through Plaid, the data lives in a dedicated Supabase Postgres database, and the app is a Next.js build deployed on Vercel.
The feature that mattered most was not the total. It was the history. Every time the app pulls balances, it writes a snapshot of every account to the database. A scheduled job runs that pull automatically every morning, and a "Pull now" button covers the days someone wants a fresh number before a meeting. The chart reads back up to 180 pulls, so the day-over-day trend builds itself without anyone maintaining a spreadsheet.
Later the app picked up two more jobs: logging transactions and reconciling them against the ledger. More on both below.
How the bank side works
Connecting a bank goes through Plaid Link, the consent flow banks expect. After each successful link, the app mints a fresh link token, so the person connecting can add bank after bank in one sitting instead of reloading between each one.
Plaid hands back a public token, and the server exchanges it for a long-lived access token. That access token never reaches the browser. It lives in one database table and in server memory during a pull, and nowhere else. The browser only ever talks to the app's own routes.
A few decisions that turned out to matter:
- Cash means cash. The headline total sums depository accounts only. Credit cards, loans, and investment accounts are pulled and shown, but they do not inflate the cash number.
- Broken connections are visible. When a bank connection fails (a changed password, an expired consent), that institution flips to "needs attention" on the dashboard instead of quietly showing a stale balance.
- Totals come from what exists now. An early version read the headline from a stored roll-up. After a bank was disconnected, the dashboard showed a total for accounts that were no longer there. We rewrote it to compute the total from the latest snapshot of each account that currently exists, so a disconnect corrects the headline immediately.
- The right API call, not the expensive one. Plaid sells real-time balance checks as a separate paid product. For a once-a-day cash snapshot, the standard accounts call returns the same balance fields and comes with the product the app already used. Switching saved roughly $75 a month at this bank count, and the daily number is just as useful.
Security was not optional
Bank data raises the bar, and Plaid makes you prove it. Production access comes with a security questionnaire, and one of its requirements is multi-factor authentication before the bank connection screen is ever shown.
The first version sat behind a single shared password. We replaced that in stages:
- An emailed one-time code as a second factor, sent through Resend. The pending code is stored as a keyed hash in a signed cookie, expires in ten minutes, and allows five attempts.
- Per-person accounts, each with its own hashed password plus the emailed code, so "who signed in" always has an answer.
- An admin-only settings page to add people, reset passwords, and grant or remove admin rights, with guards that stop anyone removing the last admin or their own account.
Alongside that: strict security headers, a privacy policy inside the app, a written map of every control against the questionnaire, and a disconnect button that revokes the bank connection at Plaid before deleting it locally.
Transactions and reconciliation
Once balances were reliable, the next question was obvious: what moved? The daily pull now also syncs transactions with Plaid's cursor-based sync, capped per run so a long backfill cannot time out the serverless function. Every pull logs new transactions with date, description, bank, category, and amount in bank-statement sign, on a transactions page linked from the dashboard.
Then reconciliation. The book side is a general ledger export from the firm's property accounting system, uploaded as a CSV. The file is parsed in the browser, so the ledger never touches the server. A column mapper detects debit and credit layouts automatically and lets the user override it.
The matcher is deliberately conservative. Pass one matches exact reference and amount. Pass two matches a unique amount within a date tolerance. Everything else is surfaced for a person rather than guessed. The report sorts into matched, needs review, bank activity with no ledger entry, and ledger entries that have not cleared. In testing against real logged transactions and a synthetic ledger, it produced zero false matches, which is the property that matters. A reconciliation tool that confidently pairs the wrong items is worse than none at all.
What changed
"How much cash do we have" now takes one click, and the answer comes with its own history. Nobody logs into a stack of portals to build the number, and the trend line exists because the system writes it every morning, not because someone remembered to.
The app was verified in production against a live bank connection: real transactions logged on the scheduled pull and rendered correctly, and the authentication gates were tested from outside (no session, wrong password, and unauthorized email were all refused).
We also audited every screen at phone width. The first version overflowed sideways on a phone and pushed the sign-out button off screen. Tables now restack into cards on small screens, so balances and amounts are always visible without scrolling sideways.
What other operators can take from this
- Store snapshots, not just today's number. The history is the part nobody has, and it costs one row per account per day.
- Pick the API call that matches the decision. A daily cash view does not need a real-time balance product.
- Plan for the security review up front. If you touch bank data through an aggregator, MFA and per-person accounts will be required. Building them early is cheaper than retrofitting them.
- Keep reconciliation conservative. Let the machine take the obvious matches and hand the rest to a person.
If your team still builds the cash number one bank portal at a time, book a Systems Audit and we will map what one screen would take.
Filed by
John McIntosh
Founder, Salvus AI
Replacing a loan management platform with a tracker that reads the loan documents
A client was pricing an off-the-shelf loan management platform for document imaging and deadline tracking. We built that part instead. Here is how it reads a closing binder, and the part we told them not to build.
A pursuit platform that scores public work before the team spends money chasing it
We built a pursuit platform for a client's construction team. It pulls public opportunities from federal sources, scores each one against what the firm actually builds, and tracks who else is bidding.
Talk to us
Want this kind of work shipped for you?
Salvus AI deploys per-client isolated MCP servers and the automations that run on them. Client success with real numbers, no shared tenancy.