Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

Building Your Own Chargeback and Fraud Ratio Monitor Before the Networks Do

Most fraud tooling answers one question: should we accept this order? Card networks care about a different question: how many of your accepted orders came back as fraud? That second number decides whether your merchant a

Most fraud tooling answers one question: should we accept this order? Card networks care about a different question: how many of your accepted orders came back as fraud? That second number decides whether your merchant account ends up in a monitoring program, with reserves, repricing, or termination as the consequences.

Second-party fraud is where these two questions come apart. A real cardholder hands their credentials to someone else, or colludes with them. The order passes AVS, passes CVV, and the account history looks clean. Your risk model approves it. Weeks later, the cardholder reports the charge as unauthorized. A fraud-coded dispute arrives, and it counts against your ratios the same way a stolen-card order would.

You can't block what your model can't see. You can, however, measure your exposure faster and more accurately than your processor's monthly report does. This article walks through how to build that measurement.

What the networks are actually counting

The exact programs and thresholds change every few years, so treat your acquirer's current documentation as the source of truth. The structure, though, is fairly stable.

  • Numerator: fraud-related events tied to your merchant ID during a reporting period. Depending on the program, this includes fraud-coded disputes (for example, Visa reason code 10.4 for card-absent fraud, or Mastercard 4837), and it may also include issuer fraud reports that never became disputes.
  • Denominator: your settled transaction count for a period. This is usually not the same period in which the original transactions happened.
  • Thresholds: usually a ratio plus a minimum absolute count, so very small merchants don't get flagged over two disputes.

Visa's current program, the Visa Acquirer Monitoring Program (VAMP), combines issuer fraud reports and disputes into a single ratio. Mastercard runs separate programs for excessive chargebacks and excessive fraud. Check the current thresholds before you hardcode anything.

The detail that catches teams off guard

Issuer fraud reports (Visa TC40, Mastercard SAFE) are filed when a cardholder tells their bank a transaction was fraudulent. A fraud report can exist even if you refunded the order and no chargeback was ever filed. If your program counts fraud reports, then refunding a suspicious order early may prevent the dispute and its fee, but it won't remove the fraud report from your ratio.

For second-party fraud this matters a lot. The cardholder is the one claiming fraud, so the fraud report is almost guaranteed.

Data model

You need three event streams joined on transaction ID:

transactions(txn_id, mid, network, settled_at, amount, card_fingerprint)
disputes(dispute_id, txn_id, network, reason_code, received_at, category)
fraud_reports(report_id, txn_id, network, reported_at, fraud_type)

Normalize reason codes into a category column (fraud, consumer, processing, authorization) at ingestion time. Raw codes differ by network and change over time. Mapping them once in a lookup table keeps the downstream queries readable.

Fraud reports often come from your acquirer as a file or report rather than a webhook. Ingest them anyway. If you only track disputes, you will undercount your exposure under a combined-ratio program.

Computing the ratio the way the network does

The most common mistake is cohort mismatch: dividing this month's disputes by the transactions those disputes came from. Networks typically divide events received in a month by transactions settled in that same month (or the prior month, depending on the program). Disputes lag transactions by weeks, so the two methods give very different numbers during growth or decline.

from collections import defaultdict

def monthly_ratios(transactions, disputes, fraud_reports, mid, network):
    txn_count = defaultdict(int)
    for t in transactions:
        if t.mid == mid and t.network == network:
            txn_count[t.settled_at.strftime('%Y-%m')] += 1

    events = defaultdict(int)
    for d in disputes:
        if d.network == network and d.category == 'fraud' and belongs_to(d.txn_id, mid):
            events[d.received_at.strftime('%Y-%m')] += 1
    for r in fraud_reports:
        if r.network == network and belongs_to(r.txn_id, mid):
            events[r.reported_at.strftime('%Y-%m')] += 1

    # NOTE: whether a txn with both a fraud report and a dispute
    # counts once or twice is program-specific. Make it a config flag.
    return {
        month: {
            'events': events[month],
            'txns': txn_count[month],
            'ratio': events[month] / txn_count[month] if txn_count[month] else None,
        }
        for month in txn_count
    }

Keep the deduplication rule configurable. Some programs count a transaction that has both a fraud report and a dispute as two events. Others count it once. Getting this wrong can make you look twice as healthy as you are.

Worked example: a small ring, a big swing

Assume a merchant processes 20,000 Visa card-not-present transactions per month. Their baseline is steady: about 40 fraud-coded events a month, a ratio of 0.20%.

Now a second-party fraud pattern starts. A handful of cardholders sell their card details to a reshipper, then file fraud claims once the goods arrive. Over three weeks, 90 orders go out. Every one passes AVS and CVV.

Here's how it plays out:

Month Settled txns Baseline events Ring events received Ratio
M1 (orders ship) 20,000 40 0 0.20%
M2 20,000 40 70 0.55%
M3 20,000 40 20 0.30%

The dashboard looked clean in M1, which is when the damage was actually done. If the network counts fraud reports and disputes separately for the same transaction, the M2 ring contribution could approach 140 events, putting the ratio near 0.90%.

No single month here necessarily crosses a threshold. But a merchant already running near the limit, or one with seasonal volume dips that shrink the denominator, can tip over from one small ring. Projection matters more than the current reading.

Projecting forward

Because disputes lag, you can estimate next month's events from orders you've already shipped. Use your historical dispute-lag distribution:

def projected_events(shipped_orders, lag_curve, risk_score_fn):
    # lag_curve[k] = share of eventual fraud events arriving k months after settlement
    # risk_score_fn = estimated probability that an order becomes a fraud event
    projection = defaultdict(float)
    for o in shipped_orders:
        p = risk_score_fn(o)
        for k, share in enumerate(lag_curve):
            month = add_months(o.settled_at, k).strftime('%Y-%m')
            projection[month] += p * share
    return projection

For second-party fraud, a generic fraud score helps little, since these orders score as clean. Use signals that show up after authorization instead:

  • The shipping address appears across several unrelated cardholders.
  • A new device places an order on an account whose card history shows no prior use from that device.
  • Card fingerprints cluster with known reshipper or freight-forwarder addresses.
  • Several first-time orders on established cards land on the same delivery address within days.

Any of these can raise p for orders already in flight. That gives you an early estimate of the incoming spike.

Alerting rules that are actually useful

Alert on trajectory, not just level.

Signal Rule Why
Current ratio > 60% of program threshold Leaves room to act before the month closes
Projected ratio Next month > 80% of threshold Catches lagged spikes like the M2 above
Absolute count Within 20% of the program's minimum count Small merchants get flagged on count first
Denominator drop Settled txns down > 25% month over month Same events, smaller base, higher ratio
Cluster detection ≥ N fraud events sharing a ship-to address Second-party rings often reuse addresses

Tune the percentages to your risk tolerance. The point is to page someone while there's still time to tighten rules on in-flight orders or talk to your acquirer.

Common mistakes

Aggregating across MIDs. Networks evaluate per merchant ID (and sometimes per acquirer). A healthy blended ratio can hide one MID that's in trouble.

Using order count instead of transaction count. Split shipments, partial captures, and recurring billing create multiple transactions per order. Count what the network counts.

Netting out refunds. A refunded transaction still settled. Whether it stays in the denominator depends on the program, so don't assume.

Treating won disputes as removed. Winning representment recovers funds. It generally does not remove the dispute from monitoring counts. Verify this for your programs, but don't build your projections on the assumption that wins reduce the ratio.

Ignoring reason-code drift. Issuers sometimes file consumer-type claims under fraud codes, and the reverse. Track the raw code alongside your normalized category so you can audit the mapping.

Testing the monitor

Build a fixture month with known values and assert against them. Include:

  • A transaction with both a fraud report and a dispute, to test the dedup flag.
  • A dispute received in a month after its transaction settled, to test cohort logic.
  • A refunded transaction that still received a fraud report.
  • Two MIDs, to make sure nothing leaks across them.

Then reconcile your computed ratio with your acquirer's monthly report. If they differ by more than rounding, your definitions are wrong. Find out which definition is wrong before an auditor does.

Where this fits

Monitoring won't stop second-party fraud. The orders look legitimate by design, and the liability usually stays with the merchant. What monitoring does is turn a surprise enrollment letter into a trend you saw coming weeks earlier. For the broader picture of why these losses land where they do, see this breakdown of second-party fraud liability.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.