The Visa Acquirer Monitoring Program (VAMP) ratio fits on a napkin: fraud reports plus disputes, divided by transactions. It is easy to get the formula right and the number wrong. A monitor that is off by a few basis points can show an acquirer as safe when it is not.
This article walks through how to compute the ratio correctly. It covers the merchant-level calculation, the portfolio rollup, a what-if model for remediation, and the mistakes that most often corrupt the number. For the business context, including why acquirers now carry this risk and how little many merchants know about it, see The VAMP Awareness Gap: Financial Institutions Need to Know About Major Changes to Visa Disputes.
The formula and what each term means
VAMP ratio = (TC40 count + TC15 count) / settled transaction count
- TC40: an issuer-reported fraud notification. It does not have to lead to a chargeback. The cardholder may say "that wasn't me" and the issuer files a TC40 even if no dispute follows.
- TC15: a dispute (chargeback) notification.
- Denominator: settled transactions for the period. VAMP focuses on card-not-present activity, so the denominator should match that scope. Check the current Visa rules for the exact definition rather than assuming.
The key detail: one fraudulent transaction that is also charged back counts twice. It produces one TC40 and one TC15. Nobody made a mistake. That is how the metric works, and a pipeline that deduplicates across the two types will understate the ratio.
Worked example: one merchant, one month
| Event | TC40 | TC15 |
|---|---|---|
| 40 fraud reports, no chargeback | 40 | 0 |
| 60 fraud reports that became chargebacks | 60 | 60 |
| 50 non-fraud disputes (e.g., item not received) | 0 | 50 |
| Total | 100 | 110 |
With 20,000 settled CNP transactions, the ratio is 210 / 20,000 = 1.05%.
If you count unique problem transactions instead (150), you get 0.75%. That is a 30-basis-point understatement, enough to hide a merchant that is drifting toward trouble.
The portfolio rollup
Acquirers are measured on the whole portfolio, not on any single merchant. The portfolio ratio is a volume-weighted sum. It is not the average of the merchant ratios.
portfolio_ratio = SUM(tc40 + tc15) / SUM(settled_txns)
Worked example: four merchants
| Merchant | Settled txns | Ratio | TC40+TC15 |
|---|---|---|---|
| A (low-risk retail) | 2,000,000 | 0.10% | 2,000 |
| B | 500,000 | 0.30% | 1,500 |
| C (subscription) | 300,000 | 1.20% | 3,600 |
| D (marketplace) | 200,000 | 1.40% | 2,800 |
| Portfolio | 3,000,000 | 0.33% | 9,900 |
No merchant breaches the 1.5% merchant threshold. Yet the portfolio sits at 0.33%, above the 0.3% level that acquirer tiers can start at.
Merchants C and D account for 16.7% of volume but 64.6% of the counts. This concentration is the normal pattern, and your tooling should surface it directly.
Data model and pipeline
You need three feeds, each keyed to a merchant and a reporting period:
- Settled transactions: merchant ID, settlement date, card-present/CNP flag, network.
- TC40 records: merchant ID, report date, ARN (acquirer reference number), fraud type.
- TC15 records: merchant ID, dispute date, ARN, reason code.
The pipeline flows like this:
raw feeds -> normalize merchant IDs -> dedupe within type -> bucket by period
-> merchant_month aggregates -> portfolio_month aggregate -> alerts
Dedupe within a type, never across types
Network files sometimes resend the same record. Deduplicate TC40s against other TC40s, and TC15s against other TC15s, using the ARN plus a type-specific key. A TC40 and a TC15 that share an ARN are both legitimate counts.
WITH tc40 AS (
SELECT DISTINCT ON (arn, fraud_report_id)
merchant_id, date_trunc('month', report_date) AS period
FROM raw_tc40
),
tc15 AS (
SELECT DISTINCT ON (arn, dispute_case_id)
merchant_id, date_trunc('month', dispute_date) AS period
FROM raw_tc15
WHERE NOT excluded_from_vamp -- flag set per current Visa rules
),
counts AS (
SELECT merchant_id, period, COUNT(*) AS n FROM tc40 GROUP BY 1, 2
UNION ALL
SELECT merchant_id, period, COUNT(*) FROM tc15 GROUP BY 1, 2
),
txns AS (
SELECT merchant_id, date_trunc('month', settle_date) AS period,
COUNT(*) AS settled
FROM settlements
WHERE network = 'VISA' AND channel = 'CNP'
GROUP BY 1, 2
)
SELECT t.merchant_id, t.period, t.settled,
COALESCE(SUM(c.n), 0) AS vamp_count,
COALESCE(SUM(c.n), 0)::numeric / NULLIF(t.settled, 0) AS vamp_ratio
FROM txns t
LEFT JOIN counts c USING (merchant_id, period)
GROUP BY t.merchant_id, t.period, t.settled;
Note the excluded_from_vamp flag. Some dispute resolution paths may be treated differently under current rules. Put that logic in one place and keep it configurable. Do not hardcode it into the query.
Modeling remediation: marginal impact
The most useful question a risk team can ask is: "If we fix or exit merchant X, where does the portfolio land?"
def portfolio_without(merchants, exclude_id):
rest = [m for m in merchants if m["id"] != exclude_id]
count = sum(m["vamp_count"] for m in rest)
txns = sum(m["settled"] for m in rest)
return count / txns
def ranked_impact(merchants):
base = sum(m["vamp_count"] for m in merchants) / sum(m["settled"] for m in merchants)
return sorted(
((m["id"], base - portfolio_without(merchants, m["id"])) for m in merchants),
key=lambda x: x[1], reverse=True,
)
Applied to the example:
- Removing D moves the portfolio from 0.33% to 7,100 / 2,800,000 = 0.254%.
- Removing C gives 6,300 / 2,700,000 = 0.233%.
Removing merchant A would actually raise the ratio, because its clean volume dilutes everyone else's counts. Rank merchants by marginal impact, not by raw ratio. A small merchant with a terrible ratio may matter less than a mid-size one that is merely bad.
A more realistic model reduces a merchant's counts by an expected remediation rate instead of deleting the merchant. The same function works with adjusted counts.
Internal alert tiers
Visa's thresholds are a ceiling. Internal tiers give you time to act before you reach it. Keep every number in config, because the network adjusts thresholds over time.
merchant_tiers:
watch: { ratio: 0.0075, min_count: 100 }
action: { ratio: 0.0100, min_count: 300 }
critical: { ratio: 0.0130, min_count: 750 }
visa_limit: { ratio: 0.0150, min_count: 1500 }
portfolio_tiers:
watch: { ratio: 0.0025 }
action: { ratio: 0.0030 }
| Tier | Trigger | Typical response |
|---|---|---|
| Watch | Ratio and count over watch values, 1 month | Notify merchant, expose ratio in dashboard |
| Action | Over action values, or watch for 2+ months | Remediation plan, increased reserve or pricing review |
| Critical | Over critical values | Weekly review, volume caps |
| Visa limit | Over network threshold | Formal program handling; consider exit if no progress |
The min_count gate matters. Without it, a merchant with 50 transactions and 2 disputes trips a 4% alarm every month and buries real signals. Visa applies a similar count minimum at the merchant level, so a ratio-only alert does not match how the program works.
Projecting mid-month
Waiting for month-end means alerting after the damage is done. Project forward instead.
def projected_ratio(count_mtd, settled_mtd, day, days_in_month, lag_factor=1.0):
# lag_factor > 1 compensates for TC40/TC15 that arrive after the transaction
return (count_mtd * lag_factor) / settled_mtd
Fraud reports and disputes trail the original transaction, often by weeks. Early in the month the counts look artificially low. Calibrate lag_factor per merchant category from your own history: compare day-N counts to the final monthly count across past periods.
Common mistakes and how to catch them
1. Deduping across TC40 and TC15. Write a test fixture with a single fraud chargeback and assert that it counts as 2.
2. Wrong denominator. Including card-present transactions, other networks, or authorizations instead of settlements all inflate the denominator and deflate the ratio. Reconcile your settled-transaction count against acquirer settlement reports every month.
3. Averaging ratios. AVG(vamp_ratio) across merchants is meaningless at portfolio level. Add a test where one large clean merchant and one small dirty merchant produce different weighted and unweighted results, and assert you return the weighted one.
4. Merchant ID fragmentation. One business with several MIDs can look healthy across each MID individually. Maintain a merchant-entity mapping and compute ratios at both levels.
5. Period misalignment. Counts are bucketed by report date, while transactions are bucketed by settlement date. That mismatch is inherent to the metric, but your periods must use the same timezone and cutoff as the network's reporting. Off-by-one-day bucketing at month boundaries is a frequent source of discrepancies.
6. Silent feed gaps. A missing TC40 file produces a beautiful ratio. Alert on volume anomalies in the inputs, such as zero records for a merchant that normally has dozens, before trusting the output.
Testing checklist
- Fraud chargeback counts as 2
- Duplicate TC40 resent in a later file counts as 1
- Card-present transactions are excluded from the denominator
- Portfolio ratio equals the summed numerator over the summed denominator
- Merchants below
min_countnever raise merchant-tier alerts - Excluded dispute paths are removed only via the config flag
- Reconciliation diff against the network's monthly figures stays within tolerance
The formula is trivial. The value is in matching the network's definitions exactly and getting the number early enough to act on it.
Top comments (0)