Preventing Payout Fraud with Account Resolution
Resolve every bank account with Paystack before sending money to it. Compare the resolved name against the user profile name. Flag mismatches for manual review. Enforce a hold period on first payouts to new accounts. Track bank detail changes and apply velocity limits. These measures catch the most common payout fraud patterns: account takeover, social engineering, and mistyped account numbers.
Common Payout Fraud Patterns
Payout fraud takes several predictable forms. Understanding the patterns helps you build defences against each one.
Account takeover. A fraudster gains access to a legitimate user's account (through phishing, credential stuffing, or a data breach). They change the payout bank details to their own account and request a withdrawal. The money goes to the fraudster's bank account.
Social engineering. A fraudster contacts your support team pretending to be a user. They convince the agent to change the payout bank details. The next scheduled payout goes to the fraudster.
Insider fraud. Someone with admin access to your system changes payout details for multiple users, redirecting payouts to accounts they control.
Mistyped account numbers. Not fraud, but just as costly. A user mistypes one digit. The money goes to a stranger's account. Recovery depends on the stranger's goodwill.
Account resolution catches all four patterns by confirming the destination account exists and revealing who owns it before money moves.
Making Account Resolution Mandatory
The simplest and most effective rule: never create a transfer recipient without first resolving the account.
// secure-payout.js
const axios = require('axios');
async function secureCreateRecipient(userId, accountNumber, bankCode) {
// Step 1: Resolve - mandatory, no exceptions
var resolved;
try {
var response = await axios.get(
'https://api.paystack.co/bank/resolve',
{
params: { account_number: accountNumber, bank_code: bankCode },
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY },
timeout: 15000,
}
);
resolved = response.data.data;
} catch (err) {
// If resolution fails for any reason, stop
return { success: false, reason: 'Could not verify account. Try again later.' };
}
// Step 2: Get user profile for comparison
var user = await db.query(
'SELECT first_name, last_name FROM users WHERE id = $1',
[userId]
);
var userName = user.rows[0].first_name + ' ' + user.rows[0].last_name;
// Step 3: Compare names
var nameMatch = compareNames(userName, resolved.account_name);
// Step 4: Log everything
await logResolution(userId, accountNumber, bankCode, resolved.account_name, nameMatch);
return {
success: true,
resolved_name: resolved.account_name,
name_match: nameMatch,
requires_confirmation: !nameMatch.passes,
};
}
No bypass for anyone. Do not allow admin overrides that skip resolution. Do not create "fast path" payout routes that skip verification. Every payout, every time, goes through resolution. If the resolution endpoint is down, payouts wait.
Handling Name Mismatches
When the resolved account name does not match the user's profile name, you have several options depending on the severity of the mismatch.
Close match (first and last name present, different order or with extras): Auto-approve but show the resolved name to the user for confirmation. Log the confirmation.
Partial match (only one name component matches): Flag for manual review. Ask the user to explain the discrepancy. Common legitimate reasons: married name on one record, maiden name on another. Legal name vs. preferred name.
No match (completely different names): Block the payout. This is a strong indicator that the bank account belongs to someone else. Either the user entered the wrong account or someone is trying to redirect funds.
// mismatch-action.js
function decideMismatchAction(matchResult) {
if (matchResult.score >= 0.6) {
return {
action: 'approve_with_confirmation',
message: 'Please confirm this is your account.',
};
}
if (matchResult.score >= 0.3) {
return {
action: 'manual_review',
message: 'The account name differs from your profile. Our team will review this shortly.',
};
}
return {
action: 'block',
message: 'The account name does not match your profile. Please check your bank details.',
};
}
For the details of name comparison implementation, see the fuzzy comparison guide.
Tracking Bank Detail Changes
One of the strongest fraud signals is a change to bank details shortly before a payout request. Track every change and apply rules.
// track-changes.js
async function trackBankDetailChange(userId, oldAccount, newAccount) {
// Log the change
await db.query(
'INSERT INTO bank_detail_changes ' +
'(user_id, old_account_number, old_bank_code, new_account_number, new_bank_code, changed_at) ' +
'VALUES ($1, $2, $3, $4, $5, NOW())',
[userId, oldAccount.number, oldAccount.bankCode, newAccount.number, newAccount.bankCode]
);
// Check change frequency
var recentChanges = await db.query(
'SELECT COUNT(*) as count FROM bank_detail_changes ' +
'WHERE user_id = $1 AND changed_at > NOW() - INTERVAL '30 days'',
[userId]
);
var changeCount = parseInt(recentChanges.rows[0].count);
if (changeCount >= 3) {
// Flag account for review
await flagAccountForReview(userId, 'Frequent bank detail changes: ' + changeCount + ' in 30 days');
return { blocked: true, reason: 'Too many bank detail changes. Account under review.' };
}
// Enforce cooldown after change
return {
blocked: false,
cooldown_hours: 24,
message: 'Bank details updated. Payouts to this account available after 24 hours.',
};
}
24-hour cooldown after every change. When a user changes their bank details, delay payouts to the new account for 24 hours. This gives the legitimate user time to notice if their account was compromised and their bank details changed by an attacker.
Notify on change. Send an email and SMS to the user whenever bank details are changed. If the legitimate user did not make the change, they can act immediately.
First-Payout Hold Period
The first payout to any new bank account should be held for review, regardless of match quality.
Why this works: Fraudsters need speed. They want the money out before anyone notices the account takeover. A 24-hour hold gives the real account owner time to log in, notice something wrong, and alert you. It also gives your fraud team time to review if any flags were raised.
// first-payout-hold.js
async function shouldHoldPayout(userId, recipientCode) {
// Check if we have ever successfully sent money to this recipient
var previousPayouts = await db.query(
'SELECT COUNT(*) as count FROM payouts ' +
'WHERE user_id = $1 AND recipient_code = $2 AND status = $3',
[userId, recipientCode, 'success']
);
if (parseInt(previousPayouts.rows[0].count) === 0) {
return {
hold: true,
hold_until: new Date(Date.now() + 24 * 60 * 60 * 1000),
reason: 'First payout to new account - standard security hold',
};
}
return { hold: false };
}
Communicate clearly. Tell the user: "For your security, first payouts to new bank accounts are reviewed before processing. Your payout will be sent within 24 hours." Users who understand the reason accept the delay.
Skip for trusted users. If a user has Tier 3 KYC verification (BVN) and a long history of clean transactions, consider waiving the first-payout hold. Balance security with user experience.
Admin Controls and Insider Fraud Prevention
Protect against internal threats with access controls and audit logging.
No single person should be able to change bank details and approve payouts. Separate the roles. One person handles user support (can view details). Another person approves flagged payouts. No one can do both.
Log all admin actions. Every time an admin views, edits, or approves a payout or bank detail change, log who did it, when, and from what IP address.
Require dual approval for large payouts. Payouts above a threshold you define should require approval from two different admin accounts. This prevents a single compromised or malicious admin from stealing funds.
Alert on anomalies. If an admin changes bank details for multiple users in a short period, trigger an alert. Normal support operations do not involve bulk bank detail changes.
For more on access controls, see the role-based access guide.
The Complete Fraud-Resistant Payout Flow
Here is the full sequence, combining all the defences discussed:
- User submits bank details. Frontend sends account number and bank code to backend.
- Resolve account. Backend calls Paystack Resolve Account Number. If it fails, stop.
- Compare names. Backend compares resolved name with user profile. Score the match.
- Display resolved name. Frontend shows the resolved name to the user. User confirms or re-enters.
- Check for recent changes. Backend checks if bank details were recently changed. If yes, apply cooldown.
- Check limits. Backend checks per-transaction, daily, and monthly limits based on KYC tier.
- Check first-payout hold. If this is the first payout to this account, apply 24-hour hold.
- Create transfer recipient. If all checks pass, create the Paystack transfer recipient.
- Initiate transfer. After any hold period, initiate the transfer through Paystack.
- Log everything. Record every step in the audit trail.
This flow adds a few seconds for the user and a 24-hour delay on first payouts. In return, it prevents the most common and costly payout fraud patterns.
Key Takeaways
- ✓Resolve every destination account before initiating a transfer. Never send money to an unverified account number.
- ✓Compare the resolved name against the user profile. Mismatches that the user cannot explain are a strong fraud signal.
- ✓Enforce a 24-hour hold on first payouts to new bank accounts. Fraud rarely survives a cooling-off period.
- ✓Track bank detail changes. A user who changes their payout account three times in a week needs investigation.
- ✓Log every resolution, name comparison, and approval decision. Your audit trail is critical for dispute resolution.
- ✓Combine account resolution with payout limits and BVN verification for a layered defence.
Frequently Asked Questions
- Will account resolution catch all payout fraud?
- No. Account resolution catches wrong accounts and name mismatches. It does not catch a fraudster who has stolen both the victim's platform credentials and knows their bank account name. For stronger protection, combine resolution with BVN verification, payout limits, and change-tracking alerts.
- What if Paystack account resolution is slow or down?
- Hold the payout until resolution succeeds. Do not bypass resolution to speed things up. A few minutes of delay is better than sending money to an unverified account. Implement retry logic with exponential backoff for temporary failures.
- Should I resolve the account on every payout or just the first one?
- Resolve on the first payout to a new account and whenever bank details change. For repeat payouts to an already-verified account, you can reuse the cached resolution. Re-resolve periodically, perhaps monthly, to catch accounts that have been closed or changed ownership.
- How do I handle a user who legitimately sends payouts to someone else's account?
- This is valid for business use cases like paying suppliers or employees. In these cases, the name mismatch is expected. Build a separate flow for third-party payouts that requires the user to confirm the recipient name and accepts mismatches with explicit user acknowledgment. Log the acknowledgment.
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