Stripe Connect Marketplace Fraud & Risk: Card Testing, Seller Verification and Safe Payouts
How Stripe Connect platforms stop card testing, verify sellers beyond KYC, and delay payouts so marketplace fraud doesn't land on you.
Onboarding getting a seller to charges_enabled is table stakes. The expensive Connect work starts after that: card testing on new connected accounts, sellers who cash out before fulfilment, dispute liability that sits on your platform because you used destination or separate charges, and payouts that left the building before you had a reason to hold them. Stripe's Connect team has been explicit about this stack — Radar for Platforms, stronger seller verification, Identity, delayed transfers, and marketplace liability. This is how I build it.
If you still need the onboarding state machine, start with Express onboarding and KYC. If you still need who pays for a chargeback, read platform fees and charge types. This article assumes those are chosen and you are trying not to fund fraud.
Who actually eats marketplace fraud
Marketplaces typically use indirect charges (destination charges, or separate charges and transfers). The customer pays your platform; you are the merchant of record. Refunds, disputes, and card-testing losses hit your balance first. Stripe may later try to recover from the connected account. If that seller has already been paid out, you are chasing a negative balance — and after 180 days Stripe can take the remainder from a platform reserve.
Direct charges push more of that onto the connected account. Most marketplaces do not use them, because the platform wants to own checkout and the take rate. So the default Connect marketplace is also the default liable marketplace. Assigning controller.losses.payments to Stripe covers connected-account negative balances in some setups; it does not cover negative balances caused by your own destination charges. Stripe's docs say that out loud. Design as if you own the loss.
KYC answers "is this a real person Stripe will pay." Risk answers "should we let this person take money off the platform this week." Those are different products.
Card testing: the pattern you will see first
Card testers don't look like a successful marketplace. They look like a new seller (or a compromised seller account) running a burst of small charges, often $0–$5, often failing, often with the same card fingerprint and rotating billing addresses. Stripe names this fraud_card_tester when you reject an account. The cousin is fraud_card_casher: stolen cards used to buy real (or fake) goods through the seller, then a fast payout.
Radar will catch a lot of this at the payment level. You still need a seller-level tripwire, because testers will spray across many PaymentIntents on one connected account. Count failures in a short window, keyed by connected account — not by your whole platform.
// Inside a verified webhook handler. Same raw-body rules as any Stripe webhook.
if (
event.type === "payment_intent.payment_failed" ||
event.type === "charge.failed"
) {
const obj = event.data.object as {
amount: number;
on_behalf_of?: string | null;
transfer_data?: { destination?: string } | null;
payment_method_details?: { card?: { fingerprint?: string } };
};
const accountId =
obj.transfer_data?.destination ?? obj.on_behalf_of ?? null;
if (!accountId) return;
const isMicro = obj.amount <= 500; // $5.00 or less, smallest currency unit
if (!isMicro) return;
const hour = new Date().toISOString().slice(0, 13); // YYYY-MM-DDTHH
const key = `cardtest:${accountId}:${hour}`;
const n = await redis.incr(key);
if (n === 1) await redis.expire(key, 3600);
if (n >= 8) {
await pausePayoutsAndFlag(accountId, {
reason: "card_testing_burst",
window: hour,
failures: n,
});
}
}pausePayoutsAndFlag should do three things: stop automatic payouts, stop creating new transfers to that account, and put a row in admin that a human can see without opening Stripe. Don't wait for the dispute. Card testing is a leading indicator; chargebacks are the invoice.
Radar for Platforms is the monitoring layer, not a strategy
Radar for Platforms scores connected accounts, not only payments: normal, elevated, highest (roughly 50–89 and 90+ probability of loss). Scores update when transactions or business details change. Stripe also surfaces agentic explanations — related accounts on a bank it already rejected, location mismatches, "three $1–$2 charges in 12 seconds." Use those. Don't rebuild a shadow ML model.
Rules you can attach to a connected account when your platform is liable for negative balances:
- Raise a review — flag for your risk queue. Necessary, not sufficient.
- Pause payouts — hold funds while you investigate. This is the default action I want on
elevated/highestuntil a human dismisses it. - Pause payments — stop new charges on that account. Use when testing or cashing is in progress, not for a messy-but-real seller.
- Set reserves — hold a percentage of future charges (fixed or rolling) against refunds and disputes. Right for long fulfilment or a seller whose refund rate is ugly but not fraudulent.
- Request Identity — government ID + selfie, with a deadline, with pause payouts (or payments) if they miss it.
- Reject — permanent disable. Reasons matter:
fraud_card_tester,fraud_card_casher,fraud_no_intent_to_fulfill,credit,terms_of_service. Reversing a reject requires Stripe Support, so don't use it as a pause button.
Most of that lives in the Dashboard and Radar rules. Your app still needs the webhook side: account.updated when Stripe pauses capabilities, radar.early_fraud_warning.created / dispute events so you reverse transfers, and your own burst detector above. Radar without an admin queue is a notification you will ignore at 11pm.
Seller verification after KYC
Express KYC proves Stripe will transact with them. It does not prove they will fulfil. For higher-risk categories, new sellers above a volume threshold, or anyone Radar marks elevated, I add a second gate: Stripe Identity (document + selfie) or a platform-side hold until first fulfilment. Identity is an add-on; request it from the connected account with an enforcement of pause payouts if they don't complete it. Completion arrives on account.updated — same handler you already use for onboarding status.
Also store, and show in admin: requirements.disabled_reason, currently_due, Radar risk level if you ingest it, refund rate, dispute rate, days since first charge, and whether payouts are paused. A seller who is "verified" but paused should never look green in your UI.
Safe payouts: delay, don't hope
The fraud you cannot claw back is the fraud you already paid out. Three controls, in the order I reach for them:
- Don't transfer until the business event. Separate charges and transfers: charge now,
source_transactionlater, after delivery or a return window. Destination charges move money immediately — wrong default if fulfilment is delayed. That's the charge-type article. - Delay availability on the connected account. When you own fraud and dispute liability you can raise
delay_days(Accounts v1settings.payouts.schedule.delay_days, or Balance Settingssettlement_timing.delay_days_override, max 31). New sellers get a longer delay than trusted ones. - Manual payouts while risk is unknown.
schedule.interval = "manual"holds funds in the connected balance until you create a Payout. You still have to pay out within the country's holding period (often 90 days; US is longer). This is not legal escrow — Stripe is explicit — but it is how you wait for a delivery scan.
// New or elevated-risk seller: hold payouts until you release them.
await stripe.accounts.update(accountId, {
settings: {
payouts: {
schedule: {
interval: "manual", // you call Payouts API after fulfilment
// For trusted sellers, switch back to daily + delay_days: 7
},
},
},
});
// After delivery confirmed and the dispute window you chose has passed:
await stripe.payouts.create(
{ amount: availableCents, currency: "usd" },
{ stripeAccount: accountId }
);Pause payouts from the Dashboard (or your risk runbook) when Radar fires. Pausing is not rejecting: you can unpause. In-flight payouts stay pending for up to 10 days, then cancel back to the connected balance if you never unpause. Pausing payments is heavier — it deactivates card_payments / transfers. Use pause-payments when the account is actively testing cards, not when you only want to wait for a package to arrive.
Reserves when the seller is real but lumpy
Not every risky seller is a fraudster. Hotels, pre-orders, and anything with a long fulfilment tail generate refunds and disputes on a lag. Rolling or fixed connected-account reserves keep a percentage of each charge until a release date. That is credit risk, not card testing. Don't use reserves as your only card-testing control — testers never accumulate a healthy reserve; they burst and cash out.
A risk checklist I use on Connect marketplaces
- Charge type chosen with liability in mind; platform assumes destination/separate-charge losses.
- Onboarding complete is a projection of
charges_enabled,payouts_enabled, and emptycurrently_due— not a return URL. - New sellers: delayed or manual payouts until N successful fulfilments or a volume cap.
- Burst detector on micro-amount failures per connected account; auto-pause payouts and page admin.
- Radar for Platforms rules: review + pause payouts on
elevated/highest; Identity request with payout pause if ignored. - Admin can see KYC due, Radar/risk flags, refund and dispute rates, and pause state without Stripe Dashboard.
charge.dispute.createdreverses the transfer (same-region; wait on cross-border until the dispute is actually lost).- Reject is reserved for confirmed fraud, with the right Stripe reason code.
Want this in the product, not only the runbook
I build this as part of Stripe Connect integration and, when the brief is the whole multi-sided product, marketplace development — Next.js, Postgres/Supabase, roles, Connect, commissions, and admin. The Dryva courier marketplace is the production reference. If card testing, stalled payouts, or liability is already in the room, send me the details.