Files
nick-doc/.taskmaster/docs/prd-request-network-in-house-checkout.md
Siavash Sameni 0060b16912 docs: ship in-house RN checkout, scope 5 follow-up tasks (#7-11)
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>
2026-05-28 15:50:24 +04:00

20 KiB
Raw Blame History

PRD: Request Network In-House Checkout

Status: Draft — updated after 2026-05-28 dev webhook probe Date: 2026-05-27 Related: 01 - Architecture/Request Network Integration Constraints.md §1; PRD - Request Network Migration and Funds Management.md Owner: Backend payments + Frontend


1. Problem

Buyers paying through Request Network (RN) are redirected from Amanat to RN's hosted page at pay.request.network/?token=…. That page has three concrete problems:

  1. Rabby is not supported. A meaningful portion of our user base pays from Rabby. The hosted page does not detect it (no EIP-6963 enumeration, hard-coded MetaMask/WalletConnect/Coinbase). For those users, checkout fails before a transaction is even attempted.
  2. The page is brand-divergent. Buyers leave the Amanat domain mid-flow, which is a trust and conversion cost we can avoid.
  3. The page relies on RN's choice of infrastructure. Observed in our own probe: their UI wraps the payment in Safe smart accounts + ERC-4337 + a Pimlico paymaster, hitting public BSC RPCs that already rate-limited us once ("RPC endpoint returned too many errors"). We have no control to fix that.

The core RN protocol is sound. The hosted UI on top of it is the problem.

2026-05-28 reality update

The first dev BSC probe did not fail because RN stayed silent. It failed because Amanat dropped the provider callback:

  • Test transaction: 0x3a23febd9abd43d7e0851c1ea86c4ceaf08c11098852cb0425fa074e9c88350b.
  • On-chain result: successful BSC USDC transfer to Amanat's configured destination wallet.
  • RN webhook result: RN called POST /api/payment/request-network/webhook on dev.amn.gg four times from 34.34.233.192.
  • Backend result: nginx/backend returned 404 because the handler did not correlate all Request Network identifiers stored on the Payment record.
  • Frontend result: the callback page stayed on the processing state and later hit 429 from 3-second polling.

The pre-implementation gate therefore changes from "prove RN calls us" to "deploy and smoke-test the webhook correlation repair, callback polling repair, and transaction-safety gate before another paid probe".

2. Goal

Replace the redirect to RN's hosted page with an Amanat-rendered checkout that:

  • Connects any EVM wallet (Rabby, MetaMask, OKX, Trust, WalletConnect — anything wagmi's injected() connector enumerates).
  • Builds and submits the exact same on-chain calls RN's hosted page makes, so RN's existing webhook fires unchanged.
  • Stays on dev.amn.gg (or prod equivalent) for the entire flow.
  • Falls back gracefully to the hosted page for buyers who explicitly request it.

Settlement, webhook handling, and the backend Payment lifecycle do not change. Only the buyer-facing checkout surface changes.

Webhook delivery alone is not a sufficient trust signal. Amanat must run a Transaction Safety Provider gate before marking escrow funded. The initial provider checks: transaction hash present, configured confirmation depth, transfer recipient/token/amount match on-chain evidence, and future AML/sanctions provider approval.

3. Non-goals

  • Per-seller multi-chain configuration (covered in Request Network Integration Constraints.md §2 — separate PRD).
  • Per-(buyer, merchant) ephemeral wallets / wallet abstraction layer (§3 — separate PRD).
  • Replacing RN entirely with our own chain watcher (§4 — depends on this PRD's outcome).
  • Gasless / sponsored transactions for the buyer. Buyer pays own gas via their EOA. We can revisit paymaster integration in a follow-up.

4. Background: what RN's protocol actually requires

Cold-inspected from a real $12 BSC payment on RN's hosted page:

Item Value
Proxy contract (BSC, and same address on most EVMs via deterministic CREATE2) 0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9
Function transferFromWithReferenceAndFee(address token, address to, uint256 amount, bytes reference, uint256 feeAmount, address feeAddress)
Selector 0xc219a14d
Emitted event TransferWithReferenceAndFee(token, to, amount, reference, feeAmount, feeAddress)
paymentReference derivation last8Bytes(keccak256(lowercase(requestId + salt + destinationAddress)))
Fee config seen on RN's UI feeAmount = 0, feeAddress = 0x…dEaD

RN's webhook listens for TransferWithReferenceAndFee. If we emit it ourselves with the correct paymentReference, the webhook fires. The hosted UI is a UI-only convenience — not a settlement requirement.

5. Proposed flow (buyer perspective)

  1. Buyer clicks "Pay with Request Network" on the request page.
  2. Frontend calls existing POST /payment/request-network/intents (unchanged endpoint, expanded response — see §6).
  3. Frontend navigates to new in-house page: /checkout/request-network/:paymentId.
  4. Page shows: amount, token, chain, recipient (truncated, copyable), countdown timer.
  5. "Connect wallet" — wagmi injected() + metaMask() (already configured). Rabby works automatically via EIP-6963.
  6. Once connected, page checks the buyer's wallet is on the correct chain. If not, prompts switchChain (wagmi).
  7. Page also checks current allowance. If allowance(buyer, RN_PROXY) >= amount, skip approve.
  8. Buyer signs transaction 1: USDC.approve(RN_PROXY, amount). UI shows "Approving…" → "Approved".
  9. Buyer signs transaction 2: RN_PROXY.transferFromWithReferenceAndFee(token, dest, amount, ref, 0, 0x…dEaD). UI shows "Submitting payment…" → "Confirmed on-chain".
  10. Page waits for backend payment-confirmed socket event (already emitted on RN webhook → Payment.status='paid'). Shows success state.
  11. Redirect to existing payment-success route.

Escape hatches surfaced as secondary links on the page:

  • "Continue on Request Network's hosted page" → falls through to securePaymentUrl (unchanged behavior).
  • "Copy payment details" → exposes destination, amount, token, chain, paymentReference for manual entry into any wallet.

6. Backend changes

6.1 Intent response payload

Expand PayInIntentResult for RN. Today it returns paymentUrl, paymentId, providerPaymentId, amount, config.networks, config.allowedTokens, raw. Add:

inHouseCheckout: {
  destination: '0x05E280d7f3cA954f37afA8B1E4d2a51D167c573e',
  tokenAddress: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d',
  tokenSymbol: 'USDC',
  decimals: 18,                  // BSC USDC quirk; varies per token+chain
  chainId: 56,
  proxyAddress: '0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9',
  paymentReference: '0xa826e248f5de5463',  // 8-byte hex, computed from RN's requestId + salt
  feeAmount: '0',
  feeAddress: '0x000000000000000000000000000000000000dEaD',
  amountWei: '12000000000000000000',  // pre-multiplied by decimals, frontend doesn't compute
}

6.2 Sources for each field

Field Source
destination, tokenAddress, chainId Parse REQUEST_NETWORK_MERCHANT_REFERENCE env (address@eip155:chainId#ref:tokenAddr). Today done once at startup; extract into a helper.
decimals Token registry lookup. Hardcode the few we care about (USDC/USDT × BSC/ETH/Arb/Polygon) in a constant map; fall back to on-chain decimals() if missing.
proxyAddress Constant per chain. Use canonical 0x0DfbEe14…0aC9 for all EVMs unless RN deploys a non-canonical address (open question — see §10).
paymentReference Computed from RN's /v2/secure-payments response. salt is in their raw payload. Use keccak256 + slice-to-8-bytes.
feeAmount, feeAddress Constants matching what RN's hosted UI submits. Verify via cold inspection on each new chain we enable.
amountWei BigInt(amount) * 10n ** BigInt(decimals). Done backend-side to avoid frontend rounding bugs.

6.3 Files touched

  • backend/src/services/payment/requestNetwork/contract.ts — extend PayInIntentResult (or a new inHouseCheckout sub-object).
  • backend/src/services/payment/requestNetwork/requestNetworkPayInService.ts — populate the new fields.
  • backend/src/services/payment/requestNetwork/merchantReference.ts (new) — parser for the address@chain#ref:token format. Currently this is implicit in env lookups; promote to a named helper.
  • backend/src/services/payment/requestNetwork/tokens.ts (new) — { chainId, tokenAddr } → { symbol, decimals } lookup.
  • backend/src/services/payment/requestNetwork/paymentReference.ts (new) — computePaymentReference(requestId, salt, destination).
  • Unit tests for each new helper.

6.4 Webhook handler

The route remains /api/payment/request-network/webhook, but the handler must be hardened before more paid probes:

  • Correlate against every RN identifier we persist: providerPaymentId, metadata.requestNetworkRequestId, metadata.requestNetworkPaymentReference, and nested metadata.requestNetworkData ids/references.
  • Reject unsigned or test-header callbacks unless explicit test mode is enabled.
  • Store raw webhook evidence on the Payment record for support/replay.
  • Call the Transaction Safety Provider before marking Payment.status='completed' / escrowState='funded'.
  • Return a successful pending response when safety is not yet satisfied, so the delivery is not lost but escrow is not credited.

7. Frontend changes

7.1 New page

frontend/src/pages/checkout/request-network/[paymentId].tsx (or whatever the router convention is).

State machine (one component, reducer or zustand):

idle
  → connecting (wallet not connected)
  → wrong-chain (connected, chain mismatch)
  → ready (correct chain, allowance unknown)
  → checking-allowance
  → needs-approve     → approving → approve-confirming → ready-to-pay
  → ready-to-pay
  → paying → pay-confirming → confirmed
  → error (with retry)
  → expired (timer ran out)

7.2 Reusable parts from SHKeeper

frontend/src/web3/components/manual-payment.tsx already implements: address-with-QR, copy-to-clipboard, countdown timer, localStorage persistence per request, socket-driven status update. Reuse the layout chrome. The wallet-interaction half is new.

7.3 Wagmi calls

Two contracts. The ERC-20 ABI is already in viem/wagmi. RN proxy ABI is a single function — define it inline.

const RN_PROXY_ABI = [{
  inputs: [
    { name: 'tokenAddress', type: 'address' },
    { name: 'to',           type: 'address' },
    { name: 'amount',       type: 'uint256' },
    { name: 'paymentReference', type: 'bytes' },
    { name: 'feeAmount',    type: 'uint256' },
    { name: 'feeAddress',   type: 'address' },
  ],
  name: 'transferFromWithReferenceAndFee',
  outputs: [], stateMutability: 'nonpayable', type: 'function',
}] as const;

Hooks used:

  • useAccount, useChainId, useSwitchChain
  • useReadContract for allowance
  • useWriteContract + useWaitForTransactionReceipt for approve and proxy call

7.4 Routing change

Today, createRequestNetworkIntent consumers do window.location = response.paymentUrl. Replace with router.push('/checkout/request-network/' + response.paymentId). Keep response.paymentUrl available so the in-house page can render the escape-hatch link.

7.5 Files touched

  • frontend/src/pages/checkout/request-network/[paymentId].tsx — new page.
  • frontend/src/web3/components/rn-in-house-checkout.tsx — new component holding the state machine.
  • frontend/src/web3/contracts/rn-fee-proxy.ts — ABI + address-per-chain constants.
  • frontend/src/sections/request/components/buyer-steps/… — change the existing RN button handler from window.location to router.push.
  • Tests/Storybook for the new component.

8. Acceptance criteria

  1. A buyer using Rabby can complete an RN payment end-to-end on dev.amn.gg without leaving the domain.
  2. The two on-chain transactions emit a TransferWithReferenceAndFee event with the same paymentReference RN's hosted page would have produced.
  3. RN's existing webhook fires on the event and the Payment doc transitions to completed / escrowState='funded' after safety approval.
  4. If buyer connects on the wrong chain, they see a one-click "Switch network" CTA.
  5. If buyer already has sufficient allowance, the approve step is skipped.
  6. If the buyer abandons the page, returning to it restores their state from localStorage and resumes from the correct step.
  7. "Continue on Request Network's hosted page" link is visible on the in-house page and works exactly like today's redirect.
  8. Page handles tx failure (user reject, insufficient gas, RPC error) with a clear retry path that does not corrupt the Payment doc.
  9. Existing non-RN payment flows (SHKeeper, others) are untouched.

9. Out of scope (explicit non-decisions)

  • Multi-chain at checkout (BSC + Arb + ETH together) — needs the per-seller acceptedChains config; separate PRD.
  • WalletConnect integration — currently disabled in web3/config.ts for SSR reasons; not regressed but not added either.
  • Migration plan to remove RN entirely — kept as §4 open option in the architecture doc.
  • Mobile Mini App rendering of the checkout (different surface — handled in Task 5.4).

10. Open questions for review

These are the items we should discuss with the second developer before starting implementation.

  1. Proxy address universality. Is 0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9 the RN ERC20FeeProxy address on every chain we plan to support (BSC, Arb, ETH mainnet, Polygon, Base)? We confirmed BSC + Arbitrum from RN's payments-subgraph repo. The other chains need explicit confirmation — preferably an on-chain probe per chain.
  2. API pricing for hosted-UI vs API-only. RN's pricing model may assume hosted-UI usage. If we use them as a notification primitive only, are we still in their pricing terms? Worth a direct question to RN account management.
  3. Approval UX. RN's hosted UI uses approve(spender, MAX_UINT256) for gas efficiency. Some Amanat users have voiced concern in past audits about unbounded approvals. Options:
    • Mirror RN: MAX_UINT256 (best UX, repeat payments don't need re-approve).
    • Exact-amount: approve(spender, amount). Slightly worse UX (every payment requires an approve), better security posture.
    • Recommend: exact-amount by default, opt-in "remember this approval" for power users. Discuss.
  4. Tokens with non-standard ERC-20 behavior. USDT on Ethereum mainnet famously requires approve(0) before changing a non-zero allowance. We need to handle this if we add USDT-ETH later. Not relevant on day one (BSC config is USDC), but worth flagging now.
  5. Cancel and timeout semantics. Today the Payment doc transitions on the webhook. If the buyer signs the approve but bails on the proxy call, the doc sits in pending forever. Options:
    • 30-minute TTL on the pending doc (existing partial unique index would let a re-attempt succeed).
    • "Cancel intent" button on the in-house page that calls a new backend endpoint to mark the doc cancelled.
    • Both. Discuss.
  6. Manual reconciliation. If a buyer pays the correct amount to the correct destination but bypasses the proxy (e.g., raw transfer), no event fires and no webhook arrives. They'll think they paid but our system won't know. Today RN's hosted UI prevents this; our in-house UI inherits the same risk only if we add a "raw transfer" escape hatch — which we won't. But: should we proactively detect orphan transfers (chain watcher) and surface them in support? Out of scope for v1.
  7. Feature flag and rollout. Ship behind a flag and A/B against the current redirect for one week of dev usage? Or hard-cut once acceptance criteria pass? Recommend A/B for at least one full release cycle.
  8. Telemetry. What events do we instrument so we can prove the in-house page outperforms the redirect on conversion? Suggested: rn_checkout_opened, rn_wallet_connected, rn_approve_signed, rn_pay_signed, rn_pay_confirmed, rn_pay_failed{reason}, rn_escape_to_hosted_clicked.

11. Risks

  • Webhook handling failure. RN delivered the 2026-05-28 dev webhook, but Amanat returned 404. A deployed correlation fix and smoke test are now the load-bearing gate before another paid probe.
  • Webhook durability. The main app is too unstable to be the only callback landing zone. Put a Cloudflare Worker in front of Request Network webhooks to durably store raw delivery evidence, forward to the backend, and replay after outages.
  • False-positive payment credit. A signed provider event must not be enough to fund escrow. Transaction Safety Provider gates completion on on-chain evidence, confirmation depth, transfer matching, and future AML/sanctions checks.
  • Chain-specific divergences. RN may have deployed non-canonical proxy contracts on some chains. We mitigate by hard-coding per-chain proxy addresses and treating "unknown chain → fall back to hosted page" as the safe default.
  • Wallet support drift. EIP-6963 is mature but not universal. We rely on wagmi's injected() connector enumeration; if a wallet doesn't implement EIP-6963, it falls into the generic window.ethereum slot. Acceptable.
  • Buyer signs approve, then we change ABI before they sign the proxy call. Not really a risk if both txs are in the same session, but worth a smoke test on a long-lived session.

12. Implementation phases

Roughly two weeks of checkout work, now gated on confirmation reliability repair.

Phase Work Gate
0 — Probe Fire one real dev BSC payment and inspect nginx/backend logs Completed: RN webhook reached nginx/backend; app returned 404
0A — Confirmation repair Deploy correlation fix, callback polling fix, signed-webhook smoke test, and Transaction Safety Provider Unsigned/test callbacks rejected unless explicitly enabled; real webhook can find the Payment
1 — Backend Expand intent response with inHouseCheckout fields; add helpers and tests Existing RN smoke test still passes; new unit tests pass
2 — Frontend skeleton New page + state machine + wallet-connect; no on-chain calls yet, mocked confirmations Local clickthrough on dev.amn.gg with Rabby connected
3 — On-chain wiring Real approve + transferFromWithReferenceAndFee calls; allowance check; chain-switch One real end-to-end payment on dev BSC, webhook fires, Payment.status='completed', safety approved
4 — Hardening Error handling, timer, persistence, telemetry, escape-hatch link Acceptance criteria 19 demonstrably pass
5 — Durable ingress Cloudflare Worker receives RN webhooks, stores delivery records, forwards to backend, supports replay Backend outage no longer loses webhook evidence
6 — Rollout Behind feature flag, A/B in dev for one cycle, then prod Conversion telemetry; no regressions in non-RN flows

13. Durable webhook ingress roadmap

Add a Cloudflare Worker as the public Request Network webhook target:

  1. Receive the raw RN webhook body, headers, request id, delivery id, source IP, and timestamp.
  2. Verify the RN signature at the edge if raw-body secret handling is straightforward; otherwise store first and let the backend perform canonical verification.
  3. Write an immutable delivery record to durable storage. Candidate stack: Cloudflare Queues for handoff plus D1/R2/KV for indexed replay metadata and raw payload retention.
  4. Forward to the primary backend and optionally a secondary endpoint.
  5. Return 2xx only after durable enqueue/store succeeds.
  6. Provide operator replay by delivery id, payment reference, request id, or time window.

The Worker is only ingress/evidence buffering. The backend remains the trust boundary for signature verification, idempotency, Transaction Safety Provider checks, ledger updates, and marketplace state transitions.

14. References