In-house Request Network checkout went fully end-to-end on dev today. A real 0.01 USDC payment flowed through wallet connect -> approve -> ERC20FeeProxy.transferFromWithReferenceAndFee -> RN webhook -> TransactionSafetyProvider -> Payment.status=completed -> page success state. Tx 0x494c77a29161b5100d8e0b1ac675f1822955d0bb3633ecdbfafb886f84f2f320. Docs: - New PRD: Wallet, Multichain, Confirmations, AML, Trezor (5 follow-ups, each sized for an independent contributor) - Updated PRD: Request Network In-House Checkout (phases 0..3 done, phase 4 partial, phases 5-6 not started) - Updated handoff: deployed versions, what is working end-to-end, follow-up tasks index Taskmaster: 5 new top-level tasks (#7..#11) covering ephemeral destination wallets, multichain proxy registry + USDC/USDT, runtime confirmation thresholds, optional seller-paid AML screening, and Trezor signing for admin actions. Tasks are scoped fine-grained so each is independent enough for kimi to pick up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
16 KiB
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
destinationIdinside the merchant reference:<address>@eip155:<chainId>#<contractId>:<tokenAddress>. RN doesn't bind this to an Amanat-level identity; it's just where the funds end up. - Each
POST /v2/secure-paymentsrequest can pass a differentdestinationId. RN doesn't reject divergent destinations across requests from the same client. paymentReferenceis 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
- Key custody model. Options:
- Deterministic HD wallet rooted at one Amanat master seed; derive per-
(buyer, seller)path (e.g.m/44'/60'/0'/<buyerIdx>/<sellerIdx>). 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.
- Deterministic HD wallet rooted at one Amanat master seed; derive per-
- 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.
- 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. - 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
- New module
backend/src/services/payment/wallets/derivedDestinations.tswithgetDestinationFor(buyerId, sellerOfferId)returning{ address, derivationPath, chainId }. - Migration on
Paymentschema to addmetadata.derivedDestination(address + derivation path snapshot). - RN intent creation calls
getDestinationFor(...)and overrides the destination half ofREQUEST_NETWORK_MERCHANT_REFERENCE. - Sweep job (cron + manual-trigger admin endpoint) under Transaction Safety Provider gate.
- 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
- Two payments from the same buyer to two different sellers land on two different addresses on-chain.
- RN's webhook fires correctly for both, regardless of the destination divergence.
- Sweep runs idempotently — re-running it on an already-swept address is a no-op.
- Admin UI shows the address, its balance, last sweep tx (link to BscScan), and current ownership status.
- 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
ERC20FeeProxyaddress 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
acceptedChainsconfig; out of scope here — seeRequest Network Integration Constraints.md§2).
Hard-known facts
- RN published their canonical
ERC20FeeProxyfor BSC + Arbitrum as0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9(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
- Probe script
backend/scripts/probe-rn-chains.tsthat walks every chain insupported-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. - Promote
tokens.tsto load from a JSON file + override layer for admin-managed entries. Keep the canonical defaults committed. - Backend route
GET /api/admin/rn/networksreturning the registry plus probe status. - Frontend admin page
/dashboard/admin/networksrendering the table. buildInHouseCheckoutBlockconsults the chain registry; returnsreason='unsupported_chain:<chainId>'cleanly.- Add the USDT-mainnet
approve(0)quirk to the frontend approve step. When detected, the approve flow doesapprove(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
- Probe script run on dev confirms RN proxy address on at least BSC, Arbitrum, Polygon, Ethereum, Base. Differences are documented.
- Token registry has entries for USDC + USDT on those 5 chains, with correct decimals.
- In-house checkout supports USDT on BSC end-to-end (a paid probe).
- USDT-mainnet
approve(0)reset is handled in the UI when needed. - 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
- Persist per-chain confirmation thresholds in a
ConfigKVcollection (or extend an existing settings model) so admin can adjust without redeploy. - The
TransactionSafetyProvider's confirmation check reads the runtime threshold first, falls back to env. - Frontend admin UI to view + edit per-chain thresholds, and a "payments awaiting confirmation" table that shows
confirmations / thresholdupdated live.
Scope
- New collection / extension to existing
Settingmodel:{ key: 'confirmation_threshold:<chainId>', value: <int>, updatedBy, updatedAt }. - Public-to-admin endpoint
GET /api/admin/settings/confirmation-thresholdsandPATCH /api/admin/settings/confirmation-thresholds/:chainId. - Wire
transactionSafetyProviderto read from this store; cache for 30s to avoid Mongo hammering. - 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".
- Frontend admin page
/dashboard/admin/payments/awaiting-confirmation:- Lists payments with
escrowState !== 'funded'andmetadata.transactionSafety.lastCheck.status === 'pending'. - For each: tx hash (linked to explorer), current confirmations (poll-driven), threshold, ETA.
- Lists payments with
Non-goals
- Per-asset thresholds (only per-chain).
- Per-seller overrides.
Acceptance criteria
- 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.
- Awaiting-confirmation table updates live as new blocks arrive (poll every 12s on BSC).
- 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
- Pick a provider (Chainalysis Address Screening, TRM Labs, Elliptic — open question). Default recommendation: Chainalysis Address Screening (cheapest, simplest API).
- Per-offer setting:
requireAmlCheck: true|falseplusamlCheckPricePaidBy: 'seller'. - 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_screeningcheck. - 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
- Provider choice. Need a 1-page comparison: per-check cost, response latency, supported chains, sanctions-coverage scope.
- 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.
- Cost accounting. Per-check cost is small; do we round up to nearest cent, batch by day, or pass-through exact?
Scope
- Add
requireAmlCheck+amlBlockOnFailurefields to theOfferschema (orSellerOffer). - New
backend/src/services/payment/safety/amlProvider.tsinterface +chainalysisProvider.tsimpl. Behind env flagTRANSACTION_SAFETY_AML_PROVIDER=chainalysis. - Transaction Safety Provider's
aml_screeningcheck now real, withmetadata.amlResultpersisted on the Payment record for audit. - Cost accounting: deduct per-check cost from seller's escrow on payment completion; surface this as a line item on the payment-details view.
- Frontend offer-edit UI: a toggle "Require AML on incoming payments (cost: $X per payment, paid by you)".
- 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
- A seller can opt into AML on a specific offer. Toggle persists.
- An incoming payment to that offer triggers a real Chainalysis API call; the result is stored on the Payment record.
- Verdict
sanctionsblocks the escrow gate; verdictcleanlets it through. - The seller's settled amount is reduced by the AML check cost; a corresponding ledger entry is created.
- Admin can rotate the Chainalysis API key without redeploy.
- If the provider is down and
amlBlockOnFailure=true, the payment stays in pending; aprovider_unavailablereason 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:
- Admin connects a Trezor via WebUSB in the admin dashboard.
- 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.
- The backend never has the private key. The Trezor seed never touches a network.
Hard-known facts
@trezor/connect-webis 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
- 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.
- Trezor model. Trezor One vs Model T have different signing UX. We target both; the lib abstracts it.
- 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
- New module
frontend/src/web3/trezor/trezorConnector.tswrapping@trezor/connect-web. - Admin actions (release/refund/sweep) get a "Sign with Trezor" button that:
- Hits backend
POST /api/admin/actions/build-txreturning unsigned tx bytes. - Sends to Trezor for signing.
- Submits signed tx via wagmi
sendTransaction. - Calls
POST /api/admin/actions/confirm-txwith the tx hash.
- Hits backend
- Backend supports both
confirmReleaseTxflow (existing) and the new build-tx pattern. - Admin settings page to "register Trezor": stores the Trezor's address(es) so backend can reject signatures from unauthorized devices.
- 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
- Admin can register a Trezor address; subsequent admin actions show "sign with Trezor" CTA.
- End-to-end release of escrow: build → Trezor approves → tx broadcast → backend confirms.
- If Trezor is unregistered or admin tries to sign with a different device, the backend rejects the confirm step.
- Audit log entries include admin user, Trezor address, tx hash, action, before/after escrow state.
- 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.