Affiliate fraud in a small SaaS program is rarely sophisticated. It is usually one of two things: someone buying through their own link to pocket the commission on their own subscription, or an affiliate's coupon ending up on a deals aggregator where it converts people who were already going to buy.
Neither one looks like fraud in your dashboard. Both look like growth.
These are the checks worth wiring in before the program gets big enough for it to hurt.
Self-referral: match on the card, not the email
Email matching is the obvious check and the easiest to beat. Anyone who wants the commission will use a second address.
Stripe gives you something harder to fake. Every PaymentMethod carries card.fingerprint, which stays the same for the same physical card even across different customers.
// on invoice.paid, before you write the commission row
const invoice = event.data.object;
const pm = await stripe.paymentMethods.retrieve(
invoice.default_payment_method,
{ expand: ['card'] }
);
const fp = pm.card?.fingerprint;
if (fp && fp === affiliate.cardFingerprint) {
return holdCommission(invoice, 'self_referral:card_match');
}
This needs the affiliate's fingerprint on file, which you have if they are also a paying customer. If they are not, fall back on cheaper signals and treat them as a score rather than a verdict:
- Normalized email match (strip dots and
+tagson Gmail before comparing) - Same billing postal code and same
last4 - Signup IP matching the IP recorded on the referral click
- Under 60 seconds between the click and checkout
Any one of those alone is noise. Two or three stacking up on an affiliate's first ever conversion is worth a human look.
Coupon leaks: compare redemptions to clicks
If you hand affiliates a coupon code as well as a link, the code will eventually escape onto a deals site. Then you are paying recurring commission on customers who found you through Google and searched for a discount on the way to checkout.
The signal is a redemption count with no relationship to the affiliate's traffic.
const clicks = await countClicks(affiliate.id, last30Days);
const redemptions = await countRedemptions(affiliate.couponId, last30Days);
// an affiliate sending real traffic converts a fraction of their clicks
if (redemptions > 10 && redemptions / Math.max(clicks, 1) > 1) {
flag(affiliate, 'coupon_leak');
}
An affiliate with 300 redemptions and 20 clicks is not sending you anyone. Their code is just loose on the internet.
Cookie stuffing: heavy clicks, dead conversion
This one runs the other direction. Someone loads your referral URL in a hidden iframe or an image tag, so every visitor to their site gets tagged, and they collect on whoever was going to convert anyway.
It shows up as click volume far above your program norm with a conversion rate far below it, usually concentrated in a couple of referrer domains that are not real content pages. Log the Referer header and user agent on every click and you can spot it in a single query.
Hold the commission, do not delete it
The mistake I would avoid: auto-rejecting on a flag. False positives happen, and an affiliate who loses a legitimate commission to your fraud rule will tell people about it.
Mark the commission pending with a reason instead, keep a normal payout hold window (30 days covers most monthly plans), and review the flagged ones by hand. At small volume there will not be many.
I build Referralful, affiliate software for SaaS built on Stripe, so I spend a lot of time in this corner of the API. The checks above are worth thinking through whatever you run your program on, including a homegrown one.
What have you actually seen get abused in your program?
Top comments (0)