AhmadRaza365 Logo

AhmadRaza365

Blog Post

Fraud Prevention for Online Stores: Practical Patterns for MERN + Stripe Radar + Rules

April 2, 2026
Fraud Prevention for Online Stores: Practical Patterns for MERN + Stripe Radar + Rules

Fraud isn’t just “chargebacks happen sometimes”. It’s an engineering problem: attackers probe your checkout like a buggy API until they find a gap.

A reliable anti-fraud setup for a MERN ecommerce store is layered:

  • Frictionless first: better signals + smarter rules (not CAPTCHA everywhere)
  • Step-up only when needed: 3DS / additional verification based on risk
  • Idempotent, observable payment flows so you can trust your data
  • A small internal rules engine + Stripe Radar so you’re not blind

This post is the practical playbook I install on client stores: what to log, what to block, what to review, and how to wire it into Stripe cleanly.


1) Think in “attack surfaces”, not “fraud features”

Most stores leak money because fraud controls are bolted on after the fact. Start by mapping where attackers interact:

  • Account creation + login (credential stuffing)
  • OTP/SMS abuse
  • Address changes + gift cards
  • Checkout (card testing, BIN attacks, stolen cards)
  • Refund flow (refund to different instrument, social engineering)
  • Webhooks (spoofing, replay, race conditions)

Your goal is simple:

Make the cheapest attacks unprofitable, and push high-risk attempts into review/3DS.


2) Baseline hygiene (you’d be surprised how often these are missing)

Before complex ML-style scoring, do the basics.

Rate limits + bot controls

  • Rate limit login, signup, OTP, checkout session creation.
  • Block known bad ASN/hosting ranges (optional, but helpful at scale).
  • Put checkout behind Cloudflare / WAF rules if you’re getting hammered.

Implementation (Express + rate-limit):

import rateLimit from 'express-rate-limit';

export const checkoutLimiter = rateLimit({
  windowMs: 60_000,
  max: 20,
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => req.ip, // also consider req.userId when logged in
});

app.post('/api/checkout/create', checkoutLimiter, handler);

Never trust client pricing

Fraud isn’t only stolen cards. Price tampering is also fraud.

  • Recompute totals server-side.
  • Re-validate inventory.
  • Store a cart snapshot (SKU, qty, unit price, discounts applied) as evidence.

3) Collect the right signals (without creeping out customers)

If you can’t measure risk, you’ll either block good customers or approve bad ones.

Signals I always capture for an order/payment attempt

  • ip, userAgent, acceptLanguage
  • country (from GeoIP), timezoneOffset
  • email, phone, email domain reputation (basic)
  • billingCountry, shippingCountry, mismatch flag
  • isFirstOrder, accountAgeDays, ordersCount
  • Velocity features (attempts per IP/email/card fingerprint)
  • Stripe signals (Radar risk level, risk score, rule outcomes)

Store it as a risk_snapshot

Mongo document idea:

{
  orderId,
  userId,
  ip,
  userAgent,
  geo: { country, city },
  accountAgeDays,
  isFirstOrder,
  mismatch: { billingShipping: true },
  velocity: { ip1h: 7, email24h: 3 },
  stripe: { riskLevel: 'elevated', riskScore: 62, rule: 'block_if_high_risk' },
  createdAt
}

Add indexes to support fast “velocity” queries:

await db.collection('risk_snapshots').createIndex({ ip: 1, createdAt: -1 });
await db.collection('risk_snapshots').createIndex({ email: 1, createdAt: -1 });
await db.collection('risk_snapshots').createIndex({ createdAt: -1 });

4) Stripe Radar: use it as a signal + action layer

Stripe Radar is strong, but you should integrate it thoughtfully.

What I recommend using from Radar

  • Risk level / score: on the Charge outcome (outcome.risk_level, outcome.risk_score)
  • Rule outcomes: which rule matched / why it was blocked
  • 3DS routing: step-up authentication when risk is elevated

Don’t treat Stripe as your only decision-maker

Why?

  • Fraud can happen before payment (promo abuse, COD abuse, refund abuse).
  • Your business context matters (high-value SKUs, shipping risk regions, repeat customer trust).

Use Stripe to reduce card-related fraud, but keep a store-level risk policy too.


5) A simple “rules engine” that works (and is explainable)

I like rule-based scoring because it’s debuggable.

Example: compute a risk score (server-side)

export function computeRisk({ accountAgeDays, isFirstOrder, mismatch, velocity, orderValue }) {
  let score = 0;

  if (isFirstOrder) score += 15;
  if (accountAgeDays < 7) score += 10;
  if (mismatch.billingShipping) score += 20;
  if (velocity.ip1h >= 5) score += 25;
  if (orderValue > 30000) score += 10; // PKR example

  return score;
}

export function riskDecision(score) {
  if (score >= 60) return 'BLOCK';
  if (score >= 35) return 'REVIEW';
  return 'ALLOW';
}

What to do for each decision

  • ALLOW: proceed normally.
  • REVIEW: allow payment but hold fulfillment; or require step-up (3DS / OTP).
  • BLOCK: don’t create a payment attempt, or cancel immediately.

Important: blocking should be clean and fast. Don’t create partial “paid but cancelled” mess.


6) Step-up authentication (3DS) without killing conversions

3DS is useful, but if you force it for everyone your conversion rate will drop.

Recommended: dynamic 3DS

  • Require 3DS for high risk.
  • Allow frictionless for repeat low-risk customers.

Operationally, your job is:

  • Capture requires_action properly on the frontend.
  • Make the flow idempotent so retries don’t duplicate orders.

7) Webhooks: fraud controls must be replay-safe

Fraud + payments live in async land: webhooks will retry and arrive out-of-order.

Minimal safe webhook requirements:

  • Verify signature (raw body)
  • Deduplicate by event ID
  • Process via a queue (BullMQ)
  • Make order finalization idempotent

If your webhook handler directly “marks order paid” without dedupe/queue, attackers (and reality) will eventually break it.


8) Post-payment controls: fulfillment is where you actually lose money

Card fraud isn’t the only loss. The real loss happens when you ship.

My standard fulfillment holds

Hold fulfillment for manual review when:

  • First order + high order value
  • Billing/shipping mismatch + high risk
  • Unusually high quantity of the same SKU
  • High velocity from same IP/email

What reviewers need (build this into admin)

  • Risk snapshot + Stripe risk signals
  • Customer history (orders count, refunds, chargebacks)
  • Address normalization + phone
  • Device/IP history

A small “Review Queue” in admin usually pays for itself quickly.


9) Practical starter rules (copy/paste policy)

Good defaults (adjust to your market):

Block

  • 10+ checkout attempts from same IP in 10 minutes
  • Known disposable email domains (basic list)
  • Shipping to high-risk region + first order + high value (your internal list)

Review

  • Billing/shipping mismatch + first order
  • Order value above your 90th percentile + account age < 30 days
  • Repeated failed payments then a success

Allow

  • Returning customer with 2+ successful fulfilled orders
  • Low order value + low velocity

10) The metric that matters: fraud loss rate vs conversion

Measure both. Always.

Track monthly:

  • Chargeback rate (count and value)
  • Fraud blocks (count, false positives)
  • Review approval rate
  • Conversion drop due to step-up

If you can’t see false positives, you’ll accidentally punish good customers.


Closing: I can help you harden fraud without destroying conversion

If you’re seeing chargebacks, card testing spikes, or suspicious checkout activity, a focused hardening sprint usually fixes it fast.

I’ll help you:

  • implement layered controls (rate limits → rules → step-up),
  • wire Stripe Radar signals into your admin decisions,
  • and add the monitoring that tells you what’s working.

Send me your stack + a few example chargebacks and I’ll point out the biggest gaps.

You can find me on different platforms