Files
nick-doc/03 - API Reference/Scanner API.md
2026-05-31 15:21:28 +04:00

7.0 KiB

title, tags, created
title tags created
Scanner API
api
scanner
payment
2026-05-30

Scanner API

HTTP API exposed by the AMN Pay Scanner microservice. Default port: 8080.

All endpoints except /health require Authorization: Bearer <SCANNER_API_KEY> when the key is configured in the environment (production). In dev mode (key not set) all requests are allowed.

Base URL (dev): http://localhost:8080


Authentication

Authorization: Bearer <SCANNER_API_KEY>
  • Uses constant-time comparison to prevent timing attacks.
  • Returns 401 {"error":"unauthorized"} on failure.
  • /health is explicitly excluded from auth — always open.

POST /intents

Register a new payment intent. The scanner will watch the specified chain for a matching transfer and call back to callbackUrl when confirmed.

Request body (application/json):

Field Type Required Notes
intentId string yes Caller-supplied unique ID (UUID recommended)
chainId integer yes Numeric chain ID (e.g. 56, 137, 728126428)
tokenAddress string yes Token contract address. EVM/Tron: lowercase 0x hex. TON: exact base64url or raw format
destination string yes Receiving wallet address. EVM/Tron: 0x hex. TON: base64url
amount string yes Amount in smallest unit (wei / token decimals) as a base-10 integer string
callbackUrl string yes URL the scanner POSTs to on confirmation
callbackSecret string yes HMAC key for X-AMN-Signature verification
confirmations integer no Requested confirmation count. The scanner raises it to the chain acceptance floor if lower.

Example request:

{
  "intentId": "a1b2c3d4-...",
  "chainId": 56,
  "tokenAddress": "0x55d398326f99059ff775485246999027b3197955",
  "destination": "0xAbCd1234...",
  "amount": "10000000000000000000",
  "callbackUrl": "https://api.amn.gg/api/payment/amn-scanner/webhook",
  "callbackSecret": "abc123...",
  "confirmations": 200
}

Response 200 OK:

{
  "intentId": "a1b2c3d4-...",
  "paymentReference": "0x1a2b3c4d5e6f7a8b",
  "checkoutBlock": {
    "destination": "0xabcd1234...",
    "tokenAddress": "0x55d398326f99059ff775485246999027b3197955",
    "tokenSymbol": "USDT",
    "decimals": 18,
    "chainId": 56,
    "proxyAddress": "0x0DfbEe143b42B41eFC5A6F87bFD1fFC78c2f0aC9",
    "paymentReference": "0x1a2b3c4d5e6f7a8b",
    "feeAmount": "0",
    "feeAddress": "0x000000000000000000000000000000000000dEaD",
    "amountWei": "10000000000000000000"
  }
}

Confirmation floor: Built-in accepted thresholds are currently BSC 200, Ethereum 50, Polygon 300, Arbitrum 2400, Base 300, Tron 200, and TON 120. Callers may raise a requirement but cannot lower an intent below the chain floor.

Idempotency: If intentId already exists the existing intent's checkout block is returned (no error).

Error cases:

Status Body Cause
400 {"error":"intentId is required"} Missing field
400 {"error":"amount must be a positive integer string (base-10 wei)"} Non-numeric or zero amount
400 {"error":"unsupported chainId: 999"} Chain not in supported-chains.json
500 {"error":"internal error"} DB write failure

GET /intents/{intentId}

Fetch the current state of a payment intent.

Response 200 OK: Full Intent object (see Data Models below).

callbackSecret is excluded from the response regardless of auth state.

Error cases:

Status Body Cause
404 {"error":"intent not found"} Unknown intentId

GET /scanner/status

Returns scan progress for all verified chains.

Response 200 OK:

{
  "chains": [
    {
      "chainId": 56,
      "name": "BSC",
      "chainType": "evm",
      "lastScannedBlock": 39000000,
      "chainHead": 39000015,
      "lag": 15,
      "pendingIntents": 3
    },
    {
      "chainId": 728126428,
      "name": "TRX",
      "chainType": "tron",
      "lastScannedBlock": 1748500000000,
      "chainHead": 1748500015000,
      "lag": 15000,
      "pendingIntents": 1
    },
    {
      "chainId": 1100,
      "name": "TON",
      "chainType": "ton",
      "lastScannedBlock": 1748500000,
      "chainHead": 1748500015,
      "lag": 15,
      "pendingIntents": 0
    }
  ]
}

Note on lag units: For EVM and Tron chains, lag is in blocks (or ms-timestamp difference). For TON, lag is in seconds (Unix timestamps).


POST /admin/webhooks/retry

Immediately trigger a re-delivery attempt for all webhook_failed intents. Normally the scanner retries automatically every WEBHOOK_RETRY_HOURS; this endpoint forces an immediate pass.

Response 200 OK:

{ "queued": 2 }

Each retry is dispatched in a separate goroutine. Success resets the intent status to confirmed and records webhook_delivered_at.


GET /health

Health check. No authentication required.

Response 200 OK:

{ "status": "ok", "time": "2026-05-30T12:00:00Z" }

Used by Docker HEALTHCHECK and upstream load balancers / Gatus monitoring.


Webhook delivery (outbound)

When an intent is confirmed the scanner POSTs to callbackUrl:

Headers:

Header Value
Content-Type application/json
X-AMN-Signature hex(HMAC-SHA256(body, callbackSecret))
X-AMN-Delivery-ID intentId
X-AMN-Retry true (only on manual retry via /admin/webhooks/retry)

Body:

{
  "intentId": "a1b2c3d4-...",
  "paymentReference": "0x1a2b3c4d5e6f7a8b",
  "txHash": "0xdeadbeef...",
  "blockNumber": 39000010,
  "confirmations": 200,
  "amount": "10000000000000000000",
  "token": "0x55d398326f99059ff775485246999027b3197955",
  "chainId": 56,
  "status": "confirmed"
}

confirmations is the accepted confirmation count. Once the intent is confirmed, the scanner caps this value at confirmationsRequired; it does not keep reporting a live, ever-growing block count.

Retry schedule (on non-2xx or network error): 5 s → 30 s → 2 min → 10 min → 1 h → webhook_failed.

The backend should verify X-AMN-Signature to reject forged callbacks:

const expected = createHmac('sha256', callbackSecret).update(rawBody).digest('hex');
if (!timingSafeEqual(Buffer.from(received), Buffer.from(expected))) reject();

Data models

Intent object

{
  "intentId": "string",
  "chainId": 56,
  "chainType": "evm",
  "tokenAddress": "0x...",
  "destination": "0x...",
  "amount": "10000000000000000000",
  "paymentReference": "0x1a2b3c4d",
  "topicRef": "0xdeadbeef...",
  "status": "pending | confirming | confirmed | expired | webhook_failed",
  "confirmationsRequired": 200,
  "txHash": null,
  "logIndex": null,
  "blockNumber": null,
  "confirmations": 0,
  "salt": "hex64chars",
  "webhookDeliveredAt": null,
  "createdAt": "2026-05-30T10:00:00Z",
  "updatedAt": "2026-05-30T10:00:00Z"
}

Note: callbackUrl and callbackSecret are present in the DB but callbackSecret is always omitted from API responses.