Card testing and fake accounts: where email signals help and where they don't
· 6 min read · Disposable email
Card testing is stopped mainly by controls on the payment flow itself: rate limits on card setup and checkout, CAPTCHA, risk rules (Stripe Radar or your processor's equivalent), 3D Secure, and keeping the payment form behind a login. Email signals play a supporting role. They help when the attacker has to create accounts before reaching your card form, and they're useful inputs to a risk rule, but a card tester with a Gmail address or a script hitting your payment endpoint directly will walk past any email check.
This post is about being honest with yourself on that split, so you put the work where it pays off.
What card testing is
Stripe defines card testing as someone trying to find out whether stolen card details are valid so they can use them for purchases. Its card testing guide describes two routes:
- Card setup. Attaching or validating a card without charging it. Stripe notes this is preferred by attackers because these validations "don't typically show up on cardholder statements".
- Small payments. Low-value charges that cardholders are less likely to notice.
The attack is scripted. Thousands of card numbers get run through your form or your API keys, and the attacker keeps the ones that succeed. The damage to you is real even though they don't want your product: disputes on the payments that succeed, a high decline rate that makes issuers trust your legitimate payments less, fees, and load on your infrastructure.
Stripe's guide lists the symptoms: a spike in failed or blocked payments, a spike in API requests with 402 errors, and a spike in low-value payments "often with nonsensical customer names and emails". That last part is where email comes in.
Where email signals help
When a payment requires an account
Stripe recommends limiting access to your payment form, for example by requiring login before payment. Once you do, account creation becomes a step the attacker has to repeat at scale, and that step is cheap to screen. Scripted signups tend to use either throwaway inboxes or machine-generated addresses at real providers, and both are visible:
- Disposable domains, from a public list (
verdict.reason=disposable_domain). - Random-looking local parts like
xk7qmz2p@gmail.com(randomized_local_part), which we explain in detecting machine-generated email addresses.
Rejecting those at signup doesn't stop card testing. It raises the cost of each account, which matters when your other limits are per account.
Free trials that collect a card
A trial that asks for a card up front is a card-setup endpoint with a signup form attached. That's exactly the flow Stripe says attackers prefer. Here the email check is part of trial abuse defense too, and stopping free-trial abuse without punishing real users covers the rest of that ladder.
As an input to risk rules
Stripe Radar's rules documentation shows rules that read customer metadata, such as requesting 3D Secure when ::customer:trusted:: = 'false', and lists :is_disposable_email: as an attribute in its examples. So you can store your own email verdict on the Stripe customer and let a rule act on it, if your Radar plan supports custom rules. The email signal doesn't block anything by itself; it decides who gets the extra friction.
Where email signals don't help
Be clear about these, because none of them are unusual:
- Guest checkout. If anyone can pay without an account, the email is a free-text field the attacker fills with whatever passes your validation.
- Direct attacks on your keys. Stripe warns that card testers can use your publishable key to retry payments on your site, and that a leaked secret key lets them create payments and set up cards directly. Your signup form never runs.
- Realistic addresses. Plenty of attacks use plausible addresses on big webmail providers. A local-part heuristic catches gibberish, not
john.smith1987@gmail.com. - Volume per account. One real-looking account can attach many cards. Email screening says nothing about that; a per-account card limit does.
If you're under active attack, email work is not the first thing to ship.
The controls that actually stop it
Drawn from Stripe's card testing guidance (the ordering is ours, roughly starting with the least work):
| Control | What it does | Notes |
|---|---|---|
| Use your processor's recommended integration | Stripe says its Payment Element and Checkout come with automated controls, including rate limiters and CAPTCHA triggers | Custom integrations need to add their own controls and send risk data |
| CAPTCHA on card setup and payment requests | Stops most scripts | Validate server side, on every request that sets up a card or pays |
| Login before payment | Removes the anonymous entry point | Pairs with email screening at signup |
| Rate limits | Caps new customers per IP, cards per account, attempts per session | Stripe's example: limit new customers created by one IP per day |
| Risk rules (Radar or equivalent) | Velocity rules, CVC and postal code checks, request 3DS by risk | Custom rules depend on your Radar plan |
| 3D Secure | Adds issuer authentication; Stripe notes liability for fraud disputes typically shifts to the issuer when 3DS authenticates | Applied indiscriminately, it lowers conversion |
| Key hygiene | Keeps your secret key out of client code and repos | Rotate immediately if leaked |
Stripe also says single-heuristic filters, such as IP blocking alone, are usually not enough. That applies to email too.
Putting email in the right place
Here's an "add payment method" endpoint that combines the payment-side limits with an email verdict recorded at signup. The email check isn't repeated here; it was stored on the user when they created the account.
// add-card.ts: called before creating a SetupIntent for a logged-in user
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const MAX_CARDS_PER_ACCOUNT_PER_DAY = 3;
export async function startCardSetup(user: {
id: string;
stripeCustomerId: string;
emailVerdict: { is_spam: boolean; low_trust: boolean; reason: string | null };
emailVerified: boolean;
}) {
if (!user.emailVerified) throw new Error('verify_email_first');
if (user.emailVerdict.is_spam) throw new Error('account_under_review');
const recent = await countCardSetupsSince(user.id, Date.now() - 24 * 3600 * 1000);
if (recent >= MAX_CARDS_PER_ACCOUNT_PER_DAY) throw new Error('too_many_cards');
// Mark the customer so a Radar rule can request 3DS for less trusted accounts,
// e.g. "Request 3D Secure if ::customer:trusted:: = 'false'" (plan permitting).
await stripe.customers.update(user.stripeCustomerId, {
metadata: { trusted: user.emailVerdict.low_trust ? 'false' : 'true' },
});
return stripe.setupIntents.create({
customer: user.stripeCustomerId,
usage: 'off_session',
});
}
countCardSetupsSince is yours; store each attempt in your database. The emailVerdict comes from a signup-time lookup with pingutil's fast mode (skip_enrichment: true), as shown in fake signups: where they come from and how to filter them. An is_spam account that somehow got through signup goes to review instead of reaching the card form. A low_trust account (role or outreach addresses) still pays, but with the extra 3DS step if your rule asks for it.
Put CAPTCHA and an IP rate limit in front of this endpoint as well. The email part is the cheapest layer, not the strongest one.
If you're being hit right now
Stripe's checklist for an active attack: identify the activity in your dashboard, refund the fraudulent payments to avoid disputes, switch to a recommended integration or add mitigations, then monitor. Two extra notes from the same guide: don't keep retrying cards attached to fraudulent customers after the attack (it repeats the attack from the issuer's point of view), and check that your secret key hasn't leaked.
Then, once things are calm, look at how the accounts were created. If most had disposable or machine-generated addresses, add email screening at signup. The list of disposable domains we track is a good place to see what that layer catches. If they had realistic addresses, spend the effort on rate limits and rules instead. Building a simple email risk score from free signals shows how to combine email with other cheap signals if you want more than a yes or no.
Questions people ask
- Can blocking disposable emails stop card testing?
- Not on its own. Card testers can use a real-looking webmail address, and in a guest checkout or a direct API attack they may never touch your signup form. Email screening helps when a payment requires an account, but the main defenses are rate limits, CAPTCHA, risk rules and 3D Secure on the payment flow.
- What are the signs of a card testing attack?
- Stripe's documentation lists a spike in failed or blocked payments, a spike in API requests returning 402 card errors, and a spike in low-value payments, often with nonsensical customer names and emails.
- Why do card testers target card setup instead of payments?
- According to Stripe, validations during card setup don't typically appear on cardholder statements, so the real cardholder is less likely to notice and report them.