Account Name Matching and Fuzzy Comparison
Normalize both names to uppercase, remove punctuation, split into word tokens, sort alphabetically, and count how many tokens overlap. Require at least 2 matching tokens (typically first name and surname). For tighter verification, use Levenshtein distance to catch minor spelling differences. Never use exact string equality for name comparison in payment systems.
Why Exact String Matching Fails
When you resolve an account number with Paystack, the bank returns the name on the account. When a user signs up on your platform, they type their own name. These two names almost never match exactly.
Real examples of the same person appearing differently:
- Bank returns:
"OKAFOR JOHN CHUKWUEMEKA" - User typed:
"John Okafor"
- Bank returns:
"AKINWALE FOLASHADE M" - User typed:
"Folashade Akinwale"
- Bank returns:
"ODHIAMBO BRIAN OTIENO" - User typed:
"Brian Odhiambo"
If you compare these with === or even case-insensitive comparison, every single one fails. You would block every legitimate payout.
The differences come from several sources: name order conventions vary by bank, middle names may or may not be included, single-letter initials appear in some records, casing differs, and some banks truncate long names.
Step 1: Normalize Both Names
Before comparing, bring both names into the same format.
// normalize.js
function normalizeName(name) {
return name
.toUpperCase() // Uniform casing
.replace(/[^A-Z\s]/g, '') // Remove punctuation, numbers, special chars
.replace(/\s+/g, ' ') // Collapse multiple spaces
.trim()
.split(' ')
.filter(function(part) {
return part.length > 1; // Remove single-letter initials like "M" or "A"
})
.sort(); // Alphabetical order removes position dependency
}
// "OKAFOR JOHN CHUKWUEMEKA" => ["CHUKWUEMEKA", "JOHN", "OKAFOR"]
// "John Okafor" => ["JOHN", "OKAFOR"]
Why remove single characters: Banks often include middle initials ("M", "A", "O") that users never type. Keeping them would reduce match accuracy.
Why sort alphabetically: This eliminates the first-name-vs-surname order problem entirely. Whether the bank says "OKAFOR JOHN" or the user says "John Okafor," after sorting both become ["JOHN", "OKAFOR"].
Why uppercase: Case differences are trivial but would cause mismatches without normalization. Always normalize to one case.
Step 2: Token-Based Matching
After normalization, count how many name tokens (words) appear in both names.
// token-match.js
function tokenMatch(name1, name2) {
var tokens1 = normalizeName(name1);
var tokens2 = normalizeName(name2);
var matches = 0;
tokens1.forEach(function(token) {
if (tokens2.indexOf(token) !== -1) {
matches++;
}
});
var minTokens = Math.min(tokens1.length, tokens2.length);
var maxTokens = Math.max(tokens1.length, tokens2.length);
return {
matchCount: matches,
minTokens: minTokens,
maxTokens: maxTokens,
ratio: maxTokens > 0 ? matches / maxTokens : 0,
passes: matches >= Math.min(2, minTokens),
};
}
// Examples:
// tokenMatch("OKAFOR JOHN CHUKWUEMEKA", "John Okafor")
// => { matchCount: 2, minTokens: 2, maxTokens: 3, ratio: 0.67, passes: true }
// JOHN and OKAFOR match. CHUKWUEMEKA has no match but 2 out of 2 user tokens matched.
// tokenMatch("AKINWALE FOLASHADE", "Bola Adeyemi")
// => { matchCount: 0, minTokens: 2, maxTokens: 2, ratio: 0, passes: false }
// No tokens match. Different person.
Threshold: at least 2 matching tokens. This requires both the first name and surname to match. If the user only has a single-word name (some people do), require 1 match.
Why not require all tokens to match: The bank often includes middle names that the user omitted. Requiring all tokens to match would reject users who simply did not type their middle name.
Step 3: Levenshtein Distance for Spelling Variations
Token matching fails when the same name is spelled slightly differently. "Chukwuemeka" and "CHUKWUEMKA" (missing an "e") are obviously the same name, but they are different strings.
Levenshtein distance measures how many single-character edits (insertions, deletions, substitutions) it takes to turn one string into another.
// levenshtein.js
function levenshtein(a, b) {
var matrix = [];
for (var i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
for (var j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
for (var i = 1; i <= b.length; i++) {
for (var j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1, // substitution
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j] + 1 // deletion
);
}
}
}
return matrix[b.length][a.length];
}
function fuzzyTokenMatch(token1, token2) {
var distance = levenshtein(token1, token2);
var maxLen = Math.max(token1.length, token2.length);
var similarity = 1 - (distance / maxLen);
return similarity >= 0.8; // 80% similarity threshold
}
// "CHUKWUEMEKA" vs "CHUKWUEMKA" => distance 1, similarity 0.91 => match
// "JOHN" vs "JOAN" => distance 1, similarity 0.75 => no match
Combine with token matching. Use Levenshtein as a fallback when exact token matching fails. If a token from name1 does not exactly match any token in name2, check if it fuzzy-matches (80%+ similarity) any token in name2.
// enhanced-match.js
function enhancedTokenMatch(name1, name2) {
var tokens1 = normalizeName(name1);
var tokens2 = normalizeName(name2);
var matches = 0;
tokens1.forEach(function(t1) {
var exactMatch = tokens2.indexOf(t1) !== -1;
if (exactMatch) {
matches++;
return;
}
// Try fuzzy match
var fuzzyMatch = tokens2.some(function(t2) {
return fuzzyTokenMatch(t1, t2);
});
if (fuzzyMatch) {
matches++;
}
});
return {
matchCount: matches,
passes: matches >= Math.min(2, Math.min(tokens1.length, tokens2.length)),
};
}
African Name Edge Cases
African names have patterns that generic name-matching libraries do not handle well. Build your matcher with these in mind.
Compound surnames. Names like "Okonkwo-Eze" or "Abubakar-Sadiq" may appear with or without hyphens. Split on hyphens during normalization so "OKONKWO-EZE" becomes ["EZE", "OKONKWO"].
Prefix variations. "Oluwatobi" and "Tobi" can refer to the same person (the "Oluwa" prefix means "God" in Yoruba and is sometimes dropped in informal use). Your matcher cannot know this. This is why user confirmation is essential.
Married names. A woman might register on your platform with her married name but her bank account still shows her maiden name. Token matching will fail. The user confirmation step catches this.
Transliteration differences. "Mohammed", "Muhammad", "Muhammed" are all the same name. Levenshtein distance handles this reasonably well since the edit distance is small.
Single names. Some people legally have only one name. If the bank returns a single token and the user provides a single token, require just 1 match. Do not reject a user because they "should have" a first and last name.
Title prefixes. Some bank records include "Chief", "Dr", "Alhaji", or "Mrs". Strip common titles during normalization.
// strip-titles.js
var TITLES = ['CHIEF', 'DR', 'MR', 'MRS', 'MS', 'ALHAJI', 'ALHAJA', 'ENGR', 'PROF', 'PASTOR', 'ELDER'];
function stripTitles(tokens) {
return tokens.filter(function(token) {
return TITLES.indexOf(token) === -1;
});
}
Confidence Scores and Decision Logic
Rather than a binary pass/fail, assign a confidence score and make decisions based on thresholds.
// confidence.js
function calculateNameConfidence(bankName, userName) {
var bankTokens = stripTitles(normalizeName(bankName));
var userTokens = stripTitles(normalizeName(userName));
if (bankTokens.length === 0 || userTokens.length === 0) {
return { score: 0, action: 'reject' };
}
var exactMatches = 0;
var fuzzyMatches = 0;
userTokens.forEach(function(ut) {
if (bankTokens.indexOf(ut) !== -1) {
exactMatches++;
} else if (bankTokens.some(function(bt) { return fuzzyTokenMatch(ut, bt); })) {
fuzzyMatches++;
}
});
var totalMatches = exactMatches + (fuzzyMatches * 0.8);
var score = totalMatches / Math.max(bankTokens.length, userTokens.length);
var action;
if (score >= 0.6) {
action = 'auto_approve';
} else if (score >= 0.3) {
action = 'manual_review';
} else {
action = 'reject';
}
return { score: score, action: action, exactMatches: exactMatches, fuzzyMatches: fuzzyMatches };
}
// "OKAFOR JOHN CHUKWUEMEKA" vs "John Okafor" => score ~0.67 => auto_approve
// "OKAFOR JOHN CHUKWUEMEKA" vs "Jane Okafor" => score ~0.33 => manual_review
// "OKAFOR JOHN CHUKWUEMEKA" vs "Bola Adeyemi" => score 0 => reject
Three-tier decision:
- Auto-approve (score >= 0.6): At least 2 tokens match. Proceed automatically.
- Manual review (score 0.3 to 0.6): Partial match. Show the user both names and ask for confirmation. If the user confirms, proceed.
- Reject (score < 0.3): Names are completely different. Block the payout and ask the user to re-enter their bank details.
The User Confirmation Step
Automated matching handles most cases, but it cannot catch everything. Always include a user confirmation step in your payout flow.
Show the resolved name clearly. After resolving the account, display: "This account is registered to: OKAFOR JOHN CHUKWUEMEKA. Is this your account?" with clear Yes and No buttons.
Log the confirmation. When the user clicks Yes, record it. This is your evidence if a dispute arises later. "The user confirmed the resolved name on [date] at [time]."
Handle "No" gracefully. If the user says the name is not theirs, let them re-enter their bank details. Do not lock them out. They may have simply selected the wrong bank or mistyped a digit.
The combination of automated fuzzy matching plus user confirmation gives you both speed (most matches resolve automatically) and safety (edge cases get human verification).
Key Takeaways
- ✓Banks format names differently. Some use SURNAME FIRSTNAME, others use FIRSTNAME SURNAME. Some include middle names, some do not. Exact comparison fails constantly.
- ✓Token-based matching (splitting names into words and counting overlaps) is the most reliable approach for African name formats.
- ✓Normalize both names: uppercase, strip punctuation, remove single-character initials, split on whitespace, sort alphabetically.
- ✓Require at least 2 matching tokens for a pass. This covers first name and surname regardless of order.
- ✓Levenshtein distance catches minor spelling variations like "Chukwuemeka" vs "CHUKWUEMKA" (missing letter).
- ✓Always show the resolved name to the user for manual confirmation, even when automated matching passes. This creates an audit trail.
Frequently Asked Questions
- What matching threshold should I use for payouts?
- Start with a 2-token minimum match for auto-approval. This means the first name and surname must both appear in the resolved name. Partial matches (1 token) should trigger manual review or user confirmation. Zero matches should block the payout pending re-entry of bank details.
- Should I use a library for fuzzy matching instead of building my own?
- Libraries like fuzzball or string-similarity work for general text matching, but they are not optimized for African name formats. Token-based matching with title stripping and Levenshtein fallback, as described in this guide, handles the specific edge cases you will encounter with bank-returned names. You can use a library for the Levenshtein calculation itself.
- What if the bank returns an empty or garbled name?
- Some banks return empty strings or placeholder text for certain account types (e.g., corporate accounts). If the resolved name is empty or clearly not a real name, skip automated matching and require manual confirmation from the user. Log the anomaly for your records.
- How do I handle corporate or business account names?
- Business accounts return the company name, not a personal name. Token matching against a person's name will always fail. Detect business accounts by checking for keywords like "LTD", "LIMITED", "PLC", "ENTERPRISES", "COMPANY" in the resolved name. For business payouts, match against the registered business name instead of the personal name.
- Can name matching be fooled by a fraudster who knows the account holder name?
- Yes. If a fraudster knows the name on a bank account, they can enter that name on your platform and pass the name match. Name matching prevents accidental errors (wrong bank, mistyped digits) but does not stop intentional fraud. For stronger protection, combine name matching with BVN verification and payout limits.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon