Once your affiliate program pays real money, someone will refer themselves. They sign up through their own link, or route a friend they coach through it, collect the commission, and on a recurring product they can churn right after the first payout. If you credit commission the moment an invoice is paid, you just funded a discount to yourself.
The good news: Stripe already hands you most of the signals to catch this. You do not need a fraud vendor to start, you need to look at the data you are storing anyway.
The strongest signal: payment method fingerprint
Every card in Stripe has a fingerprint that stays stable across customers. If the affiliate's own subscription and the customer they referred were paid with the same physical card, that is not a coincidence.
async function sharesPaymentMethod(affiliateCustomerId, referredCustomerId) {
const [a, b] = await Promise.all([
stripe.paymentMethods.list({ customer: affiliateCustomerId, type: 'card', limit: 20 }),
stripe.paymentMethods.list({ customer: referredCustomerId, type: 'card', limit: 20 }),
]);
const aPrints = new Set(a.data.map(pm => pm.card.fingerprint));
return b.data.some(pm => aPrints.has(pm.card.fingerprint));
}
A match here is close to a smoking gun. Flag it, do not pay it.
Cheaper signals worth stacking
Fingerprint is the best one, but a few more catch the sloppier cases:
-
Email overlap. Same address, obvious plus-addressing (
me+ref@), or the same domain on a personal-email product. Normalize before comparing. - Billing address match between the two customers.
- Radar / IP. If you use Stripe Radar, the charge carries risk signals and an IP. Same IP for referrer and referred is worth a flag.
- Churn inside the window. The referred subscription cancels or refunds right after the commission-eligible invoice. This is the classic grab-and-run.
None of these alone should auto-ban. Stacked, they sort the obvious abuse from the false positives.
Where to run the check
Do it at commission time, not signup. On invoice.paid, before you write the credit:
// case 'invoice.paid'
const affiliateId = invoice.customer_metadata?.affiliate_id;
if (affiliateId) {
const affiliateCustomer = await lookupAffiliateCustomer(affiliateId);
const selfReferral =
affiliateCustomer &&
await sharesPaymentMethod(affiliateCustomer.stripe_id, invoice.customer);
if (selfReferral) {
await ledger.flagForReview({ affiliate_id: affiliateId, invoice: invoice.id, reason: 'card_fingerprint_match' });
} else {
await ledger.credit({ affiliate_id: affiliateId, amount: commissionOn(invoice.amount_paid), idempotency_key: invoice.id });
}
}
The pragmatic policy that stops most of the loss: hold the commission until the referred subscription clears its refund window, and route fingerprint matches to a human instead of a payout. You lose nothing legitimate by waiting a few weeks, and you stop paying people to refer themselves.
We handle this inside Referralful (affiliate software built on Stripe, which I help build), so the fingerprint and churn checks run before a payout is ever approved. Even if you build your own, the payment method fingerprint is the single highest-value check to add first.
Top comments (1)
Fingerprint is the right first check — high precision, but low recall. It nails the lazy self-referrer paying with one card and misses anyone who drops a Privacy.com or Revolut virtual number on the referred account, which is a thirty-second move now. The harder-to-fake signal is downstream: a real referred customer uses the product, while a self-referral tends to be a dormant account that exists only to clear the commission window. You've got churn-inside-the-window for the grab-and-run; usage-inside-the-window catches the patient ones who let the sub ride. Either way, holding payout till the refund window clears is the policy that does the heavy lifting.