FifeRouter

2 September 2026 · ledger money

Free credit that a card refund cannot claw back

New accounts get a dollar. Making that safe meant asking what a refund is allowed to take, and the answer was already written in a query nobody had revisited.

The spend gate refuses at zero, so until recently a new account could not make one request without entering a card. For a product whose whole argument is visible in a single response, that is a hard first step.

So new accounts get $1. Two decisions made it safe, and the second one had already been made by accident.

It is a ledger entry

Not a special case in the balance. BR-the-ledger-is-the-balance says the cached balance is the sum of the entries, and a grant that appeared as a number without an entry would break that on day one — the first reconciliation would disagree with itself and nobody could say which side was right.

So a grant is a row, with a description saying what it is, and the balance moves because the ledger moved. Same as everything else.

It is not a top-up

This is the one worth the post.

reversible_headroom is the guard that stops a refund inventing a debt. A reversal may take back at most what an account's top-ups have put in, net of reversals already posted:

COALESCE(sum(amount_microcents) FILTER (WHERE kind = 'topup'), 0)
+ COALESCE(sum(amount_microcents) FILTER (WHERE kind = 'reversal'), 0)

If a grant were a topup, it would add headroom. Then a customer who paid $10, spent it, and disputed the payment could have their free credit reversed to cover the shortfall — credit no payment ever backed, taken to fund a chargeback against a different payment.

Nobody would notice quickly. The balance would look defensible; the sum would tie out; and a customer's starting credit would have quietly paid for someone else's dispute.

So grant is its own kind, positive-pinned like topup, and excluded from headroom. A card refund cannot reach it, because there is no card behind it.

The part that was already right

That query needed no change.

It had been written as FILTER (WHERE kind = 'topup') rather than "anything positive" or "everything except debits". So it was already correct about a ledger kind that did not exist when it was written, and adding grants was a migration and a function rather than an audit of every money query.

That is not foresight and it is worth being precise about why, because the lesson is repeatable. The query names the thing it means — payments — rather than the shape that thing currently happens to have. Sign is a shape. Kind is a meaning. Filtering on the shape works until the set of things with that shape changes, which is exactly what adding a kind does.

The guard that came second

Once per account, enforced by a unique index on dedupe_key = grant:{account_id} rather than by remembering to check.

That stops one account collecting twice and does nothing about one person collecting from fifty. The spec said so in those words — a speed bump, not a wall — and named email verification as the real fix, which is where the grant now lives: issued on confirming a link sent to the address, so collecting one costs a mailbox rather than a keystroke.

The index still enforces once per account. What changed is the price of the second account.

Sizing

$1, and configurable without a deploy, because a wall with a gate in it is still something people walk through. Enough for a few hundred routed requests, which is enough to see whether the thing works and not enough to be worth harvesting.

The number being a config value rather than a constant is the part that matters: if it turns out to be wrong, the response is an apply-env run, not a release.


← All posts