Credit maturity
Funds clear on a delay: spends drain oldest-first, so the matured balance is the cleared part of the newest run of lots, never money still in its settlement wait.
If you have ever deposited a check and watched the money sit as “pending” before you could spend it, you already understand credit maturity: fresh funds have to clear a waiting period before they count as truly yours to cash out.
Source src/maturity.ts#L219maturedBalancesrc/maturity.ts#L253maturedAtLeastsrc/maturity.ts#L44maturityHorizonMs
Not every credit a user holds can be cashed out the instant it lands. The money behind a fresh credit can still be pulled back by a card chargeback or a disputed payment, so the platform makes new funds wait out a settlement window before they count as cleared. Credit maturity is how that wait is modeled and measured.
The idea
An account’s balance isn’t one undifferentiated number. It’s a stack of dated lots: each top-up and each credit earned lands as a lot stamped with when it arrived. A lot matures once it has waited out its settlement window; until then it’s still clearing.
The matured balance is the part of the live balance whose lots have cleared their wait. A cash-out can draw only that part.
Why it exists
Spending and paying out move real value, and some of the money behind a fresh credit can still reverse. A card payment can be charged back days after it clears the app. If the platform let a user spend or cash out that credit immediately and the underlying payment then reversed, it would be out real money with nothing left to claw back.
The maturity window holds new funds just long enough that, by the time they mature, the risk of a reversal has passed.
The gate is visible at the call site. A seller whose raw earned balance covers a cash-out is still refused while that money is inside its window:
import { requestPayout, userActor, toAmount } from '@pwngh/economy-lab';
// the earned balance covers 8000, but today's sales are still clearing
const outcome = await economy.submit(
requestPayout({
idempotencyKey: 'po_2201',
actor: userActor('usr_s1'),
userId: 'usr_s1',
amount: toAmount('CREDIT', 8000n),
}),
);
// → { status: "rejected", reason: "FUNDS_IMMATURE" } — same key retries cleanly once the window passes
Lots and the FIFO tail
Spends draw oldest-first: first in, first out. So once past spends have drained the oldest lots, what’s left is the newest run of lots, the ones that together sum to the current balance. That run is the tail.
Maturity then splits the tail. A maturity horizon (now minus the settlement wait) falls somewhere along it. Lots older than the horizon have matured; the newest lots are still waiting. The matured balance is that cleared part of the tail.
The horizon depends on how the funds arrived: maturityHorizonMs returns the wait per funding source, and an unrecognized source falls back to a conservative default. A lot matures at its arrival time plus that wait.
The horizons split by environment: outside production every unset horizon is 0, so a fresh dev economy clears funds instantly; in production the card horizon must be stated explicitly and every other rail defaults to it (see the configuration reference).
How it’s measured
Computing the matured balance never scans an account’s whole history. Because the answer lives entirely in the tail, the read walks lots newest-first and stops the moment they cover the live balance.
maturedBalance returns the full matured amount as of now. maturedAtLeast answers the cheaper question its callers actually ask: is the matured balance at least this much? It returns the instant its running sum clears the threshold. A request well within cleared funds settles after a lot or two, never the full history. By construction the two agree: maturedAtLeast is just maturedBalance stopped early. (maturedBalanceFullScan keeps the naive whole-history version, used only to differential-test the fast path.)
The computation is currency-agnostic: the same call covers a user’s spendable credits and a seller’s earned balance.
What relies on it
spendgates on the matured-balance check for the buyer’s spendable credits, so a spend can’t draw funds still clearing.requestPayoutgates on the same check for a seller’s earned balance, so a payout only ever draws cleared money.- The configured maturity horizons set each wait, and solvency is the backing those cleared funds draw against.