Bonaventure OgetoBy Bonaventure Ogeto|

Velocity Checks and Card Testing Attack Defence

Card testing attacks show up as many rapid charge attempts with small amounts (NGN 100–500) across different email addresses from the same IP. Defend by: (1) rate limiting your checkout endpoint per IP (max 5 transactions per minute), (2) blocking transactions with amounts below a threshold (e.g., NGN 500) unless explicitly supporting small amounts, (3) checking for multiple failed charges from the same IP within 10 minutes, and (4) requiring CAPTCHA after 2 failed charges from the same session.

Recognizing the Card Testing Attack Pattern

A card testing attack in your transaction logs looks like:

  • Dozens to hundreds of transactions in 1-5 minutes
  • All from the same IP address or a small IP range
  • Different email addresses (often random: abc123@gmail.com, xyz456@gmail.com)
  • Small amounts: NGN 100, NGN 50, NGN 200
  • Mostly failed with responses like "Do Not Honor" or "Insufficient Funds"

The attacker finds which cards succeed, then uses those for larger fraudulent transactions elsewhere.

Velocity Check Implementation

// Middleware: rate limit checkout by IP
var redis = require('redis');
var client = redis.createClient();

async function velocityCheck(req, res, next) {
  var ip = req.ip;
  var key = 'checkout:' + ip;
  var windowSeconds = 60;
  var maxAttempts = 5;

  var count = await client.incr(key);
  if (count === 1) {
    await client.expire(key, windowSeconds);
  }

  if (count > maxAttempts) {
    return res.status(429).json({
      error: 'Too many payment attempts. Please try again later.',
    });
  }

  next();
}

// Track failed charges by IP
async function trackFailedCharge(ip) {
  var key = 'failed:' + ip;
  var count = await client.incr(key);
  if (count === 1) {
    await client.expire(key, 600); // 10-minute window
  }

  if (count >= 3) {
    // Block this IP for 1 hour
    await client.setex('blocked:' + ip, 3600, '1');
    // Alert your team
    console.error('Card testing suspected from IP:', ip, 'Failed attempts:', count);
  }
}

// Check webhook for failed charges
if (event.event === 'charge.failed') {
  var ip = event.data.ip_address;
  if (ip) await trackFailedCharge(ip);
}

Learn More

Key Takeaways

  • Card testing attacks use many rapid small-amount charges to find valid cards. They cause transaction fees even on failures.
  • Rate limit your checkout initiation endpoint: max 5 transactions per IP per minute using Redis or in-memory counters.
  • Require CAPTCHA (reCAPTCHA v3 or hCaptcha) on your checkout page to add friction for bots.
  • Block IPs with more than 3 failed charges within 10 minutes — store failed attempts in Redis with TTL.
  • Monitor for the pattern: same IP, different emails, small amounts, rapid succession. Alert on this in real time.
  • Paystack's own fraud detection also helps — but your layer of protection reduces your cost from failed attempt fees.

Frequently Asked Questions

Does Paystack detect and block card testing attacks automatically?
Paystack has its own fraud detection layer that catches many card testing patterns. However, you still pay transaction fees for failed attempts that Paystack processes before blocking. Adding your own rate limiting and velocity checks reduces the number of attempts that reach Paystack at all.
Will rate limiting block legitimate customers?
A limit of 5 transaction attempts per minute per IP is very unlikely to affect legitimate customers — normal checkout takes 1-2 minutes. Use exponential backoff in your limits (5/min → 10/10min → 20/hour) so legitimate retries still work while bots are blocked.
How do I detect card testing without Redis?
Use your database. Create a failed_attempts table with (ip_address, created_at). Query for attempts in the last 10 minutes on each checkout. Add a database index on (ip_address, created_at). Redis is faster but the database approach works for low-to-medium traffic.
What is the financial cost of a card testing attack on my Paystack account?
Paystack charges a transaction processing fee even on failed transactions in some configurations. A 500-attempt attack at NGN 30 per attempt costs NGN 15,000 in fees. Beyond direct cost, repeated fraud can trigger account review or suspension by Paystack. Defend early.

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