183 lines
7.7 KiB
Markdown
183 lines
7.7 KiB
Markdown
---
|
|
title: Payment Flow - Scanner (In-House)
|
|
tags: [flow, scanner, payment]
|
|
created: 2026-05-30
|
|
---
|
|
|
|
# Payment Flow — AMN Pay Scanner (In-House)
|
|
|
|
> **Last updated:** 2026-05-31 — documented backend `2.6.82` / scanner `0.1.7` capped accepted confirmation floors.
|
|
|
|
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 <SCANNER_API_KEY>
|
|
|
|
{
|
|
"intentId": "<payment._id>",
|
|
"chainId": 56,
|
|
"tokenAddress": "0x55d398326f99059ff775485246999027b3197955",
|
|
"destination": "0xSellerWalletAddress",
|
|
"amount": "10000000000000000000",
|
|
"callbackUrl": "https://api.amn.gg/api/payment/amn-scanner/webhook",
|
|
"callbackSecret": "<per-intent HMAC secret stored in payment doc>",
|
|
"confirmations": 200
|
|
}
|
|
```
|
|
|
|
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`. The scanner stores and reports the accepted threshold count, not an ever-growing live count.
|
|
|
|
**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,
|
|
"confirmations": 200,
|
|
"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. Backend `2.6.82+` treats scanner `status: "confirmed"` as final enough to run Transaction Safety Provider checks and persist `blockchain.confirmations`. The stored confirmation count comes from verifier evidence first, then the webhook payload, then the configured per-chain threshold fallback, but settled counts are capped at the accepted threshold so the UI can show values like `200+` instead of chasing the live chain height forever.
|
|
|
|
### 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 |
|