Visa VAMP · Mastercard ECP · Built for Merchants
Making Card Network Compliance Math Feel Like a Number You Can Read
Card networks publish chargeback-monitoring programs as flat tables of thresholds and effective dates. I built Visa × Mastercard Compliance to close the gap between that table and a merchant's actual numbers: six inputs in, and a risk gauge, a dollar-exposure figure, and a threshold classification all recompute live, no submit button, no page reload.
Why This Exists
When a merchant racks up too many chargebacks (customers disputing charges), Visa and Mastercard both step in with their own monitoring programs: Visa calls its version VAMP, Mastercard calls its version ECP. Cross their limit and the merchant's bank starts paying higher fees, filing more reports, and risking the ability to accept that card at all. Both networks explain these rules with dense rate tables that mean little to anyone outside payments. I built a simple, slider-based calculator to make those numbers easy to understand, then rebuilt it here as a portfolio piece with its own name and look.
It's a self-contained tool, not just a form you fill out and submit. Every number on screen: the gauge, the dollar amounts, the color of the status badge, updates instantly as you move a slider. Building it meant handling two networks that don't just use different numbers, they calculate risk in completely different ways.
The Challenge
Visa and Mastercard don't just set different limits, they measure risk in completely different ways. Visa splits chargebacks into two groups, fraud and non-fraud, and compares each to online (card-not-present) sales, with limits that get stricter on set future dates. Mastercard adds every chargeback together into one number and compares it against all sales, including in-person purchases, using fixed tiers instead of a sliding scale.
The tool had to show that difference honestly, not hide it. Treating both programs as if they worked the same way would give merchants the wrong picture. So switching tabs doesn't just relabel things, it swaps the entire set of questions and results, backed by two separate calculations that never mix.
This rw-monitor component cycles through the calculator's three risk tiers on a timer, so the gauge and stat changes are visible here without dragging a slider yourself.
Why React, Specifically
React wasn't a default pick here. It was chosen for three specific reasons: how this tool's state behaves, how its two calculation models share structure, and the modern React codebase it was built to sit inside.
The Problem Is Genuinely Reactive
Six inputs feed a dozen derived values: ratios, thresholds, dollar figures, a risk tier, a gauge angle, all of which have to stay in sync on every keystroke or slider tick. That's the exact shape of problem React's render model was built for: describe the output as a function of state and let re-renders happen correctly, instead of hand-patching the DOM per field.
Two Models, One Component Tree
Visa and Mastercard need completely different input sets and formulas but have to feel like one product. Component composition made that cheap: the same SliderField and GaugeRing get reused for both, driven by whichever program is active, instead of two near-duplicate calculators copy-pasted apart.
Built to Match Where It Lives
The production sites and tools this calculator was adapted from are already built in modern React. Shipping the portfolio version the same way keeps it consistent with that stack: same component patterns, easy to extend, and simple to drop into an existing React codebase without a rewrite.
Derived State, Not Stored State
None of the risk numbers live in useState. Six raw inputs: region, volume, dispute count, fraud count, fee, average order value - are the only state that exists. Everything else: ratios, thresholds, penalty dollars, liability dollars, risk classification - is recomputed from those inputs inside a useMemo, so the model can never drift out of sync with what's on the sliders.
Recalculating from scratch on every render is intentional: there's no expensive computation here, so memoized recomputation is simpler and safer than hand-patching six interdependent numbers.
The whole model, one hook
const networkAModel = useMemo( () => computeNetworkAModel({ region, volume, disputeCount, fraudCount, fee, aov }), [region, volume, disputeCount, fraudCount, fee, aov],);
Every derived number traces back to this one call - nothing on screen can drift from it.
An Animated Gauge. Drawn, Not Imported
The circular risk gauge is a single inline SVG: two concentric circles, the outer one a static track, the inner one a colored arc whose stroke-dasharray is recalculated on every render from the current ratio against a fixed max. A CSS transition on that attribute makes the ring visibly sweep to its new position instead of jumping, so a slider drag reads as continuous motion rather than a series of redraws. No charting library, no canvas: just geometry and one line of CSS.
Live component, cycling automatically
The arc sweeping between tiers on a loop is the same stroke-dasharray transition.
One Scale, Not a Hundred Invented Numbers
Tailwind keeps styling colocated with the markup instead of drifting out of sync in a separate stylesheet, and every spacing, color and radius value comes off one constrained scale instead of a hundred one-off numbers invented per component. That's what keeps a few dozen components looking like one coherent product, makes the whole thing easy to drop into another codebase (or this portfolio) since there's no stylesheet to import or class name to collide with, and keeps the styling legible right at the call site instead of needing a second file open. It also ships smaller: only the utilities actually used get included.
Keeping this sample on the same system the real tool uses is a deliberate match, not decoration: it hands off, extends and reads the way the actual codebase does. It also buys something specificity wars can't: a utility class does exactly one thing, so nothing else in the file can silently override it by accident.
<div className="flex flex-col gap-11 rounded-tight2 border border-white/103 bg-black/[0.34]4 px-4 py-35"> <div className="flex items-center justify-between gap-2">…</div> <input type="range" /> </div>
A real class list from this app, traced to what it actually draws: same five utilities, marked in the source and matched to the resolved rule.
A New Way to Declare Design Tokens
Tailwind v4's biggest change lives quietly in tokens.css: the whole color and radius scale is declared once, in a single @theme block of plain CSS custom properties, no tailwind.config.js anywhere in the project. Tailwind reads each variable's name directly and generates its matching utility class automatically. Change a value here, and every one of these updates with it.
Sliders That Span Six Orders of Magnitude
Monthly transaction volume can realistically run from a few dozen to several million. A linear slider is useless at that range, so every volume and count field runs through a piecewise logarithmic mapping between slider position and real value — fine control at low volumes, still reaching the high end without the slider becoming a blunt instrument.
- Four log-scale segments carry the curve from 1 up through 5,000,000
- The same mapping runs in reverse, so typing an exact number repositions the slider thumb correctly
- Every formula is guarded against division by zero — a healthy, zero-chargeback merchant never sees NaN
Straight from the source
// Piecewise log-scale mapping so a single slider stays usable// from single-digit volumes up through five million a month.export function volumeFromSlider(x: number): number { if (x <= 250) return Math.round( Math.exp(Math.log(1) + (Math.log(1e4) - Math.log(1)) * (x / 250)) ); // ...three more segments carry the curve up to 5,000,000}
One function, four segments, and a slider that stays precise from single digits to five million.
Try It: This Is the Real App, Running Live
The frame below loads the actual built React bundle, not a recording or a mockup. There's no backend to call: every recalculation happens client-side, so dragging a slider gives a two-minute, self-serve read on where an account stands, without a PDF full of thresholds or a wait on an acquirer alert.
- Merchants and finance teams — plug in this month's transaction and dispute counts to see how close the account is to a monitoring threshold, and what a breach would cost.
- Risk and compliance analysts — model "what if" scenarios without waiting on a report cycle or exporting to a spreadsheet.
- Sales and onboarding conversations — a live, interactive tool does more to demonstrate expertise in two minutes than a static comparison table ever could.
This is the real, running build, cleanly isolated in its own iframe so it can sit here without interfering with the rest of the site. Feel free to try it for yourself and see how it responds.
The real app's mobile layout, scaled down slightly to fit this preview
Same bundle, same CSS: the stacked layout and icon-only tabs come from the app's own responsive rules, not a separate mobile build.
Responsive by Construction, Not by Exception
The whole results layout runs on one CSS Grid and one breakpoint: a single Tailwind utility, min-[960px]:grid-cols-2, is the entire difference between the stacked mobile layout and the two-column desktop one. No JavaScript viewport detection, no server-rendered device sniffing, no separate mobile bundle: the same markup just reflows itself.
Everything downstream follows the same discipline of picking a width and committing to it: the network tabs drop their text labels down to icons-only under 500px, the stat tiles flip from a bottom-divided stack to a right-divided three-up row at 480px, and the calculator card drops its border-radius and side margin to go edge-to-edge below 500px, so nothing wastes space fighting a phone's bezel.
Result
A standalone React and TypeScript app with its own shell, its own graphite-and-blue visual identity, and zero dependency on the parent codebase: de-branded and rebuilt from a real production tool so it could live here as an honest sample of how I actually build. One pure-function risk model, a handful of presentational components, and a design that turns a rulebook nobody wants to read into a number anyone can act on in under a minute.