Flow docs updated (11 files): - Delivery Confirmation: reversed actor roles (buyer generates, seller verifies), fixed endpoint paths (/delivery-code/generate, /delivery-code/verify) - Passkey (WebAuthn): removed stub/simulated-key claims; real @simplewebauthn/server attestation is implemented; refresh tokens are persisted - Dispute: corrected resolve schema (action enum), removed non-existent statuses, documented security gaps (no role guards on status/resolve/assign), route shadowing, all socket events are TODO stubs - Seller Offer: corrected all endpoint paths, removed 'active' status, documented withdraw dead code, missing seller history page, select-offer notification gap - Notification: corrected mark-all-read method+path, fixed GET /:id broken lookup, added unread-count-update socket event - Authentication: corrected rate limiter (counts all attempts), axios 403 not handled, deleteAccount wrong endpoint bug, changePassword no UI - Password Reset: corrected 6-digit code (not 8), documented no-complexity gap on reset-with-code vs token reset - Payment Flow DePay: /create→/save, removed phantom sub-routes, SIM_ bypass risk, PaymentProvider type gap, getProviderIntentEndpoint routing bug - Payment Flow SHKeeper: removed phantom polling endpoint, fixed release/refund paths - Purchase Request: added pending_payment/active statuses, fixed sellers/attachments endpoints, corrected socket events, PUT→PATCH bug - Escrow: documented dispute resolve does not touch escrow, route shadowing, confirm-delivery auth gap Issues created (35 files in Issues/): - 9 security issues (critical) including: dispute privilege escalation ×4, unauthenticated payment/scanner endpoints ×2, SIM_ production bypass, confirm-delivery ownership gap - 26 additional major/critical bugs covering broken endpoints, missing features, data integrity gaps, and frontend-backend mismatches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
16 KiB
title, tags, related_models, related_apis
| title | tags | related_models | related_apis | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Payment Flow - DePay & Web3 |
|
|
|
[!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 infrontend/src/sections/account/account-wallet-connection.tsx; the in-checkout integration is infrontend/src/sections/request/components/buyer-steps/payment-card.tsxandfrontend/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 —
paymentscollection, withproviderdistinguishing 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_ADDRESSis set on the frontend (seefrontend/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 testnet97). - Buyer has a wallet with USDT/USDC balance and a small amount of BNB for gas.
Step-by-step narrative
Phase 1 — Connect wallet
- Buyer hits the payment step and sees both pay options. Clicking "Pay with wallet" opens the WalletConnect modal via wagmi's
useConnect(). - The connection emits an
accountsChangedevent; the web3 context (frontend/src/web3/context/web3-provider.tsx) storeswallet.addressandwallet.chainId. - If
chainId !== 56(BSC), the UI prompts awallet_switchEthereumChainrequest.
[!warning] ⚠️ SECURITY: SIM_ bypass has no environment guard
web3-provider.tsxgeneratesSIM_-prefixed transaction hashes on wallet connection failure with noprocess.env.NODE_ENVcheck. In production, if a wallet connection fails, aSIM_hash can be submitted to the verify endpoint and may bypass on-chain verification checks. An explicitif (process.env.NODE_ENV === 'production') throwguard is required before generating simulation hashes.
Phase 2 — Create intent on backend
-
Frontend POSTs
POST /api/payment/decentralized/savewith{ purchaseRequestId, sellerOfferId, amount, fromAddress: wallet.address, token: 'USDT', network: 'bsc' }. The backend records aPaymentwithprovider: '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 —
PaymentProviderThe frontendPaymentProvidertype is defined as'request.network' | 'test' | 'other'. The values'shkeeper'and'decentralized'are missing from the union. Any UI provider-switch logic that branches onproviderwill fall through to an unknown/default state for these two providers. Add both to the type definition. -
Response includes the escrow wallet address and the exact token amount (in decimals — for USDT-BEP20 that's 18 decimals; the helper
convertPaymentAmountForShkeeperis shared fromcurrencyUtils.ts).[!warning] ⚠️ NOT IMPLEMENTED —
createDePayIntent()The frontend actioncreateDePayIntent()POSTs to/payment/depay/intents, which does not exist on the backend. Calling this action will always return 404. The working intent endpoint isPOST /api/payment/decentralized/save(step 4 above). Do not usecreateDePayIntent()until a/payment/depay/intentsroute is added to the backend.[!warning] ⚠️ KNOWN BUG —
getProviderIntentEndpoint()routing ThegetProviderIntentEndpoint()factory function always resolves to/payment/request-network/intentsregardless of theproviderargument passed in. Any SHKeeper checkout that calls this helper will POST to the wrong (Request Network) intent endpoint. This function requires a properswitch/ifonproviderbefore it can be used for non-Request-Network flows.
Phase 3 — Token approval (ERC-20 / BEP-20)
- The frontend checks the user's current allowance via
useReadContract/allowance(owner, spender)on the USDT contract. - If allowance < amount, the frontend prompts an
approve(escrow, amount)transaction. After confirmation, it proceeds to step 8.- Some flows skip
approveand 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 ofweb3Service.transferToken(...)infrontend/src/web3/web3Service.ts.
- Some flows skip
Phase 4 — On-chain transfer
- Frontend calls
transfer(escrowAddress, amount)on the USDT contract via wagmi'suseWriteContract. The wallet popup asks the user to confirm gas. - The buyer signs; the transaction is broadcast.
- Frontend listens via wagmi's
useWaitForTransactionReceipt({ hash }). Onsuccess(1 confirmation), it captures thetransactionHash.
Phase 5 — Backend verification
- Frontend POSTs
POST /api/payment/decentralized/verify/:paymentIdwith body{ transactionHash }. ThepaymentIdis a path parameter. Auth: Bearer JWT required (owner or admin). - Backend
BSCTransactionVerifier.verifyTransaction(txHash)(decentralizedPaymentService.ts):- JSON-RPC
eth_getTransactionReceiptagainstbsc-dataseed.binance.org. - Confirms
receipt.status === '0x1'(success). - Computes confirmations =
current eth_blockNumber - receipt.blockNumber. - Optionally decodes the
Transferevent log to confirmfrom,to, andvaluematch the expected payment.
- JSON-RPC
- 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.
- Updates the
- Returns
{ status: 'confirmed', confirmations, blockNumber }.
[!warning] ⚠️ Stats undercounting —
'completed'not counted as successful The admin stats aggregate counts only payments withstatus === '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
- 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 hitsGET /payment/:id/status. This route does not exist — there is no/statussub-route on any payment document endpoint. The call always returns 404. Similarly,POST /payment/:id/confirmdoes not exist; no/confirmsub-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/:iddoes 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 IMPLEMENTED — createDePayIntent() target. |
— |
[!warning] ⚠️
/api/payment/payments/:id/fetch-txhas no authentication The endpointPOST /api/payment/payments/:id/fetch-tx(note the/payments/infix — the previously documented path/api/payment/fetch-tx/:paymentIdwas 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.providerdistinguishes 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-notificationto 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(inWalletConnectionCard), 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 returnsfailed; backend marksPayment.status = 'failed'. Buyer can retry. - Transaction not yet mined at verification time → verifier returns
pendingwitherror: 'Transaction not found or still pending'. Frontend retries verification on a backoff schedule. - Tx hash already used by another payment —
blockchain.transactionHashhas 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
Transferevent'svalueis less than expected orto≠ escrow). The current verifier only checks the receipt'sstatus; 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
0x1means the transaction did not revert — it does not confirm the right amount went to the right address. Decode the ERC-20Transfer(address,address,uint256)event and assertto == ESCROW_WALLET_ADDRESSandvalue >= expectedAmount. The current implementation indecentralizedPaymentService.tschecks 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)