--- title: Escrow Flow tags: [flow, escrow, payment, state-machine, custody] related_models: ["[[Payment]]", "[[PurchaseRequest]]", "[[Funds Ledger and Escrow State Machine Specification]]"] related_apis: ["POST /api/payment/:id/release", "POST /api/payment/:id/refund", "POST /api/payment/:id/release/confirm", "POST /api/payment/:id/refund/confirm"] --- # Escrow Flow The current escrow is a **hybrid custody system**, not a custom Solidity escrow contract. Buyer funds move on-chain through Request Network-compatible wallet transactions. The backend verifies the payment through signed Request Network webhooks/reconciliation plus the Transaction Safety Provider, records state in `Payment`, and records money movement in the internal funds ledger. Release/refund/sweep actions are still administered by the platform, with optional Trezor proof today and a recommended move to Safe multisig custody in [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]]. ## Actors - **Buyer** -- pays from their wallet and confirms delivery. - **Seller** -- fulfills the order and receives release. - **Admin / mediator** -- resolves disputes and initiates release/refund when manual action is required. - **Custody signer** -- Trezor today when enabled; target state is Safe multisig owners. - **Request Network** -- emits payment evidence through signed webhooks and status APIs. - **Transaction Safety Provider** -- verifies tx hash, confirmations, recipient, token, amount, and optional AML decision before funds are credited. - **MongoDB** -- stores `Payment`, `FundsLedgerEntry`, `Dispute`, and `PurchaseRequest` state. ## Current State Model `Payment.status` remains the coarse provider/business state: - `pending` - `processing` - `confirmed` - `completed` - `failed` - `cancelled` - `refunded` `Payment.escrowState` currently supports: - `funded` - `releasable` - `releasing` - `released` - `refunded` - `failed` - `cancelled` - `partial` The current model also has `Payment.disputed`, `disputeHoldReason`, and `holdUntil`. The canonical target state machine in [[Funds Ledger and Escrow State Machine Specification]] adds explicit `DISPUTED`, `REFUNDING`, and normalized uppercase enums. Treat that spec as the destination; this page describes the live hybrid implementation. ```mermaid stateDiagram-v2 [*] --> Pending : payment intent created Pending --> Processing : funds detected / webhook received Pending --> Cancelled : intent expired or buyer cancels Processing --> Funded : Transaction Safety Provider approved Processing --> Failed : verification rejected Funded --> Releasable : delivery confirmed / release authorized Funded --> DisputeHold : dispute opened Releasable --> DisputeHold : dispute opened before payout DisputeHold --> Funded : dispute rejected / no financial action DisputeHold --> Releasable : resolved for seller DisputeHold --> Refunding : resolved for buyer Releasable --> Releasing : release instruction built Releasing --> Released : tx hash confirmed Releasing --> Failed : payout failed Refunding --> Refunded : refund tx hash confirmed Refunding --> Failed : refund failed Failed --> Releasing : admin retries release Failed --> Refunding : admin retries refund Released --> [*] Refunded --> [*] Cancelled --> [*] ``` ## Step-by-step Narrative ### 1. Funding 1. Buyer accepts a seller offer and starts Request Network checkout. 2. Backend creates a `Payment` and Request Network intent through `requestNetworkPayInService.ts`. 3. When configured, `getDestinationFor({ buyerId, sellerOfferId, chainId })` assigns a per-payment derived destination and stores it in `payment.metadata.derivedDestination`. 4. Frontend renders the in-house checkout block and the buyer signs RN-compatible on-chain transactions from their wallet. 5. Request Network webhook or reconciliation reports payment evidence. 6. The Transaction Safety Provider verifies: - transaction hash exists, - chain confirmations meet the runtime/env threshold, - token, recipient, and amount match, - AML/sanctions provider result when configured. 7. Only after safety approval does the backend mark the payment funded and append ledger entries. ### 2. Holding While escrow is funded, funds are represented in two places: - **On chain:** in the derived destination or custody wallet until swept/released/refunded. - **In app accounting:** in `FundsLedgerEntry` rows and `Payment.escrowState`. Release/refund eligibility must be derived from ledger availability, not raw mutable `Payment.status` alone. In production the roadmap requires `PAYMENT_LEDGER_ENFORCEMENT=true` before custody decentralization. ### 3. Release Release is triggered by delivery confirmation, auto-release policy, or dispute resolution for the seller. 1. Admin calls `POST /api/payment/:id/release`. 2. Backend loads the payment and validates ledger availability when enforcement is enabled. 3. Backend builds a provider payment instruction. 4. Custody signer executes the transaction: - current optional control: Trezor proof when `TREZOR_SAFEKEEPING_REQUIRED=true`; - roadmap control: Safe multisig transaction proposal/execution. 5. Admin confirms with `POST /api/payment/:id/release/confirm` and tx hash. 6. Backend validates Trezor proof when required, confirms adapter state, and appends a `release` ledger entry. ### 4. Refund Refund follows the same instruction/confirmation pattern as release, but destination is the buyer/refund wallet and ledger entry type is `refund`. Refund can be triggered by dispute resolution for the buyer, pre-fulfillment cancellation, or an admin/manual recovery flow. A refund during an active dispute must be an explicit resolution path, not an accidental bypass. ### 5. Dispute Hold Opening a dispute now has backend support through `releaseHoldService.ts`: it sets hold fields on the related purchase request and payments, and release/refund gates consult those holds. Remaining alignment work: - migrate from legacy dispute status enum to the canonical spec, - make financial side effects automatic from final dispute resolution, - ensure every release/refund path calls the same policy service, - record immutable audit entries for dispute resolution and custody execution. ## Sequence Diagram - Funding ```mermaid sequenceDiagram autonumber actor B as Buyer participant FE as Frontend participant BE as Backend participant RN as Request Network participant BC as EVM Chain participant DB as MongoDB B->>FE: Start Request Network checkout FE->>BE: POST /api/payment/request-network/intents BE->>DB: Payment.create(status="pending") BE->>BE: Assign derived destination when configured BE->>RN: Create Request Network intent BE-->>FE: inHouseCheckout block B->>BC: approve + transferFromWithReferenceAndFee RN-->>BE: signed webhook / status evidence BE->>BE: Transaction Safety Provider checks BE->>DB: Payment.status="completed", escrowState="funded" BE->>DB: append FundsLedgerEntry(payment_detected / hold) ``` ## Sequence Diagram - Release / Refund ```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 A->>BE: POST /api/payment/{id}/release or refund BE->>DB: Load Payment + ledger balance BE->>BE: Check dispute hold + ledger availability BE-->>A: unsigned instruction A->>C: Request signature / Safe execution C->>BC: Broadcast tx BC-->>C: txHash A->>BE: POST /confirm { txHash, optional trezor proof } BE->>BE: Verify signer proof when required BE->>DB: append release/refund ledger entry BE->>DB: escrowState="released" or "refunded" ``` ## API Calls | Method | Endpoint | Purpose | |---|---|---| | `POST` | `/api/payment/request-network/intents` | Create Request Network pay-in intent | | `GET` | `/api/payment/request-network/:paymentId/checkout` | Rehydrate in-house checkout block | | `POST` | `/api/payment/request-network/webhook` | Receive signed RN webhook | | `POST` | `/api/payment/:id/release` | Build release instruction | | `POST` | `/api/payment/:id/release/confirm` | Confirm release tx hash / signer proof | | `POST` | `/api/payment/:id/refund` | Build refund instruction | | `POST` | `/api/payment/:id/refund/confirm` | Confirm refund tx hash / signer proof | | `GET` | `/api/payment/:id` | Read payment details | | `GET` | `/api/payment/derived-destinations` | Admin list of derived destinations | ## Side Effects And Risks - **No custom on-chain escrow contract yet.** This is deliberate; [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] recommends Safe/Trezor custody controls before a custom contract pilot. - **Ledger enforcement is configurable.** `PAYMENT_LEDGER_ENFORCEMENT` must be enabled before real custody decentralization work is considered complete. - **Trezor enforcement is configurable.** `TREZOR_SAFEKEEPING_REQUIRED=true` makes Trezor proof mandatory for release/refund confirmation, but target custody should be Safe multisig. - **Durable webhook ingress is still roadmap work.** Until the Worker/replay layer is live, backend availability remains important for Request Network webhook delivery. - **Dispute model is implemented but not fully canonical.** The current model works with legacy enum names; canonical status alignment remains required. ## Linked Flows - [[PRD - Request Network In-House Checkout]] -- current primary pay-in path. - [[Dispute Flow]] -- can block or redirect escrow. - [[Delivery Confirmation Flow]] -- happy-path release trigger. - [[Payout Flow]] -- historical payout context and release mechanics. - [[Trezor Safekeeping Flow]] -- hardware proof for admin actions. - [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] -- custody decentralization and smart-contract decision plan. ## Source Files - Backend: `backend/src/models/Payment.ts` - Backend: `backend/src/models/FundsLedgerEntry.ts` - Backend: `backend/src/services/payment/requestNetwork/requestNetworkPayInService.ts` - Backend: `backend/src/services/payment/safety/transactionSafetyProvider.ts` - Backend: `backend/src/services/payment/orchestration/releaseRefundService.ts` - Backend: `backend/src/services/payment/wallets/derivedDestinations.ts` - Backend: `backend/src/services/payment/wallets/sweepService.ts` - Backend: `backend/src/services/dispute/releaseHoldService.ts` - Backend: `backend/src/services/trezor/trezorService.ts`