# 如何用 Webhook 與 RPC 監控 USDT / USDC 收款

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

若要進行穩定幣收款監控或交易所充值偵測，請使用 Webhook、WebSocket 日誌或 HTTP 輪詢，監控 EVM 鏈上傳入的 ERC-20 USDT / USDC 轉帳。開發者與 AI Agent 使用同一套 API；處理收款前先選定鏈、代幣合約、收款地址與確認深度。在 [USDT / USDC 轉帳監控方案](https://blockvectra.com/zh-hant/use-cases/stablecoin-payments/) 中選擇充值、商戶通知或出款工作流。

* <strong>第一步：</strong>[建立訂閱並關注收款地址](#create-a-subscription-and-watch-the-recipient)，先備妥 API key 與你的 HTTPS 接收端。
* <strong>完成條件：</strong>一筆符合的轉帳通過簽章、鏈、代幣、收款地址與整數金額檢查，以收款候選只儲存一次，且接收端回傳 HTTP `204`；入帳前仍須依你的確認政策完成鏈上核驗。

[穩定幣收款工作流](https://blockvectra.com/zh-hant/use-cases/stablecoin-payments/)。

基礎轉帳監控已可用。金額與代幣篩選在你的接收端執行。伺服器端條件、多階段確認與 IM 提醒即將推出。

給開發者與 AI Agent：先備妥 [API key](https://console.blockvectra.com/login/?next=%2Fkeys%2F) 與自有 HTTPS 接收端；在應用程式中篩選代幣合約與金額。[複製 Webhook 設定](#create-a-subscription-and-watch-the-recipient)。

## 本指南協助你完成的任務

* [接收 USDT / USDC 收款通知](#receive-payments-with-webhooks)：先檢查所選鏈的 Push 支援，再於你的 HTTPS 端點接收。
* [驗證轉帳候選](#verify-deduplicate-and-validate-payments)：在套用你的鏈上核驗與確認政策之前，檢查其鏈、代幣合約、收款地址與整數金額。
* [回填缺失的轉帳日誌](#cursor-polling-and-block-range-limits)：使用有界 `eth_getLogs` 查詢與已儲存的游標。

## 選擇 Webhook、WebSocket 或輪詢

| 方式                                               | 適用於                           | 復原                                                            |
| ------------------------------------------------ | ----------------------------- | ------------------------------------------------------------- |
| [Webhook](https://docs.blockvectra.com/zh-hant/guides/webhook-push/)         | 送達你的 HTTPS 接收端的地址活動，包含傳入的代幣轉帳 | 驗證簽章、去重事件 ID，並處理 `subscription.gap` / `chain.reorg`；重放保留的符合事件 |
| [WebSocket](https://docs.blockvectra.com/en/guides/websocket-subscriptions/) | 透過持久連線的篩選後 `logs`             | 重新連線、重新訂閱並回填遺漏的區塊                                             |
| HTTP 輪詢                                          | 使用自有游標的排程監控或歷史日誌回填            | 查詢有界的 `eth_getLogs` 範圍並持久化進度                                  |

選擇 WebSocket 前，請先讀取公開 `GET /v1/chains` 回應中的 `ws` 與 `subscriptions`。Push 支援是另一項檢查：使用你的 API key 讀取 `GET /v1/push/chains`。沒有 WebSocket 的鏈，只要列在該清單中，仍可使用地址 Webhook。當你需要掃描較早的區塊，或在沒有持久連線的情況下執行時，請使用輪詢。

## 使用 Webhook 接收收款

### 建立訂閱並關注收款地址

[取得 API key](https://blockvectra.com/zh-hant/get-api-key/) 並在連接埠 443 部署 HTTPS 接收端。從認證的 Push 鏈清單中選擇 `CHAIN`，將 `RECIPIENT` 設為你的充值地址、`RECEIVER_URL` 設為你的接收端 URL。此 shell 範例需要 `jq`；`{}` 會使用該鏈的預設確認數。選擇不同確認數之前，請先檢查 `min_confirmations`、`default_confirmations` 與 `max_confirmations`。[Push OpenAPI](https://docs.blockvectra.com/openapi/push.yaml) 定義了這些請求。

```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"
```

建立後會回傳 `id` 與 `secret`。請為接收端安全儲存該密鑰；`subscription.json` 含有憑證。輪詢訂閱，直到 `applied_version >= change_version`（來自 `address-change.json`），然後記錄 `chains[CHAIN].applied_from_block`。新地址從該區塊開始比對，因此任何較早的收款區間仍須持續輪詢。

### 驗證、去重並核驗收款

將[原始主體簽章函式](https://docs.blockvectra.com/zh-hant/guides/webhook-push/#verify-signatures)儲存為 `verify-push.js`。以下接收端在 Node.js 中接受 Web API `Request`，並在 JSON 解析前讀取其原始位元組。將 `secrets` 建為訂閱 ID 字串到已儲存密鑰的 `Map`。將受信任的 `expected` 設定為 `{ chain, token, recipient, amountUnits }`：`token` 是該鏈上已核驗的穩定幣合約，`amountUnits` 是以其最小單位表示的預期正整數金額。使用 `BigInt` 比較金額，絕不要用浮點數或代幣符號。

```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 });
}
```

以持久化儲存實作 `store.transaction`。在單一交易中，`insertEventOnce` 會以唯一的 `(subscription_id, event.id)` 鍵插入事件，遇到重複時回傳 false；將它與 `recordPaymentCandidate` 或 `enqueueRecovery` 一起提交。失敗時回復所有寫入，讓重試能處理該事件。復原工作也必須具冪等性。只有在提交後才於 10 秒內回傳 2xx；請在你的 HTTP 伺服器強制 1 MiB 主體限制。

此範例只檢查一個預期收款金額。若有多筆訂單，請依鏈、代幣與收款地址查詢受信任的收款設定，並依你自己的規則對帳部分或超額付款。候選項目在入帳前仍需鏈上核驗與你的確認政策。跨訂閱與輪詢時，請依鏈、交易雜湊與日誌索引對帳同一筆轉帳，以免兩條送達路徑重複入帳；保留區塊雜湊以追蹤被替換的區塊。

### 復原缺失或被替換的區塊

對於 `subscription.gap`，請使用下方的輪詢路徑或可用的 Data API 資料集，排程掃描 `from_block` 到 `to_block`。`chain.reorg` 是免費通知，表示已送達的區塊被替換，而不是送達缺口。依 `ref` 標記或丟棄該範圍內的舊事件；在處理以新 ID 自動重新送達的規範鏈事件之前，先依 `ref` 與 `tx_hash` 將收款記錄與規範鏈對帳。按 `id` 去重這些事件。重組通知不會推進已完成進度；請按鏈記錄 `complete_through_block`，絕不要從最大的事件區塊號推斷完成。

[重放](https://docs.blockvectra.com/zh-hant/guides/webhook-push/#delivery-retries-and-replay) 接受目前 `replayable_from_block` 邊界內的 `chain` 與 `from_block`。它只重新送出保留的符合事件；不會掃描地址或鏈加入之前的期間，或訂閱離線期間。請保留輪詢游標以涵蓋那些區間與已過期的缺口。請求失敗與無效的重放範圍見[錯誤參考](https://docs.blockvectra.com/zh-hant/errors/)；送達、歷史與地址日費用見[計費規則](https://docs.blockvectra.com/en/guides/billing-rules/#webhook-push-billing)。

其餘章節將實作 ERC-20 日誌篩選與以游標為基礎的輪詢，用於監控與復原。

## Transfer 事件與篩選參數

標準 ERC-20 代幣合約在每次轉帳時發出以下事件：

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

呼叫 `eth_getLogs` 時，傳入代幣合約地址與 `topics` 陣列以篩選符合的日誌：

| 參數          | 值                                                                    | 說明                                                                                                                                                    |
| ----------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`   | 代幣合約地址（或地址陣列）                                                        | 目標穩定幣合約地址。你可以指定單一地址（例如 BSC USDT `0x55d398326f99059fF775485246999027B3197955`、Base USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`），或指定地址陣列以同時監控多個代幣 |
| `topics[0]` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | 事件簽章雜湊：`keccak256("Transfer(address,address,uint256)")`                                                                                               |
| `topics[1]` | `null`                                                               | 發送方地址（`from`）。由於充值監控接受來自任何使用者錢包的資金，傳入 `null` 即可符合任何發送方                                                                                                |
| `topics[2]` | 32 位元組零填充的收款地址                                                       | 目的地址（`to`）。依 EVM 日誌規格，`indexed` 地址參數佔用 32 位元組（64 個十六進位字元）。將 20 位元組的收款地址左側填充 12 個零位元組（24 個十六進位零字元），以形成 32 位元組的 topic。                                  |
| `fromBlock` | 起始區塊（十六進位）                                                           | 查詢區塊範圍的開始（含）                                                                                                                                          |
| `toBlock`   | 結束區塊（十六進位）                                                           | 查詢區塊範圍的結束（含）                                                                                                                                          |

未索引的 `value`（轉帳金額）會以 32 位元組十六進位 `uint256` 編碼在日誌物件的 `data` 欄位中。將此原始金額除以 10^decimals，即可得到人類可讀的代幣金額（例如 BSC USDT 為 18 位小數；Base 與以太坊 USDC 為 6 位小數）。

## 游標輪詢與區塊範圍限制

輪詢服務會以固定間隔查詢新區塊（例如每 3 到 5 秒）。

### 游標推進

在你的資料庫中維護持久游標 `last_polled_block`（已處理並提交的最高區塊）：

1. 在每個輪詢週期，設定 `fromBlock = last_polled_block + 1`。
2. 透過 `eth_blockNumber` 查詢目前鏈頭，並依你的確認深度計算安全目標高度 `safe_head`。
3. 如果 `fromBlock <= safe_head`，以分批方式查詢日誌直到 `safe_head`。成功處理每個批次後，推進游標。

### 區塊範圍限制

單次 `eth_getLogs` 呼叫的區塊跨度計算為 `toBlock − fromBlock + 1`。它不得超過 `GET /v1/chains` 中為該鏈發布的 `max_logs_block_range`。

如果請求超過此範圍，服務會以錯誤碼 `-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
    }
  }
}
```

超過區塊範圍的請求會回傳 JSON-RPC 錯誤 `-32602`（不計費）。在你的應用程式邏輯中，從 `GET /v1/chains` 讀取 `max_logs_block_range`，並對每個輪詢切片進行箝制：`chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head)`。

## 處理區塊重組與確認深度

在區塊鏈頂端附近，可能發生暫時性的區塊重組（reorg）。在沒有確認深度的情況下於 `latest` 入帳收款，會有將交易入帳到隨後被丟棄的孤支上的風險。

請套用以下防護措施來保護收款處理：

### 確認深度

不要查詢到 `latest`，而是查詢到安全目標區塊高度：

`safe_head = current_head - CONFIRMATION_DEPTH`

依你的應用程式風險承受度設定 `CONFIRMATION_DEPTH`。只查詢到 `safe_head` 可確保只處理具有足夠確認數的區塊。

### 輪詢期間的重組

標準 EVM JSON-RPC 只在 WebSocket 日誌訂閱串流中，當先前發出的事件因鏈重組而被回復時，才會在日誌物件上設定 `removed: true`。透過 HTTP 使用 `eth_getLogs` 輪詢時，查詢會回傳規範鏈的日誌；被重組的日誌只會不再出現在後續查詢中。在 `safe_head` 內輪詢可確保收款只在足夠確認的區塊上處理。

## 依 (transactionHash, logIndex) 去重

收款監聽器必須強制嚴格的冪等性：

1. **單一交易中的多筆轉帳**：單一交易可以包含多個發往同一充值地址的 `Transfer` 事件（例如拆分交換的代幣路由器或多筆出款合約）。<strong>重要：</strong>單憑 `transactionHash` 無法唯一識別一筆收款。
2. **重疊的輪詢與重試**：當輪詢服務重新啟動、從暫時性網路錯誤中復原，或回溯數個區塊以處理淺層重組時，同一區塊範圍的日誌會被查詢多次。
3. **日誌索引唯一性**：`logIndex` 標識事件日誌在區塊內的相對位置。依 EVM 規格，事件的規範複合唯一識別碼是 `(transactionHash, logIndex)`。

在關聯式資料庫結構定義中，請在充值記錄資料表上宣告複合唯一索引：

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

處理充值前，請比對現有的 `(transactionHash, logIndex)` 項目，以確保每筆鏈上轉帳只入帳一次。

## 完整程式碼範例

以下範例示範如何從 `/v1/chains` 取得網路能力、計算安全區塊範圍、在符合範圍限制下輪詢穩定幣 `Transfer` 日誌，並去重事件。

**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
```


## 計費規則與相關指南

* 關於請求計量、CU 權重與錯誤碼計費判定的詳細資訊，請參閱[計費規則：錯誤與不計費請求](https://docs.blockvectra.com/en/guides/billing-rules/)。
* 關於 `eth_getLogs` 區塊範圍限制與分段邏輯的深入指引，請參閱 [eth\_getLogs 區塊範圍限制與分段查詢](https://docs.blockvectra.com/zh-hant/guides/getlogs-block-range/)。
* 關於即時 RPC 節點查詢與已索引歷史轉帳 API 的差異，請參閱[鏈頭與已索引歷史：何時使用 eth\_getLogs 與轉帳](https://docs.blockvectra.com/zh-hant/guides/logs-vs-transfers/)。

## 下一步

* [瀏覽資料集目錄](https://blockvectra.com/zh-hant/data/)，查看 BlockVectra 索引的全部資料集。
* [查看免費方案與定價](https://blockvectra.com/zh-hant/pricing/#free)，確認你的帳戶包含哪些內容。
* [登入控制台](https://console.blockvectra.com/login/?next=%2Fkeys%2F)以建立 API key。
