10 KiB
title, tags, related_models, related_apis
| title | tags | related_models | related_apis | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Escrow Flow |
|
|
|
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, andPurchaseRequeststate.
Current State Model
Payment.status remains the coarse provider/business state:
pendingprocessingconfirmedcompletedfailedcancelledrefunded
Payment.escrowState currently supports:
fundedreleasablereleasingreleasedrefundedfailedcancelledpartial
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.
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
- Buyer accepts a seller offer and starts Request Network checkout.
- Backend creates a
Paymentand Request Network intent throughrequestNetworkPayInService.ts. - When configured,
getDestinationFor({ buyerId, sellerOfferId, chainId })assigns a per-payment derived destination and stores it inpayment.metadata.derivedDestination. - Frontend renders the in-house checkout block and the buyer signs RN-compatible on-chain transactions from their wallet.
- Request Network webhook or reconciliation reports payment evidence.
- 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.
- 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
FundsLedgerEntryrows andPayment.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.
- Admin calls
POST /api/payment/:id/release. - Backend loads the payment and validates ledger availability when enforcement is enabled.
- Backend builds a provider payment instruction.
- Custody signer executes the transaction:
- current optional control: Trezor proof when
TREZOR_SAFEKEEPING_REQUIRED=true; - roadmap control: Safe multisig transaction proposal/execution.
- current optional control: Trezor proof when
- Admin confirms with
POST /api/payment/:id/release/confirmand tx hash. - Backend validates Trezor proof when required, confirms adapter state, and appends a
releaseledger 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
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
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_ENFORCEMENTmust be enabled before real custody decentralization work is considered complete. - Trezor enforcement is configurable.
TREZOR_SAFEKEEPING_REQUIRED=truemakes 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