--- title: Payment Flow - DePay & Web3 tags: [flow, payment, web3, wagmi, walletconnect, bsc] related_models: ["[[Payment]]", "[[PurchaseRequest]]"] related_apis: ["POST /api/payment/decentralized/create", "POST /api/payment/decentralized/verify"] --- # 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. - **Frontend** — `frontend/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. - **Backend** — `decentralizedPaymentService.ts` (intent), `BSCTransactionVerifier` (on-chain verification), `decentralizedPaymentRoutes.ts`. - **Blockchain (BSC)** — verified via `https://bsc-dataseed.binance.org/` JSON-RPC. - **MongoDB** — `payments` collection, with `provider` distinguishing the legacy wallet-direct source from Request Network. - **Socket.IO** — `payment-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. ### Phase 2 — Create intent on backend 4. 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'` depending on enum extension), `direction: 'in'`, `status: 'pending'`, `blockchain.{network, token, sender, receiver: ESCROW_WALLET_ADDRESS}`. **Auth:** Bearer JWT required. 5. 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`). ### Phase 3 — Token approval (ERC-20 / BEP-20) 6. The frontend checks the user's current allowance via `useReadContract` / `allowance(owner, spender)` on the USDT contract. 7. 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 8. Frontend calls `transfer(escrowAddress, amount)` on the USDT contract via wagmi's `useWriteContract`. The wallet popup asks the user to confirm gas. 9. The buyer signs; the transaction is broadcast. 10. Frontend listens via wagmi's `useWaitForTransactionReceipt({ hash })`. On `success` (1 confirmation), it captures the `transactionHash`. ### Phase 5 — Backend verification 11. Frontend POSTs `POST /api/payment/decentralized/verify/:paymentId` with `{ transactionHash }`. **Auth:** Bearer JWT required (owner or admin). 12. 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. 13. 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. 14. Returns `{ status: 'confirmed', confirmations, blockNumber }`. ### Phase 6 — Frontend reaction 15. The checkout UI shows "Payment verified" with the block-explorer link (`https://bscscan.com/tx/{hash}`) and transitions to the awaiting-delivery state. ## Sequence diagram ```mermaid 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/create 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 | Source | |---|---|---| | `POST` | `/api/payment/decentralized/create` | `decentralizedPaymentRoutes.ts` | | `POST` | `/api/payment/decentralized/verify` | `decentralizedPaymentRoutes.ts` | | `GET` | `/api/payment/fetch-tx/:paymentId` | `paymentRoutes.ts` (manual rechecker) | ## 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). ## 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 payment** — `blockchain.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 (`/api/payment/fetch-tx/:paymentId`) 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 - [[PRD - Request Network In-House Checkout]] — current primary checkout. - [[Payment Flow - SHKeeper]] — historical sibling pay-in path retained for migration context. - [[Escrow Flow]] — funded state semantics. - [[Payout Flow]] — releasing the funded escrow to the seller. - [[Dispute Flow]] — refunds back to the buyer's verified wallet. ## 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`)