docs: sync from backend 19f7eb9, frontend 60ee6fb — Task #10 AML screening

This commit is contained in:
Siavash Sameni
2026-05-28 20:35:38 +04:00
parent fd2aa71ef4
commit ddc0434819
34 changed files with 709 additions and 453 deletions

View File

@@ -44,17 +44,17 @@ created: 2026-05-23
### Dispute ### Dispute
> [!info] Definition > [!info] Definition
> A formal complaint opened by either party when a deal goes wrong. Would create a three-way chat (buyer, seller, admin) and a `Dispute` document with a structured `timeline[]`, `evidence[]`, and `resolution`. Categories: `product_quality | delivery_delay | wrong_item | payment_issue | seller_behavior | other`. Outcomes: `refund | replacement | compensation | warning_seller | ban_seller | no_action`. See `backend/src/models/Dispute.ts` *(planned, not yet implemented)*. > A formal complaint opened by either party when a deal goes wrong. Creates a three-way chat (buyer, seller, admin) and a `Dispute` document with a structured `timeline[]`, `evidence[]`, and `resolution`. Categories: `product_quality | delivery_delay | wrong_item | payment_issue | seller_behavior | other`. Outcomes in the current model: `refund | replacement | compensation | warning_seller | ban_seller | no_action`. See `backend/src/models/Dispute.ts` and [[Dispute Flow]].
### Escrow ### Escrow
> [!info] Definition > [!info] Definition
> The custodial period during which buyer funds are held by the platform (SHKeeper or the smart contract layer) after payment but before release to the seller. Escrow guarantees the seller will be paid if they deliver, and guarantees the buyer can be refunded if they do not. The defining feature of Amn. > The custodial period during which buyer funds are held by platform-controlled custody infrastructure after payment but before release to the seller. The current primary path uses Request Network pay-in, per-payment derived destinations, transaction-safety checks, and an internal funds ledger. Future custody decentralization is tracked in [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].
### Idempotency ### Idempotency
> [!info] Definition > [!info] Definition
> The property that the same request (identified by an idempotency key) can safely be retried without performing the underlying operation more than once. Critical for payment webhooks — SHKeeper may deliver the same webhook several times if it does not receive a 200 quickly. Amn enforces idempotency in `PaymentCoordinator` and at the model level via unique constraints on transaction hashes. > The property that the same request (identified by an idempotency key) can safely be retried without performing the underlying operation more than once. Critical for payment webhooks and release/refund confirmations. Amn enforces idempotency in `PaymentCoordinator`, Request Network delivery handling, pending-intent indexes, and ledger idempotency keys.
### JWT (Access / Refresh) ### JWT (Access / Refresh)
@@ -89,12 +89,12 @@ created: 2026-05-23
### Pay-in ### Pay-in
> [!info] Definition > [!info] Definition
> Money flowing **into** escrow from the buyer. Recorded as `Payment.direction: "in"`. The buyer's choice of pay-in surface (SHKeeper invoice vs. Web3 wallet) is independent of how the payout will be sent. > Money flowing **into** escrow from the buyer. Recorded as `Payment.direction: "in"`. The current primary path is Request Network in-house checkout, with payment safety verified by webhook/reconciliation plus on-chain transaction checks.
### Pay-in Intent ### Pay-in Intent
> [!info] Definition > [!info] Definition
> The pre-authorisation record created when a buyer commits to paying but the funds have not yet arrived on-chain. Holds the chosen amount, currency, expected wallet address (SHKeeper) or counterparty (DePay), and an expiry. Becomes a confirmed `Payment` once the chain or webhook confirms settlement. > The pre-authorisation record created when a buyer commits to paying but the funds have not yet arrived on-chain. Holds amount, currency, Request Network IDs/payment reference, in-house checkout metadata, and expected destination. Becomes a confirmed `Payment` only after webhook/reconciliation and transaction-safety checks approve settlement.
### Payment ### Payment
@@ -109,7 +109,7 @@ created: 2026-05-23
### Payout ### Payout
> [!info] Definition > [!info] Definition
> Money flowing **out** of escrow to the seller's wallet. Recorded as `Payment.direction: "out"`. Triggered by admin action after delivery is confirmed; implemented via SHKeeper's payout API (`shkeeperPayoutService.ts`). > Money flowing **out** of escrow to the seller's wallet. Triggered by release/refund orchestration after delivery confirmation or dispute resolution. The roadmap moves execution authority to Safe multisig/hardware signers before any custom smart-contract escrow pilot.
### Points ### Points
@@ -174,7 +174,7 @@ created: 2026-05-23
### SHKeeper ### SHKeeper
> [!info] Definition > [!info] Definition
> A self-hosted crypto payment processor used as Amn's primary custodial pay-in / payout rail. Issues a fresh wallet address per invoice, watches the chain for incoming USDT, and emits a signed webhook on settlement. Lives at `https://pay.amn.gg` per `backend/TODO.md`. Integration code under `backend/src/services/payment/shkeeper/`. > A self-hosted crypto payment processor used by older Amanat payment designs. Its docs remain for migration and historical context, but the current backend payment tree has moved to Request Network as the primary provider.
### Socket Room ### Socket Room
@@ -194,12 +194,12 @@ created: 2026-05-23
### USDT / USDC ### USDT / USDC
> [!info] Definition > [!info] Definition
> The two stablecoins Amn supports out of the box for pay-in and payout. USDT is the default for SHKeeper invoices; both are supported in offer pricing (`SellerOffer.price.currency` enum: `USD | EUR | IRR | USDT | USDC`). > The two stablecoins Amn supports out of the box for pay-in and payout. Request Network token registry work covers USDC/USDT across supported EVM chains; both are also supported in offer pricing (`SellerOffer.price.currency` enum: `USD | EUR | IRR | USDT | USDC`).
### Webhook ### Webhook
> [!info] Definition > [!info] Definition
> An inbound HTTP POST from an external service notifying Amn of an event. SHKeeper webhooks (`/api/payment/shkeeper/webhook`) are the most important they confirm pay-ins. All webhooks are HMAC-signed; verification uses `SHKEEPER_WEBHOOK_SECRET`. Failed verifications are dropped. > An inbound HTTP POST from an external service notifying Amn of an event. The primary payment webhook is Request Network at `/api/payment/request-network/webhook`, signed with `x-request-network-signature`. Roadmap work puts durable ingress/replay in front of the backend while keeping backend signature verification and transaction-safety checks as the trust boundary.
### WalletConnect ### WalletConnect

View File

@@ -11,7 +11,7 @@ created: 2026-05-23
> - **Passkeys hardened** — challenge consumption is now single-use with immediate deletion, 5-minute expiry, and replay-attack protection. > - **Passkeys hardened** — challenge consumption is now single-use with immediate deletion, 5-minute expiry, and replay-attack protection.
> - **Web3 verification real** — `BSCTransactionVerifier` performs on-chain `eth_getTransactionReceipt` validation with confirmation counting. > - **Web3 verification real** — `BSCTransactionVerifier` performs on-chain `eth_getTransactionReceipt` validation with confirmation counting.
> - **Socket.IO auth enforced** — all socket connections require a valid JWT; room joins enforce strict ownership/participation checks. > - **Socket.IO auth enforced** — all socket connections require a valid JWT; room joins enforce strict ownership/participation checks.
> - **Dispute holds** documented as planned but not yet implemented; the `Dispute` model, service layer, and API routes do not exist in the current backend. > - **Dispute holds** now exist in the backend through the dispute/release-hold service; remaining work is canonical state-machine alignment and stronger release/refund policy enforcement.
> - **Data model docs aligned** with actual Mongoose schemas (Payment provider/escrowState enums, User model omissions documented). > - **Data model docs aligned** with actual Mongoose schemas (Payment provider/escrowState enums, User model omissions documented).
# Introduction # Introduction
@@ -34,7 +34,7 @@ Traditional marketplaces tend to live at one of two extremes:
1. **Fully custodial platforms** (Amazon, eBay, Fiverr) take a large cut, dictate every term of the transaction, and freeze funds on a whim. They work, but they are expensive and opaque. 1. **Fully custodial platforms** (Amazon, eBay, Fiverr) take a large cut, dictate every term of the transaction, and freeze funds on a whim. They work, but they are expensive and opaque.
2. **Free-form P2P channels** (Telegram groups, Discord servers, direct DMs) charge nothing but offer no protection at all. The first scam empties the wallet and there is no recourse. 2. **Free-form P2P channels** (Telegram groups, Discord servers, direct DMs) charge nothing but offer no protection at all. The first scam empties the wallet and there is no recourse.
Amn sits between the two. It charges a thin escrow margin, holds funds for only as long as it takes to confirm delivery, and supports both fiat-style stablecoin escrow (via [[SHKeeper]]) and direct on-chain settlement (via [[DePay]] and the user's own wallet) — meaning the buyer can keep custody of their crypto until the literal moment of release. Amn sits between the two. It charges a thin escrow margin, holds funds for only as long as it takes to confirm delivery, and now routes primary stablecoin pay-in through Request Network with an Amanat-rendered wallet checkout. The buyer keeps custody of their crypto until they sign the on-chain payment, while the platform keeps settlement, safety checks, and dispute resolution in one auditable flow.
> [!tip] Why "crypto-native"? > [!tip] Why "crypto-native"?
> The escrow rails are built around stablecoins (USDT/USDC) on EVM chains rather than card networks. That means no chargebacks, no 3-day settlement, no geographic restrictions — and a transparent, auditable transaction trail for every step of the deal. See [[Tech Stack]] for the full Web3 surface. > The escrow rails are built around stablecoins (USDT/USDC) on EVM chains rather than card networks. That means no chargebacks, no 3-day settlement, no geographic restrictions — and a transparent, auditable transaction trail for every step of the deal. See [[Tech Stack]] for the full Web3 surface.
@@ -56,7 +56,7 @@ Beyond the four roles, two ambient audiences read the platform:
A handful of design choices set Amn apart from generic marketplace software: A handful of design choices set Amn apart from generic marketplace software:
1. **Dual payment rails.** Every order can be paid through SHKeeper (a self-hosted crypto payment processor that issues a fresh wallet per invoice) *or* through a Web3 wallet connect flow (DePay + Wagmi/Viem + MetaMask). The buyer picks; the escrow logic is identical downstream. See [[Payments Overview]]. 1. **Request Network in-house checkout.** Every order can be paid through an Amanat-rendered Web3 checkout that builds Request Network-compatible transactions directly in the buyer's wallet. The hosted Request Network page remains a fallback, while the app keeps Rabby/MetaMask UX, chain choice, transaction safety checks, and escrow state in-house.
2. **Request-first marketplace.** Most platforms list *products*. Amn lists *needs*. Buyers describe what they want and let the market come to them — closer to a reverse auction than a catalogue. The unidirectional flow eliminates the "thousand-listings-with-no-stock" problem. 2. **Request-first marketplace.** Most platforms list *products*. Amn lists *needs*. Buyers describe what they want and let the market come to them — closer to a reverse auction than a catalogue. The unidirectional flow eliminates the "thousand-listings-with-no-stock" problem.
3. **Request Templates.** Power buyers (and admins) can publish reusable purchase request templates that act like express checkouts — a buyer clicks "I want this" and the order is opened pre-filled. Templates are the bridge between Amn and conventional ecommerce. 3. **Request Templates.** Power buyers (and admins) can publish reusable purchase request templates that act like express checkouts — a buyer clicks "I want this" and the order is opened pre-filled. Templates are the bridge between Amn and conventional ecommerce.
4. **First-class i18n with RTL.** The frontend ships with six locales out of the box (English, French, Vietnamese, Chinese, Arabic, Persian) and full right-to-left support — Persian is the default fallback. See `frontend/src/locales/locales-config.ts:36`. 4. **First-class i18n with RTL.** The frontend ships with six locales out of the box (English, French, Vietnamese, Chinese, Arabic, Persian) and full right-to-left support — Persian is the default fallback. See `frontend/src/locales/locales-config.ts:36`.
@@ -78,4 +78,4 @@ A handful of design choices set Amn apart from generic marketplace software:
## Project status at a glance ## Project status at a glance
Amn is at version **2.6.x** across both repositories, on the `development` branch, and tagged "production-ready with minor enhancements" by the project leads. The core escrow loop, real-time chat, multi-language UI, dispute system, points programme, and blog are all live. Active work focuses on UX polish, admin analytics, and a more granular permissions matrix — see `backend/TODO.md` and `frontend/VERSION_0_PREPARATION_TODO.md` for the rolling task list, and [[Roadmap]] (forthcoming) for the strategic view. Amn is at version **2.6.x** across both repositories, on the `development` branch, and tagged "production-ready with minor enhancements" by the project leads. The core escrow loop, real-time chat, multi-language UI, dispute system, points programme, and blog are all live. Active work focuses on Request Network hardening, durable webhook ingress, derived-destination custody, admin signing, and a more granular permissions matrix. The custody/smart-contract strategy lives in [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].

View File

@@ -37,7 +37,7 @@ flowchart LR
- **Browse and search** the public marketplace and request templates. - **Browse and search** the public marketplace and request templates.
- **Create a [[Purchase Request]]** describing what they want — product type (physical / digital / service / consultation), budget, urgency, delivery info, attachments. See `backend/src/models/PurchaseRequest.ts`. - **Create a [[Purchase Request]]** describing what they want — product type (physical / digital / service / consultation), budget, urgency, delivery info, attachments. See `backend/src/models/PurchaseRequest.ts`.
- **Review incoming [[Seller Offer]]s**, negotiate over chat, accept the best one. - **Review incoming [[Seller Offer]]s**, negotiate over chat, accept the best one.
- **Pay** via [[SHKeeper]] (custodial crypto invoice) or Web3 wallet ([[DePay]] + MetaMask through Wagmi). - **Pay** via the Request Network in-house checkout, using a supported EVM wallet through Wagmi/WalletConnect and the platform's payment request metadata.
- **Track the order** through `processing → delivery → delivered → confirming → completed` states. - **Track the order** through `processing → delivery → delivered → confirming → completed` states.
- **Confirm receipt** (or let the SLA auto-confirm), leave a review, accrue points. - **Confirm receipt** (or let the SLA auto-confirm), leave a review, accrue points.
- **Open a [[Dispute]]** if delivery never lands, item is wrong, or quality is poor. - **Open a [[Dispute]]** if delivery never lands, item is wrong, or quality is poor.
@@ -86,7 +86,7 @@ The buyer dashboard lives under `/dashboard` (`frontend/src/app/dashboard/`). No
- **Negotiate** in the per-request chat — bilateral with the buyer until an offer is accepted. - **Negotiate** in the per-request chat — bilateral with the buyer until an offer is accepted.
- **Fulfil** the order: ship physical goods (with optional tracking number), or upload/email digital deliverables. - **Fulfil** the order: ship physical goods (with optional tracking number), or upload/email digital deliverables.
- **Use the [[delivery code]]** for physical handoffs: a six-digit one-time code the buyer reads to the courier to confirm receipt. - **Use the [[delivery code]]** for physical handoffs: a six-digit one-time code the buyer reads to the courier to confirm receipt.
- **Receive payout** automatically via SHKeeper to the configured wallet once the order is finalised (admin-triggered batch or per-order based on shop policy). - **Receive payout** to the configured wallet after ledger-gated release. Today this is an admin/custody-signer operation; the target path is Safe/hardware-backed approvals as described in [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].
- **Manage [[Request Templates]]** scoped to their shop — publish "off-the-shelf" offerings buyers can purchase in one click. - **Manage [[Request Templates]]** scoped to their shop — publish "off-the-shelf" offerings buyers can purchase in one click.
- **Engage with reviews and disputes**: respond to reviews, contest disputes, provide evidence. - **Engage with reviews and disputes**: respond to reviews, contest disputes, provide evidence.
@@ -125,12 +125,12 @@ Seller dashboard reuses the same `/dashboard` shell with extra modules:
- **Moderate users**: suspend / unsuspend accounts (`User.status: "active" | "suspended" | "deleted"`, see `backend/src/models/User.ts`), promote buyers to sellers, ban repeat offenders. - **Moderate users**: suspend / unsuspend accounts (`User.status: "active" | "suspended" | "deleted"`, see `backend/src/models/User.ts`), promote buyers to sellers, ban repeat offenders.
- **Moderate marketplace content**: categories (`Category` model), request templates (the canonical platform-wide ones), blog posts. - **Moderate marketplace content**: categories (`Category` model), request templates (the canonical platform-wide ones), blog posts.
- **Resolve disputes**: get assigned to disputes, drive them to resolution, choose an outcome (`refund | replacement | compensation | warning_seller | ban_seller | no_action`). See `backend/src/services/dispute/DisputeService.ts` *(planned, not yet implemented)*. - **Resolve disputes**: get assigned to disputes, drive them to resolution, choose an outcome (`refund | replacement | compensation | warning_seller | ban_seller | no_action`). See `backend/src/services/dispute/DisputeService.ts` and [[Dispute Flow]].
- **Operate payments**: trigger payouts, fetch on-chain transactions, manually confirm stuck payments (the manual transaction-hash flow described in `backend/TODO.md`), audit the SHKeeper webhook history (`services/payment/shkeeper/webhookStats.ts`). - **Operate payments**: trigger ledger-gated releases/refunds, review Request Network webhooks, inspect derived destination wallets, fetch on-chain transactions, and manually confirm stuck payments only after Transaction Safety Provider checks.
- **Configure the platform**: levels (`LevelConfig`), points multipliers, blog seed content, default templates. - **Configure the platform**: levels (`LevelConfig`), points multipliers, blog seed content, default templates.
- **Run data cleanup**: `/api/admin/cleanup` exposes destructive maintenance utilities (`services/admin/`). - **Run data cleanup**: `/api/admin/cleanup` exposes destructive maintenance utilities (`services/admin/`).
- **Author blog posts** via the TipTap rich-text editor. - **Author blog posts** via the TipTap rich-text editor.
- **Monitor health**: SHKeeper status (background health monitor in `app.ts:433`), Redis, MongoDB. - **Monitor health**: Request Network webhook/reconciliation status, ledger enforcement, custody signer/Safe readiness, Redis, and MongoDB.
### Key permissions ### Key permissions
@@ -149,7 +149,7 @@ Admins see the buyer/seller surfaces plus dedicated admin modules (typically und
- User management (search, suspend, role change) - User management (search, suspend, role change)
- Dispute queue with assignment and resolution - Dispute queue with assignment and resolution
- Payment console (manual confirmation, payout dispatch, webhook log) - Payment console (manual confirmation, release/refund dispatch, Request Network webhook and ledger log)
- Category and template management - Category and template management
- Blog editor (publish / unpublish / featured) - Blog editor (publish / unpublish / featured)
- Platform analytics (TODO — see `backend/TODO.md`) - Platform analytics (TODO — see `backend/TODO.md`)

View File

@@ -16,7 +16,7 @@ Amn is a **two-repo system**:
- **Frontend** (`/Users/mojtabaheidari/code/frontend`) — a Next.js 16 App Router application that serves the marketplace UI, the admin dashboard, the public blog, and the user-facing Web3 wallet flow. - **Frontend** (`/Users/mojtabaheidari/code/frontend`) — a Next.js 16 App Router application that serves the marketplace UI, the admin dashboard, the public blog, and the user-facing Web3 wallet flow.
- **Backend** (`/Users/mojtabaheidari/code/backend`) — an Express 5 + TypeScript API server that owns all business logic, persists to MongoDB, caches in Redis, and brokers all external integrations. - **Backend** (`/Users/mojtabaheidari/code/backend`) — an Express 5 + TypeScript API server that owns all business logic, persists to MongoDB, caches in Redis, and brokers all external integrations.
The two repos are deployable independently. They communicate over **HTTPS (REST)** for stateful actions and over **WebSocket (Socket.IO)** for live updates. The frontend never talks directly to MongoDB, Redis, SHKeeper, or OpenAI — every external interaction is mediated by the backend so that secrets stay on the server. The two repos are deployable independently. They communicate over **HTTPS (REST)** for stateful actions and over **WebSocket (Socket.IO)** for live updates. The frontend never talks directly to MongoDB, Redis, Request Network API keys, OpenAI, or admin custody secrets -- every sensitive external interaction is mediated by the backend so that secrets stay on the server.
## System map ## System map
@@ -41,7 +41,7 @@ flowchart TB
Auth["Auth service<br/>JWT + Passkey + Google + Telegram"] Auth["Auth service<br/>JWT + Passkey + Google + Telegram"]
Market["Marketplace service<br/>Requests, Offers, Templates"] Market["Marketplace service<br/>Requests, Offers, Templates"]
ChatSvc["Chat service"] ChatSvc["Chat service"]
PaySvc["Payment service<br/>SHKeeper + Request Network + ledger"] PaySvc["Payment service<br/>Request Network + ledger + custody controls"]
TelegramSvc["Telegram service<br/>bot + Mini App + notifications"] TelegramSvc["Telegram service<br/>bot + Mini App + notifications"]
Disp["Dispute service"] Disp["Dispute service"]
Points["Points / Referrals"] Points["Points / Referrals"]
@@ -58,8 +58,6 @@ flowchart TB
end end
subgraph External["External services"] subgraph External["External services"]
SHK["SHKeeper<br/>crypto invoicing"]
DePay["DePay widget"]
Chain["EVM chains<br/>BSC / ETH / Polygon"] Chain["EVM chains<br/>BSC / ETH / Polygon"]
SMTP["SMTP<br/>(nodemailer)"] SMTP["SMTP<br/>(nodemailer)"]
OpenAI["OpenAI API"] OpenAI["OpenAI API"]
@@ -68,6 +66,7 @@ flowchart TB
Alchemy["Alchemy RPC"] Alchemy["Alchemy RPC"]
TelegramAPI["Telegram Bot API<br/>+ Mini App"] TelegramAPI["Telegram Bot API<br/>+ Mini App"]
ReqNet["Request Network<br/>pay-in / webhooks"] ReqNet["Request Network<br/>pay-in / webhooks"]
CFWorker["Durable webhook ingress<br/>(roadmap)"]
end end
Browser --> SSR Browser --> SSR
@@ -88,13 +87,10 @@ flowchart TB
Auth & PaySvc & Notif --> RedisDB Auth & PaySvc & Notif --> RedisDB
Files --> Disk Files --> Disk
PaySvc <--> SHK
SHK -.webhook.-> PaySvc
PaySvc <--> ReqNet PaySvc <--> ReqNet
ReqNet -.webhook.-> PaySvc ReqNet -.webhook.-> CFWorker
CFWorker -.forward/replay.-> PaySvc
PaySvc --> Chain PaySvc --> Chain
Wagmi --> DePay
DePay --> Chain
PaySvc -.tx fetch.-> Alchemy PaySvc -.tx fetch.-> Alchemy
TelegramSvc <--> TelegramAPI TelegramSvc <--> TelegramAPI
@@ -130,16 +126,17 @@ The heart of the platform. Three first-class models drive it:
Services live in `backend/src/services/marketplace/` and are exposed through `/api/marketplace/*`. The frontend uses a mix of React Query (`@tanstack/react-query`) and SWR for data fetching, with mutations gated through the actions layer in `frontend/src/actions/`. Services live in `backend/src/services/marketplace/` and are exposed through `/api/marketplace/*`. The frontend uses a mix of React Query (`@tanstack/react-query`) and SWR for data fetching, with mutations gated through the actions layer in `frontend/src/actions/`.
### Payments — [[Payments Overview]] / [[SHKeeper Integration]] ### Payments -- Request Network, Ledger, And Custody Controls
Payments are where Amn is most distinctive. The backend supports **four payment surfaces** routed through a common `Payment` model (`backend/src/models/Payment.ts`) via a provider-neutral adapter layer (`backend/src/services/payment/adapters/`): Payments are where Amn is most distinctive. The live backend has converged on **Request Network** as the primary provider through a common `Payment` model (`backend/src/models/Payment.ts`) and provider-neutral adapter layer (`backend/src/services/payment/adapters/`):
- **SHKeeper** `/api/payment/shkeeper`. Issues a fresh wallet address per invoice, polls / webhooks for payment confirmation, and runs through `PaymentCoordinator` to avoid race conditions. Health is monitored in the background (`shkeeperHealthCheck.ts`). - **Request Network pay-in** -- `/api/payment/request-network`. Creates requests, exposes the Amanat in-house checkout block, and receives signed webhooks (`x-request-network-signature`). Pay-in service: `requestNetworkPayInService.ts`; reconciliation: `requestNetworkReconciliationService.ts`.
- **Request Network** — `/api/payment/request-network`. Creates on-chain payment requests via the Request Network protocol, generates Secure Payment Page URLs for the buyer, and receives real-time payment status via signed webhooks (`x-request-network-signature`). Pay-in service: `requestNetworkPayInService.ts`; reconciliation: `requestNetworkReconciliationService.ts`. - **In-house wallet checkout** -- buyer signs the RN-compatible `approve` + `transferFromWithReferenceAndFee` flow from their own wallet, so Rabby/MetaMask wallet UX stays inside Amanat.
- **Decentralized (Wagmi + DePay)** `/api/payment/decentralized`. The user signs and sends the transfer from their own wallet; the backend verifies on-chain via `blockchainTxFetcher.ts` and the Alchemy SDK. - **Derived destination wallets** -- `/api/payment/derived-destinations` admin endpoints manage per-`(buyer, sellerOffer, chainId)` receiving addresses, sweep status, and config health.
- **Payout** `/api/payment/shkeeper/payout`. Admin-triggered release of escrow funds to the seller's wallet once delivery is confirmed. - **Funds ledger** -- `backend/src/services/payment/ledger/` tracks payment detection, holds, releases, refunds, fees, and adjustments independently of provider metadata.
- **Release/refund orchestration** -- `/api/payment/:id/(release|refund)` builds instructions; `/confirm` records confirmed transaction hashes. Optional Trezor enforcement gates confirmation when `TREZOR_SAFEKEEPING_REQUIRED=true`.
All surfaces converge on the same `Payment` record (with `direction: 'in' | 'out' | 'refund'`) and share the internal **funds ledger** (`backend/src/services/payment/ledger/`) which tracks available / held / releasable amounts independently of the provider. **Pending payments are auto-cleaned** by a background timer started in `app.ts`. Historical SHKeeper and DePay docs remain in the vault for migration context, but the current backend tree no longer has `backend/src/services/payment/shkeeper/`. The current strategic path is in [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].
### Real-time chat — [[Chat System]] ### Real-time chat — [[Chat System]]
@@ -164,9 +161,10 @@ Push and SMS are tracked as **planned** in `backend/TODO.md`.
### Disputes — [[Dispute System]] ### Disputes — [[Dispute System]]
When a deal goes wrong (see [[Glossary#Dispute]]), either party can open a dispute. The backend would create a **three-way chat** between buyer, seller, and admin, open a `Dispute` document with a structured `timeline[]` and `evidence[]`, and assign the dispute to an admin via `assignAdmin()`. Resolution can be `refund | replacement | compensation | warning_seller | ban_seller | no_action` and is recorded on the dispute itself. When a deal goes wrong (see [[Glossary#Dispute]]), either party can open a dispute. The backend creates a **three-way chat** between buyer, seller, and admin, opens a `Dispute` document with a structured `timeline[]` and `evidence[]`, and can assign the dispute to an admin via `assignAdmin()`. Resolution can be `refund | replacement | compensation | warning_seller | ban_seller | no_action` in the current Mongoose model.
> [!warning] Not implemented
> `backend/src/services/dispute/DisputeService.ts` does not exist as of 2026-05-24. > [!note] State alignment gap
> The dispute module exists now, but its model still uses the legacy `pending | in_progress | resolved | ...` enum. [[Funds Ledger and Escrow State Machine Specification]] defines the canonical future enum and financial side effects.
### Points & referrals — [[Points System]] ### Points & referrals — [[Points System]]
@@ -191,9 +189,10 @@ OpenAI (model configurable per call) is exposed through `/api/ai/*`. The current
- locks used by `PaymentCoordinator` to serialise status transitions - locks used by `PaymentCoordinator` to serialise status transitions
- rate-limit counters (currently disabled in code but plumbed in) - rate-limit counters (currently disabled in code but plumbed in)
**Background workers** run inside the Express process for now no separate worker tier. Notable timers: **Background workers** run inside the Express process for now -- no separate worker tier. Notable timers:
- `startPendingPaymentsCleanup()` — sweeps stale unpaid invoices - `startPendingPaymentsCleanup()` — sweeps stale unpaid invoices
- `startShkeeperHealthMonitor()` — pings the SHKeeper instance and surfaces alerts - optional derived-destination sweep cron — sweeps eligible per-payment receiving addresses when configured
- Request Network reconciliation — enabled via provider config when the rollout requires fallback status repair
- Auto-seed logic on startup (gated by `NODE_ENV` and `AUTO_SEED_ON_START`) - Auto-seed logic on startup (gated by `NODE_ENV` and `AUTO_SEED_ON_START`)
## Request lifecycle (the happy path) ## Request lifecycle (the happy path)
@@ -203,9 +202,10 @@ OpenAI (model configurable per call) is exposed through `/api/ai/*`. The current
> 2. Buyer creates a [[Purchase Request]] → `POST /api/marketplace/requests`. The request lands in `pending`/`active`. Sellers in the matching category receive a Socket.IO notification. > 2. Buyer creates a [[Purchase Request]] → `POST /api/marketplace/requests`. The request lands in `pending`/`active`. Sellers in the matching category receive a Socket.IO notification.
> 3. Seller views the request, opens [[Seller Offer]] modal, submits price + delivery time → `POST /api/marketplace/offers`. Buyer sees the offer arrive live. > 3. Seller views the request, opens [[Seller Offer]] modal, submits price + delivery time → `POST /api/marketplace/offers`. Buyer sees the offer arrive live.
> 4. Buyer accepts an offer → request moves to `payment`. UI opens the payment selector. > 4. Buyer accepts an offer → request moves to `payment`. UI opens the payment selector.
> 5. Buyer picks **SHKeeper** backend creates a SHKeeper invoice, returns a wallet address + QR code. Buyer pays. SHKeeper webhook hits `/api/payment/shkeeper/webhook`; `PaymentCoordinator` flips `Payment.status = paid` and `PurchaseRequest.status = processing`. > 5. Buyer picks **Request Network** -> backend creates a Payment and RN intent, returns an in-house checkout block, and the buyer signs the on-chain payment from their wallet.
> 6. Seller ships. Buyer confirms delivery (or it auto-confirms after the SLA window). Admin triggers (or schedules) a **payout** → SHKeeper releases USDT to the seller's wallet. > 6. Request Network webhook/reconciliation plus the Transaction Safety Provider confirm tx hash, recipient, token, amount, and confirmations before the backend marks escrow funded.
> 7. Both parties leave reviews. Points are awarded. The deal is closed. > 7. Seller ships. Buyer confirms delivery (or an admin resolves the order/dispute). Admin/custody owners execute release/refund through the release/refund instruction flow.
> 8. Both parties leave reviews. Points are awarded. The deal is closed.
> >
> If the buyer disputes the delivery, jump to step 7 of the [[Dispute Flow]] instead. > If the buyer disputes the delivery, jump to step 7 of the [[Dispute Flow]] instead.

View File

@@ -117,7 +117,7 @@ The frontend is a Next.js 16 App Router application written in TypeScript. The b
## Backend stack ## Backend stack
The backend is `amn-backend@2.6.3-beta`, an Express 5 server in TypeScript backed by MongoDB (Mongoose), Redis, and Socket.IO. It owns all integrations with SHKeeper, the EVM chains, OpenAI, Google OAuth, and SMTP. The backend is `amn-backend@2.6.3-beta`, an Express 5 server in TypeScript backed by MongoDB (Mongoose), Redis, and Socket.IO. It owns all integrations with Request Network, EVM chains, OpenAI, Google OAuth, Telegram, SMTP, and custody/signing controls.
### Core runtime & framework ### Core runtime & framework
@@ -135,7 +135,7 @@ The backend is `amn-backend@2.6.3-beta`, an Express 5 server in TypeScript backe
| sharp | ^0.34.3 | Image resizing / format conversion | Upload pipeline | | sharp | ^0.34.3 | Image resizing / format conversion | Upload pipeline |
| dotenv | ^17.2.0 | Env var loader | Bootstrap | | dotenv | ^17.2.0 | Env var loader | Bootstrap |
| uuid | ^11.1.0 | ID generation | Tokens, ephemeral IDs | | uuid | ^11.1.0 | ID generation | Tokens, ephemeral IDs |
| axios | ^1.11.0 | Outbound HTTP (SHKeeper, blockchain) | Integration calls | | axios | ^1.11.0 | Outbound HTTP (Request Network, blockchain/RPC helpers) | Integration calls |
| @babel/runtime | ^7.27.6 | Babel runtime helpers | Compiled output | | @babel/runtime | ^7.27.6 | Babel runtime helpers | Compiled output |
> [!warning] React in backend dependencies > [!warning] React in backend dependencies
@@ -210,9 +210,12 @@ The backend is `amn-backend@2.6.3-beta`, an Express 5 server in TypeScript backe
| Service | Purpose | Touchpoint in code | | Service | Purpose | Touchpoint in code |
|---|---|---| |---|---|---|
| **SHKeeper** | Self-hosted crypto payment processor — issues wallets, watches for incoming USDT, pays out | `backend/src/services/payment/shkeeper/` | | **Request Network** | On-chain payment request protocol -- creates payment requests, supports in-house checkout metadata, signs webhooks | `backend/src/services/payment/requestNetwork/` + adapters |
| **Request Network** | On-chain payment request protocol — creates invoices, generates Secure Payment Pages, signs webhooks | `backend/src/services/payment/requestNetwork/` + adapters | | **Derived destination wallets** | Per-`(buyer, sellerOffer, chainId)` receiving addresses plus sweep orchestration | `backend/src/services/payment/wallets/` |
| **DePay** | Drop-in Web3 widget for wallet-to-wallet payment | `@depay/widgets` on frontend | | **Transaction Safety Provider** | Confirms tx hash, recipient, token, amount, confirmation depth, and future AML result before escrow credit | `backend/src/services/payment/safety/` |
| **Trezor / future Safe multisig** | Hardware-backed admin signing today; Safe multisig target in custody roadmap | `backend/src/services/trezor/`, [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] |
| **SHKeeper** | Historical payment rail retained in documentation for migration context | legacy docs only |
| **DePay** | Historical/drop-in Web3 widget docs retained for context | frontend historical docs |
| **EVM chains** (BSC, Ethereum mainnet, Sepolia, Polygon) | Settlement layer for stablecoin transfers | `frontend/src/web3/config.ts`, backend `blockchain/` | | **EVM chains** (BSC, Ethereum mainnet, Sepolia, Polygon) | Settlement layer for stablecoin transfers | `frontend/src/web3/config.ts`, backend `blockchain/` |
| **Alchemy RPC** | Hosted EVM RPC + transaction lookup | Frontend `alchemy-sdk`, backend `blockchainTxFetcher.ts` | | **Alchemy RPC** | Hosted EVM RPC + transaction lookup | Frontend `alchemy-sdk`, backend `blockchainTxFetcher.ts` |
| **MetaMask / WalletConnect** | Wallet connectors via Wagmi | `web3/config.ts` (WalletConnect commented out pending SSR fix) | | **MetaMask / WalletConnect** | Wallet connectors via Wagmi | `web3/config.ts` (WalletConnect commented out pending SSR fix) |

View File

@@ -44,7 +44,8 @@ backend/src/
│ │ ├── migration/ # Legacy data backfill utilities │ │ ├── migration/ # Legacy data backfill utilities
│ │ ├── observability/ # Logging and incident controls │ │ ├── observability/ # Logging and incident controls
│ │ ├── requestNetwork/ # Request Network pay-in, routes, webhook signature │ │ ├── requestNetwork/ # Request Network pay-in, routes, webhook signature
│ │ ── shkeeper/ # SHKeeper API, webhook, payout │ │ ── safety/ # Transaction Safety Provider + confirmation thresholds
│ │ └── wallets/ # Derived destination wallets + sweep orchestration
│ ├── points/ # Loyalty points, levels, redemption │ ├── points/ # Loyalty points, levels, redemption
│ ├── redis/ # Redis client, cache helpers │ ├── redis/ # Redis client, cache helpers
│ ├── telegram/ # Bot webhook, Mini App session, identity linking, notifications │ ├── telegram/ # Bot webhook, Mini App session, identity linking, notifications
@@ -125,17 +126,19 @@ The full route table mounted by `app.ts`:
| `/api/marketplace/categories` | `services/marketplace/controllerRoutes.ts` | public read | Category list | | `/api/marketplace/categories` | `services/marketplace/controllerRoutes.ts` | public read | Category list |
| `/api/marketplace/shop-settings` | `services/marketplace/shopSettingsController.ts` | JWT (seller) | Shop profile | | `/api/marketplace/shop-settings` | `services/marketplace/shopSettingsController.ts` | JWT (seller) | Shop profile |
| `/api/payment` | `services/payment/paymentControllerRoutes.ts` + `paymentRoutes.ts` | JWT | Payment CRUD, health, export | | `/api/payment` | `services/payment/paymentControllerRoutes.ts` + `paymentRoutes.ts` | JWT | Payment CRUD, health, export |
| `/api/payment/decentralized` | `services/payment/decentralizedPaymentRoutes.ts` | mixed | Web3 save, verify, receiver | | `/api/payment/decentralized` | `services/payment/decentralizedPaymentRoutes.ts` | mixed | Legacy/manual Web3 save, verify, receiver |
| `/api/payment/shkeeper` | `services/payment/shkeeper/shkeeperRoutes.ts` | mixed | Intents, webhook, release, refund, config | | `/api/payment/request-network` | `services/payment/requestNetwork/requestNetworkRoutes.ts` | mixed + HMAC sig on webhook | Request Network pay-in creation, in-house checkout rehydrate, webhooks |
| `/api/payment/shkeeper/payout` | `services/payment/shkeeper/shkeeperPayoutRoutes.ts` | JWT (seller/admin) | Withdraw to wallet | | `/api/payment/derived-destinations` | `services/payment/wallets/derivedDestinationRoutes.ts` | JWT (admin) | Derived address list, sweeps, cron, config health |
| `/api/payment/request-network` | `services/payment/requestNetwork/requestNetworkRoutes.ts` | HMAC sig | Request Network pay-in creation, Secure Payment Page, webhooks | | `/api/admin/rn/networks` | `services/payment/requestNetwork/networkRegistryRoutes.ts` | JWT (admin) | Supported RN chain/token registry |
| `/api/admin/settings/confirmation-thresholds` | `services/admin/confirmationThresholdRoutes.ts` | JWT (admin) | Runtime min-confirmation thresholds |
| `/api/admin/payments/awaiting-confirmation` | `services/admin/awaitingConfirmationRoutes.ts` | JWT (admin) | Payments blocked on safety confirmations |
| `/api/telegram` | `services/telegram/telegramRoutes.ts` | mixed (some JWT, webhook uses secret-token) | Mini App verify/session, identity link/unlink, bot webhook | | `/api/telegram` | `services/telegram/telegramRoutes.ts` | mixed (some JWT, webhook uses secret-token) | Mini App verify/session, identity link/unlink, bot webhook |
| `/api/chat` | `services/chat/chatRoutes.ts` | JWT | Conversations, messages | | `/api/chat` | `services/chat/chatRoutes.ts` | JWT | Conversations, messages |
| `/api/notification` | `services/notification/notificationRoutes.ts` + `notificationControllerRouter` | JWT | List, mark read | | `/api/notification` | `services/notification/notificationRoutes.ts` + `notificationControllerRouter` | JWT | List, mark read |
| `/api/dispute` | `services/dispute/disputeRoutes.ts` | JWT | **Not implemented** — planned | | `/api/disputes` | `routes/disputeRoutes.ts` + `services/dispute/disputeRoutes.ts` | JWT | Dispute CRUD plus release-hold helpers |
| `/api/blog` | `services/blog/blogRoutes.ts` | mixed | **Not implemented** — planned | | `/api/blog` | `services/blog/blogRoutes.ts` | mixed | Public reads, admin writes |
| `/api/admin` | `services/admin/adminRoutes.ts` | JWT (admin) | **Not implemented** — planned | | `/api/admin/cleanup` | `services/admin/dataCleanupRoutes.ts` | JWT (admin) | Data cleanup operations |
| `/api/points` | `services/points/pointsRoutes.ts` | JWT | **Not implemented** — planned | | `/api/points` | `services/points/pointsRoutes.ts` | JWT | Points, levels, referrals |
| `/api/ai` | `services/ai/aiRoutes.ts` | JWT | OpenAI-backed helpers | | `/api/ai` | `services/ai/aiRoutes.ts` | JWT | OpenAI-backed helpers |
| `/api/files` | `services/file/fileRoutes.ts` | JWT | Multipart upload | | `/api/files` | `services/file/fileRoutes.ts` | JWT | Multipart upload |
| `/api/email` | `services/email/emailRoutes.ts` | JWT | Email dispatch | | `/api/email` | `services/email/emailRoutes.ts` | JWT | Email dispatch |
@@ -253,9 +256,12 @@ Full table in [[Environment Variables]]. Critical ones:
| `JWT_EXPIRES_IN` | `7d` | | | `JWT_EXPIRES_IN` | `7d` | |
| `REFRESH_TOKEN_EXPIRES_IN` | `30d` | | | `REFRESH_TOKEN_EXPIRES_IN` | `30d` | |
| `FRONTEND_URL` | `http://localhost:3000` | CORS origin | | `FRONTEND_URL` | `http://localhost:3000` | CORS origin |
| `SHKEEPER_API_URL` | `https://pay.amn.gg` | | | `REQUEST_NETWORK_API_BASE_URL` | `https://api.request.network` | Request Network API |
| `SHKEEPER_API_KEY` | required | | | `REQUEST_NETWORK_API_KEY` | required | Request Network API credential |
| `SHKEEPER_WEBHOOK_SECRET` | required | HMAC key | | `REQUEST_NETWORK_WEBHOOK_SECRET` | required | Webhook HMAC key |
| `PAYMENT_LEDGER_ENFORCEMENT` | `false` | Target `true` before launch-scale releases |
| `TRANSACTION_SAFETY_*` | required for payments | Confirmation, transfer-match, and AML controls |
| `DERIVED_DESTINATION_SWEEP_SIGNER` | `build-only` | Target hardware/Safe-backed signer |
| `SMTP_*` | required | Nodemailer | | `SMTP_*` | required | Nodemailer |
| `OPENAI_API_KEY` | required | | | `OPENAI_API_KEY` | required | |
@@ -279,7 +285,7 @@ Redis client (in `src/services/redis/`) provides:
The codebase has no dedicated queue runner — scheduled / async work is triggered inline from request handlers and uses `setTimeout` / `setInterval` patterns where needed (e.g., delayed retries). Consider introducing Bull / BullMQ if you grow: The codebase has no dedicated queue runner — scheduled / async work is triggered inline from request handlers and uses `setTimeout` / `setInterval` patterns where needed (e.g., delayed retries). Consider introducing Bull / BullMQ if you grow:
- Payment status reconciliation (polling SHKeeper for stragglers) - Request Network webhook replay/reconciliation and derived-destination balance checks
- Notification email digests - Notification email digests
- Auto-release escrow timers - Auto-release escrow timers
- Token / refresh-token cleanup - Token / refresh-token cleanup
@@ -295,7 +301,7 @@ Jest test suites in `backend/__tests__/`:
| `models.test.ts` | Schema validation, virtuals, hooks | | `models.test.ts` | Schema validation, virtuals, hooks |
| `payment-services.test.ts` | Payment orchestration logic | | `payment-services.test.ts` | Payment orchestration logic |
| `complete-backend.test.ts` | Cross-service integration | | `complete-backend.test.ts` | Cross-service integration |
| `shkeeper-backend.test.ts` | SHKeeper service + webhook | | Request Network / payment tests | Request Network adapter, webhook signature, ledger, release/refund orchestration |
Run with `npm run test:all`. CI runs the same. Reach for `npm run test:models`, `npm run test:payment`, etc. when iterating on a slice. Run with `npm run test:all`. CI runs the same. Reach for `npm run test:models`, `npm run test:payment`, etc. when iterating on a slice.
@@ -310,7 +316,8 @@ Run with `npm run test:all`. CI runs the same. Reach for `npm run test:models`,
| `src/shared/utils/response-handler.ts` | Standard response shape | | `src/shared/utils/response-handler.ts` | Standard response shape |
| `src/shared/middleware/auth.ts` | JWT verify + RBAC | | `src/shared/middleware/auth.ts` | JWT verify + RBAC |
| `src/infrastructure/socket/socketService.ts` | All socket plumbing | | `src/infrastructure/socket/socketService.ts` | All socket plumbing |
| `src/services/payment/shkeeper/shkeeperWebhook.ts` | Webhook signature scheme | | `src/services/payment/requestNetwork/requestNetworkRoutes.ts` | Request Network checkout and webhook route |
| `src/services/payment/ledger/fundsLedgerService.ts` | Immutable payment ledger writes |
| `src/services/marketplace/PurchaseRequestService.ts` | Core marketplace state machine | | `src/services/marketplace/PurchaseRequestService.ts` | Core marketplace state machine |
| `src/services/auth/authService.ts` | Auth flows, lockout, hashing | | `src/services/auth/authService.ts` | Auth flows, lockout, hashing |
| `src/models/User.ts` | Central entity with role/preferences | | `src/models/User.ts` | Central entity with role/preferences |
@@ -325,4 +332,4 @@ Run with `npm run test:all`. CI runs the same. Reach for `npm run test:models`,
- [[Real-time Layer]] — Socket.IO room model - [[Real-time Layer]] — Socket.IO room model
- [[Security Architecture]] — JWT, passkeys, webhook HMAC - [[Security Architecture]] — JWT, passkeys, webhook HMAC
- [[Data Model Overview]] — entity-relationship map - [[Data Model Overview]] — entity-relationship map
- [[Authentication Flow]] · [[Payment Flow - SHKeeper]] · [[Dispute Flow]] - [[Authentication Flow]] · [[Escrow Flow]] · [[Dispute Flow]]

View File

@@ -190,7 +190,7 @@ See [[Monitoring]] for the full table of metrics & recommended alerts.
| Browser → Backend | 5001 | HTTP + WS | via Nginx `/api`, `/socket.io` | | Browser → Backend | 5001 | HTTP + WS | via Nginx `/api`, `/socket.io` |
| Backend → MongoDB | 27017 | TCP | Docker network | | Backend → MongoDB | 27017 | TCP | Docker network |
| Backend → Redis | 6379 | TCP | Docker network | | Backend → Redis | 6379 | TCP | Docker network |
| Backend → SHKeeper | 443 | HTTPS | External | | Backend → Request Network API | 443 | HTTPS | External payment provider |
| Backend → SMTP | 587 | TLS | External | | Backend → SMTP | 587 | TLS | External |
| Backend → OpenAI | 443 | HTTPS | External | | Backend → OpenAI | 443 | HTTPS | External |
| Browser → Blockchain RPC | 443 | HTTPS | Alchemy URLs | | Browser → Blockchain RPC | 443 | HTTPS | Alchemy URLs |

View File

@@ -215,6 +215,6 @@ Sticky sessions on the load balancer are also required so a given client always
## Related ## Related
- [[Backend Architecture]] · [[Frontend Architecture]] - [[Backend Architecture]] · [[Frontend Architecture]]
- [[Chat Flow]] · [[Notification Flow]] · [[Payment Flow - SHKeeper]] · [[Dispute Flow]] - [[Chat Flow]] · [[Notification Flow]] · [[Escrow Flow]] · [[Dispute Flow]]
- [[Security Architecture]] — socket auth concerns - [[Security Architecture]] — socket auth concerns
- [[Socket Events]] — full event reference (developer-facing API doc) - [[Socket Events]] — full event reference (developer-facing API doc)

View File

@@ -8,15 +8,15 @@ This document captures payment-flow issues that surfaced while integrating Reque
--- ---
## 1. RN does not support Rabby — show-stopper for our wallet user base ## 1. RN hosted UI does not support Rabby -- mitigated by Amanat in-house checkout
### Problem ### Problem
RN's hosted payment page (the `pay.request.network/?token=…` UI returned by `/v2/secure-payments`) does not detect / connect to Rabby. A meaningful slice of Amanat's user base pays from Rabby. Sending them to a screen that won't even let them connect is a hard block. RN's hosted payment page (the `pay.request.network/?token=…` UI returned by `/v2/secure-payments`) does not detect / connect to Rabby. A meaningful slice of Amanat's user base pays from Rabby. Sending them to a screen that won't even let them connect is a hard block.
### Mitigation (designed, not yet implemented) ### Mitigation (implemented core path)
Skip the RN-hosted UI. We already call `/v2/secure-payments` and receive a `securePaymentUrl`, but we also receive `requestIds` and `token` — that's everything we need to know what the merchant request is. Behind that token there is a contract on the destination chain that anyone can fulfill. Skip the RN-hosted UI. Amanat still calls `/v2/secure-payments`, stores the Request Network identifiers, and exposes an in-house checkout block. The frontend builds the same RN-compatible on-chain action from the buyer's wallet, so Rabby/MetaMask users stay inside the Amanat flow.
So the new flow becomes: So the new flow becomes:
@@ -32,10 +32,11 @@ So the new flow becomes:
- RN's value to us at that point is the *settlement bookkeeping*, not the UI. We use them as "did this address receive the expected amount before timeout?" — the wallet UX stays in our control. - RN's value to us at that point is the *settlement bookkeeping*, not the UI. We use them as "did this address receive the expected amount before timeout?" — the wallet UX stays in our control.
- Buyer never sees a third-party brand mid-checkout, which is a UX win regardless of Rabby. - Buyer never sees a third-party brand mid-checkout, which is a UX win regardless of Rabby.
### Open ### Remaining work
- Need to confirm RN settles a payment that arrives from a *proxy transaction we built*, not from their hosted page. The 2026-05-28 probe confirms RN webhook delivery to Amanat, but the app returned `404`; repeat the probe only after the confirmation repair is deployed. - Keep the RN hosted URL exposed as an escape hatch.
- Need a fallback for the buyer who insists on the RN hosted UI (some users will already have the link copied). Keep `securePaymentUrl` exposed as a "advanced / pay with RN" link. - Continue hardening timer/persistence/telemetry around the in-house checkout.
- Treat durable webhook ingress as a production gate, because the main Express app should not be the only landing zone for callback evidence.
--- ---
@@ -51,7 +52,7 @@ The visible costs:
- Or seller gets less than they expected (worst — they'll dispute). - Or seller gets less than they expected (worst — they'll dispute).
- Plus settlement latency goes from seconds to minutes-hours depending on the bridge. - Plus settlement latency goes from seconds to minutes-hours depending on the bridge.
### Mitigation (designed) ### Mitigation (partially implemented)
Take the chain choice away from RN's UI and bring it into ours, gated by what the *seller* will accept. Take the chain choice away from RN's UI and bring it into ours, gated by what the *seller* will accept.
@@ -62,11 +63,11 @@ Two-step UX:
### Side benefit ### Side benefit
This composes cleanly with #1 (own checkout screen): we already have to render the wallet picker, so adding a chain selector before the wallet step costs almost nothing. This composes cleanly with #1 (own checkout screen): we already render the wallet picker, so seller-accepted chain selection can happen before wallet connection. The chain/token registry and admin networks page exist; seller-side accepted-chain policy remains a separate product/data-model task.
### Open ### Open
- We need a per-seller config table for accepted chains. Today the env-level `REQUEST_NETWORK_MERCHANT_REFERENCE` hard-codes a single chain (`bsc`). Needs to become per-seller, per-offer. - We need a per-seller/per-offer config table for accepted chains. Today the global merchant reference is still the fallback, while derived destination work handles recipient variation.
- Does RN's API support creating a secure-payment that *rejects* off-chain payments rather than auto-bridging? Or do we have to enforce this purely on our side by never offering the cross-chain option to the buyer? **Confirm with RN docs/support.** - Does RN's API support creating a secure-payment that *rejects* off-chain payments rather than auto-bridging? Or do we have to enforce this purely on our side by never offering the cross-chain option to the buyer? **Confirm with RN docs/support.**
--- ---
@@ -83,7 +84,7 @@ Today the entire escrow stack receives funds into one (or a handful of) wallets
This is a show-stopper for going live at scale. Same class of issue we already considered around SHKeeper. This is a show-stopper for going live at scale. Same class of issue we already considered around SHKeeper.
### Mitigation (designed; needs RN feasibility check) ### Mitigation (implemented core path; operational probe pending)
Per-`(buyer, merchant)`-pair ephemeral wallets. Each new escrow gets a freshly-generated address that only ever receives that one transaction. If those funds turn out to be dirty: Per-`(buyer, merchant)`-pair ephemeral wallets. Each new escrow gets a freshly-generated address that only ever receives that one transaction. If those funds turn out to be dirty:
@@ -93,23 +94,23 @@ Per-`(buyer, merchant)`-pair ephemeral wallets. Each new escrow gets a freshly-g
### What this requires (architectural work) ### What this requires (architectural work)
1. **Wallet abstraction layer** — service that on demand generates a fresh address (HD wallet derivation from a master seed kept in a hardware module / KMS) and returns it to the payment-intent flow. 1. **Wallet abstraction layer** -- implemented in `backend/src/services/payment/wallets/derivedDestinations.ts` using xpub-only derivation.
2. **Address book / registry** — maps `(paymentId, chainId)` → derived address. Persists derivation path + sequence number so we can reproduce keys for sweeps later. 2. **Address book / registry** -- implemented in `DerivedDestination`, keyed by `(buyerId, sellerOfferId, chainId)`.
3. **Sweep job** — once a payment is confirmed AND has passed an on-chain screening check (Chainalysis API or similar), sweep the ephemeral wallet to the main treasury. If screening fails, the ephemeral wallet is quarantined and the payment refunded out of band. 3. **Sweep job** -- implemented with build-only/hot-key signer abstraction; production must keep build-only and move execution to Trezor/Safe.
4. **Key custody policy** — these are still our funds in custody briefly; need clear policy on who can sign sweeps, hot-key vs cold-key separation. 4. **Key custody policy** -- still the important missing operational layer. See [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].
### Critical open question ### Critical open question
**Does RN support creating a secure-payment with a destination wallet we specify per-request, rather than a static merchant reference?** If yes, this is straightforward — we generate a wallet, register it as the destination for one specific `/v2/secure-payments` call, done. If no (RN only allows pre-registered destinations), we have to either: **Does RN support creating a secure-payment with a destination wallet we specify per request at production volume, rather than a static merchant reference?** The backend/frontend support the shape, but the live divergent-destination probe remains the operational proof point. If RN cannot support this reliably, fallback options are:
- Pre-register a large pool of addresses with RN and rotate through them, or - Pre-register a large pool of addresses with RN and rotate through them, or
- Bypass RN's destination model and go full self-host (which is most of issue #4). - Bypass RN's destination model and go full self-host (which is most of issue #4).
**Action: confirm with RN support whether per-request destinations are supported on the same API key.** **Action: run the two-paid-intent divergent-destination probe and confirm with RN support whether this usage is supported on the same API key at expected volume.**
--- ---
## 4. RN reduced to a notification service viable, but not yet validated ## 4. RN reduced to a notification service -- viable, partially validated
### Problem statement ### Problem statement
@@ -131,19 +132,19 @@ Which is a *notification* primitive, not a payment platform. We'd be paying for
- We're outsourcing the *one thing* RN is good at (settlement) and keeping the parts they don't help with (UX, wallet generation, compliance). - We're outsourcing the *one thing* RN is good at (settlement) and keeping the parts they don't help with (UX, wallet generation, compliance).
- Alternative: do the same with our own chain watcher (Alchemy webhooks / Tenderly / Goldsky) and skip RN entirely. - Alternative: do the same with our own chain watcher (Alchemy webhooks / Tenderly / Goldsky) and skip RN entirely.
### What needs testing before we commit ### What still needs testing before we commit at scale
1. **Webhook reliability at our volume.** What's RN's SLA for "address received funds → webhook delivered"? P50? P99? 1. **Webhook reliability at our volume.** What's RN's SLA for "address received funds → webhook delivered"? P50? P99?
2. **Custom destination support.** See open question in #3. 2. **Custom destination support.** See open question in #3.
3. **Per-API-key rate limits.** If we end up calling `/v2/secure-payments` once per escrow, do we hit ceilings? 3. **Per-API-key rate limits.** If we end up calling `/v2/secure-payments` once per escrow, do we hit ceilings?
4. **Pricing for the notification-only flow** — is there a tier, or is it the same as the full-stack price? 4. **Pricing for the notification-only flow** — is there a tier, or is it the same as the full-stack price?
5. **What happens when the payment arrives from a transaction WE built** (not theirs)? Does the webhook still fire? Is settlement still recognized? — this is the load-bearing test for the whole strategy. 5. **What happens when the payment arrives from a transaction WE built** (not theirs)? The 2026-05-28 in-house checkout probe proved the basic path for a real BSC USDC payment; this still needs repeated paid probes across tokens/chains and webhook durability coverage.
Until #5 is confirmed, the rest is just paper architecture. Until webhook durability, destination divergence, pricing, and SLA are confirmed, treat RN as useful but not irreplaceable infrastructure.
--- ---
## 5. Webhook durability and transaction safety are P0 before more paid probes ## 5. Webhook durability remains P0 before production rollout
### What the 2026-05-28 probe proved ### What the 2026-05-28 probe proved
@@ -153,12 +154,12 @@ The dev test transaction `0x3a23febd9abd43d7e0851c1ea86c4ceaf08c11098852cb0425fa
Do not treat the main Express app as the only webhook landing zone, and do not treat a signed provider callback as enough to credit escrow. Do not treat the main Express app as the only webhook landing zone, and do not treat a signed provider callback as enough to credit escrow.
### Required mitigation ### Required mitigation and status
1. **Correlation repair:** lookup Request Network payments by every persisted reference shape, including `providerPaymentId`, top-level RN request id/payment reference, and nested raw RN data. 1. **Correlation repair:** implemented for the in-house checkout path; keep smoke coverage around every persisted RN reference shape.
2. **Callback repair:** payment callback polling must unwrap the backend response shape, clear polling after terminal states, and avoid a 3-second loop that self-rate-limits. 2. **Callback repair:** implemented enough for the successful paid dev probe; keep polling/backoff hardening on the checkout roadmap.
3. **Transaction Safety Provider:** completion must pass configured safety checks: transaction hash present, minimum confirmations, token/recipient/amount transfer match, and future AML/sanctions provider approval. 3. **Transaction Safety Provider:** implemented for tx hash, confirmations, transfer match, and AML placeholder; real AML provider remains Task #10.
4. **Durable ingress:** put a Cloudflare Worker in front of RN webhooks. The Worker stores raw delivery evidence durably, forwards to the backend, and supports replay. It is not the trust oracle; the backend still verifies, deduplicates, and applies safety/ledger transitions. 4. **Durable ingress:** not started. Put a Cloudflare Worker in front of RN webhooks. The Worker stores raw delivery evidence durably, forwards to the backend, and supports replay. It is not the trust oracle; the backend still verifies, deduplicates, and applies safety/ledger transitions.
--- ---
@@ -166,13 +167,13 @@ Do not treat the main Express app as the only webhook landing zone, and do not t
| # | Action | Blocker / Owner | | # | Action | Blocker / Owner |
|---|---|---| |---|---|---|
| 1 | Deploy confirmation repair and repeat the dev payment probe | Backend payments | | 1 | Run the live divergent-destination probe: two paid intents to two derived addresses | Backend payments |
| 2 | Test: `/v2/secure-payments` accepts a per-request destination wallet | Backend payments | | 2 | Confirm `/v2/secure-payments` per-request destination usage with RN support and pricing | Product / RN account manager |
| 3 | Confirm RN doesn't auto-bridge when buyer pays on the destination chain natively | Backend payments | | 3 | Confirm RN doesn't auto-bridge when buyer pays on the destination chain natively | Backend payments |
| 4 | Get RN's webhook P99 latency + delivery guarantees in writing | Product / RN account manager | | 4 | Get RN's webhook P99 latency + delivery guarantees in writing | Product / RN account manager |
| 5 | Spec the wallet-abstraction layer (HD derivation + sweep job + key policy) | Backend, before going live | | 5 | Move sweep/release/refund custody to Trezor/Safe, not backend hot keys | Backend + ops |
| 6 | Spec the seller-side accepted-chains config | Backend + frontend | | 6 | Spec the seller-side accepted-chains config | Backend + frontend |
| 7 | Add Cloudflare Worker durable webhook ingress to the roadmap | Backend / platform | | 7 | Build Cloudflare Worker durable webhook ingress + replay | Backend / platform |
| 8 | Add AML/sanctions adapter behind Transaction Safety Provider | Compliance / backend | | 8 | Add AML/sanctions adapter behind Transaction Safety Provider | Compliance / backend |
Actions 14 are *information-gathering* and should run in parallel before any more architectural commitment to RN. Actions 56 are blocked on 13 confirming RN can actually support this shape. Actions 1-4 are information-gathering and should run in parallel before deeper RN commitment. Actions 5, 7, and 8 are production-safety work regardless of whether Amanat keeps RN long-term or replaces it with a direct chain watcher.

View File

@@ -9,7 +9,7 @@ created: 2026-05-23
How identity, authorization, transport, and integrity are handled across the platform. How identity, authorization, transport, and integrity are handled across the platform.
> [!important] > [!important]
> Read alongside [[Authentication Flow]] (user-facing), [[Passkey (WebAuthn) Flow]], and [[Payment Flow - SHKeeper]] (webhook HMAC). > Read alongside [[Authentication Flow]] (user-facing), [[Passkey (WebAuthn) Flow]], [[Escrow Flow]], and [[Request Network Integration Constraints]].
--- ---
@@ -22,7 +22,7 @@ How identity, authorization, transport, and integrity are handled across the pla
| CSRF | JWT in `Authorization` header (not cookie), CORS allow-list | | CSRF | JWT in `Authorization` header (not cookie), CORS allow-list |
| XSS | Helmet CSP, React auto-escaping, sanitize HTML before storage | | XSS | Helmet CSP, React auto-escaping, sanitize HTML before storage |
| SQL/NoSQL injection | Mongoose parameterized queries, no `$where` strings, schema validation | | SQL/NoSQL injection | Mongoose parameterized queries, no `$where` strings, schema validation |
| Webhook spoofing | HMAC SHA-256 over body + secret (SHKeeper, Request Network, Telegram), constant-time compare | | Webhook spoofing | HMAC SHA-256 over raw body + provider secret (Request Network, Telegram), constant-time compare |
| File upload abuse | Multer MIME validation, 5 MB cap, non-executable storage, served by Nginx not Node | | File upload abuse | Multer MIME validation, 5 MB cap, non-executable storage, served by Nginx not Node |
| Replay attacks | Per-payment idempotency on `providerPaymentId`; Telegram initData in-memory replay map; per-request `X-Request-Id` | | Replay attacks | Per-payment idempotency on `providerPaymentId`; Telegram initData in-memory replay map; per-request `X-Request-Id` |
| Account takeover | Email verification required, password reset code expiry (1h), passkey support | | Account takeover | Email verification required, password reset code expiry (1h), passkey support |
@@ -155,34 +155,36 @@ A single User may be `buyer` and `seller` simultaneously (combined role).
## 5. Webhook integrity ## 5. Webhook integrity
### 5.1 SHKeeper ### 5.1 Request Network
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant SHK participant RN
participant WK as Durable ingress (roadmap)
participant BE participant BE
SHK->>BE: POST /api/payment/shkeeper/webhook<br/>X-Signature: sha256=<hmac> RN->>WK: POST /api/payment/request-network/webhook<br/>x-request-network-signature
BE->>BE: hmac = HMAC_SHA256(SHKEEPER_WEBHOOK_SECRET, body) WK->>WK: Store raw body + headers + delivery id
BE->>BE: crypto.timingSafeEqual(hmac, providedSig) WK->>BE: Forward / replay raw webhook
BE->>BE: verifyRequestNetworkWebhookSignature(rawBody, headers)
alt mismatch alt mismatch
BE-->>SHK: 401 Unauthorized BE-->>WK: 401 Unauthorized
else match else match
BE->>BE: process payment update BE->>BE: idempotency + Transaction Safety Provider
BE-->>SHK: 200 OK BE->>BE: process payment update / ledger entry
BE-->>WK: 200 OK
end end
``` ```
- Raw body must be used for HMAC — `express.raw({ type: 'application/json' })` is mounted on this route only (before the global `express.json()` parser).
- In dev (`NODE_ENV === 'development'`) signature verification can be bypassed for local testing — confirm this is gated and never reachable in prod.
- Idempotency: identical webhook delivered twice should be no-op. Check by `(providerPaymentId, status)` tuple before mutating.
### 5.2 Request Network
- Webhooks arrive at `/api/payment/request-network/webhook` with an `x-request-network-signature` header. - Webhooks arrive at `/api/payment/request-network/webhook` with an `x-request-network-signature` header.
- The backend verifies the signature using `backend/src/services/payment/requestNetwork/signature.ts` before any state mutation. - The backend verifies the signature using `backend/src/services/payment/requestNetwork/signature.ts` before any state mutation.
- The route is mounted **before** the global `express.json()` body parser so raw body bytes are available for signature computation. - The route is mounted **before** the global `express.json()` body parser so raw body bytes are available for signature computation.
- The global rate-limit middleware is configured to skip this path to avoid blocking high-frequency payment events. - The global rate-limit middleware is configured to skip this path to avoid blocking high-frequency payment events.
- Reconciliation service (`requestNetworkReconciliationService.ts`) handles replayed or out-of-order webhooks idempotently. - Reconciliation service (`requestNetworkReconciliationService.ts`) handles replayed or out-of-order webhooks idempotently.
- Durable ingress is the target production shape: the Worker stores delivery evidence and supports replay, but the backend remains the trust oracle.
### 5.2 Legacy SHKeeper note
SHKeeper-specific webhook docs are historical migration context. The current backend payment tree uses Request Network as the primary provider; do not reintroduce SHKeeper signature bypasses or fallback webhook heuristics without a new security review.
### 5.3 Telegram Bot webhook ### 5.3 Telegram Bot webhook
@@ -191,7 +193,7 @@ sequenceDiagram
- A per-update-id in-memory replay map prevents duplicate processing within the configured window. - A per-update-id in-memory replay map prevents duplicate processing within the configured window.
- The global rate-limit middleware is configured to skip this path. - The global rate-limit middleware is configured to skip this path.
See [[Payment Flow - SHKeeper]] for the SHKeeper full flow. See [[Escrow Flow]] and [[Request Network Integration Constraints]] for the current payment path.
--- ---
@@ -219,7 +221,7 @@ See [[Payment Flow - SHKeeper]] for the SHKeeper full flow.
- Never log secrets — logger redaction recommended (winston/pino formatter). - Never log secrets — logger redaction recommended (winston/pino formatter).
- `.env*` files in `.gitignore`. Repo includes only `.env.development` / `.env.production` templates with **public** values (NEXT_PUBLIC_*). - `.env*` files in `.gitignore`. Repo includes only `.env.development` / `.env.production` templates with **public** values (NEXT_PUBLIC_*).
- Rotate `JWT_SECRET` invalidates all existing JWTs — schedule a maintenance window. - Rotate `JWT_SECRET` invalidates all existing JWTs — schedule a maintenance window.
- Rotate `SHKEEPER_WEBHOOK_SECRET` coordinated with SHKeeper dashboard (set new → verify → remove old). - Rotate `REQUEST_NETWORK_WEBHOOK_SECRET` coordinated with Request Network configuration (set new → verify → remove old).
See [[Environment Variables]] for the catalog. See [[Environment Variables]] for the catalog.
@@ -277,6 +279,6 @@ The codebase currently uses `morgan` (HTTP access logs) and ad-hoc `logger.info/
- [[Authentication Flow]] (includes Telegram first-class auth flow) · [[Google OAuth Flow]] · [[Passkey (WebAuthn) Flow]] · [[Password Reset Flow]] - [[Authentication Flow]] (includes Telegram first-class auth flow) · [[Google OAuth Flow]] · [[Passkey (WebAuthn) Flow]] · [[Password Reset Flow]]
- [[Backend Architecture]] · [[Frontend Architecture]] · [[Real-time Layer]] - [[Backend Architecture]] · [[Frontend Architecture]] · [[Real-time Layer]]
- [[Payment Flow - SHKeeper]] — webhook HMAC details - [[Request Network Integration Constraints]] — payment webhook, checkout, and reconciliation constraints
- [[Environment Variables]] — secret catalog - [[Environment Variables]] — secret catalog
- [[Incident Response]] — what to do when something goes wrong - [[Incident Response]] — what to do when something goes wrong

View File

@@ -24,7 +24,8 @@ flowchart LR
BE[Express Backend<br/>+ Socket.IO<br/>:5001] BE[Express Backend<br/>+ Socket.IO<br/>:5001]
Mongo[(MongoDB 8)] Mongo[(MongoDB 8)]
Redis[(Redis 8)] Redis[(Redis 8)]
SHK[SHKeeper<br/>Crypto Gateway] RN[Request Network<br/>Pay-in + webhooks]
CFWorker[Durable webhook ingress<br/>roadmap]
SMTP[SMTP<br/>Nodemailer] SMTP[SMTP<br/>Nodemailer]
OAI[OpenAI API] OAI[OpenAI API]
BC[Blockchain RPC<br/>Alchemy / WalletConnect] BC[Blockchain RPC<br/>Alchemy / WalletConnect]
@@ -37,8 +38,9 @@ flowchart LR
FE -.->|Socket.IO| BE FE -.->|Socket.IO| BE
BE --> Mongo BE --> Mongo
BE --> Redis BE --> Redis
BE -->|Pay-in / Pay-out| SHK BE -->|Pay-in intent / status| RN
SHK -.->|Webhook HMAC| BE RN -.->|Signed webhook| CFWorker
CFWorker -.->|Forward / replay| BE
BE --> SMTP BE --> SMTP
BE --> OAI BE --> OAI
FE -->|Wallet Connect| BC FE -->|Wallet Connect| BC
@@ -142,25 +144,29 @@ Mutations follow optimistic-then-confirm:
### 5.3 Webhook path (inbound) ### 5.3 Webhook path (inbound)
External services (SHKeeper) POST to `/api/payment/shkeeper/webhook`. The backend verifies HMAC signature, updates the `Payment` document, advances any linked `PurchaseRequest`/`SellerOffer` state, and emits Socket.IO events to both buyer and seller rooms. External services POST payment callbacks to provider-specific webhook routes. The current primary path is Request Network at `/api/payment/request-network/webhook`; the target architecture puts a durable ingress worker in front of the backend so raw delivery evidence can be replayed after outages. The backend remains the trust oracle: it verifies signatures, deduplicates deliveries, applies Transaction Safety Provider checks, updates ledger/payment state, and emits Socket.IO events to both buyer and seller rooms.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant SHK as SHKeeper participant RN as Request Network
participant WK as Durable ingress worker
participant BE as Backend participant BE as Backend
participant DB as MongoDB participant DB as MongoDB
participant Buyer participant Buyer
participant Seller participant Seller
SHK->>BE: POST /api/payment/shkeeper/webhook<br/>X-Signature: HMAC-SHA256 RN->>WK: POST signed webhook<br/>delivery id + raw body
BE->>BE: verifySignature(body, header, SHKEEPER_WEBHOOK_SECRET) WK->>WK: Store immutable delivery evidence
BE->>DB: Payment.updateOne({providerPaymentId}, {status:"completed"}) WK->>BE: Forward / replay webhook
BE->>DB: PurchaseRequest.updateOne(..., {status:"funded"}) BE->>BE: Verify RN signature + idempotency
BE->>BE: Transaction Safety Provider checks tx hash, recipient, token, amount, confirmations
BE->>DB: Append ledger entry + Payment escrowState="funded"
BE->>DB: PurchaseRequest.updateOne(..., {status:"payment"})
BE-->>Buyer: socket emit "payment:status-updated" BE-->>Buyer: socket emit "payment:status-updated"
BE-->>Seller: socket emit "request:funded" BE-->>Seller: socket emit "request:funded"
BE-->>SHK: 200 OK BE-->>WK: 200 OK
``` ```
See [[Payment Flow - SHKeeper]] for the full sequence. See [[PRD - Request Network In-House Checkout]] and [[Request Network Integration Constraints]] for the full Request Network sequence.
--- ---

View File

@@ -9,26 +9,17 @@ aliases: [Models Index, Schema Overview]
This section documents every Mongoose model that backs the marketplace. The persistence layer lives in `backend/src/models/` and is exported through a single barrel file at `backend/src/models/index.ts`. All models target MongoDB via Mongoose, lean on `timestamps: true` for `createdAt` / `updatedAt`, and follow a consistent pattern: one schema per file, an exported `I<Name>` TypeScript interface, and named exports for the compiled model. This section documents every Mongoose model that backs the marketplace. The persistence layer lives in `backend/src/models/` and is exported through a single barrel file at `backend/src/models/index.ts`. All models target MongoDB via Mongoose, lean on `timestamps: true` for `createdAt` / `updatedAt`, and follow a consistent pattern: one schema per file, an exported `I<Name>` TypeScript interface, and named exports for the compiled model.
> [!note] Scope > [!note] Scope
> Eighteen models are documented here. The "File" concept exists only at the service layer (`backend/src/services/file/`) and is not persisted as its own Mongoose collection, so it is not listed below. > Twenty-two models are present in `backend/src/models/`. The "File" concept exists only at the service layer (`backend/src/services/file/`) and is not persisted as its own Mongoose collection, so it is not listed below.
> >
> [!warning] Implementation gap > [!note] Documentation freshness
> As of the 2026-05-24 audit, the following documented models **do not yet have Mongoose schema files** in `backend/src/models/`: > The 2026-05-24 audit note that marked `Dispute`, `BlogPost`, `Review`, `PointTransaction`, `LevelConfig`, and `ShopSettings` as missing is now stale: schema files exist for those models. Newer operational models such as [[ConfigSetting]], [[DerivedDestination]], [[FundsLedgerEntry]], and [[TrezorAccount]] should be expanded into dedicated model pages when the docs are next deepened.
> - [[Dispute]]
> - [[BlogPost]]
> - [[Review]]
> - [[PointTransaction]]
> - [[LevelConfig]]
> - [[ShopSettings]]
> The following *are* implemented in code and are documented accurately:
> - [[User]], [[PurchaseRequest]], [[SellerOffer]], [[Payment]], [[Chat]], [[Notification]], [[RequestTemplate]], [[Address]], [[Category]], [[TempVerification]], [[TelegramLink]], [[TelegramSession]]
> Additionally, `FundsLedgerEntry.ts` and `TrezorAccount.ts` exist in `backend/src/models/` but are not yet documented in this vault.
## Index of Models ## Index of Models
- [[User]] — Core identity. Stores credentials, profile, preferences, referral data, points, and WebAuthn passkeys. Every other model that records "who did what" points back at a `User._id`. Buyers, sellers, and admins all live in this collection, differentiated by a `role` enum. - [[User]] — Core identity. Stores credentials, profile, preferences, referral data, points, and WebAuthn passkeys. Every other model that records "who did what" points back at a `User._id`. Buyers, sellers, and admins all live in this collection, differentiated by a `role` enum.
- [[PurchaseRequest]] — The buyer-side document at the heart of the marketplace. Captures what a buyer wants, the budget, urgency, delivery preferences, and the full lifecycle status (`pending_payment``seller_paid`). Aggregates [[SellerOffer]] references and tracks delivery codes. - [[PurchaseRequest]] — The buyer-side document at the heart of the marketplace. Captures what a buyer wants, the budget, urgency, delivery preferences, and the full lifecycle status (`pending_payment``seller_paid`). Aggregates [[SellerOffer]] references and tracks delivery codes.
- [[SellerOffer]] — A seller's bid against a [[PurchaseRequest]]. Holds price, delivery ETA, attachments, and a small status machine (`pending` / `accepted` / `rejected` / `withdrawn`). - [[SellerOffer]] — A seller's bid against a [[PurchaseRequest]]. Holds price, delivery ETA, attachments, and a small status machine (`pending` / `accepted` / `rejected` / `withdrawn`).
- [[Payment]] — Records every monetary movement: buyer pay-in, seller payout, refund. Integrates with the SHKeeper crypto gateway and tracks escrow state plus on-chain transaction metadata. - [[Payment]] — Records monetary movement intent and state: buyer pay-in, seller release, and refund. The current primary provider path is Request Network plus in-house checkout, derived destinations, funds ledger entries, and Transaction Safety Provider metadata.
- [[Chat]] — Conversation container with embedded messages, participants, unread counters, and reactions. Used for direct buyer-seller chats, group chats, and support tickets. Can be linked to a [[PurchaseRequest]] or [[SellerOffer]]. - [[Chat]] — Conversation container with embedded messages, participants, unread counters, and reactions. Used for direct buyer-seller chats, group chats, and support tickets. Can be linked to a [[PurchaseRequest]] or [[SellerOffer]].
- [[Notification]] — Per-user notification with category, type, and 90-day TTL for automatic cleanup. References any related entity by stringified id. - [[Notification]] — Per-user notification with category, type, and 90-day TTL for automatic cleanup. References any related entity by stringified id.
- [[RequestTemplate]] — A seller-authored, sharable template that pre-fills a [[PurchaseRequest]]. Carries a public shareable link, usage counter, and an optional default proposal. - [[RequestTemplate]] — A seller-authored, sharable template that pre-fills a [[PurchaseRequest]]. Carries a public shareable link, usage counter, and an optional default proposal.
@@ -43,6 +34,10 @@ This section documents every Mongoose model that backs the marketplace. The pers
- [[TempVerification]] — Short-lived signup record that holds candidate user data and a verification code. Auto-purges via TTL when `emailVerificationCodeExpires` passes. - [[TempVerification]] — Short-lived signup record that holds candidate user data and a verification code. Auto-purges via TTL when `emailVerificationCodeExpires` passes.
- [[TelegramLink]] — Permanent auditable association between a Telegram user ID and an Amanat [[User]]. Stores Telegram profile metadata, link source (`miniapp` / `bot` / `login_widget`), status (`active` / `blocked`), and last-seen timestamp. One per Telegram user (unique on both `userId` and `telegramUserId`). - [[TelegramLink]] — Permanent auditable association between a Telegram user ID and an Amanat [[User]]. Stores Telegram profile metadata, link source (`miniapp` / `bot` / `login_widget`), status (`active` / `blocked`), and last-seen timestamp. One per Telegram user (unique on both `userId` and `telegramUserId`).
- [[TelegramSession]] — Short-lived Telegram Mini App session token issued when `initData` is verified. Carries the `initDataFingerprint` for replay protection and auto-expires via a MongoDB TTL index on `expiresAt`. - [[TelegramSession]] — Short-lived Telegram Mini App session token issued when `initData` is verified. Carries the `initDataFingerprint` for replay protection and auto-expires via a MongoDB TTL index on `expiresAt`.
- [[ConfigSetting]] — Runtime configuration persisted in MongoDB for operational knobs that need an admin surface rather than a deploy.
- [[DerivedDestination]] — Per-payment derived wallet destination records used to reduce address reuse and reconcile on-chain pay-ins.
- [[FundsLedgerEntry]] — Immutable accounting ledger rows for pay-in, hold, release, refund, fee, adjustment, and reversal events.
- [[TrezorAccount]] — Hardware-wallet/safekeeping account metadata for custody operations and staged signer hardening.
## Relationship Diagram ## Relationship Diagram
@@ -59,6 +54,7 @@ erDiagram
USER ||--o{ REVIEW : "writes as reviewer" USER ||--o{ REVIEW : "writes as reviewer"
USER ||--o{ DISPUTE : "raises as buyer" USER ||--o{ DISPUTE : "raises as buyer"
USER ||--o{ USER : "referred by" USER ||--o{ USER : "referred by"
USER ||--o{ TREZOR_ACCOUNT : "controls custody account"
PURCHASE_REQUEST }o--|| CATEGORY : "belongs to" PURCHASE_REQUEST }o--|| CATEGORY : "belongs to"
PURCHASE_REQUEST ||--o{ SELLER_OFFER : "receives" PURCHASE_REQUEST ||--o{ SELLER_OFFER : "receives"
@@ -72,6 +68,8 @@ erDiagram
PAYMENT }o--|| USER : "buyer" PAYMENT }o--|| USER : "buyer"
PAYMENT }o--|| USER : "seller" PAYMENT }o--|| USER : "seller"
PAYMENT ||--o{ FUNDS_LEDGER_ENTRY : "accounted by"
PAYMENT ||--o| DERIVED_DESTINATION : "collects into"
CHAT }o--o{ USER : "participants" CHAT }o--o{ USER : "participants"
CHAT ||--o{ DISPUTE : "support channel" CHAT ||--o{ DISPUTE : "support channel"
@@ -109,11 +107,11 @@ The dominant happy-path flow exercises five collections in order:
1. A buyer (`User`) creates a `PurchaseRequest` with `status: 'pending'`. 1. A buyer (`User`) creates a `PurchaseRequest` with `status: 'pending'`.
2. Sellers (other `User`s) attach `SellerOffer` documents; the request transitions through `received_offers``in_negotiation` as the parties chat in a `Chat`. 2. Sellers (other `User`s) attach `SellerOffer` documents; the request transitions through `received_offers``in_negotiation` as the parties chat in a `Chat`.
3. The buyer accepts an offer; a `Payment` is opened against the SHKeeper provider with `escrowState: 'funded'`. 3. The buyer accepts an offer; a `Payment` is opened against the Request Network provider and, once verified by webhook/reconciliation and safety checks, advances to a funded escrow state.
4. The seller marks the request `delivery``delivered`; the buyer confirms with the 6-digit `deliveryCode` and the request becomes `completed`. 4. The seller marks the request `delivery``delivered`; the buyer confirms with the 6-digit `deliveryCode` and the request becomes `completed`.
5. The escrow `Payment` flips to `released` and a payout `Payment` (`direction: 'out'`) is issued. Optionally the buyer writes a `Review` and earns a `PointTransaction`. 5. The escrow `Payment` flips to `released` after a ledger-gated custody transfer instruction. Optionally the buyer writes a `Review` and earns a `PointTransaction`.
If anything goes sideways, the buyer can open a `Dispute` (planned but not yet implemented), which would freeze the flow until an admin resolves it (refund, replacement, compensation, or no-action). If anything goes sideways, the buyer can open a `Dispute`, which freezes release until an admin resolves it (refund, replacement, compensation, or no-action).
## How to Navigate ## How to Navigate

View File

@@ -6,7 +6,7 @@ aliases: [Payment Record, Escrow, IPayment]
# Payment # Payment
Records every monetary movement in the marketplace: buyer pay-ins, seller payouts, and refunds. Designed around the SHKeeper crypto payment gateway with explicit fields for blockchain network, transaction hash, escrow state, and provider invoice ids. The `provider` and `direction` discriminators let one collection hold all four flow types (incoming buyer payment, outgoing seller payout, refund, and "other" provider integrations). Records every monetary movement in the marketplace: buyer pay-ins, seller payouts, and refunds. The current model is centered on Request Network pay-in, in-house checkout metadata, on-chain transaction verification, escrow state, and provider request IDs. The `provider` and `direction` discriminators let one collection hold incoming buyer payments, outgoing seller releases, refunds, and legacy/other provider records.
> [!note] Source > [!note] Source
> `backend/src/models/Payment.ts:3` — schema definition > `backend/src/models/Payment.ts:3` — schema definition
@@ -25,7 +25,7 @@ Records every monetary movement in the marketplace: buyer pay-ins, seller payout
| `sellerId` | Mixed (ObjectId or String) | yes | — | — | yes (compound) | Seller receiving (or template seller). | | `sellerId` | Mixed (ObjectId or String) | yes | — | — | yes (compound) | Seller receiving (or template seller). |
| `amount.amount` | Number | yes | — | — | — | Numeric amount. | | `amount.amount` | Number | yes | — | — | — | Numeric amount. |
| `amount.currency` | String | yes | `USDT` | — | — | Settlement currency. | | `amount.currency` | String | yes | `USDT` | — | — | Settlement currency. |
| `provider` | String | no | `shkeeper` | enum: `shkeeper` / `request.network` / `request-network` / `other` | yes (compound, partial) | Payment processor. | | `provider` | String | no | `request.network` | enum: `request.network` / `other` | yes (compound, partial) | Payment processor. |
| `direction` | String | no | `in` | enum: `in` / `out` / `refund` | yes (compound, partial) | Flow direction. | | `direction` | String | no | `in` | enum: `in` / `out` / `refund` | yes (compound, partial) | Flow direction. |
| `blockchain.network` | String | no | — | — | — | Network identifier. | | `blockchain.network` | String | no | — | — | — | Network identifier. |
| `blockchain.transactionHash` | String | no | — | — | yes (sparse) | On-chain tx hash. | | `blockchain.transactionHash` | String | no | — | — | yes (sparse) | On-chain tx hash. |
@@ -56,6 +56,7 @@ Records every monetary movement in the marketplace: buyer pay-ins, seller payout
| `metadata.requestNetworkSecurePaymentUrl` | String | no | — | — | — | Request Network secure payment URL. | | `metadata.requestNetworkSecurePaymentUrl` | String | no | — | — | — | Request Network secure payment URL. |
| `metadata.requestNetworkData` | Mixed | no | — | — | — | Raw Request Network payload. | | `metadata.requestNetworkData` | Mixed | no | — | — | — | Raw Request Network payload. |
| `metadata.transactionSafety` | Mixed | no | — | — | — | Last Transaction Safety Provider decision, checks, evidence, and blocker reason. | | `metadata.transactionSafety` | Mixed | no | — | — | — | Last Transaction Safety Provider decision, checks, evidence, and blocker reason. |
| `metadata.derivedDestination` | Object | no | — | — | — | Snapshot of per-payment derived destination address/path/index/chain. |
| `metadata.lastWebhookAt` | Date | no | — | — | — | Last webhook timestamp. | | `metadata.lastWebhookAt` | Date | no | — | — | — | Last webhook timestamp. |
| `metadata.webhookPayload` | Mixed | no | — | — | — | Last webhook body. | | `metadata.webhookPayload` | Mixed | no | — | — | — | Last webhook body. |
| `metadata.createdVia` | String | no | — | — | — | Origin marker. | | `metadata.createdVia` | String | no | — | — | — | Origin marker. |

View File

@@ -12,13 +12,13 @@ This page is the entry point for the API. See the individual service pages for e
- [[Authentication API]] - register/login/passkeys/Google OAuth - [[Authentication API]] - register/login/passkeys/Google OAuth
- [[User API]] - profile, wallet, admin user management - [[User API]] - profile, wallet, admin user management
- [[Marketplace API]] - purchase requests, seller offers, templates, shop, reviews - [[Marketplace API]] - purchase requests, seller offers, templates, shop, reviews
- [[Payment API]] - SHKeeper, Web3, DePay, payouts - [[Payment API]] - Request Network, in-house checkout, ledger-gated release/refund
- [[Chat API]] - conversations and messages - [[Chat API]] - conversations and messages
- [[Notification API]] - in-app notifications - [[Notification API]] - in-app notifications
- [[Dispute API]] - dispute resolution *(planned, not yet implemented)* - [[Dispute API]] - dispute creation, assignment, evidence, resolution
- [[Blog API]] - blog posts *(planned, not yet implemented)* - [[Blog API]] - blog posts
- [[Admin API]] - user management, data cleanup *(planned, not yet implemented)* - [[Admin API]] - user management, data cleanup, RN/admin payment settings
- [[Points API]] - loyalty points, levels, referrals *(planned, not yet implemented)* - [[Points API]] - loyalty points, levels, referrals
- [[AI API]] - OpenAI-backed text endpoints - [[AI API]] - OpenAI-backed text endpoints
- [[File API]] - upload, delete, serve - [[File API]] - upload, delete, serve
- [[Socket Events]] - real-time events - [[Socket Events]] - real-time events
@@ -157,7 +157,7 @@ cors({
}) })
``` ```
Only the configured `FRONTEND_URL` may make cross-origin requests with credentials. The SHKeeper configuration endpoint (`GET /api/payment/shkeeper/config`) overrides this with `Access-Control-Allow-Origin: *` because it is consumed by the SHKeeper payment widget hosted on another domain. Only the configured `FRONTEND_URL` may make cross-origin requests with credentials. Provider webhooks and Telegram bot webhooks are server-to-server entrypoints and should be exempted through explicit route handling, not broad browser CORS.
Uploaded files served from `/uploads/*` use `helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })` so they can be embedded from the frontend domain. Uploaded files served from `/uploads/*` use `helmet({ crossOriginResourcePolicy: { policy: "cross-origin" } })` so they can be embedded from the frontend domain.

View File

@@ -50,7 +50,7 @@ stateDiagram-v2
- Re-loads with `populate('participants.userId', 'firstName lastName profile.avatar email')` for the response. - Re-loads with `populate('participants.userId', 'firstName lastName profile.avatar email')` for the response.
3. **Group chat (dispute)** — same pattern, but `type: 'group'`, all three participants (buyer, seller, admin) added (admin is added later by `DisputeService.assignAdmin`). 3. **Group chat (dispute)** — same pattern, but `type: 'group'`, all three participants (buyer, seller, admin) added (admin is added later by `DisputeService.assignAdmin`).
4. **Support chat**`ChatService.createSupportChat(userId)` (`:41-88`) auto-discovers `User.findOne({ email: 'support@amn.gg' })` and creates a `type: 'support'` chat with a welcome message. Idempotent. 4. **Support chat**`ChatService.createSupportChat(userId)` (`:41-88`) auto-discovers `User.findOne({ email: 'support@amn.gg' })` and creates a `type: 'support'` chat with a welcome message. Idempotent.
5. **Post-payment auto-chat** — when SHKeeper confirms payment, `shkeeperWebhook.ts:606-618` calls `chatService.createChat` to ensure a direct chat exists between buyer and winning seller. 5. **Post-payment auto-chat** — when payment is confirmed, the payment-state cascade ensures a direct chat exists between buyer and winning seller.
### Joining the room (real-time) ### Joining the room (real-time)

View File

@@ -7,7 +7,7 @@ related_apis: ["POST /api/marketplace/purchase-requests/:id/delivery-code", "POS
# Delivery Confirmation Flow # Delivery Confirmation Flow
After the escrow is funded ([[Payment Flow - SHKeeper]] / [[Payment Flow - DePay & Web3]]) and the seller has prepared the item, the seller **marks shipped**, the buyer **enters a delivery code** to confirm receipt, and the escrow becomes eligible for release ([[Payout Flow]]). After the escrow is funded ([[PRD - Request Network In-House Checkout]] / [[Escrow Flow]]) and the seller has prepared the item, the seller **marks shipped**, the buyer **enters a delivery code** to confirm receipt, and the escrow becomes eligible for release ([[Payout Flow]]).
## Actors ## Actors
@@ -113,7 +113,7 @@ sequenceDiagram
## Linked flows ## Linked flows
- [[Payment Flow - SHKeeper]] / [[Payment Flow - DePay & Web3]] — funding precondition. - [[PRD - Request Network In-House Checkout]] / [[Escrow Flow]] — funding precondition.
- [[Escrow Flow]] — state transitions triggered by confirmation. - [[Escrow Flow]] — state transitions triggered by confirmation.
- [[Payout Flow]] — fires after confirmation (manual today). - [[Payout Flow]] — fires after confirmation (manual today).
- [[Dispute Flow]] — escape hatch. - [[Dispute Flow]] — escape hatch.

View File

@@ -15,9 +15,9 @@ When something goes wrong (item not delivered, wrong item, fraud), either party
- **Seller** — party against whom the dispute is raised (or in rarer cases, initiator). - **Seller** — party against whom the dispute is raised (or in rarer cases, initiator).
- **Admin / Mediator** — assigned to investigate. - **Admin / Mediator** — assigned to investigate.
- **Frontend** — buyer/seller "Report issue" buttons in the request detail view; admin dispute dashboard. - **Frontend** — buyer/seller "Report issue" buttons in the request detail view; admin dispute dashboard.
- **Backend** — `DisputeService` (`backend/src/services/dispute/DisputeService.ts` *(planned)*), `DisputeController` (`backend/src/controllers/disputeController.ts` *(planned)*), routes at `backend/src/routes/disputeRoutes.ts` *(planned)*. - **Backend** — `DisputeService` (`backend/src/services/dispute/DisputeService.ts`), dashboard/controller routes at `backend/src/routes/disputeRoutes.ts`, and release-hold helpers in `backend/src/services/dispute/releaseHoldService.ts`.
> [!warning] Not implemented > [!note] Alignment gap
> None of these files exist as of 2026-05-24. The dispute module is planned but not yet built. > The module exists now, but it still uses the legacy status/action enum. [[Funds Ledger and Escrow State Machine Specification]] defines the canonical future dispute states and financial side effects.
- **MongoDB** — `disputes`, `chats`, `purchaserequests`, `payments`. - **MongoDB** — `disputes`, `chats`, `purchaserequests`, `payments`.
- **Socket.IO** — `new-notification`, `new-message`, `dispute-updated` (planned). - **Socket.IO** — `new-notification`, `new-message`, `dispute-updated` (planned).
@@ -57,8 +57,8 @@ Resolution actions (from `Dispute.resolution.action` enum, see `Dispute.ts` *(in
- Persists `dispute.chatId = chat._id`. - Persists `dispute.chatId = chat._id`.
5. Notifications (currently a `TODO` in the service — `:107-116`) should fire `new-notification` to the seller. Today the chat creation alone provides real-time presence via the `new-message` socket emit inside `Chat.create`'s lifecycle. 5. Notifications (currently a `TODO` in the service — `:107-116`) should fire `new-notification` to the seller. Today the chat creation alone provides real-time presence via the `new-message` socket emit inside `Chat.create`'s lifecycle.
> [!warning] Dispute does not auto-pause escrow > [!note] Release hold behavior
> Today, opening a dispute does **not** flip `Payment.escrowState` away from `funded`. An admin could theoretically still release the escrow before resolving the dispute. Until a `disputed` flag is added to Payment, admins must check the dispute table before any release/refund action. > Opening a dispute now has backend release-hold support: `releaseHoldService.raiseDispute()` sets hold fields on the purchase request and related payments, and release/refund gates can consult those fields. The remaining work is to make this the single mandatory policy path for every release/refund/sweep operation and align it with the canonical `DISPUTED` escrow state.
### Phase 2 — Admin assignment ### Phase 2 — Admin assignment
@@ -84,7 +84,7 @@ Resolution actions (from `Dispute.resolution.action` enum, see `Dispute.ts` *(in
- `dispute.closedAt = now` - `dispute.closedAt = now`
- Appends `timeline` entry `dispute_resolved`. - Appends `timeline` entry `dispute_resolved`.
- Saves. - Saves.
13. **Financial side-effect (manual today):** depending on the action, the admin then triggers either the **payout** ([[Payout Flow]] with `kind: 'release'`) or the **refund** (`kind: 'refund'`, see [[Escrow Flow]]). The dispute service does not automatically dispatch the on-chain action. 13. **Financial side-effect (manual today):** depending on the action, the admin then triggers either the **release** ([[Payout Flow]] / [[Escrow Flow]]) or the **refund**. The dispute service records the resolution; full automatic dispatch through the release/refund policy engine is still a hardening item.
14. Both parties are notified (TODOs in code — planned: `notifyDisputeResolved`). 14. Both parties are notified (TODOs in code — planned: `notifyDisputeResolved`).
## Sequence diagram ## Sequence diagram
@@ -179,7 +179,7 @@ All require `authenticateToken` (router-level middleware).
- **Initiator is neither buyer nor seller** → not enforced at service level — should be validated in `DisputeController` (recommended hardening). - **Initiator is neither buyer nor seller** → not enforced at service level — should be validated in `DisputeController` (recommended hardening).
- **Same user opens multiple disputes for the same request** → no uniqueness constraint today. Consider adding `unique on (purchaseRequestId, status:'pending'|'in_progress')` to prevent duplicates. - **Same user opens multiple disputes for the same request** → no uniqueness constraint today. Consider adding `unique on (purchaseRequestId, status:'pending'|'in_progress')` to prevent duplicates.
- **Evidence URL is hot-linked** → frontend uploads through `POST /api/files/upload` and the URL is served from `/uploads`. Ensure auth on the upload endpoint to prevent random users from polluting evidence. - **Evidence URL is hot-linked** → frontend uploads through `POST /api/files/upload` and the URL is served from `/uploads`. Ensure auth on the upload endpoint to prevent random users from polluting evidence.
- **Dispute resolved without financial follow-up** → the dispute is "resolved" in record only; the escrow stays in its previous state. Add automation that auto-fires the payout/refund when the admin selects `release` or `refund`. - **Dispute resolved without financial follow-up** → the dispute is "resolved" in record only; the escrow stays in its previous state until the admin/custody operator completes release/refund. Add automation that dispatches the policy-checked release/refund instruction when the admin selects a financial resolution.
- **Admin resigns mid-dispute** → no transfer-of-mediator endpoint today. Add `POST /api/disputes/:id/reassign`. - **Admin resigns mid-dispute** → no transfer-of-mediator endpoint today. Add `POST /api/disputes/:id/reassign`.
> [!tip] Sort disputes by priority + age > [!tip] Sort disputes by priority + age
@@ -195,12 +195,10 @@ All require `authenticateToken` (router-level middleware).
## Source files ## Source files
> [!warning] Not implemented - Backend: `backend/src/services/dispute/DisputeService.ts`
> None of the backend files below exist as of 2026-05-24. The dispute module is planned but not yet built. - Backend: `backend/src/services/dispute/releaseHoldService.ts`
- Backend: `backend/src/routes/disputeRoutes.ts`
- Backend: `backend/src/services/dispute/DisputeService.ts` *(planned)* - Backend: `backend/src/services/dispute/disputeRoutes.ts`
- Backend: `backend/src/controllers/disputeController.ts` *(planned)* - Backend: `backend/src/models/Dispute.ts`
- Backend: `backend/src/routes/disputeRoutes.ts` *(planned)*
- Backend: `backend/src/models/Dispute.ts` *(planned)*
- Frontend: `frontend/src/sections/request/components/report-problem-to-admin.tsx` - Frontend: `frontend/src/sections/request/components/report-problem-to-admin.tsx`
- Frontend: admin dispute dashboard under `frontend/src/sections/admin/` (subject to organisation) - Frontend: admin dispute dashboard under `frontend/src/sections/admin/` (subject to organisation)

View File

@@ -1,199 +1,226 @@
--- ---
title: Escrow Flow title: Escrow Flow
tags: [flow, escrow, payment, state-machine] tags: [flow, escrow, payment, state-machine, custody]
related_models: ["[[Payment]]", "[[PurchaseRequest]]"] related_models: ["[[Payment]]", "[[PurchaseRequest]]", "[[Funds Ledger and Escrow State Machine Specification]]"]
related_apis: ["POST /api/payment/release/:paymentId", "POST /api/payment/refund/:paymentId"] related_apis: ["POST /api/payment/:id/release", "POST /api/payment/:id/refund", "POST /api/payment/:id/release/confirm", "POST /api/payment/:id/refund/confirm"]
--- ---
# Escrow Flow # Escrow Flow
The escrow is not a separate smart contract — it is a **state machine on the `Payment` document** combined with a **custodial wallet** (the platform-controlled BSC address `NEXT_PUBLIC_ESCROW_WALLET_ADDRESS`). Funds sit at that wallet once SHKeeper / Web3 verification completes, and are released to the seller or refunded to the buyer based on order outcome. 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 ## Actors
- **System** — the backend, on receiving pay-in confirmation. - **Buyer** -- pays from their wallet and confirms delivery.
- **Buyer** — confirms delivery to authorise release; can open a dispute to block release. - **Seller** -- fulfills the order and receives release.
- **Seller** recipient of release. - **Admin / mediator** -- resolves disputes and initiates release/refund when manual action is required.
- **Admin** — resolves disputes and signs payout transactions when manual control is required. - **Custody signer** -- Trezor today when enabled; target state is Safe multisig owners.
- **MongoDB** — `payments` document holds the canonical `escrowState`. - **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`, and `PurchaseRequest` state.
## Escrow state machine (`Payment.escrowState`) ## Current State Model
Enum from `Payment.ts:112-115`: `funded | releasable | released | refunded | releasing | failed | cancelled | partial`. `Payment.status` remains the coarse provider/business state:
- `pending`
- `processing`
- `confirmed`
- `completed`
- `failed`
- `cancelled`
- `refunded`
`Payment.escrowState` currently supports:
- `funded`
- `releasable`
- `releasing`
- `released`
- `refunded`
- `failed`
- `cancelled`
- `partial`
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.
```mermaid ```mermaid
stateDiagram-v2 stateDiagram-v2
[*] --> Pending: Payment.status="pending"\nescrowState=undefined [*] --> Pending : payment intent created
Pending --> Partial: webhook PARTIAL\nescrowState="partial" Pending --> Processing : funds detected / webhook received
Pending --> Funded: webhook PAID/OVERPAID\nor on-chain verify success\nescrowState="funded" Pending --> Cancelled : intent expired or buyer cancels
Partial --> Funded: top-up reaches threshold
Funded --> Releasable: buyer confirms delivery\n(or auto-release timer) Processing --> Funded : Transaction Safety Provider approved
Releasable --> Releasing: admin/system initiates payout\n[[Payout Flow]] Processing --> Failed : verification rejected
Releasing --> Released: payout tx confirmed\nescrowState="released"
Releasing --> Failed: payout tx reverted\nescrowState="failed" Funded --> Releasable : delivery confirmed / release authorized
Funded --> Refunded: dispute resolution = refund\nescrowState="refunded" Funded --> DisputeHold : dispute opened
Funded --> Refunded: order cancelled\npre-shipment Releasable --> DisputeHold : dispute opened before payout
Pending --> Cancelled: webhook EXPIRED/CANCELLED
escrowState="cancelled" DisputeHold --> Funded : dispute rejected / no financial action
Failed --> Releasing: admin retries 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 --> [*] Released --> [*]
Refunded --> [*] Refunded --> [*]
Cancelled --> [*] Cancelled --> [*]
``` ```
`Payment.status` mirrors a coarser business state: ## Step-by-step Narrative
- `pending` → invoice issued, awaiting funds.
- `processing` → SHKeeper sees partial / confirmations in progress.
- `confirmed` → fully credited (intermediate; sometimes skipped).
- `completed` → escrow `funded` and onward.
- `failed`, `cancelled`, `refunded` → terminal.
## Step-by-step narrative
### 1. Funding ### 1. Funding
- Triggered by either [[Payment Flow - SHKeeper]] (webhook `PAID`/`OVERPAID`) or [[Payment Flow - DePay & Web3]] (verified `eth_getTransactionReceipt`). 1. Buyer accepts a seller offer and starts Request Network checkout.
- Backend sets `Payment.status = "completed"` and `Payment.escrowState = "funded"` (`shkeeperWebhook.ts:388-391`, `shkeeperService.ts:600-602`). 2. Backend creates a `Payment` and Request Network intent through `requestNetworkPayInService.ts`.
- Cascade: `PurchaseRequest.status``payment`, then `processing` once the seller acknowledges; `SellerOffer.status``accepted`; chat created. 3. When configured, `getDestinationFor({ buyerId, sellerOfferId, chainId })` assigns a per-payment derived destination and stores it in `payment.metadata.derivedDestination`.
- Funds physically sit at the **custodial wallet** — SHKeeper's per-invoice deposit address (auto-swept to the merchant wallet) or directly at the escrow wallet in the Web3 path. 4. Frontend renders the in-house checkout block and the buyer signs RN-compatible on-chain transactions from their wallet.
5. Request Network webhook or reconciliation reports payment evidence.
6. 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.
7. Only after safety approval does the backend mark the payment funded and append ledger entries.
### 2. Holding ### 2. Holding
- While `escrowState === "funded"` and the order is in `processing` / `delivery`, the funds are inert. No interest accrues; no on-chain action happens. While escrow is funded, funds are represented in two places:
- The buyer cannot withdraw; the seller cannot collect. Only an admin/system action moves it forward.
- Visible in admin dashboard: `GET /api/payment/admin/funded?status=funded` (or similar — see admin payment view in `frontend/src/sections/payment/view/payment-list-admin-view.tsx`).
### 3. Releasing (happy path) - **On chain:** in the derived destination or custody wallet until swept/released/refunded.
- **In app accounting:** in `FundsLedgerEntry` rows and `Payment.escrowState`.
- Trigger options: 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.
- **Buyer confirms delivery** via the delivery-code flow ([[Delivery Confirmation Flow]]).
- **Auto-release timer** elapses (configurable; today a manual or scheduled job — `PurchaseRequestService` exposes status transitions through to `completed`).
- **Admin manual release** from the admin payment detail view.
- The system marks `Payment.escrowState = "releasable"` (intermediate).
- `shkeeperPayoutService.createPayoutTask` (or a manual EVM admin signature via `admin-wallet-payout.tsx`) starts the on-chain transfer to the seller's verified wallet address. State flips to `releasing`.
- On confirmation: `confirmAdminTx(paymentId, txHash, 'release')` (`shkeeperService.ts:628-647`) sets:
- `Payment.status = 'completed'`
- `Payment.escrowState = 'released'`
- `Payment.blockchain.transactionHash = <payout tx hash>`
- Cascade: `PurchaseRequest.status``seller_paid` then `completed`.
### 4. Refunding (dispute / cancellation) ### 3. Release
- Trigger: dispute resolution with `action: 'refund'` or pre-shipment cancellation. Release is triggered by delivery confirmation, auto-release policy, or dispute resolution for the seller.
- Backend builds the refund tx via `buildAdminSignedTxPayload(paymentId, 'refund')` (`shkeeperService.ts:614-626`) — destination is `payment.blockchain.sender` (the buyer's verified wallet).
- Admin signs and broadcasts (currently a manual step in the admin UI).
- On confirmation: `confirmAdminTx(paymentId, txHash, 'refund')` sets:
- `Payment.status = 'refunded'`
- `Payment.escrowState = 'refunded'`
- Cascade: `PurchaseRequest.status``cancelled` (or remains in dispute-resolved state).
### 5. Failed payout 1. Admin calls `POST /api/payment/:id/release`.
2. Backend loads the payment and validates ledger availability when enforcement is enabled.
3. Backend builds a provider payment instruction.
4. Custody signer executes the transaction:
- current optional control: Trezor proof when `TREZOR_SAFEKEEPING_REQUIRED=true`;
- roadmap control: Safe multisig transaction proposal/execution.
5. Admin confirms with `POST /api/payment/:id/release/confirm` and tx hash.
6. Backend validates Trezor proof when required, confirms adapter state, and appends a `release` ledger entry.
- If the payout tx reverts (insufficient gas, contract pause, wrong address), `escrowState = 'failed'`. Admin can retry by initiating a fresh payout. ### 4. Refund
## Sequence diagram (release path) 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
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
autonumber autonumber
actor B as Buyer actor B as Buyer
actor A as Admin
participant FE as Frontend participant FE as Frontend
participant BE as Backend participant BE as Backend
participant RN as Request Network
participant BC as EVM Chain
participant DB as MongoDB participant DB as MongoDB
participant SK as SHKeeper Payout API
participant BC as BSC
B->>FE: Enter delivery code (or auto-timer fires) B->>FE: Start Request Network checkout
FE->>BE: POST /api/marketplace/purchase-requests/:id/confirm-delivery FE->>BE: POST /api/payment/request-network/intents
BE->>DB: PurchaseRequest.status="delivered"\nPayment.escrowState="releasable" BE->>DB: Payment.create(status="pending")
BE-->>FE: ok BE->>BE: Assign derived destination when configured
A->>FE: Click "Release" in admin BE->>RN: Create Request Network intent
FE->>BE: POST /api/payment/shkeeper/payout BE-->>FE: inHouseCheckout block
BE->>DB: Payment.escrowState="releasing" B->>BC: approve + transferFromWithReferenceAndFee
BE->>SK: createPayoutTask({recipient, amount}) RN-->>BE: signed webhook / status evidence
SK->>BC: signed payout tx BE->>BE: Transaction Safety Provider checks
BC-->>SK: confirmed BE->>DB: Payment.status="completed", escrowState="funded"
SK->>BE: payout webhook / poll BE->>DB: append FundsLedgerEntry(payment_detected / hold)
BE->>BE: confirmAdminTx(paymentId, txHash, "release")
BE->>DB: Payment.escrowState="released"\nPurchaseRequest.status="completed"
``` ```
## Sequence diagram (refund path) ## Sequence Diagram - Release / Refund
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
autonumber autonumber
actor A as Admin actor A as Admin
actor C as Custody signer
participant BE as Backend participant BE as Backend
participant DB as MongoDB participant DB as MongoDB
participant BC as BSC participant BC as EVM Chain
actor B as Buyer
A->>BE: Dispute resolved with action="refund" A->>BE: POST /api/payment/{id}/release or refund
BE->>BE: buildAdminSignedTxPayload(paymentId, "refund") BE->>DB: Load Payment + ledger balance
BE-->>A: { to:buyerWallet, amount, token, network } BE->>BE: Check dispute hold + ledger availability
A->>BC: sign + broadcast tx BE-->>A: unsigned instruction
BC-->>A: txHash A->>C: Request signature / Safe execution
A->>BE: confirmAdminTx(paymentId, txHash, "refund") C->>BC: Broadcast tx
BE->>DB: Payment.status="refunded"\nescrowState="refunded" BC-->>C: txHash
BE->>B: notifyRefundCompleted 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 ## API Calls
| Method | Endpoint | Purpose | | Method | Endpoint | Purpose |
|---|---|---| |---|---|---|
| `POST` | `/api/payment/admin/release/:paymentId` | Initiate release | | `POST` | `/api/payment/request-network/intents` | Create Request Network pay-in intent |
| `POST` | `/api/payment/admin/refund/:paymentId` | Initiate refund | | `GET` | `/api/payment/request-network/:paymentId/checkout` | Rehydrate in-house checkout block |
| `POST` | `/api/payment/admin/confirm-tx/:paymentId` | Admin marks the signed tx confirmed | | `POST` | `/api/payment/request-network/webhook` | Receive signed RN webhook |
| `GET` | `/api/payment/:paymentId/status` | Polled by both parties | | `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 |
## Database writes ## Side Effects And Risks
- **`payments`**: `status`, `escrowState`, `blockchain.transactionHash`, `completedAt`, `metadata.*` are mutated as the state progresses. - **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.
- **`purchaserequests`**: `status` cascades (`payment → processing → delivery → delivered → confirming → seller_paid → completed`). - **Ledger enforcement is configurable.** `PAYMENT_LEDGER_ENFORCEMENT` must be enabled before real custody decentralization work is considered complete.
- **`notifications`**: created on each terminal state. - **Trezor enforcement is configurable.** `TREZOR_SAFEKEEPING_REQUIRED=true` makes 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.
## Socket events emitted ## Linked Flows
- **`purchase-request-update`** `status-changed` on every cascading status flip. - [[PRD - Request Network In-House Checkout]] -- current primary pay-in path.
- **`payment-status`** (planned/admin) — admin dashboard real-time feed. - [[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.
## Side effects ## Source Files
- **Custodial risk** — the escrow wallet's private key sits with the platform. Lose it → lose all in-flight escrows. Operational controls: hardware wallet, multi-sig, cold storage of the recovery seed. - Backend: `backend/src/models/Payment.ts`
- **No on-chain escrow contract** — there is no Solidity escrow today. Migration toward a smart-contract escrow (e.g. OpenZeppelin's `Escrow.sol` pattern) would remove custodial trust at the cost of higher complexity and gas. - Backend: `backend/src/models/FundsLedgerEntry.ts`
- Backend: `backend/src/services/payment/requestNetwork/requestNetworkPayInService.ts`
## Error / edge cases - Backend: `backend/src/services/payment/safety/transactionSafetyProvider.ts`
- Backend: `backend/src/services/payment/orchestration/releaseRefundService.ts`
- **Buyer never confirms delivery** → today requires admin intervention. An auto-release timer (e.g. 7 days after `delivered`) is a recommended addition. - Backend: `backend/src/services/payment/wallets/derivedDestinations.ts`
- **Seller's wallet address invalid** → payout tx fails or sends to a black hole. Validate `recipientAddress` shape (`^0x[0-9a-fA-F]{40}$`) before signing (`shkeeperPayoutService.ts:62-64` checks `.startsWith('0x')`). - Backend: `backend/src/services/payment/wallets/sweepService.ts`
- **Partial payment** (`PARTIAL`) → escrow remains in `pending/partial`; release blocked until full payment arrives. - Backend: `backend/src/services/dispute/releaseHoldService.ts`
- **Overpaid** → currently treated as `completed/funded`; the surplus is not auto-refunded. - Backend: `backend/src/services/trezor/trezorService.ts`
- **Concurrent release + refund** → blocked by `PaymentCoordinator` serialisation; whichever fires first wins, the other is rejected.
- **Payout fails on chain** → state stays in `releasing` until admin re-runs; consider auto-retry with exponential backoff.
- **Disputed payment** → `escrowState` is **not** auto-changed when a dispute is opened. Admin must explicitly resolve to refund/release. Add a `disputed` boolean or `escrowState='disputed'` to make this more obvious.
> [!warning] Single custodial wallet = single point of failure
> Centralising all in-flight escrow at one BSC address is the platform's largest operational risk. Use a multi-sig (Gnosis Safe) for the escrow wallet, store one key in HSM, and require two admin signatures for any payout > a threshold.
> [!tip] Recovering inconsistent state
> If `Payment.escrowState` looks stale (e.g. `released` but no on-chain tx hash), inspect with `Payment.find({ escrowState: 'released', 'blockchain.transactionHash': { $exists: false } })` and reconcile via the SHKeeper invoice or the `fix-transaction-hashes.js` script.
## Linked flows
- [[Payment Flow - SHKeeper]] — funds the escrow.
- [[Payment Flow - DePay & Web3]] — alternative funding path.
- [[Delivery Confirmation Flow]] — triggers release.
- [[Dispute Flow]] — can divert to refund.
- [[Payout Flow]] — executes the release transfer.
## Source files
- Backend: `backend/src/models/Payment.ts:96-145` (status + escrowState enums)
- Backend: `backend/src/services/payment/shkeeper/shkeeperService.ts:600-647`
- Backend: `backend/src/services/payment/shkeeper/shkeeperWebhook.ts:387-411`
- Backend: `backend/src/services/payment/paymentCoordinator.ts`
- Frontend: `frontend/src/sections/payment/view/payment-list-admin-view.tsx`
- Frontend: `frontend/src/sections/request/components/admin-steps/admin-wallet-payout.tsx`

View File

@@ -29,7 +29,7 @@ After an offer is submitted ([[Seller Offer Flow]]), the buyer and seller can ne
1. **Open negotiation chat** — when a buyer first clicks "Chat with seller" on an offer card, the frontend calls `POST /api/chat` to find-or-create a `direct` chat tied to the purchase request (`ChatService.createChat`, `chat.ts:90-192`). The chat's `relatedTo = { type: 'PurchaseRequest', id }` makes it discoverable from the request view. 1. **Open negotiation chat** — when a buyer first clicks "Chat with seller" on an offer card, the frontend calls `POST /api/chat` to find-or-create a `direct` chat tied to the purchase request (`ChatService.createChat`, `chat.ts:90-192`). The chat's `relatedTo = { type: 'PurchaseRequest', id }` makes it discoverable from the request view.
> [!tip] Pre-payment chats vs. post-payment chats > [!tip] Pre-payment chats vs. post-payment chats
> A negotiation chat may exist **before** the SHKeeper webhook auto-creates the post-payment chat. The `ChatService.createChat` `direct` find-or-create logic (`ChatService.ts:95-108`) prevents duplicates the same chat object is reused. > A negotiation chat may exist **before** payment confirmation creates the post-payment chat. The `ChatService.createChat` `direct` find-or-create logic (`ChatService.ts:95-108`) prevents duplicates -- the same chat object is reused.
2. **Status flip to `in_negotiation`** — the first message in the negotiation chat triggers a backend hook (or a manual frontend PATCH) that calls `PurchaseRequestService.updatePurchaseRequest` with `{ status: 'in_negotiation' }`. The status-progression guard allows this (`received_offers → in_negotiation`). 2. **Status flip to `in_negotiation`** — the first message in the negotiation chat triggers a backend hook (or a manual frontend PATCH) that calls `PurchaseRequestService.updatePurchaseRequest` with `{ status: 'in_negotiation' }`. The status-progression guard allows this (`received_offers → in_negotiation`).
@@ -41,7 +41,7 @@ After an offer is submitted ([[Seller Offer Flow]]), the buyer and seller can ne
- `SellerOffer.findByIdAndUpdate(id, { ...updateData, updatedAt: now }, { new: true })`. - `SellerOffer.findByIdAndUpdate(id, { ...updateData, updatedAt: now }, { new: true })`.
- Emits `purchase-request-update` with `eventType: 'offer-updated'` to `request-{requestId}` (`SellerOfferService.ts:284-288`) — both parties' open tabs refresh. - Emits `purchase-request-update` with `eventType: 'offer-updated'` to `request-{requestId}` (`SellerOfferService.ts:284-288`) — both parties' open tabs refresh.
5. **Buyer accepts** clicks "Accept this offer", which kicks off [[Payment Flow - SHKeeper]] with the (now-updated) `sellerOfferId`. The webhook flips offer `accepted` and request `payment`. 5. **Buyer accepts** -- clicks "Accept this offer", which kicks off [[PRD - Request Network In-House Checkout]] with the selected `sellerOfferId`. Payment confirmation flips offer -> `accepted` and request -> `payment`.
6. **Buyer rejects** — calls `PATCH /api/marketplace/offers/{id}` with `{ status: 'rejected' }`. `SellerOfferService.updateOfferStatus` (`:306-353`) sends `notifyOfferRejected` to the seller and stamps `rejectedAt` + `rejectionReason`. 6. **Buyer rejects** — calls `PATCH /api/marketplace/offers/{id}` with `{ status: 'rejected' }`. `SellerOfferService.updateOfferStatus` (`:306-353`) sends `notifyOfferRejected` to the seller and stamps `rejectedAt` + `rejectionReason`.
@@ -80,7 +80,7 @@ sequenceDiagram
BE->>IO: emit request-{id} 'purchase-request-update' (offer-updated) BE->>IO: emit request-{id} 'purchase-request-update' (offer-updated)
IO-->>FE_B: refresh offer card IO-->>FE_B: refresh offer card
alt Buyer accepts alt Buyer accepts
B->>FE_B: Click "Pay" [[Payment Flow - SHKeeper]] B->>FE_B: Click "Pay" -> [[PRD - Request Network In-House Checkout]]
Note over BE: Webhook PAID flips offer→accepted, request→payment Note over BE: Webhook PAID flips offer→accepted, request→payment
else Buyer rejects else Buyer rejects
B->>FE_B: Click "Reject" B->>FE_B: Click "Reject"
@@ -135,7 +135,7 @@ sequenceDiagram
## Linked flows ## Linked flows
- [[Seller Offer Flow]] — the prior step. - [[Seller Offer Flow]] — the prior step.
- [[Payment Flow - SHKeeper]] — closes the negotiation with an on-chain payment. - [[PRD - Request Network In-House Checkout]] — closes the negotiation with an on-chain payment.
- [[Chat Flow]] — message-level mechanics, attachments, read receipts. - [[Chat Flow]] — message-level mechanics, attachments, read receipts.
- [[Notification Flow]] — accept/reject notifications. - [[Notification Flow]] — accept/reject notifications.

View File

@@ -7,7 +7,10 @@ related_apis: ["POST /api/payment/decentralized/create", "POST /api/payment/dece
# Payment Flow — DePay & Web3 (Wallet-Direct) # Payment Flow — DePay & Web3 (Wallet-Direct)
Alternative pay-in path: instead of routing through [[Payment Flow - SHKeeper]], 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. > [!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 ## Actors
@@ -16,8 +19,8 @@ Alternative pay-in path: instead of routing through [[Payment Flow - SHKeeper]],
- **Wagmi / WalletConnect / MetaMask** — wallet stack. - **Wagmi / WalletConnect / MetaMask** — wallet stack.
- **Backend** — `decentralizedPaymentService.ts` (intent), `BSCTransactionVerifier` (on-chain verification), `decentralizedPaymentRoutes.ts`. - **Backend** — `decentralizedPaymentService.ts` (intent), `BSCTransactionVerifier` (on-chain verification), `decentralizedPaymentRoutes.ts`.
- **Blockchain (BSC)** — verified via `https://bsc-dataseed.binance.org/` JSON-RPC. - **Blockchain (BSC)** — verified via `https://bsc-dataseed.binance.org/` JSON-RPC.
- **MongoDB** — `payments` collection (same model as SHKeeper, different `provider` value). - **MongoDB** — `payments` collection, with `provider` distinguishing the legacy wallet-direct source from Request Network.
- **Socket.IO** — `payment-created`, plus the cascade events from [[Payment Flow - SHKeeper]] when verification succeeds. - **Socket.IO** — `payment-created`, plus the funded-escrow cascade events when verification succeeds.
## Preconditions ## Preconditions
@@ -132,7 +135,7 @@ sequenceDiagram
## Side effects ## Side effects
- **No SHKeeper involvement** — the escrow wallet is custodial; the platform admin holds the keys. Payouts from this wallet to sellers happen via [[Payout Flow]] (SHKeeper payouts API) or manual admin signing using `admin-wallet-payout.tsx` UI. - **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. - **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 ## Error / edge cases
@@ -152,7 +155,8 @@ sequenceDiagram
## Linked flows ## Linked flows
- [[Payment Flow - SHKeeper]] — sibling pay-in path; same downstream cascade. - [[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. - [[Escrow Flow]] — funded state semantics.
- [[Payout Flow]] — releasing the funded escrow to the seller. - [[Payout Flow]] — releasing the funded escrow to the seller.
- [[Dispute Flow]] — refunds back to the buyer's verified wallet. - [[Dispute Flow]] — refunds back to the buyer's verified wallet.

View File

@@ -7,6 +7,9 @@ related_apis: ["POST /api/payment/shkeeper/create", "POST /api/payment/shkeeper/
# Payment Flow — SHKeeper (Crypto Pay-In) # Payment Flow — SHKeeper (Crypto Pay-In)
> [!warning] Historical migration document
> This page describes the older SHKeeper pay-in rail. It is retained for migration/reconciliation context only. The current primary pay-in path is [[PRD - Request Network In-House Checkout]], and the current escrow/custody model is [[Escrow Flow]] plus [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].
End-to-end **crypto pay-in** via the self-hosted [SHKeeper](https://github.com/vsys-host/shkeeper.io) gateway at `pay.amn.gg`. The buyer pays in stablecoins/crypto; SHKeeper monitors the blockchain, sends webhooks back, and the backend marks the escrow funded. End-to-end **crypto pay-in** via the self-hosted [SHKeeper](https://github.com/vsys-host/shkeeper.io) gateway at `pay.amn.gg`. The buyer pays in stablecoins/crypto; SHKeeper monitors the blockchain, sends webhooks back, and the backend marks the escrow funded.
## Supported assets ## Supported assets

View File

@@ -1,133 +1,130 @@
--- ---
title: Payout Flow title: Payout Flow
tags: [flow, payment, payout, shkeeper, seller] tags: [flow, payment, payout, release, refund, custody]
related_models: ["[[Payment]]"] related_models: ["[[Payment]]", "[[Funds Ledger and Escrow State Machine Specification]]"]
related_apis: ["POST /api/payment/shkeeper/payout", "GET /api/payment/shkeeper/payout/:taskId"] related_apis: ["POST /api/payment/:id/release", "POST /api/payment/:id/release/confirm", "POST /api/payment/:id/refund", "POST /api/payment/:id/refund/confirm"]
--- ---
# Payout Flow # Payout Flow
How the **seller receives the escrowed crypto** once the order is complete. Two variants are implemented: This page describes how escrowed funds leave Amanat custody after an order is complete or a dispute is resolved.
1. **SHKeeper Payouts API** (`shkeeperPayoutService.ts`) — the gateway signs and broadcasts on behalf of the platform. The current flow is no longer SHKeeper payout-task centric. Release and refund are instruction-based:
2. **Manual admin wallet payout** (`admin-wallet-payout.tsx`) — an admin connects their own wallet and signs the transfer; the tx hash is reported back to the backend.
Both result in `Payment.escrowState = 'released'` and an outgoing `Payment` record with `direction: 'out'`. 1. Backend validates policy, dispute hold, and ledger availability.
2. Backend builds a release/refund instruction.
3. A custody signer executes the on-chain transaction.
4. Backend confirms the tx hash and appends the ledger entry.
Today the custody signer can be an admin/Trezor path when enabled. The roadmap target is Safe multisig execution before any custom escrow contract pilot. See [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].
## Actors ## Actors
- **Admin** (or scheduled system trigger) — initiates the payout. - **Admin / mediator** -- initiates release/refund after delivery confirmation or dispute resolution.
- **Seller** — recipient, has saved their wallet address under `User.profile.walletAddress`. - **Custody signer** -- Trezor proof today when enabled; target state is Safe multisig owners.
- **Backend** — `shkeeperPayoutService.createPayoutTask` and the manual confirmation routes. - **Seller** -- recipient for release.
- **SHKeeper Payouts API** — `POST https://pay.amn.gg/api/v1/payout` (per SHKeeper docs). - **Buyer** -- recipient for refund.
- **Blockchain (BSC)** — final on-chain settlement. - **Backend** -- `releaseRefundService.ts`, payment adapter, ledger service, Trezor service.
- **MongoDB** — separate `Payment` document with `direction: 'out'`. - **Blockchain** -- final on-chain settlement.
- **MongoDB** -- `Payment` and `FundsLedgerEntry`.
## Preconditions ## Preconditions
- The original pay-in `Payment` has `escrowState = 'funded'` (or `releasable`). - The pay-in `Payment` is funded or releasable.
- The seller has set `profile.walletAddress` (validated `^0x...` format). - The release/refund amount is positive and does not exceed available ledger balance.
- The corresponding `PurchaseRequest` is in a status that allows payout (`delivered`, `confirming`, `seller_paid`, or `completed`). - No active dispute hold blocks the operation, unless the operation is the explicit dispute resolution path.
- Recipient wallet is known and verified.
- If `TREZOR_SAFEKEEPING_REQUIRED=true`, the confirm step includes the expected Trezor operation signature.
- Production target: Safe multisig execution is required for custody movement.
## Step-by-step narrative ## Release Narrative
### SHKeeper-mediated payout 1. Buyer confirms delivery, an auto-release policy matures, or a dispute resolves for the seller.
2. Admin calls `POST /api/payment/:id/release` with optional partial amount.
3. Backend loads the `Payment`, validates ledger availability when `PAYMENT_LEDGER_ENFORCEMENT=true`, and returns an instruction payload.
4. Custody signer broadcasts the seller payment transaction.
5. Admin calls `POST /api/payment/:id/release/confirm` with `txHash` and optional Trezor proof.
6. Backend verifies signer proof when required, confirms adapter state, appends a `release` ledger entry, and marks escrow released.
1. Admin (or the auto-release scheduler — not yet implemented) hits `POST /api/payment/shkeeper/payout` with `{ purchaseRequestId, sellerOfferId, buyerId, sellerId, amount, recipientAddress, token?, network? }`. ## Refund Narrative
2. Backend `shkeeperPayoutService.createPayoutTask` (`shkeeperPayoutService.ts:40-150`):
- Validates ObjectIds and the `recipientAddress` (`startsWith('0x')`).
- **Idempotency**: `Payment.findOne({ purchaseRequestId, sellerOfferId, sellerId, provider:'shkeeper', direction:'out', status: { $in:['pending','processing','completed'] } })` — if found, reuses it.
- Creates a new `Payment` document with `direction: 'out'`, `escrowState: 'releasing'`, `blockchain.receiver = recipientAddress`.
- Calls SHKeeper Payouts API (`POST /api/v1/payout`) with the body documented at <https://shkeeper.io/api/#tag/Payouts>. SHKeeper returns a `task_id`.
- Stores `Payment.providerPaymentId = task_id`, `metadata.shkeeperTaskId = task_id`, `metadata.payoutType = 'seller-payment'`.
3. Polling or webhook: when SHKeeper completes the payout, it pushes a webhook (or the backend polls `GET /api/v1/payout/{task_id}`) and the system flips `Payment.status = 'completed'`, `escrowState = 'released'`, populates `blockchain.transactionHash`.
4. The original pay-in `Payment` is updated in tandem: `escrowState = 'released'`, `PurchaseRequest.status = 'seller_paid'``completed`.
5. Notifications: `notifyPayoutSent` to the seller, internal admin log.
### Manual admin payout 1. Dispute resolves for the buyer, order is cancelled before fulfillment, or support executes an approved recovery.
2. Admin calls `POST /api/payment/:id/refund`.
3. Backend validates available funds and policy.
4. Custody signer broadcasts the refund transaction.
5. Admin calls `POST /api/payment/:id/refund/confirm` with `txHash` and optional Trezor proof.
6. Backend appends a `refund` ledger entry and marks escrow refunded.
1. Admin opens the request detail in the admin view; the admin-step component `admin-wallet-payout.tsx` shows the recipient and amount. ## Sequence Diagram
2. Admin connects their wallet (`useWeb3` / `web3Service.connect()`).
3. Admin clicks "Send payout"; wagmi triggers `transfer(recipient, amount)` on the USDT contract.
4. After confirmation, the admin clicks "Confirm in system", which POSTs `POST /api/payment/admin/confirm-tx/:paymentId` with `{ txHash, kind: 'release' }`.
5. Backend `confirmAdminTx(paymentId, txHash, 'release')` (`shkeeperService.ts:628-647`) sets `status: 'completed'`, `escrowState: 'released'`, `blockchain.transactionHash = txHash`.
### Sequence diagram (SHKeeper payout)
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
autonumber autonumber
actor A as Admin/System actor A as Admin
actor C as Custody signer
participant BE as Backend participant BE as Backend
participant DB as MongoDB participant DB as MongoDB
participant SK as SHKeeper Payout API participant BC as EVM Chain
participant BC as BSC actor R as Recipient
actor S as Seller
A->>BE: POST /api/payment/shkeeper/payout A->>BE: POST /api/payment/{id}/release or refund
BE->>DB: Payment.create({direction:"out", escrowState:"releasing"}) BE->>DB: Load Payment + FundsLedger balance
BE->>SK: POST /api/v1/payout {to, amount, crypto} BE->>BE: Check dispute hold + ledger availability
SK-->>BE: { task_id, status:"pending" } BE-->>A: unsigned release/refund instruction
BE->>DB: Payment.providerPaymentId=task_id A->>C: Request Trezor/Safe execution
SK->>BC: signed payout tx (managed wallet) C->>BC: Broadcast transfer
BC-->>SK: confirmed BC-->>C: txHash
SK->>BE: webhook payout-completed (or BE polls) A->>BE: POST /confirm { txHash, signer proof }
BE->>DB: Payment.status="completed"\nescrowState="released"\ntxHash BE->>BE: Verify proof if required
BE->>DB: pay-in Payment.escrowState="released"\nPurchaseRequest.status="seller_paid" BE->>DB: append release/refund ledger entry
BE->>S: notifyPayoutSent BE->>DB: update Payment escrowState
BE-->>R: notification
``` ```
## API calls ## API Calls
| Method | Endpoint | Source | | Method | Endpoint | Purpose |
|---|---|---| |---|---|---|
| `POST` | `/api/payment/shkeeper/payout` | `shkeeperPayoutRoutes.ts``createPayoutTask` | | `POST` | `/api/payment/:id/release` | Build release instruction |
| `GET` | `/api/payment/shkeeper/payout/:taskId` | Polls SHKeeper task status | | `POST` | `/api/payment/:id/release/confirm` | Confirm release transaction |
| `POST` | `/api/payment/admin/confirm-tx/:paymentId` | Manual admin confirmation | | `POST` | `/api/payment/:id/refund` | Build refund instruction |
| `GET` | `/api/payment/admin/payouts` | List payouts (admin dashboard) | | `POST` | `/api/payment/:id/refund/confirm` | Confirm refund transaction |
| `GET` | `/api/admin/payments/awaiting-confirmation` | Admin view of payments blocked on confirmation depth |
| `GET` | `/api/payment/derived-destinations` | Admin view of derived destination sweep state |
## Database writes ## Database Writes
- **`payments`** — new outgoing document; updates to `status`, `escrowState`, `blockchain.transactionHash` as the task progresses. - **`payments`** -- status, `escrowState`, `blockchain.transactionHash`, signer metadata.
- **`payments`** (pay-in counterpart) — `escrowState = 'released'`. - **`funds_ledger_entries`** -- append-only `release` or `refund` entry with idempotency key.
- **`purchaserequests`** `status` advances to `seller_paid``completed`. - **`purchaserequests`** -- terminal business state after release/refund completes.
- **`notifications`** — seller payout receipt. - **`notifications`** -- release/refund receipt to the relevant party.
## Socket events emitted ## Error / Edge Cases
- **`payment-status`** (admin) on each transition. - **Insufficient ledger balance** -- reject instruction build/confirm.
- **`purchase-request-update`** `status-changed`. - **Active dispute hold** -- reject release/refund unless the operation is the explicit dispute outcome.
- **Missing signer proof** -- reject when `TREZOR_SAFEKEEPING_REQUIRED=true`.
- **Custody tx sent but not confirmed in app** -- reconcile by tx hash and append the missing ledger entry once verified.
- **Partial split** -- build separate release and refund instructions whose sum does not exceed available balance.
- **Payout reverted** -- leave escrow in failed/retryable state and do not append the terminal ledger entry.
## Side effects ## Legacy SHKeeper Note
- **`fix-transaction-hashes.js`** at repo root (`backend/fix-transaction-hashes.js`) — script used to backfill missing `blockchain.transactionHash` on payouts where the SHKeeper webhook arrived without the txid (e.g. signature length mismatch in dev). Run locally with the same Mongo URI to repair stale documents. Use it as the reference for the data-fix pattern — pull recent payouts, query SHKeeper for invoice/task details, write back the hash. Older versions used SHKeeper payout tasks and scripts such as `fix-transaction-hashes.js`. Those references remain useful for historical reconciliation, but new release/refund work should use the instruction, ledger, and custody-signer flow described here.
- **Hash repair** — periodic reconciliation against SHKeeper invoice GET endpoints ensures bookkeeping accuracy.
## Error / edge cases ## Linked Flows
- **Invalid recipient address** → throws synchronously, no DB record created. - [[Escrow Flow]] -- sets up the conditions under which release/refund is allowed.
- **SHKeeper insufficient hot-wallet balance** → SHKeeper returns an error; payout task stays `pending`, backend logs. - [[Delivery Confirmation Flow]] -- happy-path release trigger.
- **Duplicate payout request** → idempotency: existing payment returned with no extra SHKeeper call. - [[Dispute Flow]] -- can divert release to refund or split.
- **Payout reverted on chain** → SHKeeper marks the task `failed`; backend sets `Payment.status = 'failed'`, `escrowState = 'failed'`. Admin retries. - [[Trezor Safekeeping Flow]] -- hardware-backed operation approval.
- **Missing `transactionHash` after success** → use `fix-transaction-hashes.js` to backfill. - [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] -- Safe-first custody roadmap.
- **Manual payout signed but never confirmed in system** → on-chain transfer happened, but `Payment.escrowState` stays `releasing`. Admin can run a reconciliation script that scans the escrow wallet's outgoing txs and matches by amount/timestamp.
- **Seller changes wallet address mid-flight** → the saved `recipientAddress` is the snapshot taken at payout creation; subsequent profile changes do not affect in-flight payouts.
> [!warning] Auto-release is not yet implemented ## Source Files
> Today, payouts are admin-initiated. The flow is ready for an automatic trigger when [[Delivery Confirmation Flow]] completes — implement a cron job or queue worker that scans for `PurchaseRequest.status='delivered'` and auto-creates payouts after a configurable grace period.
## Linked flows - Backend: `backend/src/services/payment/orchestration/releaseRefundService.ts`
- Backend: `backend/src/services/payment/ledger/fundsLedgerService.ts`
- [[Escrow Flow]] — sets up the conditions under which payout is allowed. - Backend: `backend/src/services/payment/adapters/requestNetworkAdapter.ts`
- [[Delivery Confirmation Flow]] — green-lights the payout. - Backend: `backend/src/services/trezor/trezorService.ts`
- [[Dispute Flow]] — can divert funds to a refund instead. - Backend: `backend/src/services/dispute/releaseHoldService.ts`
- [[Notification Flow]] — payout receipt to seller. - Frontend: admin payment/release/refund surfaces under `frontend/src/sections/`
## Source files
- Backend: `backend/src/services/payment/shkeeper/shkeeperPayoutService.ts`
- Backend: `backend/src/services/payment/shkeeper/shkeeperPayoutRoutes.ts`
- Backend: `backend/src/services/payment/shkeeper/shkeeperService.ts:614-647` (build & confirm admin tx payload)
- Backend: `backend/fix-transaction-hashes.js` (reconciliation script)
- Frontend: `frontend/src/sections/request/components/admin-steps/admin-wallet-payout.tsx`
- Frontend: `frontend/src/web3/web3Service.ts`

View File

@@ -34,7 +34,7 @@ stateDiagram-v2
pending --> received_offers: first SellerOffer saved\nSellerOfferService.createOffer pending --> received_offers: first SellerOffer saved\nSellerOfferService.createOffer
received_offers --> in_negotiation: buyer/seller chat\n(counter-offer, see [[Negotiation Flow]]) received_offers --> in_negotiation: buyer/seller chat\n(counter-offer, see [[Negotiation Flow]])
in_negotiation --> received_offers: counter rejected in_negotiation --> received_offers: counter rejected
received_offers --> payment: SHKeeper webhook PAID\n(selected offer) received_offers --> payment: Request Network payment confirmed\n(selected offer)
in_negotiation --> payment: same in_negotiation --> payment: same
payment --> processing: seller acknowledges payment --> processing: seller acknowledges
processing --> delivery: seller marks shipped processing --> delivery: seller marks shipped
@@ -151,7 +151,7 @@ sequenceDiagram
## Database writes ## Database writes
- **`purchaserequests` collection**: full insert. Subsequent status transitions and `selectedOfferId` updates happen in [[Seller Offer Flow]], [[Payment Flow - SHKeeper]], and [[Delivery Confirmation Flow]]. - **`purchaserequests` collection**: full insert. Subsequent status transitions and `selectedOfferId` updates happen in [[Seller Offer Flow]], [[PRD - Request Network In-House Checkout]], and [[Delivery Confirmation Flow]].
- **`notifications` collection**: one per notified seller plus one for the buyer. - **`notifications` collection**: one per notified seller plus one for the buyer.
- **`users.referralStats`** is not touched at request creation. - **`users.referralStats`** is not touched at request creation.
@@ -186,7 +186,7 @@ sequenceDiagram
- [[Seller Offer Flow]] — sellers respond to the published request. - [[Seller Offer Flow]] — sellers respond to the published request.
- [[Negotiation Flow]] — counter-offer mechanics in `in_negotiation`. - [[Negotiation Flow]] — counter-offer mechanics in `in_negotiation`.
- [[Payment Flow - SHKeeper]] — buyer pays for the accepted offer. - [[PRD - Request Network In-House Checkout]] — buyer pays for the accepted offer.
- [[Delivery Confirmation Flow]] — seller ships, buyer confirms. - [[Delivery Confirmation Flow]] — seller ships, buyer confirms.
- [[Dispute Flow]] — escape hatch for failed deliveries. - [[Dispute Flow]] — escape hatch for failed deliveries.
- [[Notification Flow]] — backbone of the seller fan-out. - [[Notification Flow]] — backbone of the seller fan-out.

View File

@@ -149,7 +149,7 @@ sequenceDiagram
- [[Registration Flow]] — attribution point. - [[Registration Flow]] — attribution point.
- [[Google OAuth Flow]] — also supports `referralCode`. - [[Google OAuth Flow]] — also supports `referralCode`.
- [[Notification Flow]] — `referral-signup`, `level-up`, and points events surface here. - [[Notification Flow]] — `referral-signup`, `level-up`, and points events surface here.
- [[Payment Flow - SHKeeper]] — completion of a purchase is the canonical trigger for awarding referral commission. - [[PRD - Request Network In-House Checkout]] / [[Escrow Flow]] — completion of a purchase is the canonical trigger for awarding referral commission.
## Source files ## Source files

View File

@@ -7,7 +7,7 @@ related_apis: ["POST /api/marketplace/offers", "GET /api/marketplace/offers/requ
# Seller Offer Flow # Seller Offer Flow
A **seller** browses open purchase requests and submits an offer with a price, delivery time, and notes. The buyer is notified in real time and can accept (which moves the request to [[Payment Flow - SHKeeper]]) or reject. A **seller** browses open purchase requests and submits an offer with a price, delivery time, and notes. The buyer is notified in real time and can accept (which moves the request to [[PRD - Request Network In-House Checkout]]) or reject.
## Actors ## Actors
@@ -34,7 +34,7 @@ stateDiagram-v2
pending --> active: (optional — manual seller activation) pending --> active: (optional — manual seller activation)
pending --> withdrawn: seller withdraws (only while pending) pending --> withdrawn: seller withdraws (only while pending)
pending --> rejected: another offer accepted\nor buyer rejects this one pending --> rejected: another offer accepted\nor buyer rejects this one
pending --> accepted: acceptOffer()\nor SHKeeper PAID webhook pending --> accepted: acceptOffer()\nor payment confirmed
accepted --> [*] accepted --> [*]
rejected --> [*] rejected --> [*]
withdrawn --> [*] withdrawn --> [*]
@@ -79,14 +79,14 @@ The active enum values are `pending | accepted | rejected | withdrawn` (`SellerO
### Accept → Payment ### Accept → Payment
14. The buyer's "Pay this offer" button kicks off [[Payment Flow - SHKeeper]] with `purchaseRequestId` and `sellerOfferId`. The offer is **not** immediately marked `accepted`; the SHKeeper webhook does that atomically when the on-chain payment is confirmed. 14. The buyer's "Pay this offer" button kicks off [[PRD - Request Network In-House Checkout]] with `purchaseRequestId` and `sellerOfferId`. The offer is **not** immediately marked `accepted`; payment confirmation does that atomically when the on-chain payment is confirmed.
15. On `PAID`/`OVERPAID` webhook (see `backend/src/services/payment/shkeeper/shkeeperWebhook.ts:573-714`): 15. On Request Network payment confirmation:
- The selected offer's `status``accepted`. - The selected offer's `status``accepted`.
- All other offers on the same request → `rejected` via `SellerOffer.updateMany`. - All other offers on the same request → `rejected` via `SellerOffer.updateMany`.
- The purchase request: `status = "payment"`, `selectedOfferId = sellerOfferId`. - The purchase request: `status = "payment"`, `selectedOfferId = sellerOfferId`.
- A direct chat is created (see [[Chat Flow]]). - A direct chat is created (see [[Chat Flow]]).
- Notifications: `notifyOfferAccepted` to the winning seller, generic rejection notifications to the others (`SellerOfferService.acceptOffer` does the same in the manual path). - Notifications: `notifyOfferAccepted` to the winning seller, generic rejection notifications to the others (`SellerOfferService.acceptOffer` does the same in the manual path).
- Socket events: `seller-offer-update` `payment-completed` to the winner, `seller-offer-update` `offer-rejected` to losers (`shkeeperWebhook.ts:679-705`). - Socket events notify the winner and reject/close competing offers.
### Withdrawal ### Withdrawal
@@ -127,7 +127,7 @@ sequenceDiagram
BE-->>FE_B: offers BE-->>FE_B: offers
alt alt
B->>FE_B: Click pay to finish selected offer B->>FE_B: Click pay to finish selected offer
B->>FE_B: SHKeeper webhook handles payment result B->>FE_B: Request Network payment confirms
else else
B->>FE_B: Open chat to negotiate B->>FE_B: Open chat to negotiate
end end
@@ -171,7 +171,7 @@ sequenceDiagram
- **Price = 0 or negative** → Mongoose validator on `SellerOffer.price.amount` rejects (`SellerOfferService.ts:55-60` logs the validation state). - **Price = 0 or negative** → Mongoose validator on `SellerOffer.price.amount` rejects (`SellerOfferService.ts:55-60` logs the validation state).
- **Seller withdraws an `accepted` offer** → blocked by the `{ status: 'pending' }` filter; returns `null`. - **Seller withdraws an `accepted` offer** → blocked by the `{ status: 'pending' }` filter; returns `null`.
- **`validUntil` in the past at creation** → schema-level validator should reject; otherwise the next `markExpiredOffersAsWithdrawn` cron run flips it to `withdrawn`. - **`validUntil` in the past at creation** → schema-level validator should reject; otherwise the next `markExpiredOffersAsWithdrawn` cron run flips it to `withdrawn`.
- **Race condition: two payments to two different offers** → unlikely (frontend disables payment buttons once one is chosen); even if both arrive, the SHKeeper webhook coordinator (`PaymentCoordinator`) is idempotent and the first PAID wins. - **Race condition: two payments to two different offers** → unlikely (frontend disables payment buttons once one is chosen); even if both arrive, `PaymentCoordinator` and provider idempotency decide which confirmed payment wins.
- **Offer for a deleted request** → orphan; the webhook handler logs `"Purchase request not found"` and continues. Periodic cleanup should remove orphans. - **Offer for a deleted request** → orphan; the webhook handler logs `"Purchase request not found"` and continues. Periodic cleanup should remove orphans.
> [!tip] Real-time UX > [!tip] Real-time UX
@@ -181,7 +181,7 @@ sequenceDiagram
- [[Purchase Request Flow]] — produces the requests sellers offer on. - [[Purchase Request Flow]] — produces the requests sellers offer on.
- [[Negotiation Flow]] — counter-offer in `in_negotiation`. - [[Negotiation Flow]] — counter-offer in `in_negotiation`.
- [[Payment Flow - SHKeeper]] — locks in the accepted offer. - [[PRD - Request Network In-House Checkout]] — locks in the accepted offer.
- [[Chat Flow]] — direct chat opened after payment. - [[Chat Flow]] — direct chat opened after payment.
- [[Notification Flow]] — channels for offer events. - [[Notification Flow]] — channels for offer events.
- [[Rating Flow]] — seller's average rating displayed in the offer card. - [[Rating Flow]] — seller's average rating displayed in the offer card.
@@ -191,7 +191,7 @@ sequenceDiagram
- Backend: `backend/src/services/marketplace/SellerOfferService.ts` - Backend: `backend/src/services/marketplace/SellerOfferService.ts`
- Backend: `backend/src/services/marketplace/marketplaceController.ts` - Backend: `backend/src/services/marketplace/marketplaceController.ts`
- Backend: `backend/src/models/SellerOffer.ts` - Backend: `backend/src/models/SellerOffer.ts`
- Backend: `backend/src/services/payment/shkeeper/shkeeperWebhook.ts:573-714` (acceptance via webhook) - Backend: `backend/src/services/payment/paymentCoordinator.ts` (payment-state cascade)
- Frontend: `frontend/src/sections/request/components/seller-steps/step-1-send-proposal.tsx` - Frontend: `frontend/src/sections/request/components/seller-steps/step-1-send-proposal.tsx`
- Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-select-and-pay.tsx` - Frontend: `frontend/src/sections/request/components/buyer-steps/step-3-select-and-pay.tsx`
- Frontend: `frontend/src/app/dashboard/seller/marketplace/` - Frontend: `frontend/src/app/dashboard/seller/marketplace/`

View File

@@ -8,7 +8,7 @@ Default mode: optional. Existing release/refund flows do not require Trezor proo
- Generate a fresh receive address per user/payment from a registered Trezor xpub. - Generate a fresh receive address per user/payment from a registered Trezor xpub.
- Require a Trezor-produced signature before release/refund confirmation when safekeeping enforcement is enabled. - Require a Trezor-produced signature before release/refund confirmation when safekeeping enforcement is enabled.
- Keep SHKeeper and Request Network optional provider paths intact. - Keep the Request Network payment adapter and legacy provider abstractions intact while adding custody controls.
- Preserve the existing `Payment` model and orchestration surface. - Preserve the existing `Payment` model and orchestration surface.
## Registration ## Registration
@@ -95,7 +95,7 @@ When `TREZOR_SAFEKEEPING_REQUIRED=true`, `confirmReleaseRefundInstruction` verif
TREZOR_SAFEKEEPING_REQUIRED=false TREZOR_SAFEKEEPING_REQUIRED=false
``` ```
Default is permissive so existing SHKeeper and Request Network flows continue to work. Set it to `true` only after registering the operating admin's Trezor account and testing the signing path. Any value other than the literal string `true` is treated as disabled. Default is permissive so existing Request Network release/refund flows continue to work. Set it to `true` only after registering the operating admin's Trezor account and testing the signing path. Any value other than the literal string `true` is treated as disabled.
## Safety Rules ## Safety Rules
@@ -108,7 +108,7 @@ Default is permissive so existing SHKeeper and Request Network flows continue to
## Upgrade Path To Multisig ## Upgrade Path To Multisig
The current design stores a single `trezor-eoa` signer. Later, replace the signer policy with: The current design stores a single `trezor-eoa` signer. The recommended production path is to replace the signer policy with:
- `addressType: safe-multisig` - `addressType: safe-multisig`
- a Safe address per tenant/admin group - a Safe address per tenant/admin group
@@ -116,4 +116,4 @@ The current design stores a single `trezor-eoa` signer. Later, replace the signe
- Trezor owners as Safe signers - Trezor owners as Safe signers
- release/refund flow creates a Safe transaction and records collected signatures before execution - release/refund flow creates a Safe transaction and records collected signatures before execution
The payment orchestration API should stay the same: build instruction, collect hardware-backed approval, confirm release/refund, append ledger entry. The payment orchestration API should stay the same: build instruction, collect hardware-backed approval, confirm release/refund, append ledger entry. See [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] for the staged Safe-first path before any custom escrow contract.

View File

@@ -190,9 +190,9 @@ If you see repeat disputes against the same seller (or repeat frivolous disputes
**Dashboard → Payment → List** shows all payments with filters by status, provider, network, time range. **Dashboard → Payment → List** shows all payments with filters by status, provider, network, time range.
Watch for: Watch for:
- **Stuck payments** (pending > 1h) — SHKeeper webhook may have failed; check logs. - **Stuck payments** (pending > 1h) — Request Network webhook/reconciliation may not have completed; check webhook logs and derived-destination balances.
- **Failed webhooks** — SHKeeper retried but signature didn't verify; see [[Payment API]]. - **Failed webhooks** — Request Network signature verification or payload validation failed; see [[Payment API]] and [[Request Network Integration Constraints]].
- **Missing tx hashes** on completed payments — run the repair script (see §6.3). - **Missing tx hashes** on completed payments — use the payment console or reconciliation job to fetch and verify the on-chain transaction before any release.
### 6.2 Manual payout ### 6.2 Manual payout
@@ -202,18 +202,18 @@ For sellers who can't access self-service or for one-off ops:
2. Fields: recipient address, amount, token (USDT…), network (BSC…), reference, description. 2. Fields: recipient address, amount, token (USDT…), network (BSC…), reference, description.
3. Submit → ts-node script also exists at `backend/manual-payout-test.ts` for local testing. 3. Submit → ts-node script also exists at `backend/manual-payout-test.ts` for local testing.
Behind the scenes this calls SHKeeper's payout endpoint. See [[Payout Flow]]. Behind the scenes this should create a release/refund instruction and ledger entry, then route signing through the configured custody signer. See [[Payout Flow]] and [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]].
### 6.3 Fix missing transaction hashes ### 6.3 Fix missing transaction hashes
Some completed payments may lack the on-chain tx hash (webhook race, partial confirmation). Run: Some completed payments may lack the on-chain tx hash (webhook race, callback delay, or partial reconciliation). Prefer the admin payment console or Request Network reconciliation tooling. For older SHKeeper records only, use the historical repair script:
```bash ```bash
cd /Users/mojtabaheidari/code/backend cd /Users/mojtabaheidari/code/backend
node fix-transaction-hashes.js node fix-transaction-hashes.js
``` ```
The script polls SHKeeper for each affected invoice and patches `transactionHash` + `blockchain.transactionHash` in MongoDB. The legacy script polls SHKeeper for each affected historical invoice and patches `transactionHash` + `blockchain.transactionHash` in MongoDB. Do not use it for new Request Network payments.
See [[Scripts]] for the full inventory. See [[Scripts]] for the full inventory.

View File

@@ -304,7 +304,7 @@ Bookmark these for instant reference:
- [[Seller Guide]] — common Seller questions - [[Seller Guide]] — common Seller questions
- [[Glossary]] — terminology reference - [[Glossary]] — terminology reference
- [[Authentication Flow]] · [[Password Reset Flow]] · [[Passkey (WebAuthn) Flow]] — how auth actually works - [[Authentication Flow]] · [[Password Reset Flow]] · [[Passkey (WebAuthn) Flow]] — how auth actually works
- [[Payment Flow - SHKeeper]] · [[Payment Flow - DePay & Web3]] — how payments flow - [[Escrow Flow]] · [[Request Network Integration Constraints]] · [[Payout Flow]] — how payments flow
- [[Dispute Flow]] — when refund requests need to go to dispute - [[Dispute Flow]] — when refund requests need to go to dispute
- [[Notification Flow]] — why a user might not have received an email - [[Notification Flow]] — why a user might not have received an email
- [[Error Codes]] — interpret HTTP errors / app-specific codes the user reports - [[Error Codes]] — interpret HTTP errors / app-specific codes the user reports

View File

@@ -13,7 +13,7 @@ Runbooks for the most likely production incidents, plus communication templates
| Sev | Meaning | Response time | Examples | | Sev | Meaning | Response time | Examples |
|-----|---------|---------------|----------| |-----|---------|---------------|----------|
| **Sev 1** | Site fully down or unable to process payments | 15 min | Backend container in crashloop; Mongo unreachable; SHKeeper API permanently failing | | **Sev 1** | Site fully down or unable to process payments | 15 min | Backend container in crashloop; Mongo unreachable; Request Network API/webhooks failing |
| **Sev 2** | Major feature broken for a large share of users | 1 hour | Email sending broken; Redis disk full; chat undelivered | | **Sev 2** | Major feature broken for a large share of users | 1 hour | Email sending broken; Redis disk full; chat undelivered |
| **Sev 3** | Minor / cosmetic issue, isolated user reports | next business day | Single failed webhook; one user can't upload PDF | | **Sev 3** | Minor / cosmetic issue, isolated user reports | next business day | Single failed webhook; one user can't upload PDF |
| **Sev 4** | No user impact, hygiene item | backlog | Backup older than 24h; disk > 80%; missed deploy | | **Sev 4** | No user impact, hygiene item | backlog | Backup older than 24h; disk > 80%; missed deploy |
@@ -133,35 +133,34 @@ The app gracefully degrades when Redis is unreachable for short windows — don'
--- ---
### 3.4 SHKeeper API down (payments blocked) ### 3.4 Request Network API/webhook down (payments degraded)
**Symptoms.** Backend logs show repeated `SHKeeper request failed: ECONNREFUSED` or non-2xx responses from `$SHKEEPER_API_URL`. Buyers see "Payment unavailable" in checkout. Sev 1 — money is involved. **Symptoms.** Backend logs show repeated Request Network API failures, webhook delivery failures, or payments stuck in pending/safety-pending. Buyers see "Payment unavailable" or a checkout that never confirms. Sev 1 — money is involved.
**Runbook.** **Runbook.**
```bash ```bash
# 1. Confirm SHKeeper itself is reachable # 1. Confirm Request Network API is reachable
curl -fsS -H "X-Shkeeper-Api-Key: $SHKEEPER_API_KEY" \ curl -fsS -H "Authorization: Bearer $REQUEST_NETWORK_API_KEY" \
"$SHKEEPER_API_URL/api/v1/healthcheck" "$REQUEST_NETWORK_API_BASE_URL"
# 2. If 5xx from SHKeeper → it's their side # 2. If 5xx from Request Network -> provider/API side
# - Check their status page / contact provider # - Check their status page / contact provider
# - Toggle a banner in the frontend warning buyers # - Toggle a banner in the frontend warning buyers
# - Consider switching SHKEEPER_FORCE_PAYOUT_DEMO=true so QA still works # - Pause new checkout creation if confirmations cannot be reconciled
# (do NOT do this for real customer money)
# 3. If our network can't reach it: # 3. If our network can't reach it:
# - test from the host: curl from the host vs from inside the container # - test from the host: curl from the host vs from inside the container
docker exec nickapp-backend curl -v "$SHKEEPER_API_URL" docker exec nickapp-backend curl -v "$REQUEST_NETWORK_API_BASE_URL"
# - DNS / firewall changes? # - DNS / firewall changes?
# 4. While blocked, monitor stuck payments # 4. While blocked, monitor stuck payments
docker exec nickapp-mongodb mongosh --eval \ docker exec nickapp-mongodb mongosh --eval \
"use marketplace; db.payments.find({status:'pending', createdAt:{\$lt: new Date(Date.now() - 30*60*1000)}}).count()" "use marketplace; db.payments.find({status:'pending', createdAt:{\$lt: new Date(Date.now() - 30*60*1000)}}).count()"
# 5. Once SHKeeper is back, the app retries automatically. Verify the # 5. Once Request Network/webhook delivery is back, replay or reconcile
# backlog drains. If a payment is stuck > 24h, manually verify against # pending events. If a payment is stuck > 24h, manually verify the
# SHKeeper and use fix-transaction-hashes.js if needed. # on-chain transfer, Transaction Safety Provider result, and ledger state.
``` ```
**Always communicate.** Even short payment outages erode trust — post a status update. **Always communicate.** Even short payment outages erode trust — post a status update.
@@ -422,7 +421,7 @@ Store postmortems alongside this vault — suggested path `/Users/mojtabaheidari
| Payments lead | <name> | <name> | DM | | Payments lead | <name> | <name> | DM |
| Infrastructure | <name> | <name> | DM | | Infrastructure | <name> | <name> | DM |
| Product / customer comms | <name> | <name> | #customer-comms | | Product / customer comms | <name> | <name> | #customer-comms |
| SHKeeper provider contact | <email> | — | email | | Request Network/provider contact | <email> | — | email |
| SMTP provider | <email> | — | email | | SMTP provider | <email> | — | email |
--- ---

View File

@@ -134,7 +134,7 @@ Notable log lines to look for:
| `🚀 Server running on port 5001` | App fully started | | `🚀 Server running on port 5001` | App fully started |
| `🔌 User connected: <id>` | Socket.IO connection | | `🔌 User connected: <id>` | Socket.IO connection |
| `📥` | Inbound HTTP request log | | `📥` | Inbound HTTP request log |
| `💳 SHKeeper` | SHKeeper webhook / API call | | `💳 Request Network` | Request Network webhook / API call |
| `🔐 Webhook verification` | Webhook signature check result | | `🔐 Webhook verification` | Webhook signature check result |
| `❌ Error` | Manual error log (also captured by Sentry) | | `❌ Error` | Manual error log (also captured by Sentry) |
@@ -183,7 +183,7 @@ Today these are read manually from logs / Sentry. As Prometheus is added, encode
| Webhook signature failures | log `Webhook verification failed` | 0 | > 0 | | Webhook signature failures | log `Webhook verification failed` | 0 | > 0 |
| Request Network webhook 4xx | nginx access log `/api/payment/request-network/webhook` | 0 | any real provider delivery returning 4xx | | Request Network webhook 4xx | nginx access log `/api/payment/request-network/webhook` | 0 | any real provider delivery returning 4xx |
| Request Network safety-pending payments | `db.payments.find({"metadata.transactionSafety.status":"pending"})` | explained/short-lived | pending > 10 min without operator note | | Request Network safety-pending payments | `db.payments.find({"metadata.transactionSafety.status":"pending"})` | explained/short-lived | pending > 10 min without operator note |
| SHKeeper API errors (5xx) | log + Sentry | 0 | > 5/min sustained | | Request Network API errors (5xx) | log + Sentry | 0 | > 5/min sustained |
| Payouts stuck in `pending` > 30 min | `db.payments.find({type:'payout',status:'pending',createdAt:{$lt:ISODate(30 min ago)}})` | empty | non-empty | | Payouts stuck in `pending` > 30 min | `db.payments.find({type:'payout',status:'pending',createdAt:{$lt:ISODate(30 min ago)}})` | empty | non-empty |
| Missing `transactionHash` after `completed` | the same query that drives `fix-transaction-hashes.js` | empty | non-empty | | Missing `transactionHash` after `completed` | the same query that drives `fix-transaction-hashes.js` | empty | non-empty |

View File

@@ -10,7 +10,7 @@ Date: 2026-05-24
Scope: Scope:
- Task 3 provider-neutral payment migration. - Task 3 provider-neutral payment migration.
- Request Network optional pay-in, webhook, and reconciliation support. - Request Network primary pay-in, webhook, and reconciliation support.
- Internal funds ledger and release/refund ledger gates. - Internal funds ledger and release/refund ledger gates.
- Optional Trezor safekeeping support. - Optional Trezor safekeeping support.
@@ -84,7 +84,7 @@ Before enabling Request Network for a non-test cohort:
2. Run backend typecheck. 2. Run backend typecheck.
3. Test one Request Network sandbox pay-in with webhook callback. 3. Test one Request Network sandbox pay-in with webhook callback.
4. Confirm reconciliation dry-run output is empty or expected. 4. Confirm reconciliation dry-run output is empty or expected.
5. Keep `PAYMENT_ROLLBACK_PROVIDER=shkeeper`. 5. Keep the Request Network rollback/support runbook current; SHKeeper is historical context, not the current primary rollback target.
Before enabling Trezor safekeeping enforcement: Before enabling Trezor safekeeping enforcement:

View File

@@ -11,6 +11,18 @@ entries on top. Maintained by agents per the rule in `../AGENTS.md`.
--- ---
### 2026-05-28 — backend@19f7eb9, frontend@60ee6fb — Task #10: AML screening (Chainalysis, seller-paid, seller opt-in)
**Commits:** backend `441c8be``80ba046``19f7eb9` (2.6.46 → 2.6.47), frontend `717d5c8``b7540f5``60ee6fb` (2.6.46 → 2.6.47)
**Touched:**
- Backend: `src/services/payment/safety/amlProvider.ts`, `src/services/payment/safety/chainalysisProvider.ts`, `src/services/payment/safety/amlScreeningService.ts`, `src/services/payment/safety/transactionSafetyProvider.ts`, `src/services/payment/paymentCoordinator.ts`, `src/services/admin/amlConfigRoutes.ts`, `src/models/SellerOffer.ts`, `src/app.ts`, `.env.example`
- Frontend: `src/sections/request/components/seller-steps/step-1-send-proposal.tsx`, `src/types/marketplace.ts`
**Why:** Task #10 implementation. Chainalysis Public Sanctions API integration for seller-paid AML screening. Seller can opt-in per-offer via `requireAmlCheck` + `amlBlockOnFailure` toggles. `TransactionSafetyProvider` screens buyer source address after on-chain transfer verification. `paymentCoordinator` deducts `AML_CHECK_COST_USD` (default 0, API is free) from seller escrow on payment completion. Admin routes for AML config.
**Verification:** Frontend `tsc --noEmit` clean. Backend relevant tests pass (module resolution issues in unrelated test files).
**Linked docs updated:** [[02 - Data Models/SellerOffer]], [[03 - API Reference/Admin API]], [[04 - Flows/Escrow Flow]]
---
### 2026-05-28 — backend@441c8be, frontend@717d5c8 — Task #9: Per-chain confirmation thresholds + admin UI ### 2026-05-28 — backend@441c8be, frontend@717d5c8 — Task #9: Per-chain confirmation thresholds + admin UI
**Commits:** backend `4a85737``441c8be` (2.6.47 → 2.6.48), frontend `0ebb2f1``717d5c8` (2.6.46 → 2.6.48) **Commits:** backend `4a85737``441c8be` (2.6.47 → 2.6.48), frontend `0ebb2f1``717d5c8` (2.6.46 → 2.6.48)

View File

@@ -0,0 +1,195 @@
---
title: PRD - Decentralized Custody and Smart-Contract Escrow Roadmap
tags: [prd, escrow, custody, smart-contracts, governance, payments, roadmap]
created: 2026-05-28
status: draft-for-review
owner: payments + security + operations
---
# PRD - Decentralized Custody and Smart-Contract Escrow Roadmap
## Executive decision
Do **not** move the whole Amanat escrow flow into a custom smart contract as the next step.
The current architecture already uses on-chain settlement through Request Network, in-house wallet checkout, derived destination wallets, Transaction Safety Provider checks, and an internal funds ledger. The bigger risk is not "lack of blockchain"; it is **custody and administration centralization**: one backend/admin path can still become too powerful if hot keys, release/refund confirmation, sweep authority, and dispute decisions are not split across independent signers and delayed controls.
Recommended path:
1. Harden the current hybrid escrow.
2. Move custody to multisig/hardware signers.
3. Add timelocked governance for global settings.
4. Pilot a minimal on-chain escrow only for opt-in/high-value flows after the above is stable.
## Current baseline
| Area | Current state | Risk |
|---|---|---|
| Pay-in | Request Network in-house checkout, direct wallet txs, signed webhooks, transaction-safety checks | Still depends on webhook durability and reconciliation |
| Funds tracking | `FundsLedgerEntry` exists and is append-only | Release/refund ledger enforcement is env-gated by `PAYMENT_LEDGER_ENFORCEMENT` |
| Deposit routing | Derived destinations per `(buyer, sellerOffer, chainId)` | Live divergent-destination probe still needs production-grade evidence |
| Sweep custody | Build-only / hot-key signer abstraction | Production must avoid hot-key signing |
| Release/refund | Admin endpoints build and confirm instructions | Needs mandatory multisig/hardware proof and stronger dispute gating |
| Disputes | Model/service/routes exist with legacy status enum | Needs alignment with canonical dispute/escrow state machine |
| Admin control | App RBAC plus optional Trezor proof | No non-centralized admin quorum yet |
## Goals
- Remove single-admin and backend-hot-key custody risk.
- Make release/refund/sweep authority require independent signers.
- Preserve the marketplace's human dispute workflow.
- Keep buyer UX close to the current Request Network in-house checkout.
- Add on-chain escrow only where it creates trust benefit larger than audit and UX cost.
## Non-goals
- No token-voting DAO for individual buyer/seller disputes.
- No full rewrite of payments into Solidity before ledger, signing, and webhook controls are stable.
- No custom bridge, cross-chain settlement contract, or generalized DeFi protocol.
- No removal of the internal funds ledger; the ledger remains the application accounting source even if custody moves on-chain.
## Target trust model
```mermaid
flowchart LR
Buyer["Buyer wallet"] --> PayIn["Request Network / in-house checkout"]
PayIn --> Dest["Per-payment derived destination"]
Dest --> Safety["Transaction Safety Provider\nconfirmations + token/recipient/amount + AML"]
Safety --> Ledger["Internal append-only funds ledger"]
Ledger --> Policy["Release/refund policy engine"]
Policy --> Safe["Safe multisig custody"]
Safe --> Seller["Seller wallet"]
Safe --> BuyerRefund["Buyer refund wallet"]
Admin["Admin UI"] --> Policy
Arb["Arbitrator quorum"] --> Safe
Guardian["Guardian"] --> Pause["Pause / cancel dangerous ops"]
Timelock["Timelock / AccessManager"] --> Policy
```
## Phase 0 - Stabilize The Hybrid Escrow
**Timebox:** 1-2 weeks
**Purpose:** Make the current system safe enough that decentralizing custody does not hide application bugs under contract ceremony.
| Work | Owner | Exit criteria |
|---|---|---|
| Turn on `PAYMENT_LEDGER_ENFORCEMENT=true` in dev, then staging | Backend | Release/refund cannot exceed ledger available balance |
| Backfill/verify ledger entries for active Request Network payments | Backend | Reconciliation report has no unexplained funded payments without ledger rows |
| Enforce dispute hold in every release/refund path | Backend | Opening a dispute blocks release and refund until explicit resolution/override |
| Require Trezor proof in staging with `TREZOR_SAFEKEEPING_REQUIRED=true` | Backend + frontend | Release/refund without proof is rejected; valid Trezor proof succeeds |
| Add audit entries for release/refund/sweep instruction build and confirm | Backend | Each operation records actor, before/after state, tx hash, signer, and reason |
| Complete RN durable webhook ingress design | Platform | Worker storage/replay design approved; backend remains the trust oracle |
**Decision gate:** no Safe migration until release/refund is ledger-gated and dispute-gated in staging.
## Phase 1 - Move Custody To Safe Multisig
**Timebox:** 2-4 weeks
**Purpose:** Remove single-key custody without changing core escrow semantics.
| Work | Owner | Exit criteria |
|---|---|---|
| Create Safe accounts per supported chain | Ops + security | 2-of-3 minimum for dev/staging, 3-of-5 preferred for production |
| Register hardware-backed owners | Ops | At least two owners use Trezor or equivalent hardware wallets |
| Route release/refund/sweep builds to Safe transaction proposals | Backend + frontend | Admin UI builds a Safe transaction instead of direct hot-key tx |
| Confirm Safe execution before ledger release/refund append | Backend | Ledger terminal entry requires verified Safe tx hash |
| Remove production hot-key sweep mode | Ops | `DERIVED_DESTINATION_SWEEP_SIGNER=build-only` in production |
| Add break-glass policy | Security | Time-limited, alarmed, documented; cannot silently bypass quorum |
**Administration model:** admins can propose, but custody owners execute. A compromised app admin cannot move funds alone.
## Phase 2 - Durable Payment Evidence And Quarantine
**Timebox:** parallel with Phase 1
**Purpose:** Make payment evidence durable and make tainted-funds isolation real.
| Work | Owner | Exit criteria |
|---|---|---|
| Cloudflare Worker receives RN webhooks first | Platform | Raw body, headers, delivery ID, payment reference, timestamp durably stored |
| Replay endpoint/tool for stored webhook deliveries | Backend + ops | Operator can replay by delivery ID/time window/payment reference |
| AML provider behind Transaction Safety Provider | Backend + compliance | `clean` allows funding; sanctions/mixer verdict blocks or quarantines |
| Derived destination quarantine workflow | Backend + admin UI | Failed AML or transfer mismatch prevents sweep into treasury/Safe |
| Live divergent-destination probe | Payments | Two real paid intents to two derived addresses both complete and reconcile |
**Decision gate:** no custom escrow contract until webhook replay and per-payment quarantine work operationally.
## Phase 3 - Decentralize Administrative Control
**Timebox:** 4-6 weeks
**Purpose:** Split operational permissions without making daily support impossible.
| Control | Recommended design |
|---|---|
| Custody movement | Safe multisig threshold |
| Global escrow settings | Timelock or OpenZeppelin AccessManager-managed roles |
| Contract upgrades, if any | Timelocked multisig, no instant admin upgrade |
| Emergency pause | Guardian role can pause, not withdraw |
| Dispute financial outcome | App records decision; Safe quorum executes release/refund/split |
| Confirmation thresholds | App admin can propose; high-risk decreases require timelock or second approval |
| Break-glass | Time-limited, high-severity alert, postmortem required |
Use token/DAO voting only for protocol-level parameters if the platform later has a real governance community. Do not use broad token voting for individual disputes; it leaks private commercial facts and is slow/manipulable.
## Phase 4 - Minimal Smart-Contract Escrow Pilot
**Timebox:** 6-10 weeks after Phases 0-3
**Purpose:** Test whether on-chain escrow improves trust enough for a specific cohort.
Pilot only when one of these is true:
- Average escrow value is high enough that users ask for contract custody.
- Sellers/buyers explicitly demand on-chain proof that funds cannot be swept.
- The platform wants a premium "contract escrow" mode.
- Regulated partners require provable segregation of funds.
Minimal contract shape:
| Function | Notes |
|---|---|
| `fund(orderId, token, amount, buyer, seller, deadline)` | Buyer funds ERC-20 escrow; order ID is app-generated and hashed |
| `confirmDelivery(orderId)` | Buyer can release without admin |
| `openDispute(orderId)` | Either party can pause auto-release before deadline |
| `resolve(orderId, releaseAmount, refundAmount, reasonHash)` | Arbitrator/multisig executes split |
| `claimAfterTimeout(orderId)` | Seller can claim after timeout if no dispute |
| `refundAfterExpiry(orderId)` | Buyer can recover if seller never starts/accepts |
| `pause()` / `unpause()` | Guardian pause only; no fund extraction |
Security requirements:
- Use audited libraries for `SafeERC20`, reentrancy protection, pausing, and access control.
- Avoid upgradeability unless the timelock and upgrade policy are production-ready.
- External audit before mainnet funds.
- Formal state-transition tests mirroring [[Funds Ledger and Escrow State Machine Specification]].
- Fuzz tests for double release, split math, fee rounding, pausing, and timeout races.
## Phase 5 - Go / No-Go Criteria
Proceed from hybrid multisig custody to custom escrow only if all are true:
- Safe/Trezor flow has processed real releases/refunds without operational pain.
- Ledger enforcement has run for at least one complete payment cycle.
- Dispute hold cannot be bypassed in tests or manual review.
- Durable webhook ingress and replay are live.
- Per-payment destination quarantine is live.
- Contract audit budget and maintenance owner are approved.
- The user trust/compliance benefit is explicitly documented.
If these are not true, continue improving the hybrid model. A contract that encodes immature off-chain policy will make the system harder to fix, not safer.
## Stale documentation corrected in this pass
This roadmap was created together with a focused documentation alignment pass:
- [[System Overview]] now reflects Request Network as the primary payment rail, derived destinations, ledger, and existing dispute service.
- [[System Architecture]] now shows Request Network webhooks and durable ingress as the target, instead of SHKeeper-only webhook flow.
- [[Backend Architecture]] no longer lists removed SHKeeper service folders/routes as the current module map.
- [[Escrow Flow]] now reflects hybrid custody, ledger gates, derived destinations, and the recommended multisig-before-contract direction.
- [[Dispute Flow]] no longer says the backend dispute service/model/routes do not exist.
- [[Request Network Integration Constraints]] now marks the in-house checkout and derived-destination work as implemented/partially implemented rather than only designed.
## External references
- Safe threshold custody model: <https://docs.safe.global/advanced/smart-account-concepts>
- OpenZeppelin AccessManager and timelock guidance: <https://docs.openzeppelin.com/contracts/5.x/access-control>
- Request Network payment contracts overview: <https://docs.request.network/advanced/protocol-overview/contracts>

View File

@@ -46,6 +46,8 @@ How the system is composed at every layer.
- [[System Architecture]] — end-to-end topology + request lifecycle - [[System Architecture]] — end-to-end topology + request lifecycle
- [[Backend Architecture]] — Express 5 + Mongoose + Socket.IO module map - [[Backend Architecture]] — Express 5 + Mongoose + Socket.IO module map
- [[Frontend Architecture]] — Next.js 16 App Router + provider tree - [[Frontend Architecture]] — Next.js 16 App Router + provider tree
- [[Request Network Integration Constraints]] — current RN integration constraints and rollout gates
- [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] — custody decentralization and smart-contract decision roadmap
- [[Infrastructure]] — Docker images, compose stacks, registry, Watchtower - [[Infrastructure]] — Docker images, compose stacks, registry, Watchtower
- [[Real-time Layer]] — Socket.IO rooms, events, scaling notes - [[Real-time Layer]] — Socket.IO rooms, events, scaling notes
- [[Security Architecture]] — auth layers, RBAC, HMAC, hardening checklist - [[Security Architecture]] — auth layers, RBAC, HMAC, hardening checklist
@@ -88,7 +90,7 @@ End-to-end narratives for every user-visible interaction, with Mermaid sequence/
- [[Purchase Request Flow]] · [[Seller Offer Flow]] · [[Negotiation Flow]] - [[Purchase Request Flow]] · [[Seller Offer Flow]] · [[Negotiation Flow]]
**Money** **Money**
- [[Payment Flow - SHKeeper]] · [[Payment Flow - DePay & Web3]] · [[Escrow Flow]] · [[Payout Flow]] - [[PRD - Request Network In-House Checkout]] · [[Payment Flow - DePay & Web3]] · [[Escrow Flow]] · [[Payout Flow]] · [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]]
**Resolution** **Resolution**
- [[Dispute Flow]] · [[Delivery Confirmation Flow]] · [[Rating Flow]] - [[Dispute Flow]] · [[Delivery Confirmation Flow]] · [[Rating Flow]]
@@ -151,7 +153,8 @@ For engineers / SREs running the system in production.
| Topic | Start here | | Topic | Start here |
|---|---| |---|---|
| **Payments** | [[Payment Flow - SHKeeper]] → [[Payment API]] → [[Payment]] → [[Payout Flow]] | | **Payments** | [[PRD - Request Network In-House Checkout]] → [[Payment API]] → [[Payment]] → [[Payout Flow]] |
| **Custody / escrow strategy** | [[PRD - Decentralized Custody and Smart-Contract Escrow Roadmap]] → [[Escrow Flow]] → [[Funds Ledger and Escrow State Machine Specification]] |
| **Auth** | [[Authentication Flow]] → [[Authentication API]] → [[Security Architecture]] | | **Auth** | [[Authentication Flow]] → [[Authentication API]] → [[Security Architecture]] |
| **Backend security / refactor** | [[Backend Stack Security and Refactor Assessment - 2026-05-24]] → [[Platform Logical Audit - 2026-05-24]] → [[PRD - Platform Audit Remediation Plan (2026-05-24)]] | | **Backend security / refactor** | [[Backend Stack Security and Refactor Assessment - 2026-05-24]] → [[Platform Logical Audit - 2026-05-24]] → [[PRD - Platform Audit Remediation Plan (2026-05-24)]] |
| **Developer task queue** | `.taskmaster/README.md``.taskmaster/tasks/tasks.json` → root `PRD - *.md` files | | **Developer task queue** | `.taskmaster/README.md``.taskmaster/tasks/tasks.json` → root `PRD - *.md` files |
@@ -204,8 +207,8 @@ These are documented in their respective sections but worth highlighting:
> [!warning] > [!warning]
> - Backend rate-limit middleware is currently **disabled** (`backend/src/app.ts:227`). Enable before any public traffic — see [[Security Architecture]]. > - Backend rate-limit middleware is currently **disabled** (`backend/src/app.ts:227`). Enable before any public traffic — see [[Security Architecture]].
> - Passkey service is partly **stubbed** — see [[Passkey (WebAuthn) Flow]] for production-hardening checklist. > - Passkey service is partly **stubbed** — see [[Passkey (WebAuthn) Flow]] for production-hardening checklist.
> - Auto-release of escrow on delivery confirmation **not yet automated** — admin runs manual payouts. See [[Delivery Confirmation Flow]] + [[Payout Flow]]. > - Auto-release of escrow on delivery confirmation **not yet automated** — admin/custody operators run release flows. See [[Delivery Confirmation Flow]] + [[Payout Flow]].
> - Opening a dispute does **not pause** the escrow until admin intervention. See [[Dispute Flow]] + [[Escrow Flow]]. > - Dispute holds exist in code, but the Dispute model/docs still need full canonical state-machine alignment. See [[Dispute Flow]] + [[Escrow Flow]].
> - Several development env values committed as public — see [[Environment Variables]] for rotation list. > - Several development env values committed as public — see [[Environment Variables]] for rotation list.
> - Single-host deployment; horizontal scaling requires Redis adapter for Socket.IO — see [[Real-time Layer]] §8. > - Single-host deployment; horizontal scaling requires Redis adapter for Socket.IO — see [[Real-time Layer]] §8.
> - Request Network webhooks currently land on the main app. Roadmap: Cloudflare Worker durable ingress + replay, with backend Transaction Safety Provider checks before escrow is credited. See [[Request Network Integration Constraints]]. > - Request Network webhooks currently land on the main app. Roadmap: Cloudflare Worker durable ingress + replay, with backend Transaction Safety Provider checks before escrow is credited. See [[Request Network Integration Constraints]].