Files
nick-doc/04 - Flows/Payout Flow.md
Siavash Sameni 7a616744f4 docs: complete code-reality alignment for remaining docs + reconcile issue set
Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
  Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
  added undocumented endpoints (ton-proof challenge, profile email verify,
  GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
  enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
  90-day notification TTL, soft-delete semantics, wallet fields

Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
  page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
  TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation

Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
  ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:15:02 +04:00

12 KiB

title, tags, related_models, related_apis
title tags related_models related_apis
Payout Flow
flow
payment
payout
release
refund
custody
Payment
Funds Ledger and Escrow State Machine Specification
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)

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

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:428requestNetwork/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:546wallets/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-completeduser-{sellerId}, emitted after admin wallet payout (backend/src/services/payment/decentralizedPaymentService.ts:911). No frontend listener.
  • payment-receiveduser-{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

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)