There is a version of the open banking pitch that goes something like this: connect to the user's bank, pull transaction history, and now you have everything you need to build meaningful financial features. Spending insights, cashflow tracking, budget recommendations, all of it waiting in that transaction feed.
We have spent the last year working with developers building exactly these kinds of features, and the gap between that pitch and reality is where most PFM projects stall. The raw transaction data that open banking APIs deliver is genuinely useful, but it is not spending insight. There is a non-trivial layer of work between a normalized transaction object and anything a user would actually find informative.
This post is about what that gap looks like in practice, why it is harder to close than it appears, and where an enrichment layer fits into the picture.
What Open Banking Actually Gives You
Open banking in its various forms, whether that is PSD2 in Europe, the CDR framework in Australia, or the FDX standard gaining traction in the US, is fundamentally about access. It gives your application permission to read a user's transaction history from their bank. That access is meaningful. Before it existed, getting this data meant asking users to hand over their banking credentials for screen scraping, which is both fragile and a security nightmare.
What you receive from a well-implemented open banking connection is a list of transaction objects. Each one typically includes: a transaction ID, a timestamp, an amount, a direction (debit or credit), a balance snapshot, and a description string.
That description string is where the problem starts.
Here is what a typical description field looks like from a mid-size US bank feed: SQ *BLUE BOTTLE COFFEE SF CA 12182025. Or: WHOLEFDS #00297 10018 W 6TH ST. Or: UBER* EATS HELP.UBER.COM CA. Or, more opaquely: ACH DEBIT 231381100 TFR 8829201A.
These strings are not created with your users in mind. They are artifacts of the payment processing chain: truncated merchant names, processor codes, acquirer identifiers, terminal location data, and various internal reference numbers, all concatenated in whatever format the acquiring bank chose to use. They vary by bank, by card network, by whether the transaction went through ACH or card rails, and sometimes by the specific terminal used.
The Enrichment Gap: What Is Actually Missing
When a product manager at a neobank asks "how much did this user spend on food last month," the answer requires resolving at least three things that open banking does not provide.
First, merchant identity. Which transactions represent food purchases? You need to recognize that WHOLEFDS #00297 is Whole Foods, that SQ *BLUE BOTTLE is a coffee shop (and specifically Blue Bottle, not just a generic cafe), and that PANERABREAD.COM is a quick-service restaurant. This is merchant name resolution, and it is genuinely hard to do at coverage depth. The long tail of merchants is enormous.
Second, category assignment. Once you know who the merchant is, you need to place it in a spending category that makes sense to the user. Whole Foods belongs in Groceries, not Food and Drink generally. Blue Bottle belongs in Coffee, which might sit under Food and Drink or under its own category depending on your taxonomy. The categorization decision depends on both the merchant identity and your category schema design.
Third, intent disambiguation. The same merchant can represent very different spending intents. A purchase at a pharmacy can be medication (Health), personal care (Personal), or household goods (Shopping). A purchase at a warehouse club like Costco could be groceries, electronics, or gasoline. The transaction amount sometimes helps, but not always.
Open banking provides the raw signal. Enrichment provides the interpretation. Without interpretation, you have a list of dollar amounts and timestamps, not spending insight.
Why Building the Interpretation Layer In-House Is Expensive
The natural reaction from a product team with engineering resources is: we can build this. And technically, yes. But the cost surface is larger than it looks from the outside.
The first iteration is usually a lookup table approach: maintain a database of merchant name patterns and their associated categories, run each transaction description against it, and assign a category when there is a match. This works reasonably well for the top 200 or 300 merchants that account for most transaction volume: the Amazons, the Walmarts, the Starbucks. But coverage drops sharply after that.
A consumer's monthly transaction history typically includes 40 to 80 distinct merchants. For the top 50 by coverage, your lookup table works. For the other 20 to 30, you either return "Uncategorized" or you fall back to some heuristic that is often wrong. Users notice Uncategorized. It erodes trust in the feature.
The deeper problem is maintenance. Payment processor strings change. Banks update their formatting. New merchants appear. Seasonal merchants (pop-up shops, fair vendors, holiday services) generate strings you have never seen. A lookup table is a living document that requires continuous curation, and the curation work scales with the diversity of your user base's spending patterns.
To do this well, you end up needing: a labeled training corpus, a model that generalizes from known merchants to unknown ones, a pipeline for ingesting and normalizing raw strings, a feedback loop for catching miscategorizations, and ongoing labeling effort. That is a data team investment. For a two- or three-person engineering team trying to ship a cashflow feature, it is not the right place to spend engineering cycles.
A Concrete Scenario: What the Gap Looks Like at Ship Time
Consider a small neobank built for freelancers and independent contractors, running on a modern banking-as-a-service stack. Their product roadmap includes a spending breakdown screen: show users where their money is going, categorized, with a month-over-month comparison.
They connect their account data feed, normalize the transaction objects, and start building the categorization layer. Two weeks in, they have good coverage for the most common merchants. But their user base includes a lot of small business spending: office supply vendors with generic business names, co-working spaces, professional subscription tools, specialized software services. These are exactly the kinds of merchants that live in the long tail and generate strings that do not match any lookup table pattern.
They could invest three to four weeks building a classifier that handles the long tail. Or they could call POST /enrich with the raw transaction description and get back a structured category, a normalized merchant name, and a confidence score, in under 100 milliseconds. The category returned for REGUS MANAGEMENT GRP is Office and Workspace. For WEWORK SOLUTIONS LLC, the same. The feature ships in one sprint instead of one quarter.
We are not saying building your own categorization layer is wrong. If transaction intelligence is your core product, you should own that layer deeply. But if it is a feature you are shipping to serve other user needs, the build cost versus coverage quality tradeoff usually points toward an enrichment API.
The Data Quality Problem That Open Banking Does Not Solve
There is a second gap worth naming: open banking standardizes access, but not data quality.
Different banks send descriptions in different formats. The same Starbucks purchase from the same credit card might arrive as STARBUCKS #1234 SEATTLE WA from one bank and as STARBUCKS STORES 800-782-7282 from another. Some banks truncate aggressively (17-character limits on description fields), which means long merchant names get mangled. International banks using SWIFT rails often include correspondent bank codes in the description that have nothing to do with the end merchant.
A robust enrichment layer normalizes across these variations. The input might be any of ten different string representations of the same merchant; the output is a consistent structured object: a canonical merchant name, a category code, an MCC mapping where available, and a confidence score. The application layer never needs to know about the upstream formatting chaos.
This normalization is a meaningful part of what makes open banking data actually usable. Open banking solved the access problem. Enrichment solves the interpretation problem. Both layers are necessary for a working spending insight feature.
What Enriched Transaction Data Actually Looks Like
When we run a typical consumer transaction set through the enrichment pipeline, the output for each transaction looks something like this:
{
"transaction_id": "txn_9a2f8c1",
"raw_description": "SQ *BLUE BOTTLE COFFEE SF CA 12182025",
"merchant": {
"name": "Blue Bottle Coffee",
"normalized": "Blue Bottle Coffee",
"mcc": "5812",
"category": "coffee_shop"
},
"category": {
"primary": "Food and Drink",
"subcategory": "Coffee Shops",
"code": "food_drink.coffee_shop",
"confidence": 0.94
},
"enrichment_version": "v2.1"
}
Every field in that response represents a decision that the application does not have to make. The category hierarchy is pre-built and consistent. The merchant name is normalized to a form that can be displayed directly in the UI. The confidence score tells you whether to surface this to the user with high certainty or treat it as a soft suggestion.
Open banking gets the transaction into your system. Enrichment makes it useful. The gap between those two things is exactly where most spending insight features either succeed or stall.
If you are at the point in your build where raw transaction strings are sitting in your database and you are not sure what to do with them next, that is the moment to evaluate whether an enrichment layer makes sense. The integration is one POST request per transaction batch. The coverage on the other side represents work we have already done so your team does not have to.