Case study

Payouts that can never pay twice

A creator marketplace pays its creators through Stripe Connect and PayPal. Money code fails in ways normal code doesn't: when a request times out, you don't know whether the money moved. This is how I built it so that no retry, duplicate webhook or race can pay someone twice.

The project

A multi-tenant marketplace for a US creative agency. Brands post paid opportunities, creators deliver content, and an approved submission becomes a payout. Each brand has its own database, and the creator chooses how to get paid: a bank account through Stripe Connect, or PayPal. I designed and built the whole platform as the only developer.

2
payment providers behind one payout flow
104
automated tests on payouts and their webhooks
10 min
between checks that compare our records with the providers
1
place in the code that may change a payout's status

What can go wrong, and what stops it

Each of these happens in real life. For each one there is a rule in the code and a test that proves it.

1

Someone clicks Pay twice

Or two administrators click at the same moment. Before the provider is called at all, the payout is locked, checked and saved as processing. The second click finds a payout that is no longer payable and is refused. The call to Stripe or PayPal happens only after that is safely stored.

2

The request times out: did the money move?

This is the classic way to pay twice: the connection drops, the code assumes failure, and someone tries again. Here an unanswered request is never treated as a failure. The payout stays in processing, because the money may have left.

A retry sends exactly the same reference as the first attempt, so the provider itself recognises it and refuses to pay again. That reference only moves on when the provider has clearly said no.

3

A confirmation arrives twice, too early, or from an old attempt

Providers confirm payments with webhooks, and they promise at least one delivery, not exactly one.

  • Every webhook is checked against the provider's signature before anything happens
  • Handling the same confirmation twice changes nothing the second time
  • A confirmation can arrive before the provider's own answer to the request, which is why the payout is saved as processing first
  • Each confirmation carries the attempt it belongs to. News about an older attempt is ignored, so a late failure can't undo a newer payment
4

A confirmation never arrives

Webhooks get lost. Without a safety net, that payout would hang forever and someone would eventually pay it by hand, a second time. So every ten minutes a scheduled check asks Stripe and PayPal directly about every payout still waiting, and settles it with the provider's answer.

It also covers brands that have been paused or archived, because money in flight still has to land. One brand with a problem never blocks the others.

5

The provider's own fine print

The dangerous cases are in the details of each provider:

  • Stripe forgets a request reference after 24 hours. A very late retry first looks up whether the transfer already exists, and sends nothing if it does
  • PayPal answers a repeated request with an error. That is not a refusal: the earlier payment exists. The code finds it and continues with it, where treating it as a failure would lead to a second payment
  • PayPal money sent to an address without an account waits unclaimed. It is cancelled, so the funds return at once and a retry can't pay twice when the creator signs up later
  • A transfer that was reversed, or that doesn't match the amount, is never marked as paid
6

The platform's balance is empty

That is the platform's problem, not the creator's. The payout goes back to waiting with a clear message for the administrator to top up the balance. The creator does not see an alarming "failed" for something that has nothing to do with them.

7

Someone changes where the money goes

Every change of payout destination sends the creator an email, as a tripwire against account takeover. Bank onboarding links are signed and single-use, and are never sent as raw links by email.

The rules I use for every piece of code that moves money

Not only on this project. Anything that changes money, credits, stock or a status goes through this list.

One transaction

Writes that belong together succeed or fail together. No "create it, then delete it again if step two fails".

Lock what you read

If the new value depends on the current one, the row is locked and read again before deciding.

The database blocks duplicates

"Was this already processed?" is not a check in code but a unique key in the database.

Every retry is safe

Every webhook, job and request is assumed to arrive twice, late, or in the wrong order.

Status changes are steps

A payout can only move along allowed steps, through one piece of code. Never a loose assignment.

A safety net behind it

A scheduled check compares our records with the provider's and settles what slipped through.

A small first step

Code review for a fixed €950

Not sure what state your system is in, or what an upgrade would take? I read the code and tell you, in writing, before you commit to anything bigger.

  • I read the code and the dependencies of one application
  • A written report in plain language: what is out of date, what is risky and what is fine
  • Security findings, ordered by how urgent they are
  • A step-by-step plan for the upgrade or repair, with a fixed quote
  • A call to walk you through it
€950
excl. VAT, one application

If you order the work from the report, the €950 is deducted from that invoice.

Is your system very large? Then I tell you before I start, not afterwards.

Ask for a code review