The minimum that belongs to someone else

Held commissions are released to creators once a brand's payout minimum is met. Group the money by creator, the obvious way, and one brand's minimum ends up holding another brand's money.

5 min readGlitz · Payout stage

On the creator marketplace, a commission is not paid the moment a buyer pays. It sits in a ledger for thirty days first, so that a refund is a cancelled entry rather than a clawback from someone who has already spent the money. The hold stage of the anatomy covers why. This note is about what happens on day thirty-one.

A scheduled job runs each morning, selects up to fifty held entries whose hold has expired, and transfers each one to the creator's connected account at the processor. That is the whole job in one sentence. Everything interesting is in the conditions around it.

Whose minimum

Brands set a payout minimum in the app. It is stored per brand, defaults to fifty dollars in the interface, and zero means none. The intent is ordinary: a brand does not want to pay processing overhead on a stream of three-dollar transfers, so commissions accumulate until they are worth sending.

The first version of the release job grouped a creator's expired entries together and tested the total against a minimum. Ask which minimum and the design falls apart. A creator who sells for two brands has two minimums. Apply the first brand's and the second brand's money waits for a threshold the second brand never asked for. Apply the higher and the same thing happens the other way round. There is no correct answer to that question because the grouping is wrong: the minimum is a property of the brand, so the money has to be grouped by brand too.

The job now groups by creator and brand together. Each brand's minimum gates only that brand's money to that creator. The comment above the loop says exactly this, in one line, and it is the kind of line that took longer to arrive at than to write.

Skip before you claim

A group that lands under its minimum is skipped, and the skip happens before anything is touched. The entries stay held, the job logs the shortfall, and they accumulate until a future run finds the total over the line. Nothing to undo, because nothing was claimed. This is the cheapest kind of correctness: put the check where a failure leaves no state behind.

For a group that passes, each entry is claimed inside a transaction before its transfer is sent. The transaction re-reads the entry, checks it is still held, and flips it to processing. A second copy of the job running at the same second reads processing instead of held and leaves the entry alone. Then the transfer goes out with an idempotency key derived from the ledger entry's id, so if the same entry is ever sent twice the processor returns the first transfer rather than making a second one.

Marking the entry paid is a separate step after the transfer, and the separation is deliberate. An earlier version wrote the paid status inside the same try block as the transfer, so a failure in the bookkeeping write looked exactly like a failure in the transfer, the entry was marked failed, and the hourly retry sent the money again. Now a bookkeeping failure raises an alert and leaves the transfer alone, because the transfer already happened and the idempotency key makes a re-run harmless.

The floor under the floor

The processor refuses transfers below fifty cents. An earlier version of the job skipped the transfer for those entries and marked them paid anyway, which is the quiet way to lose money: nothing fails, the ledger says paid, and the creator never sees the cents. Now the claim is reverted and the entry goes back to held. The comment beside that code says plainly that amounts under the floor just sit, and that batching them per creator is the fix if it ever matters. It has not mattered yet. The note is there so nobody rediscovers the question.

When it still goes wrong

A failed transfer enters a retry ladder: one hour, two, four, eight, sixteen, five attempts in all, run by an hourly job that claims each failed entry with the same transaction pattern. After the fifth failure the entry is marked permanently failed and an alert is written, which emails the team with a sentence that says the transfer needs a person. A reconciler runs every thirty minutes looking for entries stuck in processing or retrying for longer than that; a crash between claiming an entry and writing its final status would otherwise leave it in a state no query selects. Those are reverted to held and the daily job picks them up again, safely, because of the idempotency keys. A balance monitor checks the platform account each morning and alerts if it drops under a threshold, since a transfer from an empty account fails in a way that looks like every other failure.

What it costs

A daily job, an hourly job, a half-hourly job, a balance check and an alert pipeline, where a split at charge time would have been a single write. Every piece is there because the money path was audited after it was already live, and the audit found seven items, two of them open ways to lose money: the double-send on retry described above, and a transfer skipped for a creator who had not finished onboarding but still marked paid. The grouping fix, the reconciler and the floor handling landed in the same change in August 2026.

Why it belongs on this page

None of these seven items was visible from one layer. The double-send needed someone who knew the retry schedule and the shape of the transfer call. The grouping needed someone who knew where the minimum was stored and who set it. The floor needed someone who had read the processor's limits and the ledger's status machine. They were found in a single pass by the person who had written all of it, reading it as one system, and that is the argument this whole site is making.

More notes
  • The webhook that credits twiceA payment processor delivers every result at least once. The interesting engineering is in what happens when your side fails halfway through.
  • A ledger you can edit from a route handler is not a ledgerWhy every movement of value on the payments platform goes through a Postgres function, and what it costs to keep it that way.
  • Four rows that were not theirsHardening multi-tenant row-level security in four reversible phases, verified by impersonating a member and counting what they could see.
  • The bug inside the fixA payout race, the fix for it, the bug inside that fix, and why it is the best argument I have for one person owning the whole path.
  • The sale that arrives with no referrerA creator shares a link on Instagram, the buyer taps it, installs the app and purchases. Nothing in that chain carries the creator's name across. Here is what does.
  • The commission that must not mint twiceA buyer pays and the platform owes a creator a commission. Between those two facts sit a colluding pair, a call that arrives twice, and a cart with three items on one payment.
  • The rule that has to be written twiceFirestore security rules do not cascade to subcollections. Forget that in one place and a single query returns every private message on the platform.
  • Thirty days in the ledgerSplitting a payment at charge time is simpler and wrong: a refund after the creator is paid is a clawback nobody enjoys. Holding the money creates a different set of problems, and each piece of machinery around the hold answers one of them.
  • Arbitrary but consistentA gym's assessment answers become rules that swap an exercise for a member before a session. Two rules can disagree about the same movement. The code says who wins, and the comment admits how.