---
title: Payment Flow - DePay & Web3
tags: [flow, payment, web3, wagmi, walletconnect, bsc]
related_models: ["[[Payment]]", "[[PurchaseRequest]]"]
related_apis: ["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.
- **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.

> [!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

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'` — 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.

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`).

   > [!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)

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 body `{ transactionHash }`. The `paymentId` is a **path parameter**. **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 }`.

> [!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

15. 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

```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/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 IMPLEMENTED** — `createDePayIntent()` 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 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 (`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

- [[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`)
