DEV Community

Mihir kanzariya
Mihir kanzariya

Posted on

The affiliate commission you never actually pay out

The balances that never move

Run this query against your commission table: sum of unpaid commission, grouped by affiliate, where the affiliate has had no new conversion in the last twelve months. On most programs that have been running a while, the result is a long tail of accounts holding 4, 11, 27 units of currency each, and none of it is ever going anywhere.

That tail is not a bug in your payout code. It is the direct output of a rule you almost certainly wrote on purpose, and then stopped thinking about.

What a threshold actually creates

Nearly every program sets a minimum payout. Fifty is a common number. The reasoning is sound: a fixed cost attaches to each transfer, whether that cost is a processor fee, a compliance check, or ten minutes of a human reconciling something. Sending 3 units to two hundred people is a worse use of money than not sending it.

The consequence, though, is a permanent holding pen. An affiliate signs up, posts one link, drives one sale, earns 12, and drifts away. Twelve is below fifty. It will be below fifty forever, because nothing is going to move it. Your liability line grows by 12. Their balance sits at 12. Neither of you thinks about it again.

Multiply that by everyone who tried your program once. The arithmetic is not dramatic per account and it is not small in aggregate.

You have several defensible answers. Run an annual sweep that pays everything out regardless of threshold, eating the fee on the small ones as a cost of running a program. Or write an expiry into the terms, with a stated window and a warning email before it lands. Or drop the threshold to whatever amount makes the fee tolerable and let small balances flow.

What is not defensible is having no answer, because from the affiliate's side, an indefinite hold and a refusal to pay look exactly the same. They cannot see your reasoning. They can only see that they earned money and never received it.

The affiliate who never finished payout onboarding

Second version of the same problem, with a different cause. Someone signs up, drives a real conversion, and never completes the details you need in order to send money. Identity checks, bank details, whatever your processor requires. The commission is real. It is owed. It is unsendable.

Two options exist and both are fine. Hold it indefinitely, which means carrying an unbounded liability against an account that may never come back. Or expire it after a stated window, with reminders on the way.

The rule you cannot get away with is deciding this the day someone emails you fourteen months later asking where their 340 went. Whatever you pick has to be written down before it applies to anybody, because a policy invented at the moment of the dispute is not a policy.

Here is the guard that decides whether a balance is payable at all. Note that a zero return has several distinct causes, and your affiliate dashboard should say which one applies rather than showing a bare "not yet".

// Amount you can actually send right now, in minor units.
function payableAmount(affiliate, ledger, policy) {
  const owed = ledger.owedMinor(affiliate.id);

  if (owed < 0) return 0;   // carried clawback, see the reversal section below
  if (owed === 0) return 0;

  // Onboarding incomplete: keep accruing, do not attempt a transfer.
  if (!affiliate.payoutsEnabled) return 0;

  // The sweep is the escape hatch that stops small balances living forever.
  if (owed < policy.thresholdMinor && !policy.isSweepRun) return 0;

  return owed;
}
Enter fullscreen mode Exit fullscreen mode

Initiated is not paid

Now the failure that damages trust fastest, because your system reports success while the affiliate receives nothing.

You create a transfer. The API returns an object with an id. Your code writes status = 'paid' and moves on. Later, the money comes back. The destination bank account was closed. The connected account lost its ability to receive payouts between the time you checked and the time the money moved. Something in the chain rejected it.

Your ledger now says paid. Your balance says the money is still here. The affiliate says nothing, because they are waiting, and they will wait a while before they write to you.

The fix is a state name. Creating a transfer means you asked for money to move. It does not mean money moved.

const transfer = await stripe.transfers.create(
  {
    amount: payable,
    currency: "usd",
    destination: affiliate.connectedAccountId,
    metadata: { affiliate_id: affiliate.id, batch_id: batchId },
  },
  // Re-running a batch must not send twice.
  { idempotencyKey: `payout:${affiliate.id}:${batchId}` }
);

// Not "paid". The request succeeded, the money has not landed.
await ledger.transition(affiliate.id, batchId, "initiated", {
  transferId: transfer.id,
});
Enter fullscreen mode Exit fullscreen mode

The transition from initiated to settled belongs in your webhook handler, not in the function that created the transfer. The transition back to owed belongs there too, driven by whatever failure or reversal signal your processor sends.

I am deliberately not naming those events. Stripe exposes events covering transfers, reversals, and changes to a connected account's status, and the exact set available to you depends on your API version and your Connect configuration. Read the event list in your own dashboard and confirm against current docs rather than trusting a name you read in a blog post, this one included. The mechanism is what matters: something asynchronous can undo a transfer you already created, and if you have no handler for it, your ledger will be wrong and nothing will tell you.

The reversal that lands after the money left

A customer refunds in week six. You paid the commission on that sale in week four. You cannot reach into someone's bank account and take it back.

So the balance goes negative, and future commission pays it down. That part is easy to build.

The part worth deciding early: what happens when that affiliate never earns again. You are holding a negative balance against an account that has gone quiet. At some point you write it off, and the only question is whether you decided that in advance or in the moment. Decisions made in the moment tend to be made while annoyed, and they tend to be applied inconsistently across affiliates, which is exactly the thing that turns into a public complaint.

Also worth writing down: whether refunds after some window stop clawing back at all. Many programs cap the clawback period. If yours does not, say so, since an affiliate who gets a deduction eleven months after a sale will assume you invented the rule that morning.

The part that is not code

Every state I have described is a promise to a person who sent you customers.

A threshold nobody mentioned at signup. A balance that expired with no email. A payout your dashboard marked paid that never arrived. To the affiliate, all three read as being cheated, and the fact that your code did precisely what it was written to do is not a defence they can see or verify.

Two things fix most of this. Your terms and your ledger state names should describe the same reality, in the same words, so that "pending" in the affiliate's dashboard means the thing your terms say it means. And the affiliate should be able to see which state their money is in without asking you, including the unflattering states.

Decide these before your first payout run

  • What the minimum payout is, and what happens to balances that will never reach it.
  • Whether you run a sweep, when, and whether you absorb the fee on small amounts.
  • How long you hold commission for someone who never completed payout onboarding, and how many reminders they get first.
  • Which ledger state means requested and which means confirmed landed, and which one your dashboard shows the affiliate.
  • What moves a commission back to owed, and which handler owns that transition.
  • How long after a sale a refund can still claw back commission.
  • What happens to a negative balance on an affiliate who stops earning.

Write the answers into your terms first, then name your database states after them. Doing it in that order is easier than reconciling the two later.

Top comments (0)