DEV Community

Cover image for Building a First-Party Trust Eligibility Checker for Mastercard Disputes
Jarrod Wright for Chargebacks911

Posted on

Building a First-Party Trust Eligibility Checker for Mastercard Disputes

Mastercard's First-Party Trust rule lets a merchant's own transaction history defeat a "No Cardholder Authorization" chargeback (reason code 4837). The idea: if the same cardholder bought from you before, and identity signals from those earlier purchases match the disputed one, the chargeback is rejected and liability shifts to the issuer.

The network makes the final decision. You still need your own model of it. With an internal eligibility checker you can:

  • Predict which incoming 4837 disputes should bounce, and flag the ones that didn't.
  • Find gaps in your data capture before a dispute arrives, not after.
  • Answer evidence requests quickly if you use the on-dispute integration path.

This article covers how to design that checker: the data model, the matching algorithm, the edge cases that quietly break eligibility, and how to test it. For the program basics (regions, integration routes, and what issuers can do after a reject), see Mastercard First-Party Trust program requirements and data elements.

The rule, expressed as constraints

Strip away the business language and the rule comes down to a few constraints:

Constraint Value
Applicable dispute Reason code 4837 only (37 on Debit Mastercard Single Message System)
Prior transactions required At least 2
Prior transaction age 121 to 365 days before the disputed transaction
Prior transaction status Not reported as fraud
Matching elements required 3 total
Category requirement At least one element from each of two core categories, plus a third from the core set or an additional list

Public descriptions of the core categories group them roughly as device signals (device fingerprint, IP address) and contact/delivery signals (shipping address, email address). Additional elements include login ID, phone number, device location, device name, billing address, and a generic "additional factor."

Design decision #1: do not hardcode these lists. Put the categories, the window, and the minimum counts in config. Programs like this get revised, and your checker should change with a config update, not a deploy.

fpt_rules:
  reason_codes: ['4837', '37']
  window_days: { min: 121, max: 365 }
  min_prior_txns: 2
  min_total_elements: 3
  core_categories:
    device: [device_fingerprint, ip_address]
    contact: [shipping_address, email]
  additional: [login_id, phone, device_location, device_name, billing_address]
Enter fullscreen mode Exit fullscreen mode

The data model

Your checker only works with data you kept. For every card-not-present transaction, persist an evidence record:

evidence_record
  txn_id            PK
  card_fingerprint  -- tokenized PAN reference, never raw PAN
  txn_timestamp_utc
  device_fingerprint
  ip_address
  shipping_address_normalized
  email_normalized
  login_id
  phone_e164
  billing_address_normalized
  device_name
  device_location
  fraud_flag        -- set if this txn was later reported/charged back as fraud
  fraud_flag_source
Enter fullscreen mode Exit fullscreen mode

Three practical points:

  1. Retention must exceed 365 days plus your dispute window. A dispute can arrive months after the disputed purchase, and the lookback runs from the disputed transaction date. If your evidence table is pruned at 12 months, your oldest eligible priors disappear exactly when you need them.
  2. Key on a card reference, not a customer ID. The rule is about the cardholder's card. Guest checkouts often have no customer ID at all.
  3. Store both raw and normalized values. Normalize for your own matching, but keep what you actually sent to the network so you can audit any differences.

The matching algorithm

from datetime import timedelta

def check_eligibility(disputed, history, rules):
    if disputed.reason_code not in rules.reason_codes:
        return Result(eligible=False, reason='reason_code_not_covered')

    lo = disputed.txn_ts - timedelta(days=rules.window_max)
    hi = disputed.txn_ts - timedelta(days=rules.window_min)

    candidates = [
        t for t in history
        if t.card_ref == disputed.card_ref
        and lo <= t.txn_ts <= hi
        and not t.fraud_flag
    ]
    if len(candidates) < rules.min_prior_txns:
        return Result(eligible=False, reason='insufficient_priors',
                      detail=len(candidates))

    # An element counts only if it matches across ALL chosen priors.
    # Try every pair, since different pairs can match different elements.
    best = None
    for a, b in pairs(candidates):
        matched = {
            el for el in rules.all_elements
            if present(disputed, el)
            and eq(disputed, a, el) and eq(disputed, b, el)
        }
        if satisfies(matched, rules):
            return Result(eligible=True, priors=(a.txn_id, b.txn_id),
                          elements=sorted(matched))
        best = max(best, matched, key=score)

    return Result(eligible=False, reason='insufficient_matches',
                  detail=best)

def satisfies(matched, rules):
    has_device = matched & set(rules.core['device'])
    has_contact = matched & set(rules.core['contact'])
    return bool(has_device) and bool(has_contact) \
        and len(matched) >= rules.min_total_elements
Enter fullscreen mode Exit fullscreen mode

The reason and detail fields matter as much as the boolean. "Insufficient priors" points to a customer-history problem. "Insufficient matches" with no device element points to a data-capture problem. Those are different fixes.

The pairwise search grows quadratically, but candidate sets per card are small. If you have high-frequency subscribers, cap the candidates (for example, the most recent N in the window) and record that you did so.

Worked example

A dispute arrives on a transaction dated 2025-09-10. The window runs from 2024-09-10 to 2025-05-12.

Txn Date Device FP IP Email Ship addr Fraud flag
D (disputed) 2025-09-10 fp_9a1 203.0.113.7 a.lee@x.com 12 Elm St -
P1 2025-08-01 fp_9a1 203.0.113.7 a.lee@x.com 12 Elm St no
P2 2025-03-02 fp_9a1 198.51.100.4 a.lee@x.com 12 Elm St no
P3 2024-11-15 fp_9a1 198.51.100.9 a.lee@x.com 12 Elm St no
P4 2024-10-01 fp_77c 198.51.100.4 a.lee@x.com 12 Elm St yes
  • P1 is excluded because it is too recent (40 days).
  • P4 is excluded because of its fraud flag.
  • The candidates are P2 and P3.
  • Elements matching across D, P2, and P3: device fingerprint, email, and shipping address. IP fails because it changed every time.
  • The device and contact categories are both covered, with 3 elements in total. Eligible.

Note that P1 is the best match in the table and it doesn't count. Teams that eyeball "recent purchase, same everything" get this wrong all the time.

Edge cases that silently break eligibility

Window boundaries and time zones

Is "121 days" counted in calendar days, by settlement date, or by authorization timestamp? Pick one, document it, and put it in config. Store timestamps in UTC. A purchase at 23:30 local time can land on a different UTC date and fall outside the window by one day. Treat transactions within ±1 day of either boundary as "uncertain" in your reporting.

IP address instability

Mobile carriers, carrier-grade NAT, and VPNs make IP the weakest element. Don't plan around it. If your device-category coverage depends on IP, invest in device fingerprinting instead.

Device fingerprint rotation

Browser updates, cleared storage, and privacy features can rotate fingerprints. Track your fingerprint vendor's stability. A sudden drop in device-element matches across all customers usually means the fingerprint changed, not the customers.

Normalization mismatches

12 Elm St. vs 12 Elm Street, Apt 0, and A.Lee+shop@x.com vs a.lee@x.com. Your normalizer decides what your checker sees as a match, but the network applies its own logic. Normalize conservatively (case, whitespace, standard address abbreviations). Don't strip email sub-addressing unless you have confirmed how the network treats it.

Late-arriving fraud flags

A prior transaction can be clean today and get disputed as fraud next month. Recompute eligibility when a dispute arrives, not when the purchase happens. Also, your fraud flag only reflects what you know. Issuer-side fraud reports you never saw could still disqualify a prior.

Card reissue

If the cardholder's card was reissued, a tokenized card reference may not link old and new transactions. Check whether your token provider keeps a stable reference across reissues.

Deletion requests

Privacy-driven deletion can remove the exact evidence records you depend on. Define how deletion interacts with dispute-evidence retention, with input from legal counsel. Don't leave it to whichever job runs first.

Pre-auth vs. on-dispute: engineering implications

There are two participation paths, and they need different systems:

Concern Pre-auth (data shared at checkout) On-dispute (data requested later)
Where data lives Sent to the network at transaction time In your systems until requested
Latency budget Inside the checkout path, so it must be fast Asynchronous, but has a response deadline
Main failure mode Missing fields at checkout are permanent Retention gaps, slow lookups
Your checker's role Monitoring coverage, predicting outcomes Directly drives the API response

With the pre-auth path, the most valuable metric is field completeness at checkout (for example, the percentage of CNP transactions with a non-null device fingerprint). With the on-dispute path, it is lookup reliability: the evidence service needs the same availability standards as payments, because a missed deadline means the dispute moves on without you.

Testing the checker

  • Boundary fixtures: priors at day 120, 121, 365, and 366, in multiple time zones.
  • Category fixtures: three matches all from one category should fail.
  • Fraud flag fixtures: two clean priors plus one flagged prior, where only the flagged one matches.
  • Property tests: adding a non-matching prior never changes eligible to ineligible. Setting the fraud flag on a used prior never makes the result more eligible.
  • Shadow reconciliation: compare predicted outcomes against actual 4837 results each month. Every disagreement is either a checker bug or a rule interpretation you need to revisit.

Keep expectations honest

Your checker is a model of a rule you don't execute. It is useful for finding data gaps and spotting anomalies. It is not a guarantee. Confirm the current element definitions, window semantics, and regional availability with your acquirer and Mastercard's documentation. Keep every one of those in config, so the next rule revision is a one-line change.

Top comments (0)

⌂ 换网址