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>
20 KiB
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:
- 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.
- The page is brand-divergent. Buyers leave the Amanat domain mid-flow, which is a trust and conversion cost we can avoid.
- 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/webhookondev.amn.ggfour times from34.34.233.192. - Backend result: nginx/backend returned
404because the handler did not correlate all Request Network identifiers stored on thePaymentrecord. - Frontend result: the callback page stayed on the processing state and later hit
429from 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)
- Buyer clicks "Pay with Request Network" on the request page.
- Frontend calls existing
POST /payment/request-network/intents(unchanged endpoint, expanded response — see §6). - Frontend navigates to new in-house page:
/checkout/request-network/:paymentId. - Page shows: amount, token, chain, recipient (truncated, copyable), countdown timer.
- "Connect wallet" — wagmi
injected()+metaMask()(already configured). Rabby works automatically via EIP-6963. - Once connected, page checks the buyer's wallet is on the correct chain. If not, prompts
switchChain(wagmi). - Page also checks current allowance. If
allowance(buyer, RN_PROXY) >= amount, skip approve. - Buyer signs transaction 1:
USDC.approve(RN_PROXY, amount). UI shows "Approving…" → "Approved". - Buyer signs transaction 2:
RN_PROXY.transferFromWithReferenceAndFee(token, dest, amount, ref, 0, 0x…dEaD). UI shows "Submitting payment…" → "Confirmed on-chain". - Page waits for backend
payment-confirmedsocket event (already emitted on RN webhook →Payment.status='paid'). Shows success state. - 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— extendPayInIntentResult(or a newinHouseCheckoutsub-object).backend/src/services/payment/requestNetwork/requestNetworkPayInService.ts— populate the new fields.backend/src/services/payment/requestNetwork/merchantReference.ts(new) — parser for theaddress@chain#ref:tokenformat. 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 nestedmetadata.requestNetworkDataids/references. - Reject unsigned or test-header callbacks unless explicit test mode is enabled.
- Store raw webhook evidence on the
Paymentrecord 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,useSwitchChainuseReadContractforallowanceuseWriteContract+useWaitForTransactionReceiptfor 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 fromwindow.locationtorouter.push.- Tests/Storybook for the new component.
8. Acceptance criteria
- A buyer using Rabby can complete an RN payment end-to-end on
dev.amn.ggwithout leaving the domain. - The two on-chain transactions emit a
TransferWithReferenceAndFeeevent with the samepaymentReferenceRN's hosted page would have produced. - RN's existing webhook fires on the event and the
Paymentdoc transitions tocompleted/escrowState='funded'after safety approval. - If buyer connects on the wrong chain, they see a one-click "Switch network" CTA.
- If buyer already has sufficient allowance, the approve step is skipped.
- If the buyer abandons the page, returning to it restores their state from localStorage and resumes from the correct step.
- "Continue on Request Network's hosted page" link is visible on the in-house page and works exactly like today's redirect.
- Page handles tx failure (user reject, insufficient gas, RPC error) with a clear retry path that does not corrupt the
Paymentdoc. - 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
acceptedChainsconfig; separate PRD. - WalletConnect integration — currently disabled in
web3/config.tsfor SSR reasons; not regressed but not added either. - Migration plan to remove RN entirely — kept as
§4open 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.
- Proxy address universality. Is
0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9the 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. - 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.
- 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.
- Mirror RN:
- 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. - Cancel and timeout semantics. Today the
Paymentdoc transitions on the webhook. If the buyer signs the approve but bails on the proxy call, the doc sits inpendingforever. Options:- 30-minute TTL on the
pendingdoc (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.
- 30-minute TTL on the
- 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. - 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.
- 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 genericwindow.ethereumslot. 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 1–9 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:
- Receive the raw RN webhook body, headers, request id, delivery id, source IP, and timestamp.
- Verify the RN signature at the edge if raw-body secret handling is straightforward; otherwise store first and let the backend perform canonical verification.
- 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.
- Forward to the primary backend and optionally a secondary endpoint.
- Return
2xxonly after durable enqueue/store succeeds. - 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
- Cold-inspected transaction (real
$12BSC payment on RN hosted page): in conversation history, dated 2026-05-27. - RN ERC20FeeProxy spec: https://github.com/RequestNetwork/requestNetwork/blob/master/packages/advanced-logic/specs/payment-network-erc20-fee-proxy-contract-0.1.0.md
- Arbitrum proxy deployment: https://github.com/RequestNetwork/payments-subgraph/blob/main/subgraph.arbitrum-one.yaml
- Architecture constraints doc:
01 - Architecture/Request Network Integration Constraints.md - Previous RN work:
PRD - Request Network Migration and Funds Management.md