--- title: Payout Flow tags: [flow, payment, payout, release, refund, custody] related_models: ["[[Payment]]", "[[Funds Ledger and Escrow State Machine Specification]]"] related_apis: ["POST /api/payment/:id/release", "POST /api/payment/:id/release/confirm", "POST /api/payment/:id/refund", "POST /api/payment/:id/refund/confirm"] --- # Payout Flow > **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md)) This page describes how escrowed funds leave Amanat custody after an order is complete or a dispute is resolved. The current flow is no longer SHKeeper payout-task centric. Release and refund are instruction-based: 1. Backend validates policy, dispute hold, and ledger availability. 2. Backend builds a release/refund instruction. 3. A custody signer executes the on-chain transaction. 4. Backend confirms the tx hash and appends the ledger entry. Today the custody signer can be an admin/Trezor path when enabled. The roadmap target is Safe multisig execution before any custom escrow contract pilot. See [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]]. ## Actors - **Admin / mediator** -- initiates release/refund after delivery confirmation or dispute resolution. - **Custody signer** -- Trezor proof today when enabled; target state is Safe multisig owners. - **Seller** -- recipient for release. - **Buyer** -- recipient for refund. - **Backend** -- `releaseRefundService.ts`, payment adapter, ledger service, Trezor service. - **Blockchain** -- final on-chain settlement. - **MongoDB** -- `Payment` and `FundsLedgerEntry`. ## Preconditions - The pay-in `Payment` is funded or releasable. - The release/refund amount is positive and does not exceed available ledger balance. - No active dispute hold blocks the operation, unless the operation is the explicit dispute resolution path. - Recipient wallet is known and verified. - If `TREZOR_SAFEKEEPING_REQUIRED=true`, the confirm step **must** include the expected Trezor operation signature (see gate below). - Production target: Safe multisig execution is required for custody movement. ## Release Narrative 1. Buyer confirms delivery, an auto-release policy matures, or a dispute resolves for the seller. 2. Admin calls `POST /api/payment/:id/release` with optional partial amount. 3. Backend loads the `Payment`, validates ledger availability when `PAYMENT_LEDGER_ENFORCEMENT=true`, and returns an instruction payload. 4. Custody signer broadcasts the seller payment transaction. 5. Admin calls `POST /api/payment/:id/release/confirm` with `txHash` and (when safekeeping is enabled) a Trezor signature proof. 6. Backend verifies signer proof when required, confirms adapter state, appends a `release` ledger entry, and marks escrow released. ## Refund Narrative 1. Dispute resolves for the buyer, order is cancelled before fulfillment, or support executes an approved recovery. 2. Admin calls `POST /api/payment/:id/refund`. 3. Backend validates available funds and policy. 4. Custody signer broadcasts the refund transaction. 5. Admin calls `POST /api/payment/:id/refund/confirm` with `txHash` and (when safekeeping is enabled) a Trezor signature proof. 6. Backend appends a `refund` ledger entry and marks escrow refunded. ## Sequence Diagram ```mermaid sequenceDiagram autonumber actor A as Admin actor C as Custody signer participant BE as Backend participant DB as MongoDB participant BC as EVM Chain actor R as Recipient A->>BE: POST /api/payment/{id}/release or refund BE->>DB: Load Payment + FundsLedger balance BE->>BE: Check dispute hold + ledger availability BE-->>A: unsigned release/refund instruction A->>C: Request Trezor/Safe execution C->>BC: Broadcast transfer BC-->>C: txHash A->>BE: POST /confirm { txHash, trezor proof if safekeeping } BE->>BE: Verify proof if required BE->>DB: append release/refund ledger entry BE->>DB: update Payment escrowState BE-->>R: notification (no realtime socket listener — see gap below) ``` ## API Calls ### Release / Refund (custody) — correct paths These are mounted on `paymentControllerRouter` at `/api/payment` (`backend/src/services/payment/paymentControllerRoutes.ts:23-26`). Note: **no `/shkeeper/` segment**. | Method | Endpoint | Purpose | |---|---|---| | `POST` | `/api/payment/:id/release` | Build release instruction | | `POST` | `/api/payment/:id/release/confirm` | Confirm release transaction | | `POST` | `/api/payment/:id/refund` | Build refund instruction | | `POST` | `/api/payment/:id/refund/confirm` | Confirm refund transaction | | `GET` | `/api/admin/payments/awaiting-confirmation` | Admin view of payments blocked on confirmation depth | | `GET` | `/api/payment/derived-destinations` | Admin view of derived destination sweep state | ### Request Network — actually implemented routes Mounted at `/api/payment/request-network` (`app.ts:428` → `requestNetwork/requestNetworkRoutes.ts`). Only these exist: | Method | Endpoint | Purpose | |---|---|---| | `POST` | `/api/payment/request-network/pay-in` | Create a pay-in intent (authenticated) — `requestNetworkRoutes.ts:111` | | `POST` | `/api/payment/request-network/intents` | Create checkout intent — `requestNetworkRoutes.ts:289` | | `GET` | `/api/payment/request-network/:paymentId/checkout` | In-house checkout block fetcher — `requestNetworkRoutes.ts:152` | | `POST` | `/api/payment/request-network/webhook` | Provider webhook (raw body) — `requestNetworkRoutes.ts:330` | > [!warning] ⚠️ NOT IMPLEMENTED — Request Network payout/release/refund sub-routes > The following routes are **not registered anywhere** and return **404**: > - `POST /api/payment/request-network/:id/payout/initiate` > - `POST /api/payment/request-network/:id/payout/confirm` > - `POST /api/payment/request-network/:id/release/confirm` > - `POST /api/payment/request-network/:id/refund/confirm` > > Release and refund are handled exclusively by the custody routes under `/api/payment/:id/...` listed above — **not** under the `request-network` namespace. ## Custody-signer / Trezor safekeeping gate > [!warning] Safekeeping gate blocks the legacy non-custodial helpers > When `TREZOR_SAFEKEEPING_REQUIRED=true` (`backend/src/services/trezor/trezorService.ts:214`), the release/refund `confirm` endpoints require a Trezor operation signature in the request body. > > - The **active admin UI** path uses `TrezorSignDialog` (`frontend/src/components/trezor-sign-dialog/TrezorSignDialog.tsx`), wired into the awaiting-confirmation list view. It builds the signed payload via `getTrezorOperationMessage` + `trezorSignMessage` and posts `{ txHash, amount, trezor: { message, signature } }` through `confirmRelease` / `confirmRefund` (`frontend/src/actions/trezor.ts:108,133`). This path satisfies the gate. > - The **legacy helpers** `confirmReleaseTx` / `confirmRefundTx` (`frontend/src/actions/payment.ts:487,503`) post only `{ txHash, ...extra }` — by default **no Trezor proof**. They have **no UI callers** today, but if used with safekeeping enabled the backend will **reject** the payout. Prefer the `TrezorSignDialog` flow; remove or retrofit the legacy helpers to attach the signature. ## Derived-destinations sweep HD-wallet derived-destination sweep infrastructure exists but is **admin-tooling only**: - Routes: `GET /api/payment/derived-destinations` (`app.ts:546` → `wallets/derivedDestinationRoutes`). - Cron: `startSweepCron()` auto-starts only when `DERIVED_DESTINATION_SWEEP_AUTOSTART=true` (`app.ts:578-582`, `wallets/sweepService.ts`). - Model: `DerivedDestination` with statuses `active`/`swept`/`sweeping`/`quarantined` (`models/DerivedDestination.ts:35`). This is not part of the buyer/seller payout UX; it consolidates funds from per-payment derived addresses. ## Database Writes - **`payments`** -- status, `escrowState`, `blockchain.transactionHash`, signer metadata. - **`funds_ledger_entries`** -- append-only `release` or `refund` entry with idempotency key. - **`purchaserequests`** -- terminal business state after release/refund completes. - **`notifications`** -- release/refund receipt to the relevant party. ## Socket events emitted > [!warning] Real-time payout/payment events have NO frontend listeners > Two seller-facing socket events are emitted by the backend but **no frontend code subscribes to them**, so sellers receive no real-time notification: > - **`payout-completed`** → `user-{sellerId}`, emitted after admin wallet payout (`backend/src/services/payment/decentralizedPaymentService.ts:911`). No frontend listener. > - **`payment-received`** → `user-{sellerId}`, emitted on Web3 verify (`backend/src/services/payment/paymentRoutes.ts:622`) and from `marketplace/routes.ts:2611`. No frontend listener. > > Until the frontend socket layer registers handlers for these, sellers must refresh / poll to see payout and incoming-payment state. Persisted DB notifications still surface through the standard notification channel. ## Error / Edge Cases - **Insufficient ledger balance** -- reject instruction build/confirm. - **Active dispute hold** -- reject release/refund unless the operation is the explicit dispute outcome. - **Missing signer proof** -- reject confirm when `TREZOR_SAFEKEEPING_REQUIRED=true` (legacy `confirmReleaseTx`/`confirmRefundTx` helpers omit it — see gate above). - **Custody tx sent but not confirmed in app** -- reconcile by tx hash and append the missing ledger entry once verified. - **Partial split** -- build separate release and refund instructions whose sum does not exceed available balance. - **Payout reverted** -- leave escrow in failed/retryable state and do not append the terminal ledger entry. - **Wrong namespace** -- calling release/refund under `/api/payment/request-network/:id/...` returns 404 (those routes do not exist). ## Legacy SHKeeper Note Older versions used SHKeeper payout tasks and scripts such as `fix-transaction-hashes.js`. Those references remain useful for historical reconciliation, but new release/refund work should use the instruction, ledger, and custody-signer flow described here. ## Linked Flows - [[Escrow Flow]] -- sets up the conditions under which release/refund is allowed. - [[Delivery Confirmation Flow]] -- happy-path release trigger. - [[Dispute Flow]] -- can divert release to refund or split. - [[Trezor Safekeeping Flow]] -- hardware-backed operation approval. - [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] -- Safe-first custody roadmap. ## Source Files - Backend: `backend/src/services/payment/paymentControllerRoutes.ts:23-26` (release/refund routes) - Backend: `backend/src/services/payment/requestNetwork/requestNetworkRoutes.ts:111,152,289,330` (implemented RN routes) - Backend: `backend/src/services/payment/orchestration/releaseRefundService.ts` - Backend: `backend/src/services/payment/ledger/fundsLedgerService.ts` - Backend: `backend/src/services/payment/adapters/requestNetworkAdapter.ts` - Backend: `backend/src/services/trezor/trezorService.ts:214` (safekeeping gate) - Backend: `backend/src/services/dispute/releaseHoldService.ts` - Backend: `backend/src/services/payment/decentralizedPaymentService.ts:911` (`payout-completed` emit) - Backend: `backend/src/services/payment/paymentRoutes.ts:622` (`payment-received` emit) - Backend: `backend/src/services/payment/wallets/sweepService.ts`, `models/DerivedDestination.ts` (sweep infra) - Frontend: `frontend/src/components/trezor-sign-dialog/TrezorSignDialog.tsx`, `frontend/src/actions/trezor.ts:108,133` (active Trezor confirm path) - Frontend: `frontend/src/actions/payment.ts:487,503` (legacy `confirmReleaseTx`/`confirmRefundTx`, no Trezor proof)