Request payout
Open the payout saga: reserve matured earnings for disbursement, subject to minimums and intervals.
Source src/operations/requestPayout.ts#L39 · requestPayoutsrc/contract.ts#L110 · kind 'requestPayout'src/operations/registry.ts#L47 · REGISTRYsrc/ports.ts#L773-L780 · SAGA_STATES
requestPayout starts a seller's cashout. It reserves some of their earned credits and opens a payout saga that a background worker later finishes by paying real USD. It does not pay anyone itself.
Here a seller asks to cash out 25,000.00 of their earned credits, clearing the default payout minimum. The amount is their own earned balance, in CREDIT minor units:
const outcome = await economy.submit({
kind: "requestPayout",
idempotencyKey: "payout_2026_02",
actor: { kind: "user", userId: "usr_a1" },
userId: "usr_a1",
amount: { currency: "CREDIT", minor: 2_500_000n },
});
// → { status: "committed", transaction: { id: "txn_…", … } }
The idempotencyKey makes a retried request run at most once, so a double-tapped cashout opens one saga, not two.
Parameters
The payload carries the seller and the amount of earned credit to reserve.
| Field | Type | Default | Description |
|---|---|---|---|
kind | "requestPayout" | — | Tags this operation. |
idempotencyKey | string | — | Dedupe key; a retried request runs at most once. |
actor | Principal | — | Who is asking. A user may request only their own payout. |
userId | string | — | The seller whose earned balance is reserved. |
amount | Amount | — | How much earned credit to set aside. Must be CREDIT and strictly positive. |
The amount is always paid out later as USD; it never becomes spendable in-app. Only the earned account — revenue owed to a seller — is payable, so a payout drawn against spendable or promo has no meaning here.
Returns
requestPayout resolves to an Outcome.
A successful request returns committed with the reservation transaction. When the same idempotencyKey repeats, a duplicate returns the earlier result unchanged.
A business decline returns rejected with one of the reason codes below — too small, too soon, not enough funds, or not yet matured. A rejected outcome is data you inspect, never an exception.
A malformed request throws instead. An amount that isn't CREDIT raises OP.MALFORMED, and a zero or negative one raises MONEY.INVALID_AMOUNT (see payableCredit). Those are programming errors, not declines.
Postings
The commit posts one balanced double-entry transaction. Both legs are CREDIT and cancel out — no USD moves here.
| Leg | Account | Direction |
|---|---|---|
| 1 | the seller's earned | debit amount |
| 2 | PAYOUT_RESERVE | credit amount |
This moves the credits out of the seller's reach and into PAYOUT_RESERVE, which holds credits owed out as a payout. The USD side posts later, when the worker settles the saga — see the payout saga.
The same transaction opens a Saga in state RESERVED (see SAGA_STATES), so the reservation and the record commit together. The saga also records the payout rate that converts CREDIT to USD at request time. The worker later pays at that locked rate.
Authorization
requestPayout is not a privileged operation, so an end user can run it — but only for their own account. The ownership check requires the caller to own the earned account being debited; a user requesting another seller's payout is refused with AUTH.UNAUTHORIZED (see debitedUserAccounts).
A system service or a human operator may also request a payout on a seller's behalf. See actors and authorization for the full rule.
Reason codes
Each check below returns a rejected Outcome — a normal "no", never thrown. The four business checks run top to bottom, so the cheapest decline comes first. ECONOMY_PAUSED is separate: it's a maintenance gate checked up front, before any of them.
| Code | When |
|---|---|
BELOW_MINIMUM | The amount is under payoutMinimumEarnedMinor (default 2_000_000 minor). |
PAYOUT_TOO_SOON | Less than payoutMinIntervalMs (default 24h) since the seller's last request; carries retryAfter. |
INSUFFICIENT_FUNDS | The seller's earned balance is below amount. |
FUNDS_IMMATURE | The balance covers amount, but the cleared (matured) portion does not. |
ECONOMY_PAUSED | A user's request lands inside a maintenance window; carries resumesAt. |
The FUNDS_IMMATURE gate is the subtle one. The raw earned balance can pass INSUFFICIENT_FUNDS while part of it is still inside its chargeback window. So a second, stricter check asks whether the matured part alone covers amount (see source). Maturity is the holding period that must elapse before earnings are payable — see maturity.
Preconditions and invariants
The request reserves credit; it never disburses USD. After a committed outcome:
- The reserved
amounthas left the seller'searnedbalance and sits inPAYOUT_RESERVE. Both legs areCREDITand sum to zero, so the books stay balanced. - Exactly one
Sagaexists for the request, opened inRESERVED. The reservation and the saga commit in one transaction, so a record can't exist without its credits set aside, or the reverse. - No real USD has moved, and the credits are not spendable. They leave
PAYOUT_RESERVEexactly once afterward — toREVENUEon settle, or back toearnedon a reversal or timeout. Never twice, and never zero times, per the payout saga.