Once your affiliate program pays real money, someone will refer themselves. They sign up through their own link, or route a friend they coach throu...
For further actions, you may consider blocking this person and/or reporting abuse
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.
Both right, and the virtual-card point is the one I underweighted in the post. Privacy.com or a Revolut virtual number turns fingerprinting into a speed bump rather than a gate.
On usage-inside-the-window, worth being honest that it prices rather than detects. Faking usage is a couple of logins and an API call, so a determined self-referrer clears it in minutes. Still worth having, but the honest framing is that you are making the fraud cost more than the commission, not catching it.
Which is why your last line is the actual answer. The payout hold does not need to detect anything. It converts the play from get paid and churn into fund the subscription yourself for N months, and that flips the economics with no detection involved at all.
One thing I would stretch there: size the hold to the dispute window rather than the refund window. Card disputes can arrive months after the charge, well past a typical 30-day refund policy, and these schemes fairly often exit through a chargeback instead of a refund request precisely because it avoids talking to you. A hold sized to your refund policy leaves exactly that gap open.
Yeah, sizing to the dispute window changes the shape of it — a chargeback landing months later eats a refund-sized hold entirely. Then the practical problem becomes walking from a dispute back to the exact credit, since the dispute arrives on the charge while the commission was written against the invoice. Does Referralful reverse automatically when charge.dispute.created fires, or does that route to a human the way a fingerprint match does?
Automatic for the dispute, human for the fingerprint, and that split is deliberate. charge.dispute.created is a fact. A fingerprint match is a guess. Facts reverse themselves; heuristics get a person, because a false positive there means accusing an honest affiliate of fraud over a shared household IP.
On walking from the dispute back to the credit, the fix sits upstream of the lookup: store the charge id on the commission row when you write it, not just the invoice id. Then a dispute maps back in one hop. If you keep only the invoice reference you are doing a reverse lookup at precisely the moment you want the least ambiguity, and how a charge exposes its invoice is not consistent enough across API versions to lean on.
The other half is that the reversal has to be idempotent and order-independent. A refund and a dispute can both land against the same charge, and you do not want to claw back twice for one loss.
Automatic for facts, human for guesses. And putting the charge id on the commission row at write time rather than deriving it later is the part I'd carry into anything I build.
Coming at that same row from the other side: when an affiliate opens their own dashboard, are they reading that row, or a narrower projection of it? Once it carries the charge id, the answer decides how much of the merchant's side rides along with it.
Reading other founders' schemas for exactly that, which rows reach which account, is what Vollos is. Referralful's is welcome any time, and I don't charge for the first read.
A narrower projection, deliberately. An affiliate sees that a conversion happened, when it happened, what they are owed and what state it is in. Nothing that identifies the charge or the customer goes across. A charge id buys them nothing, and the moment an affiliate can see one you get a ticket asking who the customer behind it is.
The bit I would push on your framing though: build that projection from an explicit allowlist of fields, never by stripping the ones that look sensitive. A denylist is correct on the day you write it and starts leaking silently the next time somebody adds a column. That is the actual mechanism by which merchant-side data rides along. It is almost never a bad decision at design time, it is a schema that grew afterwards.
I am not going to hand the schema over, but this is the fourth useful round in a row, so I would rather keep doing it here where other people can read it.
Projection, always, and you have put a finger on the real cost of that advice. The moment the row carries merchant-side identifiers, the internal row and the affiliate-facing row have to stop being the same object.
What the affiliate should read is the commission amount, its status, a stable opaque id for the referred account, and the dates. What they should never read is the charge id, the customer's email, the invoice total, or anything about the payment method.
The invoice total is the one people leave in without thinking about it. An affiliate who can see it can infer your per-customer pricing and your discounting, and affiliate programs are open by design, so that population includes competitors who signed up as affiliates. The dashboard becomes a pricing leak before it ever becomes a privacy problem.
Kind offer, thank you. Not something I want to open up right now, but noted.
Answering your dispute question here rather than nested, the reply box would not take it further down the thread.
Automatic for the dispute, human for the fingerprint, and that split is deliberate. charge.dispute.created is a fact. A fingerprint match is a guess. Facts reverse themselves; heuristics get a person, because a false positive there means accusing an honest affiliate of fraud over a shared household IP.
On walking from the dispute back to the credit, the fix sits upstream of the lookup: store the charge id on the commission row when you write it, not just the invoice id. Then a dispute maps back in one hop. If you keep only the invoice reference you are doing a reverse lookup at precisely the moment you want the least ambiguity, and how a charge exposes its invoice is not consistent enough across API versions to lean on.
The other half is that the reversal has to be idempotent and order-independent. A refund and a dispute can both land against the same charge, and you do not want to claw back twice for one loss.