The raw transaction string that a bank sends you when a payment posts is not a merchant name. It is a concatenation of processor artifacts, terminal identifiers, geographic suffixes, truncation artifacts, and sometimes, buried somewhere in the middle, a fragment that resembles the actual business where the purchase was made.
Merchant name cleaning is the process of extracting meaningful merchant identity from this mess. It is the first step in any transaction enrichment pipeline, and it is the step that determines whether everything downstream works well or fails gracefully. A poorly cleaned merchant string sent to a classification model produces a worse category than a well-cleaned string every time.
This post is a technical walkthrough of how our cleaning pipeline works, the patterns we handle, and the edge cases that have required us to add handling over time.
The Anatomy of a Raw Transaction String
Raw payment processor strings are not standardized. Different card networks, acquiring banks, and point-of-sale processors format them differently. That said, there are recurring structural patterns:
AMZN*MKTP US AMZ8K3J2 # Amazon marketplace purchase
SQ *CORNER BAKERY # Square terminal (SQ *)
TST* FINE DINING 0922 # Toast POS terminal
PP*EBAY ITEM 23847 # PayPal purchase
WHOLEFDS MKT #10245 # Truncated Whole Foods
7-ELEVEN 38291 02 # Numbered terminal suffix
DEBIT CARD PURCHASE WA ST # Generic bank passthrough
ACH PMT LANDLORD LLC # ACH payment (not card)
Each of these has a different prefix pattern, a different suffix pattern, and a different amount of noise between the prefix and the actual merchant information. There is no single regex that handles all of them. The cleaning pipeline is a sequence of operations that progressively strips noise until the residual string is as close to the actual merchant name as possible.
The Cleaning Pipeline: Six Stages
Stage 1: Uppercase normalization and encoding repair
Raw strings arrive in ALL CAPS from most processors. The first operation converts to title case and repairs common encoding artifacts: pipes that should be spaces, asterisks used as separators, repeated whitespace, non-printable characters from truncation. This is deterministic and fast.
Stage 2: Processor prefix stripping
We maintain a curated list of known processor prefixes and strip them. The current list includes over 120 prefixes including SQ * (Square), TST* (Toast), PP* (PayPal), AMZN*, APL* (Apple), SP * (Shopify), GOOGLE*, and dozens of regional acquirer codes. When a prefix is stripped, we record which processor it came from in the enriched response, since processor identity is sometimes relevant downstream.
Prefix stripping is not purely mechanical. Some prefixes are ambiguous: SQ is Square, but SQ FT or SQUAR might be unrelated. We match on the full prefix pattern including the separator character (asterisk, space, hyphen) rather than just the letter sequence to reduce false-positive stripping.
Stage 3: Geographic and terminal suffix removal
After the merchant name proper, many strings append geographic information (city, state code, country code) and terminal identifiers (store number, register ID, transaction sequence number). These need to be removed without accidentally removing merchant names that themselves contain geographic terms.
The heuristics here are pattern-based. A two-letter uppercase sequence at the end of a string preceded by a city-like token is likely a state code and gets removed. A 5-8 digit numeric sequence at the end is likely a terminal or transaction ID. Strings matching the pattern [CITY] [STATE] at the end of the string, where STATE matches a US state abbreviation, get the geographic suffix stripped.
The edge case that has caused the most corrections: merchant names that themselves contain state or city names. A business called "Tampa Bay Roasters" should not have "Tampa Bay" stripped as a geographic suffix. We handle this by checking whether the cleaned residual after geographic stripping is longer than 3 characters and semantically meaningful. Short residuals get the strip reverted.
Stage 4: Abbreviation expansion
Merchant names are often truncated to fit the 22-character limit that many legacy payment processors enforce. We maintain an expansion map for common abbreviations: WHOLEFDS to "Whole Foods," WM SUPERCENTER to "Walmart Supercenter," CRVL to "CarVana," and hundreds of others. This map is built and maintained from ground-truth labeling on transactions we have confirmed against known merchants.
Abbreviation expansion is higher risk than the other stages because incorrect expansions are hard to catch. We apply expansion only when the abbreviation matches exactly and the expanded form is unambiguous. Partial matches or ambiguous abbreviations are left unexpanded and go into the classification stage as-is.
Stage 5: Type classification (card vs. ACH vs. check vs. other)
Not all transaction strings represent point-of-sale merchant transactions. Bank transfers, ACH payments to landlords, peer-to-peer payments, loan disbursements, and payroll deposits all appear in a transaction feed with merchant-like strings but they are not merchant transactions. The cleaning pipeline identifies these via pattern matching on phrases like ACH PMT, WIRE TRANSFER, CHECK #, and P2P, and marks them with a transaction_type flag before they reach the classification stage.
Non-merchant transactions get categorized differently. An ACH rent payment gets category housing.rent based on the payee string, not from a merchant classification model. A peer-to-peer transfer gets flagged as transfers.p2p and excluded from spending summaries by default unless the client opts in to including transfers in their cashflow calculations.
Stage 6: Canonical name resolution
After the first five stages, the cleaned string goes through a fuzzy match against our canonical merchant name registry. If the cleaned string matches a known merchant within an edit distance threshold, the canonical form is returned as the merchant.name_normalized field. The raw string is preserved in merchant.name_raw.
The canonical name matters for UI display and for merchant-level aggregation. If you want to show a user "You spent $340 at Whole Foods this month," you need all the variant strings (WHOLEFDS MKT #10245, WHOLE FOODS MARKET, WFM MARKET) to resolve to the same canonical name. Without canonical name resolution, a user with eight Whole Foods transactions might see eight distinct merchant entries in their spending view.
What Our Cleaning Pipeline Still Gets Wrong
We want to be specific about the failure cases rather than implying this is a solved problem.
International merchants with non-ASCII characters in their names require separate handling that our pipeline handles less reliably than Latin-script names. The abbreviation expansion map is almost entirely US-focused. International neobanks connecting to their customers' accounts will see lower cleaning quality on foreign merchant strings until we have more labeled training data for international markets.
White-label merchants, businesses that operate under a parent company name rather than their own brand, produce confusing cleaned output. A gas station branded under a fuel company's name might appear with the parent company string after cleaning, even though the user thinks of it as the named station they visited. We address this partially through the merchant.doing_business_as field, but coverage is uneven.
Newly created businesses with no registry presence produce cleaned strings that look correct but cannot be canonicalized because we have not seen them before. These get a cleaned name, but no canonical resolution, and the category confidence is lower. This is the inherent limitation of any normalization system: coverage is a function of how much data has flowed through the pipeline.
What the Response Looks Like
The merchant object in the Openaggr enrichment response after cleaning:
{
"merchant": {
"name_raw": "AMZN*MKTP US AMZ8K3J2",
"name_cleaned": "Amazon Marketplace",
"name_normalized": "Amazon",
"processor": "amazon_pay",
"mcc": "5999",
"mcc_label": "Miscellaneous and Specialty Retail",
"is_online": true,
"geo": null
}
}
The name_cleaned field is what our cleaning pipeline produces from the raw string. The name_normalized field is the canonical resolved form matched against our registry. For display in your app, use name_normalized when it is present, falling back to name_cleaned. Use name_raw only for debugging, never for display.
The merchant name cleaning stage is not the most algorithmically interesting part of the enrichment pipeline, but it is the one where poor execution most visibly degrades the user experience. A transaction that shows as "AMZN*MKTP US AMZ8K3J2" in a user's spending view destroys the credibility of the entire PFM feature. A transaction that shows as "Amazon" does what the feature promises. The distance between those two strings is the cleaning pipeline.