Card testing fraud turns your donation form into a free validator for stolen cards. Someone fires hundreds of tiny charges through it, watches which ones approve, and walks away with a verified list to resell or cash out somewhere else. You keep the chargebacks, the fees, and a payment processor that now thinks you are the risky one.
Donation forms are especially attractive targets because many accept arbitrarily low amounts with little friction. No cart, no shipping address, often no minimum. A donor can give $3.50 or $1.00, so a script giving $1.00 five hundred times does not look obviously wrong until the disputes land. The result can look like a $1 charge storm: a sudden spike of tiny authorizations and declines.
TL;DR
- Card testing means submitting stolen card details through payment, authorization, or card-setup flows to determine which credentials are valid. Small payments are common, but a visible charge is not always required.
- The attacker studies the authorization result and any exposed decline details. An approval confirms that the card can transact, while some decline responses can still confirm that the card number or other supplied details are valid; the money is incidental.
- Donation forms are prime targets because they accept arbitrarily low amounts with little friction.
- No single control stops it. You need layers: form controls, request-rate controls, and your payment processor's risk engine.
- Your processor’s card-testing protections and risk engine are essential transaction-level defenses. Application controls should stop as much automated testing as possible before it reaches the payment rails.
- IP reputation is a useful triage signal (is this a datacenter, a known proxy, a VPN?), not a wall. It is one input to a score, and it is evadable.
- Do not rely on a single-IP or country block as your primary defense. Both can create false positives and do little against distributed traffic.
Card testing is an automated attack that abuses the low friction of a donation form to check stolen cards. Stopping it is a layering problem, not a single-tool problem: reduce the bot volume up front, let your processor's risk engine judge what remains, and use IP signals to decide who gets extra friction rather than who gets a door slammed on them.
What a card-testing attack actually is
Fraudsters buy stolen card numbers in bulk, but an untested list is less valuable because many of its cards may already be canceled, expired, frozen, or otherwise unusable. Testing sorts the live ones from the dead ones. The attacker submits each card through a payment, authorization, or card-setup flow and reads the response: an approval confirms a usable card, while a detailed decline can still reveal which part of the credential set is valid or incorrect. Stripe describes this plainly in its guide to card testing: the goal is validation, and the transaction amount barely matters.
That is why a $1.00 donation is an attractive testing method. The attacker is not trying to steal your $1.00. They are using your form as a credential-validation oracle, and a working card gets cashed out later at a merchant that sells something resellable.
Why donation forms are the perfect target
A checkout page often has more friction built in: a cart, a fixed product, sometimes an account, and billing or shipping details. Where supported, AVS can compare the billing address or postal code with the information held by the card issuer. A donation form strips most of that away on purpose, because friction kills donations. The same design that helps real donors helps the bot.
WooCommerce says it directly in its card-testing guidance: avoid pay-what-you-want or donation products with no minimum, because fraudsters use small arbitrary amounts that a cardholder may never notice. If transaction monitoring is weak, a low-value spike may go unnoticed until cardholders begin reporting the charges. Individual payments may also be small enough that cardholders do not report them immediately.
Anatomy of the $1 charge storm
The attack often has a recognizable pattern, although more sophisticated campaigns deliberately distribute their traffic to weaken these signals.
The card list and the bot
The attacker starts with a list, often generated or bought, sometimes built by guessing card numbers within a known BIN range (the first digits that identify the issuing bank). They point an automated script at your form. It runs from wherever compute is cheap and disposable: a rented cloud box, a pool of commercial proxies, sometimes a botnet of compromised residential machines so the traffic looks like real people.
The validation run
Then it fires. Dozens or hundreds of submissions in a short window, each a small donation, each a different card. The bot does not care about your thank-you page. It cares about the API response behind it. Approvals get flagged as usable cards. Declines are inspected for details that may reveal whether the card number, expiration date, CVC, or other supplied information was valid. If your form supports guest donations and does not challenge anything, the whole run can finish before anyone logs in.
In your logs it has a signature: a burst of attempts clustered in minutes, the same one or two donation amounts repeated, a high decline rate mixed with a few approvals, many distinct payment methods, sometimes concentrated within a small set of networks, devices, accounts, or sessions. If you graph transactions per minute, the storm is a spike relative to the form’s normal transaction baseline.
What happens after
The live cards move on to a real cash-out somewhere else, and your side of it may begin days or weeks later, when cardholders report the transactions and disputes start arriving.
What it actually costs you
If it were just noise, you could ignore it. It is not.
Each unauthorized test payment that becomes a dispute can add reversal costs, dispute fees, and operational work far beyond the original transaction amount. A flood of $1.00 tests can therefore cost far more than it ever donated. JPMorgan's merchant guidance on card testing makes the sharper point: the real damage is upstream. A high rate of small declines and disputes pushes your authorization decline rate up and can trigger additional scrutiny from your acquirer or payment partners, which can mean review, held funds, higher processing costs, or in a bad case losing the account. Stripe's own card-testing documentation warns that testing activity can degrade how the network sees you.
There is a quieter cost too. Cardholders may contact your organization after noticing unfamiliar charity charges, your support inbox fills up, and your reputation takes a hit.
The defense stack, and where each layer stops the attack
The mental model that helps most: an attack can be stopped at three points, and JPMorgan frames the goal well, which is to stop it before it reaches the acquirer. The earlier you intervene, the cheaper it is.
At the form
This is where you cut raw bot volume. A challenge on suspicious sessions (a CAPTCHA or an invisible bot check) can stop many basic automated scripts. A honeypot field that humans never see but bots fill in is close to free. One useful donation-specific control is a sensible minimum amount. It raises the cost of successful tests, but the threshold should reflect your normal donation patterns and should not be treated as a complete defense. Stripe's prevention guidance leads with the same three: restrict access to the form, add a CAPTCHA, and rate limit.
The gotcha with CAPTCHA: it is not a wall by itself. Solving services and more capable bots can bypass it, while CAPTCHA itself can add friction and reduce completion rates. Use it on risky sessions, not on everyone.
At the request
Here you cap how fast anything can hit the endpoint. Combine conservative IP and subnet velocity limits with session, account, email, device, and processor-level signals. Avoid hard subnet limits that could block users behind shared carrier or corporate networks. Cap attempts per session, account, email address, and processor-provided payment-method fingerprint or identifier. Do not store or log raw card numbers for rate limiting. If ten cards get tried from one network in thirty seconds, that is not a donor changing their mind.
At the processor
This is the transaction-level backstop. Stripe’s recommended integrations include automated card-testing protections such as rate limiters, risk models, and CAPTCHA triggers. These protections are separate from Radar’s fraudulent-dispute controls, although they use overlapping risk factors. Your processor can also evaluate payment, issuer, and network signals that your application cannot see. Use its recommended integration, collect CVC and billing information where supported, evaluate AVS as one signal, and apply risk-based 3DS where appropriate.
The IP layer, and why most advice about it is wrong
Almost every "how to stop card testing" list includes two IP tips: block the attacker's IP, and block transactions from outside your country. Neither is sufficient as a primary card-testing defense, and broad country rules can create false positives.
Blocking a single IP does little against a bot that rotates through a proxy pool or a residential botnet; you will be playing whack-a-mole against a new address every few seconds. Broad country blocking can also punish legitimate donors, especially when the organization accepts international support. A diaspora nonprofit takes real donations from exactly the countries a blunt geo-block would cut off. And plenty of legitimate traffic looks "foreign" or "anonymous" when it is not: someone donating through a corporate VPN that exits in another country, or an iPhone user on iCloud Private Relay, or Chrome's IP Protection. Treat those as attackers and you have quietly turned away real money.
Think of a supporter in Berlin who donates every year through their employer's VPN, which exits in Amsterdam. A country block or an “anonymous, deny” rule could lose that donation and never tells you why.
The signals that actually predict card testing
IP data is useful when you stop using it as a blocklist and start using it as a risk input. The signals that correlate with card testing are not "which country," they are "what kind of network":
- Is the IP a datacenter or hosting provider? Hosting infrastructure is unusual for an ordinary donor’s browser session and can justify additional scrutiny, but it is not conclusive evidence of fraud. Card-testing bots can originate from rented cloud or hosting infrastructure.
- Is it a known commercial proxy or VPN, and with what confidence? A high-confidence match to a datacenter proxy service is a strong signal; a low-confidence VPN guess is not.
- Is it a residential proxy? This is the hardest case, because the traffic looks like a home connection. Residential-proxy detection is valuable because ordinary proxy checks may classify the connection as normal residential traffic.
- Is it a relay (iCloud Private Relay, Chrome IP Protection)? A relay should not raise suspicion by itself. Treat it as neutral and continue evaluating the remaining signals. Relays are used by ordinary people.
- Is there a velocity or geo anomaly? Many cards from one ASN in a minute, or a single session hopping continents, is the pattern to score on.
Several services expose some combination of these signals: IPQS, MaxMind minFraud, proxycheck.io, and IPGeolocation among them. Pick by whether they give you residential-proxy detection, provider attribution, and a confidence score, not just a raw boolean, because "it is a proxy" is less actionable than "it is associated with Zyte Proxy, with confidence 80 and a reported last-seen date". For the examples here I am using ipgeolocation.io's IP Security API because its response carries the provider names and confidence scores I want to score on, and a trial is available for testing the integration before using it in production.
A lookup against the dedicated /v3/security endpoint returns the full picture. Here is the real documented response for 2.56.188.34, which is a useful documented example of a high-risk response:
{
"ip": "2.56.188.34",
"security": {
"threat_score": 80,
"is_tor": false,
"is_proxy": true,
"proxy_provider_names": ["Zyte Proxy"],
"proxy_confidence_score": 80,
"proxy_last_seen": "2025-12-12",
"is_residential_proxy": true,
"is_vpn": true,
"vpn_provider_names": ["Nord VPN"],
"vpn_confidence_score": 80,
"vpn_last_seen": "2026-01-19",
"is_relay": false,
"relay_provider_name": "",
"is_anonymous": true,
"is_known_attacker": true,
"is_bot": false,
"is_spam": false,
"is_cloud_provider": true,
"cloud_provider_name": "Packethub S.A."
}
}
That is what you want to score on: a threat_score of 80, a named proxy (Zyte Proxy) with a confidence of 80 and a reported proxy_last_seen date, a hosting flag with the provider (Packethub S.A.), and is_known_attacker set. A country-only rule would not explain or distinguish any of those network-risk signals. Note the security lookup costs 2 credits per call, so cache and rate-limit your own lookups too.
Turning a signal into a decision
The point is not to block on any single flag. It is to fold the score into your existing risk logic and decide how much friction a session earns. High score, block or hold. Middle score, add a challenge or a 3-D Secure step-up. Low score, let it through. And a relay is not a strike against anyone.
// Node 18+.
// The IP check is one input; payment-provider controls still evaluate the transaction.
const IPGEO_KEY = process.env.IPGEO_API_KEY;
if (!IPGEO_KEY) {
throw new Error("IPGEO_API_KEY is required");
}
async function assessIp(ip) {
const url = new URL("https://api.ipgeolocation.io/v3/security");
url.searchParams.set("apiKey", IPGEO_KEY);
url.searchParams.set("ip", ip);
try {
const res = await fetch(url, {
signal: AbortSignal.timeout(1500)
});
if (!res.ok) {
throw new Error(`security lookup returned HTTP ${res.status}`);
}
const sec = (await res.json())?.security ?? {};
const score = Number(sec.threat_score ?? 0);
// Illustrative thresholds only.
// Tune them using legitimate-traffic and fraud outcomes.
if (score >= 80 || sec.is_known_attacker) {
return { action: "block", reason: "high risk", score };
}
const highConfidenceProxy =
sec.is_proxy &&
!sec.is_relay &&
Number(sec.proxy_confidence_score ?? 0) >= 80;
if (score >= 45 || sec.is_cloud_provider || highConfidenceProxy) {
return {
action: "challenge",
reason: "needs friction",
score
};
}
return {
action: "allow",
reason: sec.is_relay
? "relay without additional risk signals"
: "low risk",
score
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.warn(`IP assessment skipped for ${ip}: ${message}`);
// Fail open for this enrichment layer only.
// The payment processor still evaluates the transaction.
return {
action: "allow",
reason: "lookup unavailable"
};
}
}
Two things that trip people up here. First, get the client IP right. Behind a proxy or CDN, configure Express’s trust proxy setting to match your actual infrastructure, then use req.ip. Do not parse the raw X-Forwarded-For header yourself or blindly trust every proxy hop. Second, decide your failure mode on purpose. For a donation flow I fail open, because rejecting real donors when a third-party API blips is worse than letting one risky session reach a processor that is already scoring it. A login or a payout might fail closed instead. State the choice; do not let it be an accident.
And the honest ceiling: none of this is a wall. A residential proxy or a real-device botnet can make a bot look like a home broadband donor, and IP signals will shrug. That is expected. The IP layer buys you cheap triage, so the obvious junk gets blocked or challenged before it ever reaches your processor, and the processor's model handles the harder cases. Anyone selling IP reputation as a complete card-testing fix is overselling it.
Putting it together: a resilient donation form
Walk two sessions through the finished stack.
A real donor from Berlin on their corporate VPN hits the form. The IP looks like a VPN but the threat score is low and it is not a hosting range, so they get a CAPTCHA at worst, pass it, give $25.00, CVC and billing-address checks pass, done. Mildly annoying, not blocked.
A testing bot from a rented server hits the same form. It trips the velocity controls, and the IP lookup returns a high threat score with cloud-hosting and known-attacker signals, so the application blocks the request. Any attempts that still reach the payment processor are evaluated by its card-testing and fraud controls. The layers reduce the volume and impact of the attack before it becomes a flood.
Same form, very different outcomes, because the layers each did one job.
A few things people get wrong
Broad country blocking can feel decisive but often creates avoidable false positives. You lose legitimate international donors and a rotating botnet barely notices. Score on network type and behavior instead of geography.
CAPTCHA alone is not a solution. It stops lazy scripts and gets solved or bypassed by better ones, and a hard challenge on every donor costs you real gifts. Reserve it for risky sessions.
"Just block the IP" does not scale against address rotation. Rate limit by subnet and fingerprint, and lean on your processor for the transaction-level call.
And IP data is not enough on its own, which is the whole point of this article. It is one honest layer. The minimum amount, the request-rate caps, and the processor's risk engine are doing the heavy lifting; the IP signal just tells you who deserves a second look.
Where to start
Consider a sensible minimum that reflects your normal donation patterns, combine per-IP and per-session velocity controls, and use the card-testing protections recommended by your processor. Then add an IP risk check to decide who gets a challenge versus a clean path, and fail open for this enrichment layer so a timeout does not reject a donor by itself.
Top comments (0)