Personal finance aggregator: Part 1
I wanted one place to see everything I was spending, across five accounts UK banks I actually use day-to-day. None of these banks talk to each other, none of them export in a format the others understand, and I was tired of opening six apps to answer "how much did I actually spend this month." So: a personal finance aggregator. Pull everything into one SQLite database, categorise it, look at it in one place.
The interesting design decision wasn't the database schema or the frontend framework. It was much earlier than that: how do you actually get transaction data out of a UK bank programmatically? You don't get to talk to Nationwide's or Monzo's APIs directly as a hobbyist - you go through an Open Banking aggregator, a company that's done the compliance work to sit between you and every bank's own (wildly inconsistent) API, and exposes one normalised API on top. In the UK that space has a few players - TrueLayer, Plaid, Yapily, Enable Banking, Tink - and picking one is closer to picking a foundation than picking a library, because your whole data model downstream inherits their quirks.
I evaluated two: TrueLayer and Enable Banking. Here's how, and why TrueLayer won.
What I actually needed
Before comparing anything, I wrote down the non-negotiables:
- Coverage of my five specific banks/cards, not just "UK coverage" in the abstract - some aggregators are strong on the big five (Barclays, HSBC, Lloyds, NatWest, Santander) and weaker on challenger banks like Monzo or smaller building societies like Nationwide.
- A sandbox I could develop against without touching my real accounts on every test run. Open Banking consent flows involve redirecting to your real bank and authenticating there - fine once, painful to repeat fifty times while debugging a token refresh bug.
- A manageable auth model. I'm one person building this in spare evenings, not a fintech with a security team. Whatever the integration looked like needed to be something I could reason about end to end.
- Reasonable free-tier or sandbox access. This is a personal project, not a startup with a credit card on file for API calls.
Providers I ruled out without a deep evaluation
Worth being honest about the providers that didn't make it to a real comparison at all, and why, because "I didn't evaluate X" is a decision too, just a cheaper one.
Plaid is the name most people reach for first, and for good reason - it's probably the best-documented aggregator API that exists. But Plaid's UK/EU Open Banking coverage has historically been a secondary market for them behind US bank connectivity, and a few minutes of searching turned up enough uncertainty about Nationwide and Monzo support specifically that I didn't want to sink an evening finding out the hard way. If this were a US project, Plaid would have been the first and probably only candidate evaluated.
Yapily and Tink both looked plausible on paper - UK-focused, proper Open Banking coverage - and I'd put either one ahead of Enable Banking if I were doing this again with more time to spend on the comparison. I stopped at two candidates mostly for the mundane reason that evaluating an aggregator means actually registering, reading auth docs, and writing a throwaway script for each one, and two afternoons of that felt like the right amount for a side project. This is worth naming plainly rather than implying TrueLayer beat five contenders in some rigorous bake-off - it beat one real contender, on top of a short list of others I judged unlikely to change the outcome and didn't spend time confirming.
Enable Banking: interesting, but a heavier lift
Enable Banking's Data API is JWT-based. You register an application in their control panel, and instead of getting a client ID and secret, you get an application ID and a downloaded private key file. Every API call means signing your own JWT:
def make_token():
with open(KEY_PATH, "rb") as f:
private_key = f.read()
now = int(time.time())
return jwt.encode(
{
"iss": "enablebanking.com",
"aud": "api.enablebanking.com",
"iat": now,
"exp": now + 3600, # valid for 1 hour
},
private_key,
algorithm="RS256",
headers={"kid": APP_ID},
)That's not hard, exactly - pyjwt and cryptography do the actual work - but it's a different category of thing to get right than "store a client secret." Now I'm also responsible for keeping a private key file out of version control (on top of the usual .env), rotating it if it leaks, and understanding RS256 well enough to debug it when a token gets rejected. For a side project, that's overhead I'd rather not carry.
I did get far enough to list available banks:
resp = requests.get(
"https://api.enablebanking.com/aspsps",
params={"country": "GB"},
headers={"Authorization": f"Bearer {make_token()}"},
)
banks = resp.json()["aspsps"]
for bank in banks:
if any(name in bank["name"].lower() for name in MY_BANKS):
print(bank["name"], "-", bank.get("psu_types"))Coverage looked fine on paper - all five of my banks showed up. But two things gave me pause. First, psu_types (personal vs business customer) varied per bank in ways I didn't fully understand without reading deeper into their docs - it wasn't obvious upfront whether a given bank supported the consumer flow I needed. Second, and more importantly: I couldn't find a sandbox with a mock bank. Enable Banking's docs pointed toward testing against real ASPSPs (the actual banks) in most cases. That meant every test run of my linking flow would mean a real trip through my actual bank's login page. Workable, but it slows down iteration a lot when you're still shaping the basic flow.
TrueLayer: a normal OAuth2 flow, and a real sandbox
TrueLayer's Data API, by contrast, is standard OAuth2 authorization code flow - the same shape as "Sign in with Google." You get a client ID and client secret from their Console, same as any OAuth app registration you've done before:
AUTH_HOST = "https://auth.truelayer-sandbox.com" if SANDBOX else "https://auth.truelayer.com"
API_HOST = "https://api.truelayer-sandbox.com" if SANDBOX else "https://api.truelayer.com"
PROVIDERS = "uk-cs-mock" if SANDBOX else "uk-ob-all uk-oauth-all"
CLIENT_ID = os.environ["TL_CLIENT_ID"]
CLIENT_SECRET = os.environ["TL_CLIENT_SECRET"]
SCOPES = "info accounts balance cards transactions offline_access"
Two things stand out in that snippet, and both mattered for the decision.
The first is AUTH_HOST/API_HOST switching on a SANDBOX flag pointing at entirely separate hostnames (auth.truelayer-sandbox.com vs auth.truelayer.com). TrueLayer ships a genuine sandbox environment with a Mock Bank - uk-cs-mock as the provider ID - that you authenticate against with fake credentials, returning realistic-looking (fabricated) accounts and transactions. That meant I could build and rebuild the entire OAuth dance, the token exchange, the transaction pull, however many times I wanted, without ever touching a real bank login page until the integration actually worked.
The second is that SCOPES string. TrueLayer scopes data access the way you'd expect from any OAuth provider: accounts, balance, cards, transactions, plus offline_access for a refresh token so I'm not asking the user to re-authenticate every hour. info gets you basic account holder info. It reads like every other OAuth integration I've done, which is exactly what I wanted from something I'll be maintaining alone.
Checking real coverage
Before committing, I wrote the equivalent bank-listing check against TrueLayer's public providers endpoint (no auth required for this one - it's just metadata):
resp = requests.get(f"{AUTH_HOST}/api/providers")
providers = resp.json()
for p in providers:
if any(name in p["display_name"].lower() for name in MY_BANKS):
print(p["provider_id"], p["display_name"], p["country"])
All five showed up under sensible provider IDs (ob-nationwide, ob-monzo, ob-tesco-bank, and so on - I'm working from memory of the exact IDs here, but the pattern held). Between that and the sandbox, TrueLayer covered everything I needed to actually start building, and let me build the linking flow and the transaction pull entirely against the Mock Bank first, with zero risk to my real accounts while I was still getting the OAuth mechanics right.
The consent experience itself
One more thing that only became obvious once I actually ran both flows, not just read the docs: what the user - me - actually sees during consent matters more than I expected going in. TrueLayer's is a hosted consent page: you redirect to a TrueLayer domain, pick your bank from a list (or, if you already know which one, pass a provider_id to skip straight to it), and TrueLayer handles the visual back-and-forth with the bank's own login before redirecting you home. It looks polished, and critically, it looks the same regardless of which bank you picked - TrueLayer owns that whole surface.
Enable Banking's flow, from what I could piece together without fully building it, redirects more directly toward each ASPSP's own authentication surface, which in practice means the experience varies bank to bank in ways that are harder to predict or test for up front. Neither approach is wrong - PSD2 mandates the bank ultimately authenticates you regardless of who's brokering the flow - but "one consistent hosted screen I can screenshot and reason about" was a real point in TrueLayer's favour for something I was going to be linking and re-linking a handful of times while debugging.
The decision
I picked TrueLayer for three reasons, in order of how much they actually mattered once I started building:
- The sandbox. Being able to iterate on the OAuth flow against a mock bank, dozens of times, without touching real credentials, was worth more than I expected going in. Bugs in a token-refresh loop are much less stressful to debug when the "bank" is fake.
- A conventional auth model. Client ID, client secret, authorization code, refresh token. No private key file to protect, no JWT signing code of my own to get right. One less category of thing that could go subtly wrong.
- Coverage of the banks I actually use. Both providers had this on paper, so it came down to the other two factors - but it's worth flagging that this check happens first, always. Neither of the other two points matters if the bank you need isn't there.
What I didn't weigh heavily, and would if I were doing this commercially: cost at scale, rate limits in production, and the breadth of European bank coverage. TrueLayer's free/sandbox tier and reasonable production pricing were enough for a single-user tool with a handful of linked accounts; that calculus would look different building something for other people to use.
The enable_banking/ and (original) true_layer/ evaluation scripts are still sitting in the repo's history, artifacts of an afternoon spent figuring this out before writing a single line of the actual application. Next post: what it actually took to get a working TrueLayer Console app and credentials in hand, sandbox and production both.
