--- title: Payment Flow - Scanner (In-House) tags: [flow, scanner, payment] created: 2026-05-30 --- # Payment Flow — AMN Pay Scanner (In-House) End-to-end payment flow using the in-house AMN Pay Scanner, replacing the Request Network integration. The scanner is a separate microservice; the backend talks to it over an internal HTTP API. See also: [Scanner Architecture](../01%20-%20Architecture/Scanner%20Architecture.md), [Scanner API](../03%20-%20API%20Reference/Scanner%20API.md) --- ## 1. High-level sequence ``` Buyer Backend Scanner Chain │ │ │ │ │ initiate payment │ │ │ │────────────────────►│ │ │ │ │ POST /intents │ │ │ │───────────────────►│ │ │ │ 200 checkoutBlock │ │ │ │◄───────────────────│ │ │ checkoutBlock │ │ │ │◄────────────────────│ │ │ │ │ │ │ │ sign + submit tx ──────────────────────────────────────►│ │ │ │ (polling) │ │ │ │◄────────────────│ │ │ │ log matched │ │ │ │ confirmations… │ │ │◄───────────────────│ │ │ │ POST callbackUrl │ │ │ │ (webhook) │ │ │ │ │ │ │ payment confirmed │ │ │ │◄────────────────────│ │ │ ``` --- ## 2. Step-by-step ### Step 1 — Backend creates an intent When the buyer chooses a payment method (e.g. USDT on BSC), the backend calls: ``` POST http://scanner:8080/intents Authorization: Bearer { "intentId": "", "chainId": 56, "tokenAddress": "0x55d398326f99059ff775485246999027b3197955", "destination": "0xSellerWalletAddress", "amount": "10000000000000000000", "callbackUrl": "https://api.amn.gg/api/payment/scanner-callback", "callbackSecret": "", "confirmations": 12 } ``` The scanner responds with a `checkoutBlock` that the backend passes to the frontend. ### Step 2 — Frontend shows checkout The `checkoutBlock` contains everything the frontend needs to build the `ERC20FeeProxy.transferWithReferenceAndFee` calldata: | Field | Used for | |---|---| | `proxyAddress` | contract to call | | `tokenAddress` | ERC20 token | | `destination` | `_to` param | | `paymentReference` | `_paymentReference` param (8-byte reference) | | `amountWei` | `_amount` param | | `feeAmount` | `_feeAmount` param (always `"0"` currently) | | `feeAddress` | `_feeAddress` param (always dead address) | For Tron/TON the buyer sends a plain TRC20/Jetton transfer to `destination`; there is no proxy contract. ### Step 3 — Buyer submits transaction The buyer signs and broadcasts the transaction using their wallet. The scanner independently monitors the chain and does not require the transaction hash. ### Step 4 — Scanner detects and confirms **EVM path:** 1. `eth_getLogs` returns a `TransferWithReferenceAndFee` log matching `topicRef` 2. `validateLogMatchesIntent` verifies token address, destination, and amount 3. Intent moves to `confirming`; scanner waits for N blocks 4. Once `confirmationsRequired` blocks have been built on top, intent moves to `confirmed` **Tron path:** 1. TronGrid `Transfer` event matches `destination` (EVM-hex normalized) 2. Amount validated ≥ intent amount 3. Intent goes directly to `confirmed` (TronGrid returns only confirmed txs) **TON path:** 1. TonCenter Jetton transfer matches `destination` (exact base64url) and `jetton_master_address` 2. Amount validated ≥ intent amount 3. Intent goes directly to `confirmed` ### Step 5 — Webhook delivery The scanner POSTs to `callbackUrl` with: ```json { "intentId": "...", "paymentReference": "0x...", "txHash": "0x...", "blockNumber": 39000010, "amount": "10000000000000000000", "token": "0x55d...", "chainId": 56, "status": "confirmed" } ``` Header `X-AMN-Signature` = `HMAC-SHA256(body, callbackSecret)`. The backend verifies the signature, matches the intentId to a Payment record, and marks it paid. ### Step 6 — Backend acknowledges Backend returns a 2xx response. Scanner records `webhook_delivered_at` and the intent lifecycle ends. --- ## 3. Failure paths ### Webhook delivery failure If the backend returns non-2xx or is unreachable, the scanner retries: ``` attempt 1: after 5 s attempt 2: after 30 s attempt 3: after 2 min attempt 4: after 10 min attempt 5: after 1 h → status = webhook_failed ``` `webhook_failed` intents are retried every `WEBHOOK_RETRY_HOURS` (default 6 h) and on `POST /admin/webhooks/retry`. On startup the scanner reconciles any `confirmed` intents with `webhook_delivered_at IS NULL` (crash recovery). ### Intent expiry Intents in `pending` or `confirming` status older than `INTENT_TTL_HOURS` (default 24 h) are moved to `expired` by a background ticker running every hour. `confirming` intents can get stuck if a transaction is deep-reorganised and never re-included; the TTL frees the destination address for reuse. ### Amount underpayment Transfers where the on-chain amount is less than `intent.Amount` are silently skipped. The intent remains `pending` until the TTL. ### Wrong token or destination The EVM log decoder validates all three fields (token, destination, amount). Mismatches are logged as `REJECT` and skipped. The intent remains `pending`. --- ## 4. Key differences from Request Network integration | Dimension | Request Network | AMN Pay Scanner | |---|---|---| | Dependency | RN SDK + API | None (direct RPC) | | Payment reference | RN-generated | Internal HMAC derivation | | EVM matching | By reference hash (RN) | By Topics[1] / topicRef (indexed) | | Tron | Not supported | TRC20 Transfer events via TronGrid | | TON | Not supported | Jetton transfers via TonCenter v3 | | Confirmations | RN handled | Per-chain configurable | | Webhook | RN webhook → backend adapter | Scanner → backend directly | | State store | External (RN cloud) | Internal SQLite |