What is a card-testing attack, and did someone hack my website?
No. Nobody broke into your WordPress site. What happened is that a bot found your public donation form and started using it as a free card-validation tool.
Criminals buy lists of stolen card numbers in bulk. Most of those numbers are dead. Before they can be resold or used for a real purchase, someone has to find out which ones still work. The cheapest way to test a card is to push a tiny charge through a public payment form and watch whether the processor says yes or no. The charge is not the point. The answer is the point.
Donation forms are the preferred target for four reasons, and every one of them is something you built on purpose:
- Any amount is plausible. A $1 gift to a food bank looks completely normal. A $1 order on an e-commerce store does not.
- No shipping address, no inventory, no fulfillment. Nothing to slow the bot down and nothing that fails downstream.
- The form is public and always on. No login, no cart, no account creation.
- Nonprofits rarely have a fraud team. The bot expects nobody is watching at 3am.
This is an attack on the card networks that is being routed through your form. That distinction matters emotionally and it matters practically, because the fix is not "clean the malware." The fix is to make your form an expensive, slow, unrewarding place to test cards.
What should I do in the first hour?
Work in this order. The most common expensive mistake is deleting evidence or mass-refunding before understanding what you are looking at.
- Screenshot and export first. Export your donations to CSV and take screenshots of the Stripe or PayPal dashboard showing the burst. If your processor later asks questions, or if you need to request fee reversals, this record is the whole argument.
- Stop the bleeding. Unpublish the donation page, remove the shortcode temporarily, or put the form behind a maintenance notice. Some operators disconnect the gateway keys entirely. That is heavy-handed but it works, and a donation page that is down for two hours costs less than a page that keeps feeding the attack.
- Read the actual statuses before you refund anything. The overwhelming majority of card-testing attempts fail. A failed charge has no money to refund. Refunding is only relevant for attempts that actually succeeded, and even then you should refund quickly rather than wait for a dispute, because a dispute typically costs a fee on top of the reversed amount. Stripe's published pricing lists a dispute fee of $15.00 per dispute received (verify current figures at stripe.com/pricing, checked August 2026).
- Tell your processor you know. An open ticket that says "we were card-tested, here is what we have already changed" reads very differently than silence.
- Do not delete database rows yet. The cleanup section below explains how to do it without wrecking your totals.
Should I set a minimum donation amount to stop card testing?
Yes, and it is the cheapest fix nobody mentions. Card testers use the smallest amount that will authorize, because they are paying the fees on their own stolen cards and volume is the whole business model. Attacks in the 50-cent to $1 range are the norm. Raising your floor to $5 or $10 does not stop a determined attacker, but it removes the economic reason to pick your form over the thousands of others with no floor at all.
In Donor Merchant this lives at Settings > General > Minimum donation. The value is stored as donor_merchant_min_amount and defaults to 1. It is enforced in two places: on the form itself, and again server-side in the REST donate handler, so a bot that posts straight to the endpoint and skips your HTML gets rejected the same way. Client-side-only minimums are decorative against bots.
Practical note: for most small nonprofits, moving the minimum from $1 to $5 costs you almost nothing in real gifts. Check your own history before you decide, and if you genuinely receive $2 gifts, raise the floor temporarily during an active attack and lower it later.
Is there a WordPress donation plugin with built-in rate limiting on the donation endpoint?
Yes. Donor Merchant enforces a per-IP hourly cap on the public donate endpoint in core, with no add-on and no configuration required. By comparison, a third-party analysis of GiveWP states plainly that GiveWP does not include built-in per-IP or per-email rate limits, which is why so many of those threads end with "install a separate anti-spam plugin."
Here is the actual behavior, verified in the source:
| Endpoint | Default cap | How to change it |
|---|---|---|
| Public donate endpoint | 15 attempts per IP per hour | donor_merchant_rate_limit filter |
| Donor portal magic-link request | 8 per IP per hour | Separate bucket, fixed |
| Contact form | 5 per IP per hour | Separate bucket, fixed |
A developer can tighten the donate limit to, say, 5 per hour with a one-line filter in a site-specific plugin. Separate buckets matter: a flood against the donate endpoint should never lock a real donor out of their receipt.
Be honest about what rate limiting does and does not stop. It reliably defeats a single-source flood, which is the most common shape of these attacks. It does not stop a distributed botnet spreading 20,000 attempts across thousands of residential IPs, where each individual address stays under your cap. That is why this is layer two of five and not the whole answer.
Do I have to pay Stripe Radar fees for attempts that were blocked?
This is the part that makes people furious, and the answer is that it depends on your Radar plan. Stripe's built-in fraud tooling is included at no extra cost on standard pricing, but the advanced tier (Radar for Fraud Teams), which is where custom velocity rules live, is priced per screened transaction. Screened includes attempts that were blocked.
One nonprofit described exactly this outcome in a public WordPress.org thread: after noticing "hundreds of fraud donations in amounts from 50 cents to $10," they were pushed onto Radar for Teams and then paid "$55 in Radar processing fees for the hundreds of blocked attempts."
The lesson is not to avoid Radar. Radar rules are genuinely the strongest gateway-side layer you have, and the rules worth writing are velocity rules: block when more than N attempts come from the same IP in an hour, the same email in a day, or the same card fingerprint across multiple attempts. The lesson is that gateway-side blocking has a per-attempt cost, so the layers above it (minimum amount, rate limiting, edge blocking) are the ones that save you money, because traffic they stop never reaches Stripe to be screened at all. Confirm current Radar pricing on Stripe's pricing page before enabling anything.
How do I block bots before they ever reach WordPress?
Every attempt that reaches PHP costs you a database write, a gateway API call, and possibly a screening fee. Blocking at the CDN edge costs you nothing.
If you use Cloudflare, three things are worth turning on today:
- Bot Fight Mode (available on the free plan) challenges obvious automated traffic before it hits your origin.
- A rate-limiting rule scoped to your donation endpoint path, for example a cap of a handful of POST requests per IP per minute to
/wp-json/donor-merchant/v1/donate. - A WAF rule blocking traffic from countries or ASNs you have never received a real gift from. Use this carefully and revisit it, since it will block real people too.
Other CDNs and managed WordPress hosts offer equivalents. If you are on shared hosting with no edge layer, ask your host what request-level rate limiting they can apply to a specific URL. Many can.
What about honeypots and CAPTCHA?
A honeypot is a hidden form field that humans never see and never fill in. Bots that parse and complete every input give themselves away instantly. It costs nothing, it is invisible to donors, and it catches a meaningful share of low-effort automation. Donor Merchant uses this pattern on its contact form.
CAPTCHA is a last resort, and you should reach for it only during an active attack. A donation form is the single worst place on your website to add friction. Every extra step between "I want to give" and "given" loses gifts, and the losses fall hardest on older donors, screen reader users, people on slow rural connections, and anyone on a phone. If you have ever watched an 80-year-old major donor try to identify traffic lights in a grid of blurry squares, you know the cost is real. If you must use one, prefer an invisible or score-based challenge over an interactive puzzle, and take it back off once the attack subsides.
How do I clean up hundreds of spam donations without breaking my donor totals or year-end statements?
This is the part nobody writes about, and it is where the real damage happens. Do not blind-delete, and do not "select all, delete."
The concern is that junk rows attached to fabricated email addresses create fabricated donor records. Those donors then show up in your CSV export, in your mailing list sync, and in your annual statement run. A statement mailed to a fake address is harmless. A donor count reported to your board that is inflated by 400 phantom people is not, and a year-end statement that credits a real person with donations they never made is a genuine problem.
Here is the safe procedure in Donor Merchant. The same logic applies to any plugin that stores a status per donation:
- Export everything to CSV first. Donor Merchant's export also neutralizes spreadsheet formula injection in exported cells, so a malicious value in a name field cannot execute when you open the file in Excel.
- Filter by status. The Donations screen has status views for All, Completed, Pending, Failed, and Refunded. Card-testing junk lives almost entirely in Failed and Pending.
- Sort by date and amount and actually look. Scan for anything that could be a real donor whose card was declined for an innocent reason. Those people deserve a follow-up email, not deletion.
- Bulk delete the verified junk from the Failed and Pending views only. Never bulk-delete from the All view.
- Leave completed donations alone. If a fraudulent charge actually succeeded, refund it through Stripe or PayPal so the status becomes Refunded. Do not delete the row. Deleting a real financial transaction from your records is the one action here you cannot undo.
What failed rows can do is create empty donor records if the bot supplied an email address. After cleanup, sort your donor list by lifetime total, look at the zero-total records created during the attack window, and remove the ones that are clearly fabricated. Then re-run your year-end statement preview before mailing season and confirm the count matches your expectation.
None of this is tax advice. If a fraudulent transaction has already been receipted or reported, talk to your accountant about the correct way to reverse it in your books.
Will Stripe close my account because of card-testing donations?
A warning email is not a closure notice, and a high decline rate caused by an attack is a situation Stripe's risk team sees constantly. What causes real trouble is ignoring it. Card networks impose penalties on processors for excessive authorization attempts and excessive fraud rates, which is why Stripe escalates quickly.
The path back to good standing is straightforward: respond to the notice, describe the mitigations you have put in place (minimum amount, rate limiting, Radar velocity rules, edge protection), and show that the attempt volume dropped afterwards. Processors are looking for evidence that the hole is closed. Give them that evidence in writing.
The money angle: your platform is charging a percentage on fraud too
State this once, plainly. If your donation platform takes a percentage of every transaction, it takes that percentage on fraudulent transactions as well. A nonprofit that has just been attacked then has to open a support ticket and ask for those platform fees back.
That is not hypothetical. One operator wrote on WordPress.org that they had "nearly 20.000 non legit tries to make a donation, possible to check stolen creditcards," and described 17,000 pending donations, both gateways disconnected, manual refunds in Stripe, and a request to recover the platform application fees charged on the fraudulent transactions. Another reviewer put it more bluntly: "Took more time and money to fix than what we raised."
Donor Merchant takes no platform fee on any transaction, ever. During a fraud wave that means your exposure is whatever Stripe or PayPal charges and nothing more. There is no percentage to claw back, because there was never a percentage. That is the same reason the plugin is free with no platform fee on your good days too.
Your five-layer defense checklist
| Layer | What it stops | Cost | Where to set it |
|---|---|---|---|
| Minimum donation amount | Removes the economic incentive to pick your form | Free | Settings > General > Minimum donation |
| Per-IP rate limiting | Single-source floods, not distributed ones | Free, built in | On by default, 15/hour on the donate endpoint |
| Stripe Radar velocity rules | Repeat IP, email and card fingerprint | Per screened transaction on the advanced tier | Stripe Dashboard > Radar > Rules |
| Edge / CDN protection | Traffic before it reaches PHP or Stripe | Free tier available on Cloudflare | Bot Fight Mode plus a URL rate-limit rule |
| Honeypot field | Low-effort automation | Free, invisible to donors | Built in; CAPTCHA only as a temporary last resort |
Two more items belong on the printed version taped to your monitor. Alerting: turn on admin email notifications so you find out from a notification and not from your bank. Monthly check: once a month, open the Failed view on your donations list and glance at the volume. A slow climb is an early warning that somebody is probing.