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.
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:
"OKAFOR JOHN CHUKWUEMEKA""John Okafor""AKINWALE FOLASHADE M""Folashade Akinwale""ODHIAMBO BRIAN OTIENO""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.
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.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
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.
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 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;
});
}
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:
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).
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course