# PRD: Wallet, Multichain, Confirmations, AML, Trezor > Status: **Draft — 2026-05-28** > Author: nick + claude (after in-house RN checkout shipped on dev 2.6.38/2.6.41) > Owner: backend (payments) + frontend (admin UI + checkout) > Related: `PRD - Request Network In-House Checkout.md`, `01 - Architecture/Request Network Integration Constraints.md` Five follow-ups to the in-house Request Network checkout. They are sized so a single contributor can pick up any one of them in isolation. Each is its own Taskmaster top-level task — see `#7…#11`. --- ## 1. Per-(buyer, seller) ephemeral destination wallets — Task #7 ### Problem Today the in-house checkout sends *all* RN-routed payments to one Amanat-controlled wallet (env: `REQUEST_NETWORK_MERCHANT_REFERENCE`). That wallet is shared across every buyer, every seller, every offer. It's both an audit nightmare (no buyer↔settlement linkage at the wallet level) and a single point of compromise. ### Goal For each `(buyerId, sellerOfferId)` (or `(buyerId, sellerId)` — see open questions), generate a fresh on-chain destination address, persist it on the `Payment` record, and tell Request Network to expect funds on that address. RN's webhook flows unchanged. ### Hard-known facts (from RN docs we've cold-inspected so far) - The "destination" in RN is the `destinationId` inside the merchant reference: `
@eip155:#:`. RN doesn't bind this to an Amanat-level identity; it's just where the funds end up. - Each `POST /v2/secure-payments` request *can* pass a different `destinationId`. RN doesn't reject divergent destinations across requests from the same client. - `paymentReference` is derived per request (`last8Bytes(keccak256(requestId+salt+destination))`), so different destinations naturally produce different on-chain refs. The webhook listener keys on the ref + tx hash. ### Open questions to settle before code 1. **Key custody model.** Options: - **Deterministic HD wallet** rooted at one Amanat master seed; derive per-`(buyer, seller)` path (e.g. `m/44'/60'/0'//`). Keys live in the backend, single seed in KMS/HSM. Sweep is one tx per derived addr. - **One-shot disposable EOAs**, encrypted and stored in Mongo (or KMS), keyed by `(buyer, seller)`. Sweep then forget. - **Smart contract per offer** that auto-forwards to the master wallet on receive. Avoids holding keys at all, but costs gas + an extra hop. - Recommended starting point: HD wallet, with sweep-on-confirmation. Cheapest, most auditable. 2. **Sweep strategy.** Sweep immediately on webhook confirmation, or batch sweep cron'd nightly? Trade gas vs. exposure window. Default: sweep immediately under Transaction Safety Provider approval. 3. **Granularity.** Per `(buyer, seller)`, per `(buyer, seller, offer)`, or per single payment? Per-offer gives clean audit lineage; per-payment is overkill (extra derivations); per-`(buyer, seller)` is reusable across multi-step deals. 4. **Re-use vs. expire.** If a derived address has funds in it after sweep, do we still re-use for the same pair's next payment, or rotate? Re-use = simpler, slight privacy hit. ### Scope 1. New module `backend/src/services/payment/wallets/derivedDestinations.ts` with `getDestinationFor(buyerId, sellerOfferId)` returning `{ address, derivationPath, chainId }`. 2. Migration on `Payment` schema to add `metadata.derivedDestination` (address + derivation path snapshot). 3. RN intent creation calls `getDestinationFor(...)` and overrides the destination half of `REQUEST_NETWORK_MERCHANT_REFERENCE`. 4. Sweep job (cron + manual-trigger admin endpoint) under Transaction Safety Provider gate. 5. Admin UI (table) to view derived destinations, their balances, sweep status, and last sweep tx. ### Non-goals - Multi-chain destinations (covered in Task #8). - Buyer-side ephemeral keys (covered in `Request Network Integration Constraints.md` §3, separate PRD). - Hardware-wallet-signed sweeps (covered in Task #11). ### Acceptance criteria 1. Two payments from the same buyer to two different sellers land on two different addresses on-chain. 2. RN's webhook fires correctly for both, regardless of the destination divergence. 3. Sweep runs idempotently — re-running it on an already-swept address is a no-op. 4. Admin UI shows the address, its balance, last sweep tx (link to BscScan), and current ownership status. 5. Master seed never leaves the KMS/secret store. Backend reads derivation paths only; signs sweep txes via KMS API. --- ## 2. Multi-chain RN proxy registry + USDC/USDT support — Task #8 ### Problem `backend/src/services/payment/requestNetwork/proxyAddresses.ts` and `tokens.ts` currently hardcode BSC USDC plus a handful of token entries. The in-house checkout will fall through to "in-house checkout not available" the moment a buyer wants to pay on Arbitrum/Polygon/Ethereum, or wants USDT instead of USDC on the same chain. ### Goal Verified-from-chain registry of: - RN's `ERC20FeeProxy` address per supported chain. - USDC + USDT contract address + decimals per chain. Plus an admin "supported networks" UI that: - Lists each chain + token combo with its current status (verified-on-chain / probe failed / disabled). - Shows which chains a given seller has whitelisted (depends on per-seller `acceptedChains` config; out of scope here — see `Request Network Integration Constraints.md` §2). ### Hard-known facts - RN published their canonical `ERC20FeeProxy` for BSC + Arbitrum as `0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9` (CREATE2 deterministic). Same address on Ethereum mainnet, Polygon, Base **per the deterministic deployment claim** — needs verifying. - BSC USDC has 18 decimals (Binance-Peg). Mainnet/Arb/Polygon USDC have 6. - USDT on Ethereum requires `approve(0)` before a non-zero re-approve. Other chains' USDT don't. ### Scope 1. Probe script `backend/scripts/probe-rn-chains.ts` that walks every chain in `supported-chains.json`, calls a known view fn on the candidate proxy address, and confirms it's a real RN proxy (not just bytes). Emits a report. 2. Promote `tokens.ts` to load from a JSON file + override layer for admin-managed entries. Keep the canonical defaults committed. 3. Backend route `GET /api/admin/rn/networks` returning the registry plus probe status. 4. Frontend admin page `/dashboard/admin/networks` rendering the table. 5. `buildInHouseCheckoutBlock` consults the chain registry; returns `reason='unsupported_chain:'` cleanly. 6. Add the USDT-mainnet `approve(0)` quirk to the frontend approve step. When detected, the approve flow does `approve(spender, 0)` → `approve(spender, amount)`. ### Non-goals - Letting a seller pick which chain a given buyer can use (separate PRD, §2 in constraints doc). - Cross-chain bridging. ### Acceptance criteria 1. Probe script run on dev confirms RN proxy address on at least BSC, Arbitrum, Polygon, Ethereum, Base. Differences are documented. 2. Token registry has entries for USDC + USDT on those 5 chains, with correct decimals. 3. In-house checkout supports USDT on BSC end-to-end (a paid probe). 4. USDT-mainnet `approve(0)` reset is handled in the UI when needed. 5. Admin networks page renders the registry with a per-row "probe again" button. --- ## 3. Confirmation-counting + admin threshold UI — Task #9 ### Problem The Transaction Safety Provider already reads `TRANSACTION_SAFETY_MIN_CONFIRMATIONS` from env (default 12). That number is global, baked into env, only changeable by re-deploy. We want it tunable per-chain at runtime by an admin, with a clear UI showing each in-flight payment's current confirmation depth against the threshold. ### Goal 1. Persist per-chain confirmation thresholds in a `ConfigKV` collection (or extend an existing settings model) so admin can adjust without redeploy. 2. The `TransactionSafetyProvider`'s confirmation check reads the runtime threshold first, falls back to env. 3. Frontend admin UI to view + edit per-chain thresholds, and a "payments awaiting confirmation" table that shows `confirmations / threshold` updated live. ### Scope 1. New collection / extension to existing `Setting` model: `{ key: 'confirmation_threshold:', value: , updatedBy, updatedAt }`. 2. Public-to-admin endpoint `GET /api/admin/settings/confirmation-thresholds` and `PATCH /api/admin/settings/confirmation-thresholds/:chainId`. 3. Wire `transactionSafetyProvider` to read from this store; cache for 30s to avoid Mongo hammering. 4. Frontend admin page `/dashboard/admin/confirmation-thresholds`: - Table of chains, current threshold, recommended default (e.g. 12 for BSC). - Edit-in-place with a confirm dialog. - Audit-log style "changed by X on Y at Z". 5. Frontend admin page `/dashboard/admin/payments/awaiting-confirmation`: - Lists payments with `escrowState !== 'funded'` and `metadata.transactionSafety.lastCheck.status === 'pending'`. - For each: tx hash (linked to explorer), current confirmations (poll-driven), threshold, ETA. ### Non-goals - Per-asset thresholds (only per-chain). - Per-seller overrides. ### Acceptance criteria 1. Admin can lower BSC's threshold from 12 to 3 on the live dev stack without a redeploy and a new webhook fires the safety gate with the new value within 30s. 2. Awaiting-confirmation table updates live as new blocks arrive (poll every 12s on BSC). 3. Audit log records every threshold change with admin user, before/after, timestamp. --- ## 4. Optional AML screening (seller-paid) — Task #10 ### Problem The Transaction Safety Provider has an `evaluateAmlPlaceholder()` stub that currently returns `status: skipped`. We want a real AML pass that the seller can *opt into* per-offer, with the seller covering the per-check API cost. ### Goal 1. Pick a provider (Chainalysis Address Screening, TRM Labs, Elliptic — open question). Default recommendation: Chainalysis Address Screening (cheapest, simplest API). 2. Per-offer setting: `requireAmlCheck: true|false` plus `amlCheckPricePaidBy: 'seller'`. 3. When AML required, the in-house checkout webhook hits the provider with the buyer's source address. Result feeds into Transaction Safety Provider as a real `aml_screening` check. 4. Seller's account is debited for the per-check cost from their Amanat balance (or the payment is partially withheld for the cost). ### Hard-known facts - AML providers charge per-check (USD 0.10–0.50 typical). Some charge a flat monthly minimum. - Chainalysis returns categories like `sanctions`, `darknet_marketplace`, `mixer`. Provider doesn't return PII. - Most providers have rate limits (~50 rps). ### Open questions 1. **Provider choice.** Need a 1-page comparison: per-check cost, response latency, supported chains, sanctions-coverage scope. 2. **Failure mode.** If the AML API is down, do we pass-through (let the payment complete) or fail-closed (block)? Recommended: fail-closed only when seller explicitly opted in *and* enabled "block-on-provider-failure". Otherwise warn + log. 3. **Cost accounting.** Per-check cost is small; do we round up to nearest cent, batch by day, or pass-through exact? ### Scope 1. Add `requireAmlCheck` + `amlBlockOnFailure` fields to the `Offer` schema (or `SellerOffer`). 2. New `backend/src/services/payment/safety/amlProvider.ts` interface + `chainalysisProvider.ts` impl. Behind env flag `TRANSACTION_SAFETY_AML_PROVIDER=chainalysis`. 3. Transaction Safety Provider's `aml_screening` check now real, with `metadata.amlResult` persisted on the Payment record for audit. 4. Cost accounting: deduct per-check cost from seller's escrow on payment completion; surface this as a line item on the payment-details view. 5. Frontend offer-edit UI: a toggle "Require AML on incoming payments (cost: $X per payment, paid by you)". 6. Frontend admin UI for global AML provider configuration (provider, API key, per-chain enabled/disabled). ### Non-goals - Buyer-side AML (we screen the buyer's *source* address, not the seller's identity). - Custom AML rules / scoring beyond the provider's verdict. ### Acceptance criteria 1. A seller can opt into AML on a specific offer. Toggle persists. 2. An incoming payment to that offer triggers a real Chainalysis API call; the result is stored on the Payment record. 3. Verdict `sanctions` blocks the escrow gate; verdict `clean` lets it through. 4. The seller's settled amount is reduced by the AML check cost; a corresponding ledger entry is created. 5. Admin can rotate the Chainalysis API key without redeploy. 6. If the provider is down and `amlBlockOnFailure=true`, the payment stays in pending; a `provider_unavailable` reason surfaces in the admin dashboard. --- ## 5. Trezor support for admin signing — Task #11 ### Problem Today, admin actions that require signing (escrow release, refund, sweep of derived destinations once Task #7 ships) run from a hot-key in the backend env (`ADMIN_PRIVATE_KEY` or similar). That key is a single-point-of-compromise for all custodial funds. ### Goal Replace the hot-key flow with a Trezor-mediated browser flow: 1. Admin connects a Trezor via WebUSB in the admin dashboard. 2. Admin approves an action in the UI; the unsigned transaction is built backend-side, sent to the browser, signed by the Trezor, broadcast from the browser. 3. The backend never has the private key. The Trezor seed never touches a network. ### Hard-known facts - `@trezor/connect-web` is the maintained library for Trezor in browser. EIP-1193-compatible adapter exists. - Trezor supports EVM signing for any chain; chain ID is part of the tx. - WebUSB is Chromium-only. Firefox users need the Trezor Bridge native helper. ### Open questions 1. **Multi-admin.** If two admins both have a Trezor configured, do they both need to sign (m-of-n), or any one of them? Default: any one of them; m-of-n is out of scope here. 2. **Trezor model.** Trezor One vs Model T have different signing UX. We target both; the lib abstracts it. 3. **Fallback.** What if Trezor is unavailable when an urgent release is needed? Default: a "break-glass" hot-key path that admin can flip on for a 1-hour window, alarms blast Telegram. ### Scope 1. New module `frontend/src/web3/trezor/trezorConnector.ts` wrapping `@trezor/connect-web`. 2. Admin actions (release/refund/sweep) get a "Sign with Trezor" button that: - Hits backend `POST /api/admin/actions/build-tx` returning unsigned tx bytes. - Sends to Trezor for signing. - Submits signed tx via wagmi `sendTransaction`. - Calls `POST /api/admin/actions/confirm-tx` with the tx hash. 3. Backend supports both `confirmReleaseTx` flow (existing) and the new build-tx pattern. 4. Admin settings page to "register Trezor": stores the Trezor's address(es) so backend can reject signatures from unauthorized devices. 5. Audit log on every Trezor-signed action. ### Non-goals - Multi-sig contracts (Safe etc.) — separate decision. - Buyer-side Trezor (buyer already uses their own wallet via wagmi `injected()`). - Mobile Trezor flow (desktop only for v1). ### Acceptance criteria 1. Admin can register a Trezor address; subsequent admin actions show "sign with Trezor" CTA. 2. End-to-end release of escrow: build → Trezor approves → tx broadcast → backend confirms. 3. If Trezor is unregistered or admin tries to sign with a different device, the backend rejects the confirm step. 4. Audit log entries include admin user, Trezor address, tx hash, action, before/after escrow state. 5. Break-glass hot-key path requires an explicit admin toggle, expires after 1h, fires a Telegram alarm. --- ## Shared dependencies / order-of-operations - Task #8 (multichain) and Task #9 (confirmations) are independent and can land in parallel. - Task #7 (ephemeral wallets) depends on Task #11 (Trezor) only for the sweep step — the address-generation half can ship first, sweep can land later with hot-key, then migrate to Trezor when #11 is done. - Task #10 (AML) depends on nothing from the other four. It plugs into the existing Transaction Safety Provider. - Task #11 (Trezor) is self-contained. Tasks were sized for one experienced contributor to take any single one end-to-end without needing the others to land first. The integration glue (UI placement, navigation, telemetry) is left for the maintainer.