diff --git a/PRD - Wallet, Multichain, Confirmations, AML, Trezor.md b/PRD - Wallet, Multichain, Confirmations, AML, Trezor.md
index 05ff979..8437415 100644
--- a/PRD - Wallet, Multichain, Confirmations, AML, Trezor.md
+++ b/PRD - Wallet, Multichain, Confirmations, AML, Trezor.md
@@ -1,21 +1,23 @@
# 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)
+> Status: **Living — last edit 2026-05-28 (after Task #7 core shipped in backend/frontend 2.6.42)**
+> Author: nick + claude
> Owner: backend (payments) + frontend (admin UI + checkout)
-> Related: `PRD - Request Network In-House Checkout.md`, `01 - Architecture/Request Network Integration Constraints.md`
+> Related: `PRD - Request Network In-House Checkout.md`, `01 - Architecture/Request Network Integration Constraints.md`, `08 - Operations/Handoff - Request Network In-House Checkout - 2026-05-28.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-Payment ephemeral destination wallets — Task #7
+## 1. Per-(buyer, sellerOffer) ephemeral destination wallets — Task #7
+
+### Status: 🟡 In progress — backend + admin UI landed in 2.6.42, remaining work below.
### 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.
+The in-house checkout used to send *all* RN-routed payments to one Amanat-controlled wallet (env: `REQUEST_NETWORK_MERCHANT_REFERENCE`). That wallet was shared across every buyer, every seller, every offer — an audit nightmare and a single point of compromise.
-### Goal
-For each `Payment` record (which already represents a `(buyerId, sellerOfferId)` pair), 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.
+### Goal (achieved on the backend)
+For each `Payment` record (which represents a `(buyerId, sellerOfferId)` pair), 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.
### Key clarification: cart with multiple sellers
@@ -25,48 +27,73 @@ A buyer's cart can contain items from multiple sellers. **That's already modeled
- A 5-item cart spanning 2 sellers produces 2 Payments, 2 derived addresses, 2 RN intents, and the buyer makes 2 approve+pay tx pairs in sequence.
- RN's `ERC20FeeProxy.transferFromWithReferenceAndFee` is single-destination by design — there's no atomic multi-recipient split. A future v2 could route the cart through an Amanat splitter contract that fans out to per-seller derived addresses in one buyer tx; that's out of scope here.
-So the correct keying is **per `Payment._id`**, which collapses to per-`(buyer, sellerOfferId)` because of the existing uniqueness constraint on pending Payments. We do NOT key by `(buyer, seller)` directly — a single seller can have multiple distinct offers a buyer may pay for, and we want each to settle into its own address for audit lineage.
-
### 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.
+- Each `POST /v2/secure-payments` request *can* pass a different `destinationId`. RN doesn't reject divergent destinations across requests from the same client. **Acceptance probe still pending — see remaining work.**
- `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-Payment 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 Payment._id. Sweep then forget.
- - **Smart contract per Payment** that auto-forwards to the master wallet on receive. Avoids holding keys at all, but costs gas + an extra hop per payment.
- - Recommended starting point: HD wallet, with sweep-on-confirmation. Cheapest, most auditable. Derivation index can come from a monotonic counter (`Setting{key:'rn_derivation_next_idx'}`) so we never re-derive an exhausted address.
-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. For BSC USDC gas is cheap enough that immediate is fine; revisit if we add a costly chain.
-3. **Multi-seller cart UX.** The buyer signs N approve+pay pairs in sequence (one per Payment). Frontend MUST surface this clearly:
- - "You're paying 2 sellers for this order. Please confirm 4 transactions in your wallet." (2 approves + 2 pays)
+### Decisions taken during implementation
+These were open questions in the original draft; the shipped implementation locked them in.
+
+| Question | Decision |
+|---|---|
+| Key custody model | **HD wallet, derived addresses, reused per `(buyer, sellerOffer, chainId)` triple**. Backend holds only an xpub (`DERIVED_DESTINATION_XPUB`). The xpriv / master seed lives in KMS or Trezor (Task #11). |
+| Granularity | **Per `(buyer, sellerOffer, chainId)` triple**, reused for repeat payments to the same offer (one address can fund and then be re-funded later — sweeps happen out-of-band). The PRD draft considered single-use rotation; reuse won for simpler audit and to avoid bloating the derivation tree. |
+| Derivation index allocation | Monotonic counter in a `counters` Mongo collection (`{_id: 'derived_destination_index', seq: }`) updated atomically via `findByIdAndUpdate { $inc: { seq: 1 } }`. No re-derivation, no race window. |
+| Sweep strategy | **Cron-based** by default (`DERIVED_DESTINATION_SWEEP_INTERVAL_MS=300000`, i.e. 5 min) **plus manual admin trigger**. Both go through the same `sweepService` and the same Transaction Safety Provider checks. Auto-start on backend boot is not wired yet — admins start the cron via `POST /api/payment/derived-destinations/cron/start`. |
+| Signing | **`DERIVED_DESTINATION_SWEEP_SIGNER=build-only`** in prod — the backend builds the sweep tx but doesn't sign it (Trezor flow in Task #11 will). For local dev, `DERIVED_DESTINATION_SWEEP_SIGNER=hot-key` plus `DERIVED_DESTINATION_XPRIV` lets the backend sign — DO NOT USE IN PROD. |
+
+### Still open
+1. **Multi-seller cart UX** — not built. Today's frontend assumes 1 Payment per checkout page. The PRD copy from the original draft still applies:
+ - "You're paying 2 sellers for this order. Please confirm 4 transactions in your wallet."
- Progress indicator: "Paid 1 of 2 sellers — continue?"
- - If buyer aborts mid-cart, the sellers who already received funds are settled; the rest stay pending (existing Payment lifecycle handles this).
- - Out of scope for v1: an atomic splitter contract that fans out in one buyer tx.
-4. **Re-use vs. rotate.** A derived address is single-use by default (one Payment = one address). After sweep, the address is empty and we never reuse it — derivation index marches forward monotonically. This is the cleanest audit story.
-5. **Cold-payment recovery.** What if RN reports a payment to the derived address but our backend never created the Payment record (RN paid to a wrong address user inputted manually)? Out of scope for v1: the in-house UI never asks the buyer to type an address. Manual recovery via support.
+ - Mid-cart abandonment: the sellers who already received funds are settled; the rest stay pending. No special backend work — the existing Payment lifecycle covers it.
+2. **Cold-payment recovery** — out of scope for v1. Buyer never types an address into our UI, so the only way funds land on a derived address without a Payment record is RN's webhook arriving for a payment we don't recognise. Manual support recovery is acceptable.
+3. **Live "RN accepts divergent destinations" probe** — has to happen on dev with two real paid intents to two different derived addresses, with the webhook firing correctly for both. Until this probe passes, treat the divergent-destination capability as an unverified assumption.
-### Scope
-1. New module `backend/src/services/payment/wallets/derivedDestinations.ts` with `getDestinationForPayment(paymentId)` returning `{ address, derivationPath, chainId }`. Idempotent — calling it twice for the same Payment returns the already-allocated address.
-2. New `Setting` doc with key `rn_derivation_next_idx` tracking the monotonic derivation counter (atomic `findOneAndUpdate $inc`).
-3. Migration on `Payment` schema to add `metadata.derivedDestination` (address + derivation path + chainId snapshot).
-4. RN intent creation in `requestNetworkPayInService.ts` calls `getDestinationForPayment(payment._id)` and overrides the destination half of the merchant reference before `createSecurePaymentRequest`. Uses the new `buildMerchantReference` helper (already in `merchantReference.ts`).
-5. Sweep job (cron + manual-trigger admin endpoint) under Transaction Safety Provider gate. Sweep target = the master wallet from the env's `REQUEST_NETWORK_MERCHANT_REFERENCE`.
-6. Admin UI (table) at `/dashboard/admin/derived-destinations` to view: Payment id, derived address, balance, sweep status, last sweep tx (BscScan link), age, ownership status.
-7. Cart-aware buyer UX in the in-house checkout (if a cart spans multiple sellers): clear progress UI, sequential approval flow, recoverable mid-cart abandonment.
+### What landed in 2.6.42
+- **Backend (`backend/src/services/payment/wallets/`):**
+ - `derivedDestinations.ts` — `getDestinationFor({ buyerId, sellerOfferId, chainId })` returning `{ address, derivationPath, derivationIndex, chainId }`. Idempotent: checks for an existing row first; on E11000 from a race, re-reads the racer's row. Validates the env xpub rejects xpriv / tprv prefixes. Exposes `resolveExpectedRecipientForPayment` for the Transaction Safety Provider's verification path.
+ - `sweepService.ts` — pluggable signer abstraction (`build-only` / `hot-key`), ERC-20 `balanceOf` reads via Alchemy/public RPC, sweep orchestration, and an interval-based cron.
+ - `derivedDestinationRoutes.ts` — admin-only REST: `GET /` (list with status/chain/address filters and pagination), `POST /sweep` (sweep all eligible), `POST /:id/sweep` (per-row), `GET /config/health`, `POST /cron/start`, `POST /cron/stop`, `GET /cron/status`.
+- **Backend integrations:**
+ - `models/DerivedDestination.ts` — Mongo model with sweep status + history. Indexed on `(buyerId, sellerOfferId, chainId)` unique.
+ - `models/Payment.ts` — `metadata.derivedDestination` snapshot field.
+ - `adapters/types.ts` — `CreatePayInIntentInput.merchantReference` for per-payment override.
+ - `requestNetwork/contract.ts` — `buildRequestNetworkMerchantReference` accepts `input.merchantReference` as highest priority.
+ - `requestNetwork/merchantReference.ts` — `buildMerchantReference()` inverse of the parser.
+ - `requestNetwork/requestNetworkPayInService.ts` — calls `getDestinationFor`, builds the per-payment merchant reference via `buildMerchantReference`, persists `metadata.derivedDestination` on the Payment, passes the override into `adapterResult` via `input.merchantReference`.
+ - `requestNetwork/inHouseCheckout.ts` — accepts `destinationOverride`; the on-chain-paymentReference compute-fallback now uses the actual recipient (was reading `parsed.recipient` — hidden bug because RN's response provides the ref directly, but the fallback path was broken for derived destinations).
+ - `safety/transactionSafetyProvider.ts` — `resolveExpectedRecipient` checks `payment.metadata.derivedDestination.address` first, then legacy fallback chain.
+ - `app.ts` — mounts `/api/payment/derived-destinations`.
+- **Frontend (`frontend/src/sections/admin/derived-destinations/`, `frontend/src/app/dashboard/admin/derived-destinations/`):**
+ - List view with filters (status, chain, address search), pagination, sweep-all, cron start/stop.
+ - Per-row UI: address with copy + BscScan link, status chip, derivation path, balance, sweep count, last sweep tx link, per-row sweep action.
+- **Env additions** (see `backend/.env.example`): `DERIVED_DESTINATION_XPUB` (required), `DERIVED_DESTINATION_XPRIV` (dev only), `DERIVED_DESTINATION_BASE_PATH=m/44'/60'/0'`, `DERIVED_DESTINATION_CHAIN_ID=56`, `DERIVED_DESTINATION_SWEEP_SIGNER=build-only`, `DERIVED_DESTINATION_MIN_SWEEP_AMOUNT=0`, `DERIVED_DESTINATION_SWEEP_INTERVAL_MS=300000`.
-### Non-goals
+### Remaining work for Task #7 (kimi)
+
+| # | What | Where | Notes |
+|---|------|-------|-------|
+| A | **Cart-aware buyer UX** on the in-house checkout page. | `frontend/src/sections/payment/checkout/rn-in-house-checkout-view.tsx` (and `provider-payment.tsx` for the entry flow). | Today the button calls `createRequestNetworkIntent` once per cart and pushes to `/checkout/request-network/`. For multi-seller carts, the entry needs to: (a) walk each `sellerOfferId` in the cart and create N intents sequentially, (b) stash all N intent responses in `sessionStorage`, (c) navigate to a new wrapper page or extend the current page to iterate through them. Surface a clear header ("N approvals required from 2 sellers") and per-Payment progress. Mid-cart abandonment must leave the already-paid Payments settled and the rest in `pending`. |
+| B | **Unit tests** for the new modules. | `backend/__tests__/derived-destinations.test.ts` + `backend/__tests__/sweep-service.test.ts`. | Minimum: `getDestinationFor` idempotency, E11000 race fallback, xpub rejection of xpriv/tprv, `deriveAddressAtIndex` determinism, `recordSweep` idempotency (re-running on a swept row is a no-op — currently `$setOnInsert` on `totalSwept` looks suspicious for an `$inc` style accumulation; please verify it actually accumulates and add a test). |
+| C | **Live divergent-destination probe** on dev. | Manual test, no code. | Run two paid intents on the in-house page to two different `sellerOfferId`s (so two different derived addresses), confirm both `TransferWithReferenceAndFee` events fire, both webhooks land, and both Payments transition to `completed`. Record the tx hashes in the handoff doc. |
+| D | **Auto-start the sweep cron on boot** (optional but recommended). | `backend/src/app.ts` after the route mount, behind an env flag like `DERIVED_DESTINATION_SWEEP_AUTOSTART=true`. | Today admin has to click "start cron" after each redeploy. |
+| E | **Fix `recordSweep` accumulation** (if test in B confirms the bug). | `backend/src/services/payment/wallets/derivedDestinations.ts`. | The current shape uses `$setOnInsert: { totalSwept: amount }` inside `findByIdAndUpdate`, which only writes on first insert. For an existing row this means `totalSwept` never advances. Probably wants `$inc: { totalSwept: amount }` (with `totalSwept` as a string-encoded bigint or normalized to a Decimal128 field). |
+| F | **Update Activity Log + API Reference doc** in `nick-doc/` with the new admin endpoints. | `nick-doc/03 - API Reference/...`, `nick-doc/00 - Overview/Activity Log.md` if it exists. | Endpoints: `GET /api/payment/derived-destinations`, `POST /api/payment/derived-destinations/sweep`, `POST /api/payment/derived-destinations/:id/sweep`, `GET /api/payment/derived-destinations/config/health`, `POST /api/payment/derived-destinations/cron/start`, `POST /api/payment/derived-destinations/cron/stop`, `GET /api/payment/derived-destinations/cron/status`. All admin-only. |
+
+### Non-goals (carried forward unchanged)
- 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).
+- Hardware-wallet-signed sweeps (covered in Task #11 — task #7 ships the `build-only` plumbing that Task #11 plugs into).
### 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.
+1. ✅ Two payments from the same buyer to two different sellers land on two different addresses on-chain. (Backend logic shipped; verify live in remaining work item C.)
+2. ⏳ RN's webhook fires correctly for both, regardless of the destination divergence. (Pending C.)
+3. ⏳ Sweep runs idempotently — re-running it on an already-swept address is a no-op. (Needs test from item B.)
+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 from xpub only; production signing path is `build-only` (Task #11 Trezor). Dev hot-key is documented as dev-only.
+6. ⏳ Multi-seller cart UX completes N approve+pay pairs sequentially with clear progress UI. (Pending A.)
---