# Cách giám sát thanh toán USDT / USDC bằng Webhook và RPC

> Source: https://docs.blockvectra.com/vi/guides/stablecoin-payments/

Để giám sát thanh toán stablecoin hoặc phát hiện tiền nạp sàn, hãy theo dõi các chuyển khoản ERC-20 USDT / USDC đến trên các chuỗi EVM bằng Webhook, log WebSocket hoặc thăm dò HTTP. Nhà phát triển và AI Agent dùng cùng các API; chọn chuỗi, hợp đồng token, người nhận và độ sâu xác nhận trước khi xử lý thanh toán. Chọn quy trình nạp tiền, thông báo người bán hoặc chi trả trong [giải pháp giám sát chuyển USDT / USDC](https://blockvectra.com/vi/use-cases/stablecoin-payments/).

* **Bước đầu tiên:** [Tạo đăng ký và theo dõi người nhận](#create-a-subscription-and-watch-the-recipient), bắt đầu với API key và bộ nhận HTTPS của bạn.
* **Hoàn thành khi:** Chuyển khoản khớp vượt qua kiểm tra chữ ký, chuỗi, token, người nhận và số tiền nguyên, được lưu một lần thành khoản thanh toán cần xác minh và bộ nhận trả HTTP `204`; xác minh trên chuỗi theo chính sách xác nhận của bạn trước khi ghi có.

[Quy trình thanh toán stablecoin](https://blockvectra.com/vi/use-cases/stablecoin-payments/).

Giám sát chuyển tiền cơ bản đã khả dụng. Việc lọc số tiền và token được thực hiện tại bộ nhận của bạn. Điều kiện phía máy chủ, nhiều giai đoạn xác nhận và cảnh báo IM sẽ sớm ra mắt.

Dành cho nhà phát triển và AI Agent: bắt đầu với [API key](https://console.blockvectra.com/login/?next=%2Fkeys%2F) và bộ nhận HTTPS của bạn; lọc hợp đồng token và số tiền trong ứng dụng. [Sao chép cấu hình Webhook](#create-a-subscription-and-watch-the-recipient).

## Các tác vụ hướng dẫn này giúp bạn hoàn thành

* [Nhận thông báo thanh toán USDT / USDC](#receive-payments-with-webhooks) tại endpoint HTTPS sau khi kiểm tra hỗ trợ Push cho chuỗi đã chọn.
* [Kiểm tra khoản chuyển tiền cần xác minh](#verify-deduplicate-and-validate-payments) bằng cách kiểm tra chuỗi, hợp đồng token, người nhận và số tiền nguyên trước khi áp dụng chính sách xác minh trên chuỗi và xác nhận của bạn.
* [Truy xuất bổ sung log chuyển tiền bị thiếu](#cursor-polling-and-block-range-limits) bằng truy vấn `eth_getLogs` có giới hạn và con trỏ đã lưu.

## Chọn Webhook, WebSocket hoặc thăm dò

| Phương thức                                      | Dùng cho                                                                   | Khôi phục                                                                                                         |
| ------------------------------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| [Webhook](https://docs.blockvectra.com/vi/guides/webhook-push/)              | Hoạt động địa chỉ gửi tới bộ nhận HTTPS, bao gồm chuyển token đến          | Xác minh chữ ký, loại trùng ID sự kiện và xử lý `subscription.gap` / `chain.reorg`; phát lại sự kiện khớp còn lưu |
| [WebSocket](https://docs.blockvectra.com/en/guides/websocket-subscriptions/) | `logs` đã lọc qua kết nối duy trì liên tục                                 | Kết nối lại, đăng ký lại và truy xuất bổ sung các khối bị bỏ lỡ                                                   |
| Thăm dò HTTP                                     | Giám sát theo lịch hoặc truy xuất bổ sung log lịch sử bằng con trỏ của bạn | Truy vấn các khoảng `eth_getLogs` có giới hạn và lưu tiến độ bền vững                                             |

Đọc `ws` và `subscriptions` trong phản hồi công khai `GET /v1/chains` trước khi chọn WebSocket. Hỗ trợ Push cần kiểm tra riêng: đọc `GET /v1/push/chains` bằng API key. Chuỗi không có WebSocket có thể dùng Webhook địa chỉ nếu có trong danh sách đó. Dùng thăm dò khi cần quét các khối trước đó hoặc chạy mà không duy trì kết nối liên tục.

## Nhận thanh toán bằng Webhook

### Tạo đăng ký và theo dõi người nhận

[Lấy API key](https://blockvectra.com/vi/get-api-key/) và triển khai bộ nhận HTTPS trên port 443. Chọn `CHAIN` từ danh sách chuỗi Push có xác thực, đặt `RECIPIENT` thành địa chỉ nạp tiền và `RECEIVER_URL` thành URL bộ nhận. Ví dụ shell này cần `jq`; `{}` dùng số xác nhận mặc định của chuỗi. Kiểm tra `min_confirmations`, `default_confirmations` và `max_confirmations` trước khi chọn số khác. [Push OpenAPI](https://docs.blockvectra.com/openapi/push.yaml) định nghĩa các yêu cầu này.

```bash
set -eu
umask 077
: "${BLOCKVECTRA_API_KEY:?Set your API key}"
: "${CHAIN:?Select a chain from the Push chain list}"
: "${RECIPIENT:?Set the watched EVM recipient address}"
: "${RECEIVER_URL:?Set your HTTPS receiver URL}"
PUSH_URL='https://api.blockvectra.com/v1/push'

curl --fail-with-body -sS "$PUSH_URL/chains" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" > push-chains.json
jq -e --arg chain "$CHAIN" 'any(.chains[]; .chain == $chain)' push-chains.json
jq -n --arg url "$RECEIVER_URL" --arg chain "$CHAIN" \
  '{url: $url, chains: {($chain): {}}}' > create.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' -d @create.json > subscription.json

SUBSCRIPTION_ID=$(jq -er '.id' subscription.json)
jq -n --arg recipient "$RECIPIENT" '{addresses: [$recipient]}' > addresses.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID/addresses/add" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY" \
  -H 'Content-Type: application/json' -d @addresses.json > address-change.json
curl --fail-with-body -sS "$PUSH_URL/subscriptions/$SUBSCRIPTION_ID" \
  -H "x-api-key: $BLOCKVECTRA_API_KEY"
```

Khi tạo, phản hồi trả `id` và `secret`. Lưu secret an toàn cho bộ nhận; `subscription.json` chứa thông tin xác thực. Thăm dò đăng ký cho đến khi `applied_version >= change_version` từ `address-change.json`, rồi ghi lại `chains[CHAIN].applied_from_block`. Địa chỉ mới bắt đầu khớp từ khối đó, vì vậy tiếp tục thăm dò cho mọi khoảng thanh toán trước đó.

### Xác minh, loại trùng và kiểm tra thanh toán

Lưu [hàm chữ ký body gốc](https://docs.blockvectra.com/vi/guides/webhook-push/#verify-signatures) thành `verify-push.js`. Bộ nhận bên dưới chấp nhận Web API `Request` trong Node.js và đọc byte gốc trước khi phân tích JSON. Tạo `secrets` dưới dạng `Map` ánh xạ chuỗi ID đăng ký tới secret đã lưu. Đặt cấu hình `expected` đáng tin cậy thành `{ chain, token, recipient, amountUnits }`: `token` là hợp đồng stablecoin đã xác minh trên chuỗi đó và `amountUnits` là số tiền nguyên dương dự kiến theo đơn vị nhỏ nhất. So sánh số tiền bằng `BigInt`, tuyệt đối không dùng số dấu phẩy động hoặc ký hiệu token.

```js
import { verifyPush } from './verify-push.js';

export function selectPayment(data, event, expected) {
  if (data.chain !== expected.chain || event.type !== 'token.transfer' ||
      event.standard !== 'erc20') return null;
  const address = value => typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value);
  if (![event.token, event.to, expected.token, expected.recipient].every(address)) return null;
  if (event.token.toLowerCase() !== expected.token.toLowerCase() ||
      event.to.toLowerCase() !== expected.recipient.toLowerCase()) return null;
  const integer = value => typeof value === 'string' && /^[1-9][0-9]{0,77}$/.test(value);
  if (!integer(event.amount) || !integer(expected.amountUnits)) return null;
  const amount = BigInt(event.amount);
  if (amount >= (1n << 256n) || amount !== BigInt(expected.amountUnits)) return null;
  if (typeof event.id !== 'string' || typeof event.ref !== 'string' ||
      !/^0x[0-9a-f]{64}$/.test(event.tx_hash) ||
      !/^0x[0-9a-f]{64}$/.test(event.block_hash) ||
      !Number.isSafeInteger(event.log_index) || event.log_index < 0 ||
      !Number.isSafeInteger(event.block_number) || event.block_number < 0) return null;
  return {
    eventId: event.id, ref: event.ref, chain: data.chain,
    token: event.token, recipient: event.to, amountUnits: event.amount,
    txHash: event.tx_hash, logIndex: event.log_index,
    blockHash: event.block_hash, blockNumber: event.block_number,
  };
}

export async function receivePayments(request, expected, secrets, store) {
  const rawBody = Buffer.from(await request.arrayBuffer());
  const headers = Object.fromEntries(request.headers);
  if (!verifyPush(rawBody, headers, secrets)) return new Response(null, { status: 401 });
  let message;
  try { message = JSON.parse(rawBody.toString('utf8')); }
  catch { return new Response(null, { status: 400 }); }
  const data = message?.data;
  if (message?.type !== 'push.events' ||
      !Number.isSafeInteger(data?.subscription_id) || data.subscription_id <= 0 ||
      String(data.subscription_id) !== headers['bv-subscription-id'] ||
      data.chain !== expected.chain || !Array.isArray(data.events)) {
    return new Response(null, { status: 400 });
  }
  try {
    await store.transaction(async tx => {
      for (const event of data.events) {
        if (!event || typeof event.id !== 'string') continue;
        const recovery = event.type === 'subscription.gap' || event.type === 'chain.reorg';
        const payment = selectPayment(data, event, expected);
        if (!recovery && !payment) continue;
        if (!await tx.insertEventOnce(data.subscription_id, event)) continue;
        if (recovery) await tx.enqueueRecovery(data.chain, event);
        else await tx.recordPaymentCandidate(payment);
      }
    });
  } catch {
    return new Response(null, { status: 503 });
  }
  return new Response(null, { status: 204 });
}
```

Triển khai `store.transaction` bằng lưu trữ bền vững. Trong một giao dịch, `insertEventOnce` chèn sự kiện theo khóa duy nhất `(subscription_id, event.id)` và trả false nếu trùng; commit cùng với `recordPaymentCandidate` hoặc `enqueueRecovery`. Hoàn tác toàn bộ ghi khi thất bại để lần thử lại có thể xử lý sự kiện. Tác vụ khôi phục cũng phải có tính lũy đẳng. Chỉ trả 2xx trong vòng 10 giây sau khi commit; áp dụng giới hạn body 1 MiB trên máy chủ HTTP.

Ví dụ này kiểm tra một số tiền thanh toán dự kiến. Với nhiều đơn hàng, tra cấu hình thanh toán đáng tin cậy theo chuỗi, token và người nhận, rồi đối chiếu thanh toán thiếu hoặc dư theo quy tắc của bạn. Khoản thanh toán cần xác minh vẫn phải được xác minh trên chuỗi và đáp ứng chính sách xác nhận trước khi ghi có. Giữa các đăng ký và thăm dò, đối chiếu cùng một chuyển khoản theo chuỗi, hash giao dịch và chỉ số log để hai đường gửi không ghi có hai lần; giữ hash khối để theo dõi khối bị thay thế.

### Khôi phục các khối bị thiếu hoặc thay thế

Với `subscription.gap`, đưa tác vụ quét từ `from_block` đến `to_block` vào hàng đợi bằng đường thăm dò bên dưới hoặc các bộ dữ liệu Data API khả dụng. `chain.reorg` là thông báo miễn phí rằng các khối đã gửi bị thay thế, không phải khoảng trống gửi sự kiện. Đánh dấu hoặc loại bỏ sự kiện cũ trong khoảng đó theo `ref`; đối chiếu bản ghi thanh toán theo `ref` và `tx_hash` với chuỗi chuẩn trước khi xử lý sự kiện chuẩn được tự động gửi lại với ID mới. Loại trùng các sự kiện đó theo `id`. Thông báo tái tổ chức không đẩy tiến độ đã hoàn thành; ghi `complete_through_block` theo từng chuỗi, tuyệt đối không suy ra hoàn thành từ số khối lớn nhất của sự kiện.

[Phát lại](https://docs.blockvectra.com/vi/guides/webhook-push/#delivery-retries-and-replay) chấp nhận `chain` và `from_block` trong giới hạn `replayable_from_block` hiện tại. Nó chỉ gửi lại sự kiện khớp còn lưu; không quét khoảng trước khi địa chỉ hoặc chuỗi được thêm, hay lúc đăng ký ngoại tuyến. Giữ con trỏ thăm dò để bao phủ các khoảng đó và khoảng trống đã hết thời gian lưu. Lỗi yêu cầu và khoảng phát lại không hợp lệ được mô tả trong [tham chiếu lỗi](https://docs.blockvectra.com/vi/errors/); phí gửi sự kiện, lịch sử và địa chỉ-ngày được giải thích trong [quy tắc thanh toán](https://docs.blockvectra.com/en/guides/billing-rules/#webhook-push-billing).

Các phần còn lại triển khai lọc log ERC-20 và thăm dò bằng con trỏ để giám sát và khôi phục.

## Sự kiện Transfer và tham số lọc

Hợp đồng token ERC-20 tiêu chuẩn phát sự kiện sau cho mỗi lần chuyển tiền:

```solidity
event Transfer(address indexed from, address indexed to, uint256 value);
```

Khi gọi `eth_getLogs`, truyền địa chỉ hợp đồng token và mảng `topics` để lọc log khớp:

| Tham số     | Giá trị                                                              | Mô tả                                                                                                                                                                                                                                        |
| ----------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`   | Địa chỉ hợp đồng token (hoặc mảng địa chỉ)                           | Địa chỉ hợp đồng stablecoin mục tiêu. Có thể chỉ định một địa chỉ (ví dụ BSC USDT `0x55d398326f99059fF775485246999027B3197955`, Base USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`), hoặc mảng địa chỉ để giám sát đồng thời nhiều token |
| `topics[0]` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | Hash chữ ký sự kiện: `keccak256("Transfer(address,address,uint256)")`                                                                                                                                                                        |
| `topics[1]` | `null`                                                               | Địa chỉ người gửi (`from`). Vì giám sát tiền nạp chấp nhận tiền từ bất kỳ ví người dùng nào, truyền `null` để khớp mọi người gửi                                                                                                             |
| `topics[2]` | Địa chỉ người nhận đệm số 0 thành 32 byte                            | Địa chỉ đích (`to`). Theo đặc tả log EVM, tham số địa chỉ `indexed` chiếm 32 byte (64 ký tự hex). Đệm bên trái địa chỉ người nhận 20 byte bằng 12 byte 0 (24 ký tự 0 thập lục phân) để tạo topic 32 byte.                                    |
| `fromBlock` | Khối bắt đầu (thập lục phân)                                         | Đầu khoảng khối truy vấn (bao gồm)                                                                                                                                                                                                           |
| `toBlock`   | Khối kết thúc (thập lục phân)                                        | Cuối khoảng khối truy vấn (bao gồm)                                                                                                                                                                                                          |

`value` không được đánh chỉ mục (số tiền chuyển) được mã hóa trong trường `data` của đối tượng log dưới dạng `uint256` thập lục phân 32 byte. Chia số tiền thô này cho 10^decimals để được số lượng token dễ đọc (ví dụ 18 chữ số thập phân cho BSC USDT; 6 chữ số cho Base và Ethereum USDC).

## Thăm dò bằng con trỏ và giới hạn khoảng khối

Dịch vụ thăm dò truy vấn khối mới theo chu kỳ đều đặn (chẳng hạn mỗi 3 đến 5 giây).

### Tiến con trỏ

Duy trì con trỏ bền vững `last_polled_block` (khối cao nhất đã xử lý và commit) trong cơ sở dữ liệu:

1. Với mỗi chu kỳ thăm dò, đặt `fromBlock = last_polled_block + 1`.
2. Truy vấn đầu chuỗi hiện tại bằng `eth_blockNumber` và tính độ cao mục tiêu an toàn `safe_head` theo độ sâu xác nhận.
3. Nếu `fromBlock <= safe_head`, truy vấn log theo từng phần đến `safe_head`. Sau khi xử lý thành công mỗi phần, tiến con trỏ.

### Giới hạn khoảng khối

Độ rộng khối của một lần gọi `eth_getLogs` được tính bằng `toBlock − fromBlock + 1`. Nó không được vượt quá `max_logs_block_range` công bố cho chuỗi đó trong `GET /v1/chains`.

Nếu yêu cầu vượt khoảng này, dịch vụ từ chối lệnh gọi với mã lỗi `-32602`:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "eth_getLogs block range too large: max 1000 blocks",
    "data": {
      "reason": "logs_range_too_large",
      "docs_url": "https://docs.blockvectra.com/en/errors/#logs_range_too_large",
      "retryable": false
    }
  }
}
```

Yêu cầu vượt khoảng khối trả lỗi JSON-RPC `-32602` (không tính phí). Trong logic ứng dụng, đọc `max_logs_block_range` từ `GET /v1/chains` và giới hạn mỗi phần thăm dò: `chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head)`.

## Xử lý tái tổ chức khối và độ sâu xác nhận

Gần đầu chuỗi, có thể xảy ra tái tổ chức khối tạm thời (reorg). Ghi có thanh toán tại `latest` mà không có độ sâu xác nhận có nguy cơ ghi có giao dịch trên nhánh khối mồ côi sau đó bị loại bỏ.

Áp dụng các biện pháp sau để bảo vệ việc xử lý thanh toán:

### Độ sâu xác nhận

Thay vì truy vấn đến `latest`, truy vấn đến độ cao khối mục tiêu an toàn:

`safe_head = current_head - CONFIRMATION_DEPTH`

Đặt `CONFIRMATION_DEPTH` theo mức chấp nhận rủi ro của ứng dụng. Chỉ truy vấn đến `safe_head` đảm bảo chỉ xử lý khối có đủ xác nhận.

### Tái tổ chức trong khi thăm dò

JSON-RPC EVM tiêu chuẩn chỉ đặt `removed: true` trên đối tượng log trong luồng đăng ký log WebSocket khi sự kiện đã phát bị đảo ngược do tái tổ chức chuỗi. Khi thăm dò qua HTTP bằng `eth_getLogs`, truy vấn trả log từ chuỗi chuẩn; log bị tái tổ chức đơn giản sẽ không xuất hiện trong các truy vấn tiếp theo. Thăm dò trong `safe_head` đảm bảo thanh toán chỉ được xử lý trên các khối đủ xác nhận.

## Loại trùng theo (transactionHash, logIndex)

Bộ theo dõi thanh toán phải bảo đảm tính lũy đẳng nghiêm ngặt:

1. **Nhiều chuyển khoản trong một giao dịch**: Một giao dịch có thể chứa nhiều sự kiện `Transfer` đến cùng địa chỉ nạp (ví dụ bộ định tuyến token chia giao dịch hoán đổi hoặc hợp đồng chi trả nhiều lần). **Quan trọng:** Chỉ riêng `transactionHash` không duy nhất cho từng khoản thanh toán.
2. **Thăm dò chồng lấn và thử lại**: Khi dịch vụ thăm dò khởi động lại, khôi phục sau lỗi mạng tạm thời hoặc lùi vài khối để xử lý tái tổ chức nông, log từ cùng khoảng khối được truy vấn nhiều lần.
3. **Tính duy nhất của chỉ số log**: `logIndex` xác định vị trí tương đối của log sự kiện trong khối. Theo đặc tả EVM, định danh duy nhất kết hợp chuẩn của sự kiện là `(transactionHash, logIndex)`.

Trong lược đồ cơ sở dữ liệu quan hệ, khai báo chỉ mục duy nhất kết hợp trên bảng bản ghi tiền nạp:

```sql
CREATE UNIQUE INDEX idx_transfers_tx_log ON deposit_records (transaction_hash, log_index);
```

Trước khi xử lý tiền nạp, đối chiếu các mục `(transactionHash, logIndex)` hiện có để bảo đảm mỗi chuyển khoản trên chuỗi chỉ được ghi có đúng một lần.

## Ví dụ mã hoàn chỉnh

Các ví dụ bên dưới minh họa việc lấy khả năng mạng từ `/v1/chains`, tính khoảng khối an toàn, thăm dò log `Transfer` stablecoin tuân thủ giới hạn khoảng và loại trùng sự kiện.

**TypeScript**

```ts
import { createPublicClient, formatUnits, http, parseAbiItem } from "viem";

const apiKey = process.env.BLOCKVECTRA_API_KEY;
if (!apiKey) {
  throw new Error("BLOCKVECTRA_API_KEY environment variable is not set");
}

const CHAIN = "bsc_mainnet";
const RPC_URL = "https://api.blockvectra.com/v1/bsc_mainnet";
const CHAINS_URL = "https://api.blockvectra.com/v1/chains";

// Target stablecoin contract address (BSC USDT used in this example)
const TOKEN_CONTRACT = "0x55d398326f99059fF775485246999027B3197955" as const;
const TOKEN_DECIMALS = 18;

// Monitored deposit address
const RECIPIENT_ADDRESS = "0xdded13D555B6DA811103cC1794D3d4330F69632C" as const;

// Confirmation depth to guard against chain reorgs
const CONFIRMATION_DEPTH = 15n;

// 1. Fetch chain capabilities from public metadata endpoint (unauthenticated, unbilled)
const chainsRes = await fetch(CHAINS_URL);
const { chains } = (await chainsRes.json()) as {
  chains: Array<{
    chain: string;
    ws: boolean;
    subscriptions: string[];
    max_logs_block_range: number;
  }>;
};

const chainConfig = chains.find((c) => c.chain === CHAIN);
if (!chainConfig) {
  throw new Error(`Chain ${CHAIN} not found in /v1/chains`);
}

const maxLogsRange = BigInt(chainConfig.max_logs_block_range || 1000);
console.log(`Chain: ${CHAIN} | WebSocket supported: ${chainConfig.ws} | Max logs range: ${maxLogsRange}`);

// 2. Initialize viem client with x-api-key header
const client = createPublicClient({
  transport: http(RPC_URL, {
    fetchOptions: {
      headers: { "x-api-key": apiKey },
    },
  }),
});

// Set to track processed events by composite key: (transactionHash, logIndex)
const processedLogs = new Set<string>();

// 3. Compute query range: subtract confirmation depth from current head
const currentHead = await client.getBlockNumber();
const safeHead = currentHead - CONFIRMATION_DEPTH;

// For demonstration, start cursor 10 blocks before safeHead
let cursor = safeHead > 10n ? safeHead - 10n : 0n;

console.log(`Current head: ${currentHead} | Safe head: ${safeHead} | Polling cursor: ${cursor}`);

while (cursor <= safeHead) {
  const chunkEnd = cursor + maxLogsRange - 1n < safeHead ? cursor + maxLogsRange - 1n : safeHead;

  const logs = await client.getLogs({
    address: TOKEN_CONTRACT,
    event: parseAbiItem(
      "event Transfer(address indexed from, address indexed to, uint256 value)"
    ),
    args: {
      to: RECIPIENT_ADDRESS,
    },
    fromBlock: cursor,
    toBlock: chunkEnd,
  });

  for (const log of logs) {
    const dedupKey = `${log.transactionHash}-${log.logIndex}`;
    if (processedLogs.has(dedupKey)) {
      continue;
    }
    processedLogs.add(dedupKey);

    const tokenAmount = formatUnits(log.args.value ?? 0n, TOKEN_DECIMALS);

    console.log(
      `[Payment Received] Amount: ${tokenAmount} | ` +
      `Tx: ${log.transactionHash} | Log: ${log.logIndex} | Block: ${log.blockNumber}`
    );
  }

  cursor = chunkEnd + 1n;
}

// Run with: npx tsx example.mts
```


  **Python**

```python
from decimal import Decimal
import os
import requests

api_key = os.environ.get("BLOCKVECTRA_API_KEY")
if not api_key:
    raise RuntimeError("BLOCKVECTRA_API_KEY environment variable is not set")

CHAIN = "bsc_mainnet"
RPC_URL = "https://api.blockvectra.com/v1/bsc_mainnet"
CHAINS_URL = "https://api.blockvectra.com/v1/chains"

# Target stablecoin contract address (BSC USDT used in this example)
TOKEN_CONTRACT = "0x55d398326f99059fF775485246999027B3197955"
TOKEN_DECIMALS = 18

# Monitored deposit address
RECIPIENT_ADDRESS = "0xdded13D555B6DA811103cC1794D3d4330F69632C"

# Transfer(address,address,uint256) signature hash
TRANSFER_TOPIC0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

# Left-pad 20-byte address to 32 bytes (64 hex characters)
padded_recipient = f"0x{RECIPIENT_ADDRESS.lower()[2:].rjust(64, '0')}"

# Confirmation depth to guard against chain reorgs
CONFIRMATION_DEPTH = 15

# 1. Fetch chain capabilities from public metadata endpoint (unauthenticated, unbilled)
chains_res = requests.get(CHAINS_URL, timeout=10)
chains_res.raise_for_status()
chain_list = chains_res.json().get("chains", [])

chain_config = next((c for c in chain_list if c["chain"] == CHAIN), None)
if not chain_config:
    raise RuntimeError(f"Chain {CHAIN} not found in /v1/chains")

max_logs_range = chain_config.get("max_logs_block_range", 1000)
ws_supported = chain_config.get("ws", False)
print(f"Chain: {CHAIN} | WebSocket supported: {ws_supported} | Max logs range: {max_logs_range}")

def rpc_request(method: str, params: list):
    res = requests.post(
        RPC_URL,
        headers={
            "Content-Type": "application/json",
            "x-api-key": api_key,
        },
        json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
        timeout=15,
    )
    res.raise_for_status()
    payload = res.json()
    if "error" in payload:
        err = payload["error"]
        raise RuntimeError(f"JSON-RPC error {err.get('code')}: {err.get('message')}")
    return payload["result"]

# 2. Query latest block number and calculate safe head
current_head_hex = rpc_request("eth_blockNumber", [])
current_head = int(current_head_hex, 16)
safe_head = max(0, current_head - CONFIRMATION_DEPTH)

# For demonstration, start cursor 10 blocks before safe_head
cursor = max(0, safe_head - 10)
print(f"Current head: {current_head} | Safe head: {safe_head} | Polling cursor: {cursor}")

# In-memory deduplication set using (transactionHash, logIndex)
processed_logs = set()

while cursor <= safe_head:
    chunk_end = min(cursor + max_logs_range - 1, safe_head)

    logs = rpc_request(
        "eth_getLogs",
        [
            {
                "address": TOKEN_CONTRACT,
                "fromBlock": hex(cursor),
                "toBlock": hex(chunk_end),
                "topics": [
                    TRANSFER_TOPIC0,
                    None,  # match any sender
                    padded_recipient,  # match monitored recipient
                ],
            }
        ],
    )

    for log in logs:
        tx_hash = log["transactionHash"]
        log_index = int(log["logIndex"], 16)
        dedup_key = (tx_hash, log_index)

        if dedup_key in processed_logs:
            continue
        processed_logs.add(dedup_key)

        raw_amount = int(log["data"], 16)
        token_amount = Decimal(raw_amount) / (Decimal(10) ** TOKEN_DECIMALS)
        block_number = int(log["blockNumber"], 16)

        print(
            f"[Payment Received] Amount: {token_amount} | "
            f"Tx: {tx_hash} | Log: {log_index} | Block: {block_number}"
        )

    cursor = chunk_end + 1

# Run with: python example.py
```


## Quy tắc thanh toán và hướng dẫn liên quan

* Để biết chi tiết về đo lường yêu cầu, trọng số CU và xác định tính phí theo mã lỗi, xem [Quy tắc thanh toán: lỗi và yêu cầu không tính phí](https://docs.blockvectra.com/en/guides/billing-rules/).
* Để tìm hiểu sâu về giới hạn khoảng khối `eth_getLogs` và logic chia phần, xem [Giới hạn khoảng khối eth\_getLogs và truy vấn theo phần](https://docs.blockvectra.com/vi/guides/getlogs-block-range/).
* Để biết khác biệt giữa truy vấn nút RPC thời gian thực và API chuyển tiền lịch sử được lập chỉ mục, xem [Đầu chuỗi và lịch sử được lập chỉ mục: khi nào dùng eth\_getLogs hay Transfers](https://docs.blockvectra.com/vi/guides/logs-vs-transfers/).

## Các bước tiếp theo

* [Duyệt danh mục bộ dữ liệu](https://blockvectra.com/vi/data/) để xem mọi bộ dữ liệu BlockVectra lập chỉ mục.
* [Xem gói miễn phí và giá](https://blockvectra.com/vi/pricing/#free) để kiểm tra những gì tài khoản bao gồm.
* [Đăng nhập bảng điều khiển](https://console.blockvectra.com/login/?next=%2Fkeys%2F) để tạo API key.
