Full-codebase-audit 2026-05-30 outputs: - Audit report: 09 - Audits/Full Codebase Audit - 2026-05-30.md - 81 issue files ISSUE-055..135 (decisions + 1 skipped no-brainer). - Scanner docs from scratch (was zero): architecture, data model, API ref, payment flow, operations runbook + repo README. - Doc-sync updates across API reference, data models, flows, design system. - Secret Rotation Runbook (08 - Operations) for the exposed credentials. - Reusable workflow guide (07 - Development) + .claude/workflows/full-codebase-audit.js. Issues remain status:open intentionally — the code fixes are uncommitted-then-committed working-tree changes per repo and aren't "resolved" until merged/deployed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.0 KiB
title, tags, created
| title | tags | created | |||
|---|---|---|---|---|---|
| Payment Flow - Scanner (In-House) |
|
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, Scanner API
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/scanner-callback",
"callbackSecret": "<per-intent HMAC secret stored in payment doc>",
"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:
eth_getLogsreturns aTransferWithReferenceAndFeelog matchingtopicRefvalidateLogMatchesIntentverifies token address, destination, and amount- Intent moves to
confirming; scanner waits for N blocks - Once
confirmationsRequiredblocks have been built on top, intent moves toconfirmed
Tron path:
- TronGrid
Transferevent matchesdestination(EVM-hex normalized) - Amount validated ≥ intent amount
- Intent goes directly to
confirmed(TronGrid returns only confirmed txs)
TON path:
- TonCenter Jetton transfer matches
destination(exact base64url) andjetton_master_address - Amount validated ≥ intent amount
- Intent goes directly to
confirmed
Step 5 — Webhook delivery
The scanner POSTs to callbackUrl with:
{
"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 |