Automated Marketplace Payouts with Stripe Connect: Idempotent Sweeps and Clawbacks
The scary part of a marketplace isn't taking the customer's money. It's paying everyone else back out. On the laundry marketplace I built, once a bag was delivered the platform owed the shop money — minus a delivery fee, minus a commission, minus a weekly listing charge — and it had to do that on a schedule, for hundreds of orders, without a human checking the maths.
I wrote a scheduled settlement sweep on top of Stripe Connect to do it. This is the part the manual-capture post promised was a story of its own.
The rule: pay 48 hours after delivery, net of everything
Each order splits into three pieces the moment it completes: a flat delivery fee, a percentage commission, and a separate weekly listing subscription. All of that is config-driven and overridable per supplier, because the business kept changing its mind about the numbers. What a shop is actually owed for an order is the captured amount minus its share of those fees.
I don't pay per order. I run a sweep on a schedule that groups a shop's eligible orders — anything delivered more than 48 hours ago and not yet settled — sums what's owed, subtracts any pending clawbacks, and sends one transfer.
// One settlement run for one supplier
const orders = await getSettleableOrders(shopId, { deliveredBefore: hoursAgo(48) });
const gross = orders.reduce((sum, o) => sum + o.supplierShareCents, 0);
const clawbacks = await getPendingClawbacksCents(shopId);
const net = gross - clawbacks;
if (net <= 0) {
// Nothing to pay, or we're still recovering a refund — record and stop.
await recordSettlement({ shopId, net: 0, clawbacksApplied: clawbacks, orders });
return;
}
await stripe.transfers.create(
{ amount: net, currency: "aud", destination: shop.connectAccountId },
{ idempotencyKey: `settle:${shopId}:${runId}` }, // <- the whole safety net
);
That idempotencyKey is the line that lets me sleep. Scheduled jobs get retried —
the box restarts mid-run, the cron fires twice, a deploy interrupts the loop. With
a key that's deterministic per shop per run, Stripe treats a replay as the same
transfer and returns the original result instead of paying twice. I never want to
reason about "did this already send?" from my own database state; I want Stripe to
enforce it.
Clawbacks: when the money's already gone
Here's the case that breaks naive payout systems. A customer gets refunded after the supplier has already been paid for that order. The platform is now out of pocket, and the money is sitting in the shop's Stripe balance.
I don't try to pull it back directly. I write a clawback — a negative amount
owed to that shop — and let the next sweep absorb it. The shop's following payout
is reduced by what they owe. That's why the sweep computes gross - clawbacks and
guards net <= 0: a supplier with more clawbacks than new earnings simply gets a
zero payout that run, and the remaining debt carries forward. The payout is
never negative — you can't send a negative transfer, and you shouldn't try to
model one.
Why a sweep beats per-order transfers
I'll say it plainly: I prefer batch settlement over paying each order as it completes, and it's not close.
- Fewer transfers, fewer fees, fewer moving parts. One transfer per shop per cycle instead of one per order.
- Clawbacks have somewhere to net against. If you pay per order the instant it's done, a later refund has nothing to subtract from and you're chasing money.
- The 48-hour window is a built-in dispute buffer. Most "actually, that was wrong" events happen right after delivery. Settling immediately means settling before you know the order is really final.
The one thing a sweep demands is idempotency, because a batch job that half-runs and retries is the perfect way to double-pay. Get the key right and the rest is arithmetic.
Keep the money maths in one tested place
Every amount here is integer cents, computed on the server, in a single pricing module that's unit-tested on its own. The supplier share, the commission, the GST-inclusive totals — none of that is scattered across route handlers. When finance asks "why did this shop get exactly this much this week," I can point at one function and the settlement record it wrote, which lists every order, the fees applied, and the clawbacks netted.
Takeaways
- Settle marketplaces on a scheduled sweep, not per order — it gives fees and clawbacks a place to net.
- Make every transfer idempotent with a deterministic key; assume the job will retry.
- Model refunds-after-payout as clawbacks that carry forward, and guarantee the payout can never go negative.
- Compute every figure in integer cents on the server, in one place you can test and point to later.
None of this is exotic Stripe API usage. It's a handful of ordinary calls wrapped in the right guarantees — which is exactly what moving other people's money should feel like.
I build payment systems, marketplaces, and SaaS platforms end-to-end. If you're working on something similar, let's talk.