Files
nick-doc/04 - Flows/Payment Flow - DePay & Web3.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

18 KiB

title, tags, related_models, related_apis
title tags related_models related_apis
Payment Flow - DePay & Web3
flow
payment
web3
wagmi
walletconnect
bsc
Payment
PurchaseRequest
POST /api/payment/decentralized/save
POST /api/payment/decentralized/verify/:paymentId

Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)

[!caution] Audit — 2026-05-29 This document was reviewed against the live codebase. 12 corrections applied — endpoint paths, missing route bugs, TypeScript type gaps, a security issue, and stats undercounting. See inline ⚠️ callouts throughout.

Payment Flow — DePay & Web3 (Wallet-Direct)

[!warning] Historical/legacy path This page describes the older wallet-direct payment path. The current primary checkout is PRD - Request Network In-House Checkout with Request Network metadata, derived destinations, and Transaction Safety Provider checks. Keep this page for migration and verification context only.

Legacy alternative pay-in path: the buyer connects their own wallet (MetaMask / WalletConnect / Coinbase Wallet) and signs an on-chain transfer to the escrow wallet directly. The backend then verifies the transaction against the BSC RPC.

Actors

  • Buyer — owner of the wallet doing the on-chain transfer.
  • Frontendfrontend/src/web3/ (wagmi provider, web3-provider context); wallet-connect UI in frontend/src/sections/account/account-wallet-connection.tsx; the in-checkout integration is in frontend/src/sections/request/components/buyer-steps/payment-card.tsx and frontend/src/sections/request/components/buyer-steps/step-3-components/step-3-payment.tsx.
  • Wagmi / WalletConnect / MetaMask — wallet stack.
  • BackenddecentralizedPaymentService.ts (intent), BSCTransactionVerifier (on-chain verification), decentralizedPaymentRoutes.ts.
  • Blockchain (BSC) — verified via https://bsc-dataseed.binance.org/ JSON-RPC.
  • MongoDBpayments collection, with provider distinguishing the legacy wallet-direct source from Request Network.
  • Socket.IOpayment-created, plus the funded-escrow cascade events when verification succeeds.

Preconditions

  • NEXT_PUBLIC_ESCROW_WALLET_ADDRESS is set on the frontend (see frontend/src/global-config.ts). This is the destination address — typically a custodial BSC wallet operated by the platform admin.
  • Wagmi is configured with WalletConnect projectID and supported chains (BSC mainnet 56, optionally BSC testnet 97).
  • Buyer has a wallet with USDT/USDC balance and a small amount of BNB for gas.

Step-by-step narrative

Phase 1 — Connect wallet

  1. Buyer hits the payment step and sees both pay options. Clicking "Pay with wallet" opens the WalletConnect modal via wagmi's useConnect().
  2. The connection emits an accountsChanged event; the web3 context (frontend/src/web3/context/web3-provider.tsx) stores wallet.address and wallet.chainId.
  3. If chainId !== 56 (BSC), the UI prompts a wallet_switchEthereumChain request.

[!warning] ⚠️ SECURITY: SIM_ bypass has no environment guard web3-provider.tsx generates SIM_-prefixed transaction hashes on wallet connection failure with no process.env.NODE_ENV check. In production, if a wallet connection fails, a SIM_ hash can be submitted to the verify endpoint and may bypass on-chain verification checks. An explicit if (process.env.NODE_ENV === 'production') throw guard is required before generating simulation hashes.

Phase 2 — Create intent on backend

  1. Frontend POSTs POST /api/payment/decentralized/save with { purchaseRequestId, sellerOfferId, amount, fromAddress: wallet.address, token: 'USDT', network: 'bsc' }. The backend records a Payment with provider: 'other' (or 'decentralized' — see TypeScript type note below), direction: 'in', status: 'pending', blockchain.{network, token, sender, receiver: ESCROW_WALLET_ADDRESS}. Auth: Bearer JWT required.

    [!warning] ⚠️ TypeScript type gap — PaymentProvider The frontend PaymentProvider type is defined as 'request.network' | 'test' | 'other'. The values 'shkeeper' and 'decentralized' are missing from the union. Any UI provider-switch logic that branches on provider will fall through to an unknown/default state for these two providers. Add both to the type definition.

  2. Response includes the escrow wallet address and the exact token amount (in decimals — for USDT-BEP20 that's 18 decimals; the helper convertPaymentAmountForShkeeper is shared from currencyUtils.ts).

    [!warning] ⚠️ NOT IMPLEMENTED — createDePayIntent() The frontend action createDePayIntent() POSTs to /payment/depay/intents, which does not exist on the backend. Calling this action will always return 404. The working intent endpoint is POST /api/payment/decentralized/save (step 4 above). Do not use createDePayIntent() until a /payment/depay/intents route is added to the backend.

    [!warning] ⚠️ KNOWN BUG — getProviderIntentEndpoint() routing The getProviderIntentEndpoint() factory function always resolves to /payment/request-network/intents regardless of the provider argument passed in. Any SHKeeper checkout that calls this helper will POST to the wrong (Request Network) intent endpoint. This function requires a proper switch/if on provider before it can be used for non-Request-Network flows.

Phase 3 — Token approval (ERC-20 / BEP-20)

  1. The frontend checks the user's current allowance via useReadContract / allowance(owner, spender) on the USDT contract.
  2. If allowance < amount, the frontend prompts an approve(escrow, amount) transaction. After confirmation, it proceeds to step 8.
    • Some flows skip approve and use direct transfer instead (transfer(to, amount) — no approval needed because the buyer is the holder, not a contract pulling from them). The codebase favours direct transfer; see usages of web3Service.transferToken(...) in frontend/src/web3/web3Service.ts.

Phase 4 — On-chain transfer

  1. Frontend calls transfer(escrowAddress, amount) on the USDT contract via wagmi's useWriteContract. The wallet popup asks the user to confirm gas.
  2. The buyer signs; the transaction is broadcast.
  3. Frontend listens via wagmi's useWaitForTransactionReceipt({ hash }). On success (1 confirmation), it captures the transactionHash.

Phase 5 — Backend verification

  1. Frontend POSTs POST /api/payment/decentralized/verify/:paymentId with body { transactionHash }. The paymentId is a path parameter. Auth: Bearer JWT required (owner or admin).
  2. Backend BSCTransactionVerifier.verifyTransaction(txHash) (decentralizedPaymentService.ts):
    • JSON-RPC eth_getTransactionReceipt against bsc-dataseed.binance.org.
    • Confirms receipt.status === '0x1' (success).
    • Computes confirmations = current eth_blockNumber - receipt.blockNumber.
    • Optionally decodes the Transfer event log to confirm from, to, and value match the expected payment.
  3. On success the backend:
    • Updates the Payment: status = 'completed', escrowState = 'funded', blockchain.transactionHash, blockchain.confirmations, blockchain.confirmedAt = now.
    • Triggers the same funded-escrow cascade: mark winning offer accepted, reject others, transition request to payment, create chat, send notifications, emit socket events.
  4. Returns { status: 'confirmed', confirmations, blockNumber }.

[!warning] ⚠️ Stats undercounting — 'completed' not counted as successful The admin stats aggregate counts only payments with status === 'confirmed' as successful. DePay and SHKeeper payments reach 'completed' as their terminal state (not 'confirmed'), so the admin success count will be artificially low. The aggregate must include both 'confirmed' and 'completed' in the success set.

Phase 6 — Frontend reaction

  1. The checkout UI shows "Payment verified" with the block-explorer link (https://bscscan.com/tx/{hash}) and transitions to the awaiting-delivery state.

[!warning] ⚠️ Non-existent status/confirm endpoints — dispute payment card The dispute payment card "Verify" button calls getPaymentStatus(), which internally hits GET /payment/:id/status. This route does not exist — there is no /status sub-route on any payment document endpoint. The call always returns 404. Similarly, POST /payment/:id/confirm does not exist; no /confirm sub-route is registered. Remove both from any frontend code paths and rely on socket events (payment-update, payment-completed) or the verify endpoint instead.

Additionally, cancelPayment() in the web3 context is a local UI state reset only — it does not make an HTTP call. DELETE /payment/:id does not exist; there is no DELETE handler on any payment route.

Sequence diagram

sequenceDiagram
    autonumber
    actor B as Buyer
    participant W as Wallet (MetaMask/WC)
    participant FE as Frontend (wagmi)
    participant BE as Backend
    participant BC as BSC RPC
    participant DB as MongoDB
    participant IO as Socket.IO

    B->>FE: Click "Pay with wallet"
    FE->>W: connect()
    W-->>FE: { address, chainId }
    opt chainId != 56
        FE->>W: wallet_switchEthereumChain(0x38)
    end
    FE->>BE: POST /api/payment/decentralized/save
    BE->>DB: Payment.create({provider:"other", direction:"in", receiver:ESCROW})
    BE-->>FE: { paymentId, escrowAddress, amount }
    opt allowance < amount
        FE->>W: approve(escrow, amount)
        W-->>FE: tx confirmed
    end
    FE->>W: transfer(escrow, amount)
    W-->>FE: tx broadcast
    W-->>BC: signed tx
    BC-->>W: tx confirmed
    FE->>BE: POST /api/payment/decentralized/verify/:paymentId { txHash }
    BE->>BC: eth_getTransactionReceipt(txHash)
    BC-->>BE: { status:0x1, blockNumber, logs }
    BE->>BC: eth_blockNumber
    BC-->>BE: currentBlock
    BE->>BE: confirmations = currentBlock - txBlock
    BE->>DB: Payment.status="completed"\nescrowState="funded"\ntx hash + confirmations
    BE->>BE: cascade (mark offer accepted, others rejected,\nrequest→payment, chat, notifications)
    BE->>IO: emit payment-completed events
    BE-->>FE: { status:"confirmed", confirmations }
    FE-->>B: "Payment verified ✓" + BscScan link

API calls

Method Endpoint Notes Source
POST /api/payment/decentralized/save Create intent decentralizedPaymentRoutes.ts
POST /api/payment/decentralized/verify/:paymentId paymentId is a path param decentralizedPaymentRoutes.ts
POST /api/payment/payments/:id/fetch-tx Manual tx rechecker — NO AUTH (exploitable without credentials) paymentRoutes.ts
POST /api/payment/decentralized/create ⚠️ 404 — does not exist. Use /save instead.
GET /payment/:id/status ⚠️ 404 — does not exist. No /status sub-route.
POST /payment/:id/confirm ⚠️ 404 — does not exist. No /confirm sub-route.
DELETE /payment/:id ⚠️ 404 — does not exist. cancelPayment() is UI-only.
POST /payment/depay/intents ⚠️ NOT IMPLEMENTEDcreateDePayIntent() target.

[!warning] ⚠️ /api/payment/payments/:id/fetch-tx has no authentication The endpoint POST /api/payment/payments/:id/fetch-tx (note the /payments/ infix — the previously documented path /api/payment/fetch-tx/:paymentId was wrong on both method and path) accepts requests without any authentication check. Any unauthenticated caller can trigger a blockchain re-fetch for any payment ID. This must be gated behind at minimum an admin JWT before production use.

Request Network sub-routes — NOT IMPLEMENTED

The following four Request Network payout/release/refund sub-paths are not registered in the backend router. All return 404:

Path Status
POST /api/payment/request-network/:id/payout/initiate ⚠️ NOT IMPLEMENTED — 404
POST /api/payment/request-network/:id/payout/confirm ⚠️ NOT IMPLEMENTED — 404
POST /api/payment/request-network/:id/release/confirm ⚠️ NOT IMPLEMENTED — 404
POST /api/payment/request-network/:id/refund/confirm ⚠️ NOT IMPLEMENTED — 404

Database writes

  • payments — same model as the Request Network flow. provider distinguishes the source.
  • selleroffers, purchaserequests, chats, notifications — identical funded-escrow cascade (offer accepted, others rejected, request → payment, chat created, notifications fanned out).

Payment status values

status escrowState Meaning
pending Intent created, awaiting on-chain transfer
completed funded On-chain transfer verified (terminal success for DePay/wallet-direct)
failed Transaction reverted or verification failed

escrowState values (backend-authoritative)

escrowState Meaning
funded Escrow received the on-chain transfer
releasable Escrow funds cleared for release to seller
releasing Release to seller in progress (intermediate state)
released Funds sent to seller
refunding Refund to buyer in progress
refunded Funds returned to buyer

[!note] 'completed' is not counted as a successful payment in stats paymentService.getPaymentStats counts only status === 'confirmed' as successfulPayments. DePay/wallet-direct payments terminate at 'completed', so they are excluded from the success count. The aggregate must include 'completed' alongside 'confirmed' to avoid undercounting.

Socket events emitted

  • payment-created (admin dashboard) on intent creation.
  • seller-offer-update 'payment-completed' and 'offer-rejected' post-verification.
  • purchase-request-update 'status-changed'.
  • new-notification to both parties.

Side effects

  • No provider custody — the escrow wallet is custodial; the platform admin/custody signer controls the keys. Releases from this wallet to sellers should follow Payout Flow and the Safe/hardware-backed roadmap in PRD - Decentralized Custody and Smart-Contract Escrow Roadmap.
  • Verified-wallet field — the buyer's connected wallet is also saved against User.profile.walletAddress (in WalletConnectionCard), which is later used to refund this same wallet if the order is disputed.

Error / edge cases

  • Wrong network → frontend forces a chain switch; if user refuses, transaction will fail or hit the wrong chain (escrow is BSC-only today).
  • Insufficient gas (BNB) → wallet rejects the tx; nothing to verify.
  • Transaction reverted (receipt.status === '0x0') → verifier returns failed; backend marks Payment.status = 'failed'. Buyer can retry.
  • Transaction not yet mined at verification time → verifier returns pending with error: 'Transaction not found or still pending'. Frontend retries verification on a backoff schedule.
  • Tx hash already used by another paymentblockchain.transactionHash has a sparse index (Payment.ts:178); a duplicate verification attempt finds the existing payment and returns its status.
  • Wrong amount or wrong recipient — must be enforced by log decoding (the verifier should reject if the Transfer event's value is less than expected or to ≠ escrow). The current verifier only checks the receipt's status; tightening this is recommended.
  • RPC throttling — public BSC dataseed is generous but rate-limits exist; consider a dedicated RPC (Ankr, QuickNode) for production.
  • User closes the browser before verification — the on-chain transfer still happened. A periodic reconciliation job (POST /api/payment/payments/:id/fetch-tx) or admin tool can replay verification from the txHash.
  • Confirmation depth — currently 1 confirmation triggers completed. For larger amounts consider gating release until ≥ 12 confirmations on BSC.

[!warning] Verify the event log, not just the receipt A receipt status of 0x1 means the transaction did not revert — it does not confirm the right amount went to the right address. Decode the ERC-20 Transfer(address,address,uint256) event and assert to == ESCROW_WALLET_ADDRESS and value >= expectedAmount. The current implementation in decentralizedPaymentService.ts checks only the receipt status; harden this before accepting large payments.

Linked flows

Source files

  • Backend: backend/src/services/payment/decentralizedPaymentService.ts
  • Backend: backend/src/services/payment/decentralizedPaymentRoutes.ts
  • Backend: backend/src/services/payment/paymentCoordinator.ts
  • Backend: backend/src/models/Payment.ts
  • Frontend: frontend/src/web3/web3Service.ts
  • Frontend: frontend/src/web3/context/wagmi-provider.tsx
  • Frontend: frontend/src/web3/context/web3-provider.tsx
  • Frontend: frontend/src/sections/account/account-wallet-connection.tsx
  • Frontend: frontend/src/sections/request/components/buyer-steps/payment-card.tsx
  • Frontend: frontend/src/sections/request/components/buyer-steps/step-3-components/step-3-payment.tsx
  • Frontend: frontend/src/global-config.ts (ESCROW_WALLET_ADDRESS)