Thirty days in the ledger

Splitting 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.

4 min readGlitz · Hold stage

When a buyer pays on the creator marketplace, the platform takes the whole charge and writes a commission entry into a ledger with a status of held and a release date thirty days out. Nothing moves to the creator that day. The comment above the write says why in one line: it protects the platform from refund losses. A refund inside the hold cancels the entry, and that is the end of it. A refund after the money has gone out means reversing a transfer that has already landed in someone else's account, and the code for that path ends in an alert that says, in so many words, manual clawback required. The hold exists so that alert is rare.

The release

A scheduled job runs every morning and asks the ledger for held entries whose date has passed, fifty at a time. It groups them by creator and brand, because payout minimums are set per brand and one brand's minimum must not hold another brand's money, which is its own note. Then, for each entry, it claims it inside a transaction: read the entry fresh, confirm it still says held, flip it to processing, and only then move money. Two overlapping runs cannot both take the same entry, because the second one reads processing and steps away.

The transfer itself is one shared function used by every path that moves money out. It refuses amounts under the processor's floor and leaves them held rather than marking them paid. It refuses when the creator has no connected account, for the same reason. And every transfer carries an idempotency key built from the ledger entry's id and the role being paid, so if the same entry is ever sent twice the processor returns the first transfer instead of making a second. Marking the entry paid is a separate step, after the money is out, and if that bookkeeping write fails the function writes an alert and still reports success. Reporting failure there would trigger a retry of a transfer that had gone through, and the idempotency key would catch it, but the point is not to need catching.

The ladder

When a transfer fails, the entry is marked failed with the error text, a retry count, and a time for the next attempt: one hour times two to the power of the attempts so far. One hour, then two, four, eight and sixteen. A second job runs hourly and picks up failed entries whose time has come and whose count is under five, claims each one the same way, failed to retrying, and calls the same transfer function. After the fifth failure the entry becomes permanently failed and a platform alert is written, which a third function turns into an email marked critical to one internal address. There is no array holding that ladder and no constant holding the five. Both are inline, twice, and a future reader has to know that.

The reconciler

A claim is a promise to finish. If the function crashes or times out between flipping an entry to processing and writing its final status, that entry is orphaned: no query selects processing, so nothing will ever touch it again. A job every thirty minutes looks for entries in processing or retrying that have not been updated in thirty minutes and puts them back to held, where the morning job will find them. It does not ask the processor what happened and it does not move money itself. It can afford not to, because the idempotency keys make a re-run safe: a transfer that did go out is reused, not doubled, within the processor's window for that key. That job arrived in August 2026, in the pass that hardened the whole payout path, five months after the hold itself.

The balance monitor, said plainly

A daily job reads the platform's available balance and writes an alert when it falls under a threshold, five hundred by default, changeable from a config document. That is all it does. It does not add up the held entries coming due and compare, which is the monitor a reader might assume from the phrase. It is a smoke detector, not a forecast, and the honest description is the one that lets the next engineer decide whether a forecast is worth building.

What it cost

A great deal more machinery than a split at charge time. Nine statuses, written as string literals with the transitions implied by scattered updates rather than declared anywhere: held, processing, paid, failed, retrying, permanently failed, cancelled, and two for clawbacks. The mobile app's model still documents the four it had in March, and its dashboard renders seven of the nine. The hold length is a literal in two files, so changing it means changing both. And none of this has an automated test; the pre-launch checklist verifies the release by editing a hold date in the console and watching the morning job. Every one of those is a real cost, written down here so the reader does not have to find it.

The shape is the same one that appears twice already on this site, in the webhook note and the commission note: derive the key from the thing itself so a retry collides with its own first attempt. Here it is the third time, on the outbound side of the money, and it is the reason a reconciler can be a status flip instead of a forensic exercise. One idea, carried the whole way through one platform by the one person who wrote all three ends of it.

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 minimum that belongs to someone elseHeld 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.
  • 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.
  • 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.