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.

FieldTypeDefaultDescription
kind"requestPayout"Tags this operation.
idempotencyKeystringDedupe key; a retried request runs at most once.
actorPrincipalWho is asking. A user may request only their own payout.
userIdstringThe seller whose earned balance is reserved.
amountAmountHow 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.

LegAccountDirection
1the seller's earneddebit amount
2PAYOUT_RESERVEcredit 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.

CodeWhen
BELOW_MINIMUMThe amount is under payoutMinimumEarnedMinor (default 2_000_000 minor).
PAYOUT_TOO_SOONLess than payoutMinIntervalMs (default 24h) since the seller's last request; carries retryAfter.
INSUFFICIENT_FUNDSThe seller's earned balance is below amount.
FUNDS_IMMATUREThe balance covers amount, but the cleared (matured) portion does not.
ECONOMY_PAUSEDA 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 amount has left the seller's earned balance and sits in PAYOUT_RESERVE. Both legs are CREDIT and sum to zero, so the books stay balanced.
  • Exactly one Saga exists for the request, opened in RESERVED. 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_RESERVE exactly once afterward — to REVENUE on settle, or back to earned on a reversal or timeout. Never twice, and never zero times, per the payout saga.

See also