# Handoff: Request Network In-House Checkout — 2026-05-28

Status: **fully end-to-end working on dev.amn.gg as of 2.6.38 backend / 2.6.41 frontend**. A 0.01 USDC payment (tx `0x494c77a2…`) flowed: page render → wallet connect (Rabby/injected) → approve → `transferFromWithReferenceAndFee` → RN webhook → backend marks completed → page flips to "پرداخت تأیید شد ✓" → continue → `/dashboard/payment/<id>`.

## What's live

- **Backend 2.6.38** — `/api/payment/request-network/intents` returns an `inHouseCheckout` block (destination, tokenAddress, decimals, chainId, proxyAddress, paymentReference 8-byte hex, feeAmount, feeAddress, amountWei). `GET /api/payment/request-network/:paymentId/checkout` rehydrates the block for an existing Payment record (lazy-enriches legacy records that pre-date 2.6.34 by calling RN's `GET /v2/request/:id`). Public `GET /api/version` for the version badge. `PaymentCoordinator.updatePurchaseRequestStatus` guards both `template-checkout-` and `template-tc-` prefixes (plus regex fallback for any non-ObjectId) — earlier the `template-tc-` blindspot crashed webhook processing on template-checkout payments and stranded escrow.
- **Frontend 2.6.41** — `/checkout/request-network/[paymentId]` page with wagmi state machine: connect → switch-chain → check-allowance → approve → pay → wait-for-webhook. Destination + payment-reference + approve-tx + pay-tx hashes are copyable and click through to BscScan. Once a pay tx is in flight the page no longer reverts to "approve" even though the proxy call consumed the allowance. A 10-second `GET /api/payment/:id` poll runs as a fallback when the socket misses `payment-update`. Success-state continue button handles synthetic purchaseRequestId prefixes (`template-checkout-`, `template-tc-`) by routing to `/dashboard/payment/<id>` instead of the 404-prone `/dashboard/request/<syntheticId>`. WagmiProvider is now rendered unconditionally + the checkout page also self-wraps in its own WagmiProvider for defensive isolation.

Verify which versions are running by hovering the version chip at bottom-left of any page on dev.amn.gg, or `curl https://dev.amn.gg/api/version`.

## Where things stand

A real 0.01 USDC payment ran clean through the in-house path on 2026-05-28. Webhook delivery is durable enough for dev usage; durability for prod is Phase 5 (Cloudflare Worker ingress, not started). Five follow-up tasks were scoped immediately after — see `PRD - Wallet, Multichain, Confirmations, AML, Trezor.md` and Taskmaster `#7..#11`.

## Known issues / open work

- **TypeScript-error CI false-success**: pipelines #40 and #41 reported ✅ green in Woodpecker while `yarn build` was actually failing at the TS step and no image was pushed. Memory entry: `woodpecker_silent_build_fail.md`. **Always verify** `dev-<version>` exists in `git.manko.yoga` before trusting CI green. The wagmi `chainId` field requires `as any` because of its literal-union type — keep that pattern when adding new wagmi calls.
- **Existing/legacy Payment records** (created before backend 2.6.34) don't have RN's request details cached. The GET endpoint lazy-enriches them via `GET /v2/request/:requestId` on first visit, then persists. If RN's API is down at that moment, falls back to the hosted-page link.
- **Mongo access is denied** to the auto-mode classifier on dev — debugging payment records currently requires either the user's mongo creds or relying on the 409 `debug` block surfaced through the frontend.
- **Wagmi provider isolation (2.6.39)**: The checkout page wraps itself in its own `WagmiProvider`. The root `Web3Provider` also renders `WagmiProvider` unconditionally as of 2.6.38. The doubling is intentional defensiveness — if one provider has an issue, the other still serves the checkout flow. Can be simplified later if both prove stable.
- **PRD Phase 5 — Cloudflare Worker durable webhook ingress** — not started. Taskmaster `3.13`. Current dev relies on `dev.amn.gg` being up at the moment RN's webhook fires. For prod, RN webhooks need to land in a durable Cloudflare Worker that buffers + replays into the backend.

## Files changed (recent)

Backend (`/Users/manwe/CascadeProjects/escrow/backend`):
- `src/services/payment/requestNetwork/contract.ts` — spreads full RN response into `raw`
- `src/services/payment/requestNetwork/inHouseCheckout.ts` — block builder, reads `paymentReference` from `rnRaw.requestDetails.paymentReference`
- `src/services/payment/requestNetwork/merchantReference.ts`, `tokens.ts`, `proxyAddresses.ts`, `paymentReference.ts` — helpers
- `src/services/payment/requestNetwork/requestNetworkPayInService.ts` — calls `GET /v2/request` after intent creation
- `src/services/payment/requestNetwork/requestNetworkRoutes.ts` — `GET /:paymentId/checkout` + lazy enrichment + debug response
- `src/services/payment/requestNetwork/networkClient.ts` — already had `getRequestStatus`
- `src/app.ts` — `GET /api/version`, exempt from rate limit
- `__tests__/rn-in-house-checkout.test.ts` — 12 unit tests, all green

Frontend (`/Users/manwe/CascadeProjects/escrow/frontend`):
- `src/web3/contracts/rn-fee-proxy.ts` — RN proxy + ERC20 ABIs
- `src/web3/context/wagmi-provider.tsx` — removed the mount-gate that caused `WagmiProviderNotFoundError`
- `src/web3/components/provider-payment.tsx` — `router.push` to in-house page + sessionStorage stash
- `src/sections/payment/checkout/types.ts` + `rn-in-house-checkout-view.tsx` — state machine, local WagmiProvider wrap
- `src/app/checkout/request-network/[paymentId]/page.tsx` — app router entry
- `src/components/version-logger.tsx` — version chip + tooltip showing backend version

## Memory entries added

- `MEMORY.md` index updated with:
  - `arcane_dev_stack.md` (env/project IDs, two-step deploy note)
  - `woodpecker_silent_build_fail.md` (CI green ≠ image pushed)
  - and existing `rn_webhook_event_field.md`, `backend_rate_limits.md`, `telegram_notify_no_parse_mode.md`, `devEscrow_nginx_after_redeploy.md`, `parallel_agents_on_escrow.md`

## Open PRD questions still to decide

From `PRD - Request Network In-House Checkout.md` §10:
- Proxy address universality across chains (currently BSC + Arb confirmed; Task #8 will probe Polygon/ETH/Base)
- API pricing for hosted-UI-less usage (need RN account-mgmt question)
- Approval UX — exact-amount vs MAX_UINT256 (current: exact-amount)
- Cancel / timeout semantics for abandoned intents
- Telemetry events for in-house vs hosted A/B

## Follow-up tasks (Taskmaster + PRD)

Five follow-ups scoped for kimi to pick up independently. Full spec in `PRD - Wallet, Multichain, Confirmations, AML, Trezor.md`. Quick index:

| # | Task                                                          | Priority | Status |
|---|---------------------------------------------------------------|----------|--------|
| 7 | Per-(buyer, sellerOffer) ephemeral RN destination wallets     | high     | 🟡 In progress — backend + admin UI + cart-aware UX + tests shipped in 2.6.45/2.6.46; **live RN-accepts-divergent-destination probe remains manual** |
| 8 | Multichain RN proxy registry + USDC/USDT support              | high     | 🟡 In progress — backend registry + probe script + frontend admin page + USDT-mainnet reset shipped in 2.6.46; **BSC USDT paid probe + Base proxy address hunt remain** |
| 9 | Per-chain confirmation thresholds + admin UI                  | medium   | ⏳ Not started |
| 10 | Optional AML screening on incoming payments (seller-paid)    | medium   | ⏳ Not started |
| 11 | Trezor signing for admin actions (release/refund/sweep)      | high     | ⏳ Not started |

## Task #7 — what landed in 2.6.42

**Backend** (`backend/src/services/payment/wallets/` + plumbing)
- `DerivedDestination` model: `(buyerId, sellerOfferId, chainId)` → address, derivation path, status, sweep history.
- `derivedDestinations.ts`: xpub-driven HD address derivation, atomic counter-based index allocation, idempotent `getDestinationFor`, race-safe upsert. Backend holds `DERIVED_DESTINATION_XPUB` only — master seed lives in KMS / Trezor (Task #11).
- `sweepService.ts`: pluggable signer abstraction (`build-only` default; `hot-key` for dev), ERC-20 balance queries, sweep orchestration, interval-based cron.
- `derivedDestinationRoutes.ts`: admin-only REST endpoints (list, sweep-all, sweep-one, config health, cron start/stop/status). Mounted at `/api/payment/derived-destinations`.
- `requestNetworkPayInService.ts` now calls `getDestinationFor(buyer, sellerOffer, chainId)`, builds the per-payment merchant reference via `buildMerchantReference`, persists `metadata.derivedDestination`, and passes the override to RN.
- `inHouseCheckout.ts` accepts a `destinationOverride`; the on-chain `paymentReference` compute-fallback now uses the actual destination (previously read `parsed.recipient` — hidden bug because RN's response provides the ref directly, but the fallback was broken for derived destinations).
- `TransactionSafetyProvider.resolveExpectedRecipient` checks `metadata.derivedDestination.address` first, then legacy fallback.

**Frontend** (admin only)
- `/dashboard/admin/derived-destinations` page (table view, filters by status/chain/address, 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.

**Env additions** (see `backend/.env.example`):
- `DERIVED_DESTINATION_XPUB` — required for address derivation.
- `DERIVED_DESTINATION_XPRIV` — only when `DERIVED_DESTINATION_SWEEP_SIGNER=hot-key` (dev shortcut).
- `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`

## Task #7 — completion status

| Item | Status | Notes |
|------|--------|-------|
| Backend model + HD derivation | ✅ | `DerivedDestination`, `derivedDestinations.ts`, counter-based index |
| Sweep service + cron | ✅ | `sweepService.ts`, `build-only`/`hot-key` signers, auto-start in `app.ts` |
| Admin API + UI | ✅ | `/api/payment/derived-destinations/*`, admin dashboard page |
| RN intent integration | ✅ | `requestNetworkPayInService.ts` passes per-seller destination |
| Transaction Safety Provider | ✅ | `resolveExpectedRecipient` checks `metadata.derivedDestination` first |
| A — Cart-aware buyer UX | ✅ | `MultiSellerProviderPayment` + `RnMultiCheckoutView` + template checkout wiring |
| D — Auto-start sweep cron | ✅ | `app.ts` boots cron when `DERIVED_DESTINATION_SWEEP_AUTOSTART=true` |
| E — `recordSweep` accumulation fix | ✅ | `$inc: { totalSwept }` instead of `$setOnInsert` |
| F — API docs | ✅ | Derived-destination endpoints added to `Payment API.md` |
| B — Unit tests | ✅ | 46 tests across 3 files (see below) |
| C — Live divergent-destination probe | 🔄 | Protocol prepared; **requires manual browser + wallet execution** (see §Live multi-seller probe below). Dev is running 2.6.46 with all backend/frontend code deployed. |

## Task #8 — completion status

| Item | Status | Notes |
|------|--------|-------|
| Probe script | ✅ | `scripts/probe-rn-chains.ts` — 4/5 chains verified (BSC, Arbitrum, Ethereum, Polygon). Base proxy not deployed at canonical address. |
| Token registry JSON | ✅ | `tokens.json` — USDC + USDT on 4 verified chains with correct decimals |
| Chain registry JSON | ✅ | `supportedChains.json` — 4 verified chains + Base in `_unverified` |
| Admin route | ✅ | `GET /api/admin/rn/networks`, `POST /api/admin/rn/networks/reload` |
| Frontend admin page | ✅ | `/dashboard/admin/networks` renders registry with reload button |
| `unsupported_chain` reason | ✅ | `buildInHouseCheckoutBlock` returns `unsupported_chain:<id>` |
| Frontend wagmi multichain | ✅ | Added arbitrum + base to wagmi config with RPC transports |
| Per-chain explorers | ✅ | Checkout view uses correct explorer per chainId |
| USDT-mainnet approve reset | ✅ | **Implemented but unverified** — see note in [[Handoff - RN Multichain Probe - 2026-05-28]] |
| BSC USDT paid probe | 🔄 | **Pending manual execution** — needs real wallet + test BSC USDT |
| Base proxy hunt | 🔄 | Canonical address has no code on Base. Need RN official docs for actual address. |

### B — Unit tests (backend@34f542e, 2.6.45)

**`__tests__/derived-destinations.test.ts`** (26 tests)
- `validateXpub` rejects xpriv, tprv, garbage, empty, null/undefined
- `deriveAddressAtIndex` is deterministic; different indices → different addresses; checksummed; rejects negative/non-integer
- `getDestinationFor` idempotency: same `(buyer, sellerOffer, chainId)` returns same row, counter increments exactly once
- `getDestinationFor` E11000 race fallback: simulates concurrent insert, verify second caller re-reads racer's row
- `getDestinationFor` non-E11000 errors are re-thrown
- `recordSweep` `$inc` accumulation: run twice, assert `totalSwept` equals sum (regression lock-in for item E)
- `recordSweep` handles string and negative amounts
- `resolveExpectedRecipientForPayment` prefers `metadata.derivedDestination`, falls back to `blockchain.receiver`
- `listDerivedDestinations` pagination
- `verifyDerivedDestinationConfig` ok / missing xpub / invalid xpub

**`__tests__/sweep-service.test.ts`** (18 tests)
- `getSweepSigner` returns `build-only` by default, `hot-key` when configured
- `queryTokenBalance` parses bigint, returns 0n for empty balance, null on RPC failure, null for unsupported chain+token
- `sweepDerivedDestinations` skips below-threshold balances, dry-run returns amount without broadcasting, build-only signer returns error without updating record, handles balance query failure, respects `destinationIds` filter, throws when master wallet missing
- Cron lifecycle: start/stop/idempotent/zero-interval

**`__tests__/request-template-orphan-cleanup.test.ts`** (2 tests) — **non-negotiable regression lock-in for Gap 2 fix**
- Asserts `Payment.find` during orphan cleanup is scoped to `provider: 'shkeeper'`
- Asserts a pending `provider: 'request.network'` Payment is **NOT** deleted when a shkeeper orphan exists for the same buyer
- Asserts request.network orphan is untouched even when no shkeeper orphan is present

### C — Live multi-seller probe protocol

**Goal:** Prove RN accepts divergent `destinationId` across consecutive `POST /v2/secure-payments` from the same buyer session, and that the multi-checkout cart UX creates two Payments landing on two different derived addresses.

**Prerequisites:**
- Dev backend running ≥ 2.6.45 with `DERIVED_DESTINATION_XPUB` configured
- Dev frontend running ≥ 2.6.44
- Two seller accounts on `dev.amn.gg` with wallet addresses set
- Buyer account with a Rabby/Metamask wallet holding ≥ 0.02 testnet BSC USDC

**Steps:**

1. **Create two template offers** (one from each seller):
   - Seller A: `https://dev.amn.gg/dashboard/shops/templates/new` → fill title, price 0.01 USDC, publish
   - Seller B: same, price 0.01 USDC, publish
   - Capture both `shareableLink` values

2. **As buyer, add both to cart**:
   - Visit Seller A's shareable link → Add to cart
   - Visit Seller B's shareable link → Add to cart
   - Go to `/dashboard/shops/checkout/?step=2`
   - Confirm cart shows 2 items from 2 different sellers

3. **Select crypto payment and proceed**:
   - Choose "پرداخت با Request Network"
   - Click the multi-seller button (should show `۲ فروشنده`)
   - Browser navigates to `/checkout/request-network/multi?session=<id>`

4. **Pay Seller A** (first checkout page):
   - Connect wallet
   - Approve 0.01 USDC for RN proxy
   - Call `transferFromWithReferenceAndFee`
   - Wait for "پرداخت تأیید شد ✓"
   - Click "ادامه به پرداخت بعدی"

5. **Pay Seller B** (second checkout page):
   - Approve 0.01 USDC (or reuse allowance if proxy unchanged)
   - Call `transferFromWithReferenceAndFee`
   - Wait for confirmation
   - Click "پایان"

6. **Capture evidence**:
   - **Derived addresses:** Check `GET /api/payment/derived-destinations?buyerId=<buyerId>` — expect 2 rows with different `address` and `derivationIndex`
   - **Tx hashes:** Copy both approve and both pay tx hashes from the UI; verify on `https://testnet.bscscan.com` (or mainnet BscScan if dev uses mainnet)
   - **Transfer events:** On BscScan, verify both `TransferWithReferenceAndFee` events show different `recipient` addresses
   - **Webhooks:** Check backend logs for `payment-update` socket emissions or `POST /api/payment/request-network/webhook` handling for both payments
   - **Payment records:** Query Mongo for the two `Payment` docs — both should have `status: 'completed'` and different `metadata.derivedDestination.address`
   - **PurchaseRequests:** Verify `convertTemplatesToRequests` created 2 `PurchaseRequest` docs with `status: 'payment'`

7. **Document**: Paste the full evidence block below under "Live multi-seller probe — execution record".

---

### Live multi-seller probe — execution record

> _To be filled after manual execution of the protocol above._
>
> | Field | Value |
> |-------|-------|
> | Date | |
> | Buyer ID | |
> | Seller A ID / Offer | |
> | Seller B ID / Offer | |
> | Derived address A | |
> | Derived address B | |
> | Approve tx A | |
> | Pay tx A | |
> | Approve tx B | |
> | Pay tx B | |
> | Payment ID A | |
> | Payment ID B | |
> | PurchaseRequest ID A | |
> | PurchaseRequest ID B | |
> | RN webhook fired for both? | |
> | Both Payments completed? | |
> | Escrow funded for both? | |

---

**Remaining in task #7:**
- Item C: execute the live probe protocol above and fill the execution record table.
- After C passes: flip PRD §1 acceptance criteria #1 and #2 from ⏳ → ✅ and mark Task #7 done in Taskmaster.
