334 lines
17 KiB
Markdown
334 lines
17 KiB
Markdown
# PRD - Request Network Migration and Funds Management
|
|
|
|
Date: 2026-05-24
|
|
Status: Draft for architecture review
|
|
Owner: Platform / Payments
|
|
Codebase reviewed: `/Users/manwe/CascadeProjects/escrow`
|
|
|
|
## Executive Summary
|
|
|
|
Replacing SHKeeper with Request Network is not a provider swap. SHKeeper currently acts like a deposit-wallet and payout-task provider: the backend creates a payment intent, receives a generated wallet address, watches SHKeeper webhooks and BSC wallet balances, then optionally creates an outgoing payout task.
|
|
|
|
Request Network is request/payment-reference based. The platform creates a Request Network payment request or secure payment page, the payer pays through Request Network contracts or hosted flow, and reconciliation happens through Request Network payment events. Payouts are API-created payment requests/calldata that still require wallet execution. This means Amanat needs a first-class funds ledger and a release/refund orchestration layer, not just renamed `/payment/shkeeper/*` endpoints.
|
|
|
|
Primary recommendation: run a phased migration behind a provider adapter. Introduce Request Network for new pay-ins first using Secure Payment Pages, keep SHKeeper read-only for existing payments, then add funds ledger, release/refund gates, and Request Network payout flows.
|
|
|
|
## Source Findings
|
|
|
|
Current code:
|
|
|
|
- Backend mounts SHKeeper at `/api/payment/shkeeper` through `backend/src/services/payment/shkeeper/shkeeperRoutes.ts` and `shkeeperPayoutRoutes.ts`.
|
|
- Frontend calls SHKeeper through `frontend/src/actions/payment.ts`, `frontend/src/web3/components/shkeeper-payment.tsx`, `shkeeper-widget.tsx`, and `shkeeper-payout.tsx`.
|
|
- `Payment.provider` only allows `shkeeper` or `other`; SHKeeper-specific fields live directly under `Payment.metadata`.
|
|
- Pay-in completion is accepted from SHKeeper webhook, manual confirmation, or wallet monitor, all coordinated through `PaymentCoordinator`.
|
|
- Current payout paths mix SHKeeper payout tasks, simulated marketplace release, and direct admin wallet payout.
|
|
- There is no durable internal funds ledger that tracks available, held, releasable, disputed, released, refunded, and fee amounts per purchase request.
|
|
|
|
Request Network documentation checked:
|
|
|
|
- API v2 supports programmatic request creation, secure payment pages, payouts, webhooks, payment search, platform fees, and payee destinations: <https://docs.request.network/llms.txt>
|
|
- Secure Payment Pages are hosted payment URLs returned by `POST /v2/secure-payments`, returning `requestIds`, `token`, and `securePaymentUrl`: <https://beta.docs.request.network/api-features/secure-payment-pages>
|
|
- Secure Payment Pages validate official Request Network contracts before payer signing: <https://beta.docs.request.network/api-features/secure-payment-pages>
|
|
- Webhooks are scoped by Client ID and include `x-request-network-signature`, delivery ID, retry count, and optional test header: <https://requestnetwork.mintlify.app/api-reference/webhooks>
|
|
- Request Network webhooks retry failed deliveries up to 3 retries with short backoff windows: <https://requestnetwork.mintlify.app/api-reference/webhooks>
|
|
- Payout endpoints return unsigned transaction data that the platform/wallet must execute: <https://docs.request.network/request-network-api/recurring-payments>
|
|
- Batch payouts can return approval and batch payment transactions; the application sends those wallet transactions: <https://docs.request.network/request-network-api/batch-payments>
|
|
- Request Network protocol fee is 5 bps with stablecoin caps, and platform fees can be included through `feePercentage` and `feeAddress`: <https://docs.request.network/request-network-api/fees>
|
|
- Payment networks use payment references for detection, rather than unique deposit wallets per invoice: <https://docs.request.network/advanced/protocol-overview/how-payment-networks-work>
|
|
|
|
## Target Architecture
|
|
|
|
### Payment Provider Adapter
|
|
|
|
Create a provider-neutral payment interface:
|
|
|
|
- `createPayInIntent`
|
|
- `getPayInStatus`
|
|
- `handleProviderWebhook`
|
|
- `createHostedPaymentLink`
|
|
- `createReleaseInstruction`
|
|
- `createRefundInstruction`
|
|
- `getPayoutStatus`
|
|
- `searchProviderPayments`
|
|
|
|
Implement:
|
|
|
|
- `ShkeeperProvider` for legacy compatibility.
|
|
- `RequestNetworkProvider` for new flows.
|
|
|
|
### Funds Ledger
|
|
|
|
Add an internal ledger separate from provider metadata:
|
|
|
|
- `fundsAccount`: one per purchase request or order.
|
|
- `ledgerEntry`: immutable entries for authorization, payment detected, platform fee, provider fee, hold, release, refund, chargeback/dispute hold, adjustment.
|
|
- `fundsBalance`: derived or cached view containing `expected`, `paid`, `held`, `releasable`, `released`, `refunded`, `disputed`, `fees`, and `available`.
|
|
|
|
Ledger invariants:
|
|
|
|
- Seller payout cannot exceed paid amount minus refunds, fees, and active dispute holds.
|
|
- Release requires payment status `completed`, escrow state `releasable`, no open dispute, and a verified seller destination.
|
|
- Refund requires available held balance and must mark the purchase request/payment state consistently.
|
|
- Provider webhooks never directly mark funds as released; they create/reconcile ledger entries.
|
|
|
|
### Payment Flow
|
|
|
|
Pay-in:
|
|
|
|
1. Buyer accepts seller offer.
|
|
2. Backend creates internal payment + funds account.
|
|
3. Backend calls Request Network `POST /v2/secure-payments` or `POST /v2/request`.
|
|
4. Frontend redirects to `securePaymentUrl` or executes returned calldata in wallet.
|
|
5. Request Network webhook confirms payment.
|
|
6. Backend verifies signature, idempotency, amount, request ID, payment reference, currency, and merchant reference.
|
|
7. Ledger moves from `expected` to `held`.
|
|
8. Purchase request enters `payment` or `processing`.
|
|
|
|
Release:
|
|
|
|
1. Buyer delivery confirmation or admin release marks funds `releasable`.
|
|
2. Backend creates Request Network payout/release instruction, or admin wallet transaction instruction.
|
|
3. Admin/operator wallet signs and broadcasts transaction.
|
|
4. Backend records transaction hash as `release_pending`.
|
|
5. Webhook/search confirms settlement.
|
|
6. Ledger moves `held -> released`.
|
|
|
|
Refund:
|
|
|
|
1. Refund request validates available held balance.
|
|
2. Backend creates refund transaction instruction.
|
|
3. Admin/operator wallet executes.
|
|
4. Confirmation updates ledger `held -> refunded`.
|
|
|
|
## PRD 1 - Provider-Neutral Payment Adapter
|
|
|
|
Priority: P0
|
|
|
|
Goal: decouple checkout, webhook, and payout flows from SHKeeper-specific routes and metadata.
|
|
|
|
Scope:
|
|
|
|
- Add `paymentProvider` abstraction in backend.
|
|
- Add `provider: 'shkeeper' | 'request_network' | 'manual' | 'admin_wallet'` to payment model.
|
|
- Keep `/api/payment/shkeeper/*` operational for legacy records.
|
|
- Add `/api/payment/providers/request-network/*` or `/api/payment/request-network/*` for new Request Network flows.
|
|
- Add a feature flag: `PAYMENT_PROVIDER=request_network|shkeeper`.
|
|
|
|
Acceptance Criteria:
|
|
|
|
- New checkout can create a Request Network payment link without touching SHKeeper service code.
|
|
- Existing SHKeeper payments remain readable and can still process late webhooks.
|
|
- Frontend does not import provider-specific action names outside provider-specific components.
|
|
- Provider metadata is namespaced under `metadata.providers.requestNetwork` or a dedicated provider table.
|
|
|
|
Risk Assessment:
|
|
|
|
- Impact: High. Payment creation is core revenue flow.
|
|
- Likelihood: Medium. Existing code has many direct `shkeeper` references.
|
|
- Main risks: broken checkout, duplicate payment records, orphaned webhooks, mixed provider states.
|
|
- Mitigation: use provider feature flag, preserve SHKeeper routes, add idempotency keys, run Request Network only for new payments during rollout.
|
|
- Rollback: set `PAYMENT_PROVIDER=shkeeper`; keep Request Network records read-only.
|
|
|
|
## PRD 2 - Request Network Pay-In Integration
|
|
|
|
Priority: P0
|
|
|
|
Goal: replace generated SHKeeper wallet addresses with Request Network request/payment-reference based payment collection.
|
|
|
|
Scope:
|
|
|
|
- Store Request Network `requestId`, `paymentReference`, `securePaymentUrl`, `token`, `merchantReference`, `network`, `invoiceCurrency`, and `paymentCurrency`.
|
|
- Use Secure Payment Pages for first launch to reduce frontend wallet/calldata complexity.
|
|
- Include Amanat purchase request ID and payment ID as merchant reference or metadata.
|
|
- Validate supported networks/currencies before creating payment links.
|
|
- Remove frontend dependence on SHKeeper-generated `walletAddress` for new provider flow.
|
|
|
|
Acceptance Criteria:
|
|
|
|
- Buyer can start payment and receive a Request Network hosted payment URL.
|
|
- Backend records internal payment as `pending` with Request Network identifiers.
|
|
- Webhook `payment.confirmed` reconciles only the matching internal payment.
|
|
- Amount, currency, provider request ID, and merchant reference are verified before setting `escrowState='funded'`.
|
|
- Late or duplicate webhook deliveries are idempotent.
|
|
|
|
Risk Assessment:
|
|
|
|
- Impact: High.
|
|
- Likelihood: Medium.
|
|
- Main risks: currency/network mismatch, hosted redirect UX breakage, buyer pays wrong amount, missing webhook.
|
|
- Mitigation: backend-only creation, strict metadata correlation, periodic fallback reconciliation using Request Network payment search, idempotency by delivery ID and request ID.
|
|
- Rollback: disable Request Network provider for new payments; keep pending Request Network payments visible with manual support workflow.
|
|
|
|
## PRD 3 - Funds Ledger and Escrow State Machine
|
|
|
|
Priority: P0
|
|
|
|
Goal: introduce internal funds management so payment, hold, release, refund, dispute, and fee states are auditable and provider-independent.
|
|
|
|
Scope:
|
|
|
|
- Add `FundsAccount` model keyed by purchase request/payment.
|
|
- Add immutable `LedgerEntry` model with provider references and actor/source.
|
|
- Add derived `FundsBalance` query/service.
|
|
- Define state transitions: `expected -> held -> releasable -> releasing -> released`, plus `refunded`, `partially_refunded`, `disputed`, `failed`.
|
|
- Prevent release/refund from reading raw `Payment.status` alone.
|
|
|
|
Acceptance Criteria:
|
|
|
|
- Every successful pay-in creates a ledger entry for gross paid amount.
|
|
- Platform fee, Request Network protocol fee, and net seller amount are represented explicitly.
|
|
- Release API checks ledger invariants before creating payout/refund instructions.
|
|
- Dispute hold blocks payout until resolved.
|
|
- Admin UI/payment detail view can show gross paid, fees, held, releasable, released, refunded.
|
|
|
|
Risk Assessment:
|
|
|
|
- Impact: Very High.
|
|
- Likelihood: Medium.
|
|
- Main risks: double-release, incorrect fee accounting, drift between Payment and ledger state.
|
|
- Mitigation: immutable entries, unique idempotency keys, transaction/session writes where Mongo supports them, reconciliation job, read-only migration report before enforcing ledger.
|
|
- Rollback: keep old payment fields as source of truth until ledger backfill passes; feature flag release enforcement.
|
|
|
|
## PRD 4 - Request Network Webhook and Reconciliation Service
|
|
|
|
Priority: P1
|
|
|
|
Goal: replace SHKeeper webhook and wallet monitor semantics with signed Request Network event processing and periodic reconciliation.
|
|
|
|
Scope:
|
|
|
|
- Add `/api/payment/request-network/webhook`.
|
|
- Verify raw body using `x-request-network-signature`.
|
|
- Store delivery ID, retry count, event type, request ID, payment reference, payload hash, and processed status.
|
|
- Support test webhooks via `x-request-network-test`.
|
|
- Add scheduled reconciliation using Request Network payment search/status APIs.
|
|
- Retire SHKeeper wallet monitor for new Request Network payments.
|
|
|
|
Acceptance Criteria:
|
|
|
|
- Invalid signatures are rejected in all environments except explicit local test mode.
|
|
- Duplicate delivery IDs are acknowledged without duplicating ledger entries.
|
|
- `payment.confirmed` and partial/over/failed states map to internal status consistently.
|
|
- Reconciliation job can repair missed webhook state without relying on buyer/frontend callbacks.
|
|
|
|
Risk Assessment:
|
|
|
|
- Impact: High.
|
|
- Likelihood: Medium.
|
|
- Main risks: raw body parsing mismatch, webhook downtime, event mapping mistakes.
|
|
- Mitigation: raw-body middleware scoped to webhook route, event fixture tests, dead-letter queue/table, operator replay endpoint.
|
|
- Rollback: pause webhook processor and rely on manual/admin reconciliation for Request Network records.
|
|
|
|
## PRD 5 - Release, Refund, and Payout Orchestration
|
|
|
|
Priority: P1
|
|
|
|
Goal: replace SHKeeper payout tasks and simulated marketplace release with auditable transaction instruction and confirmation flows.
|
|
|
|
Scope:
|
|
|
|
- Define release/refund service that consumes ledger balances, not raw request status.
|
|
- Generate Request Network payout instructions or direct admin wallet transaction instructions.
|
|
- Store unsigned transaction payloads, signer, submitted tx hash, confirmation status, and provider status.
|
|
- Add batch payout support as a later optimization after single release is stable.
|
|
- Require admin/operator authorization and dispute checks before instruction creation.
|
|
|
|
Acceptance Criteria:
|
|
|
|
- Seller payout requires verified seller wallet/payee destination.
|
|
- Release cannot occur if funds are unpaid, already released, refunded, or disputed.
|
|
- Transaction hash confirmation updates ledger only once.
|
|
- Admin can see pending release instructions and retry/cancel safely.
|
|
- Existing `/purchase-requests/:id/release-payment` simulated path is removed or hidden behind dev-only mode.
|
|
|
|
Risk Assessment:
|
|
|
|
- Impact: Very High.
|
|
- Likelihood: High.
|
|
- Main risks: Request Network payouts are not custodial magic; operator wallet still signs transactions, so process failure can leave funds pending.
|
|
- Mitigation: explicit release queue, two-step admin approval, signer audit trail, reconciliation against tx hash/provider status.
|
|
- Rollback: keep direct admin wallet payout as emergency fallback with mandatory ledger entry and reason code.
|
|
|
|
## PRD 6 - Frontend Checkout and Admin Migration
|
|
|
|
Priority: P1
|
|
|
|
Goal: update buyer payment, admin release, seller payout, and payment details UI for Request Network flows.
|
|
|
|
Scope:
|
|
|
|
- Replace `ShkeeperPayment` with provider-neutral `CryptoPayment`.
|
|
- Add `RequestNetworkPayment` redirect flow for secure payment pages.
|
|
- Keep `ShkeeperPayment` available only for legacy/unpaid SHKeeper records if needed.
|
|
- Replace `ShkeeperPayout` with release queue/admin payout UI.
|
|
- Payment detail page shows provider, request ID, payment reference, hosted link, transaction hashes, ledger balances, webhook/reconciliation status.
|
|
|
|
Acceptance Criteria:
|
|
|
|
- Buyer checkout no longer expects provider-generated `walletAddress` for Request Network payments.
|
|
- Admin release UI displays available/releasable funds and blocks unsafe release states.
|
|
- Seller payout status comes from internal release instruction/ledger, not SHKeeper task polling.
|
|
- Legacy SHKeeper labels are not shown for Request Network payments.
|
|
|
|
Risk Assessment:
|
|
|
|
- Impact: Medium-High.
|
|
- Likelihood: Medium.
|
|
- Main risks: confusing redirect UX, old Persian labels referencing SHKeeper, admin accidentally using old payout button.
|
|
- Mitigation: provider-specific copy, feature flag UI sections, legacy badge, clear payment detail diagnostics.
|
|
- Rollback: route checkout back to SHKeeper component via provider flag.
|
|
|
|
## PRD 7 - Data Migration and Legacy Decommission
|
|
|
|
Priority: P2
|
|
|
|
Goal: safely migrate historical SHKeeper payment records and phase out provider-specific code.
|
|
|
|
Scope:
|
|
|
|
- Backfill provider metadata namespace for existing SHKeeper payments.
|
|
- Create ledger entries for completed SHKeeper payments from trusted records.
|
|
- Mark historical records as `legacyProvider='shkeeper'`.
|
|
- Keep SHKeeper webhook active during webhook tail period.
|
|
- Remove SHKeeper wallet monitor, simple auto webhook, test callback routes, and payout routes after cutoff.
|
|
|
|
Acceptance Criteria:
|
|
|
|
- Migration produces counts for total, migrated, skipped, ambiguous, and failed records.
|
|
- No historical completed payment loses transaction hash/provider invoice/task metadata.
|
|
- Legacy SHKeeper webhook can still reconcile existing pending records until cutoff date.
|
|
- Decommission checklist includes env vars, docs, frontend labels, backend routes, and runbooks.
|
|
|
|
Risk Assessment:
|
|
|
|
- Impact: Medium.
|
|
- Likelihood: Medium.
|
|
- Main risks: ambiguous historical records, template checkout string IDs, mixed status meanings.
|
|
- Mitigation: dry-run migration, manual review bucket, no destructive migration, keep original metadata.
|
|
- Rollback: ledger backfill is additive; old records remain intact.
|
|
|
|
## Implementation Sequence
|
|
|
|
1. Add provider adapter and Request Network config without changing default provider.
|
|
2. Add funds ledger models/services and dry-run backfill report.
|
|
3. Implement Request Network secure pay-in flow behind feature flag.
|
|
4. Implement signed webhook receiver and reconciliation job.
|
|
5. Enable Request Network for limited new checkout cohort.
|
|
6. Add release/refund orchestration using ledger gates.
|
|
7. Migrate admin/frontend views.
|
|
8. Backfill legacy records.
|
|
9. Decommission SHKeeper once no active records depend on it.
|
|
|
|
## Open Decisions
|
|
|
|
- Use Request Network Secure Payment Pages first, or execute Request Network calldata directly in the existing wallet modal?
|
|
- Keep funds in a platform-controlled escrow wallet, pay sellers via admin/operator release, or route pay-ins directly to seller with only platform fee collection?
|
|
- Which chains/currencies are required for launch: BSC USDT parity with today, or Request Network supported stablecoin routes first?
|
|
- Should platform fee be paid by buyer, seller, or absorbed by Amanat?
|
|
- Does Amanat need crypto-to-fiat/offramp later, which adds KYC/payment detail requirements?
|
|
|
|
## Recommendation
|
|
|
|
Start with Secure Payment Pages and a platform escrow/payee destination controlled by Amanat. This best matches the current escrow mental model while reducing frontend transaction-building risk. Do not route pay-ins directly to sellers until dispute handling, refund logic, and service fee economics are fully redesigned.
|
|
|