# Webhook と RPC を使用して USDT / USDC の支払いを監視する方法

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

ステーブルコイン決済の監視や取引所の入金検知を行うには、Webhook、WebSocket ログ、または HTTP ポーリングを使用して、EVM チェーン上の ERC-20 USDT / USDC の受取送金を監視します。開発者と AI エージェントは同じ API を使用します。支払いを処理する前に、チェーン、トークンコントラクト、受取先、および確認深度を選択してください。[USDT / USDC 送金監視ソリューション](https://blockvectra.com/en/use-cases/stablecoin-payments/) で、入金、店舗通知、または出金のワークフローを選択してください。

基本的な送金監視が利用可能です。金額とトークンのフィルタリングはレシーバーで実行されます。サーバー側の条件設定、複数段階の確認、および IM 通知は近日提供予定です。

開発者および AI エージェント向け：まずは [API key](https://console.blockvectra.com/login/?next=%2Fkeys%2F) と独自の HTTPS レシーバーを用意し、アプリケーション側でトークンコントラクトと金額をフィルタリングします。[Webhook の設定をコピー](#create-a-subscription-and-watch-the-recipient)。

## このガイドで達成できること

* 選択したチェーンの Push サポートを確認した上で、HTTPS エンドポイントで [USDT / USDC の支払い通知を受信](#receive-payments-with-webhooks) します。
* オンチェーン検証および確認ポリシーを適用する前に、チェーン、トークンコントラクト、受取先、および整数金額を確認して、[送金候補レコードを検証](#verify-deduplicate-and-validate-payments) します。
* 有界な `eth_getLogs` クエリと保存されたカーソルを使用して、[欠落した送金ログをバックフィル](#cursor-polling-and-block-range-limits) します。

## Webhook、WebSocket、またはポーリングの選択

| 方式                                               | 主な用途                                | リカバリ                                                                          |
| ------------------------------------------------ | ----------------------------------- | ----------------------------------------------------------------------------- |
| [Webhook](https://docs.blockvectra.com/en/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/en/get-api-key/)し、ポート 443 で HTTPS レシーバーをデプロイします。認証付きの Push チェーン一覧から `CHAIN` を選択し、`RECIPIENT` に入金先アドレスを、`RECEIVER_URL` にレシーバー URL を設定します。このシェルスクリプトの例では `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` には認証情報が含まれます。`address-change.json` の `applied_version >= change_version` になるまでサブスクリプションをポーリングし、`chains[CHAIN].applied_from_block` を記録します。新しいアドレスはそのブロックから一致するため、それより前の支払い期間についてはポーリングを継続してください。

### 支払いの検証、重複排除、および妥当性確認

[raw-body 署名関数](https://docs.blockvectra.com/en/guides/webhook-push/#verify-signatures)を `verify-push.js` として保存します。以下のレシーバーは Node.js で Web API の `Request` を受け取り、JSON 解析の前にその元のバイト列を読み取ります。サブスクリプション ID 文字列から保存されたシークレットへの `Map` として `secrets` を構築します。信頼できる `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` は永続ストレージを使用して実装してください。1 つのトランザクション内で、`insertEventOnce` は一意の `(subscription_id, event.id)` キーの下にイベントを挿入し、重複の場合は false を返します。これを `recordPaymentCandidate` または `enqueueRecovery` と一緒にコミットします。失敗した場合はすべての書き込みをロールバックして、再試行時にイベントを処理できるようにします。リカバリジョブも冪等でなければなりません。コミット後にのみ 10 秒以内に 2xx を返します。HTTP サーバーで 1 MiB のボディ制限を適用してください。

この例では、1 つの期待される支払い金額をチェックします。複数の注文がある場合は、チェーン、トークン、および受取先によって信頼できる支払い設定を検索し、独自のルールに基づいて部分的な支払いまたは超過支払いを調整します。候補レコードであっても、残高へ反映する前にオンチェーンでの検証と確認ポリシーの適用が必要です。サブスクリプションとポーリングの両方において、2 つの配信パスが同じ送金を二重に反映しないよう、チェーン、トランザクションハッシュ、およびログインデックスによって同一の送金を照合します。置き換えられたブロックを追跡するためにブロックハッシュを保持してください。

### 欠落または置き換えられたブロックのリカバリ

`subscription.gap` の場合、以下のポーリングパスまたは利用可能な Data API データセットを使用して、`from_block` から `to_block` までのスキャンをキューに入れます。`chain.reorg` は、配信されたブロックが置き換えられたことを示す無料の通知であり、配信ギャップではありません。その範囲内の古いイベントを `ref` でマークまたは破棄します。新しい ID で自動的に再配信される正規チェーンのイベントを処理する前に、`ref` と `tx_hash` によって支払い記録を正規チェーンと照合します。これらのイベントは `id` で重複排除します。reorg 通知によって完了済みの進捗が進むことはありません。チェーンごとに `complete_through_block` を記録し、最大のイベントブロック番号から完了を推測しないでください。

[リプレイ](https://docs.blockvectra.com/en/guides/webhook-push/#delivery-retries-and-replay)は、現在の `replayable_from_block` 境界内の `chain` と `from_block` を受け付けます。保持されている一致イベントのみを再送信し、アドレスまたはチェーンが追加される前の期間や、サブスクリプションがオフラインだった期間はスキャンしません。それらの期間や期限切れのギャップをカバーするために、ポーリングカーソルを維持してください。リクエストの失敗と無効なリプレイ範囲は[エラーリファレンス](https://docs.blockvectra.com/en/errors/)で説明されています。配信、履歴、およびアドレス日数の料金は[請求ルール](https://docs.blockvectra.com/en/guides/billing-rules/#webhook-push-billing)で説明されています。

残りのセクションでは、監視とリカバリのための ERC-20 ログフィルタリングとカーソルベースのポーリングを実装します。

## 送金イベントとフィルタパラメータ

標準の 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`（送金額）は、ログオブジェクトの `data` フィールドに 32 バイトの十六進数 `uint256` としてエンコードされています。この生の金額を 10^decimals で割ることで、可読性のあるトークン金額が得られます（例：BSC USDT の場合は 18 桁、Base および Ethereum 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）と確認深度の処理

ブロックチェーンの先端付近では、一時的なブロック再編成（reorg）が発生する可能性があります。確認深度を設けずに `latest` で支払いを反映すると、その後破棄される孤立したブランチ上のトランザクションを反映してしまうリスクがあります。

支払い処理を保護するために、以下のセーフガードを適用してください：

### 確認深度

`latest` までクエリするのではなく、安全な目標ブロック高までクエリします：

`safe_head = current_head - CONFIRMATION_DEPTH`

アプリケーションのリスク許容度に応じて `CONFIRMATION_DEPTH` を設定します。`safe_head` までのみクエリすることで、十分な確認数を持つブロックのみが処理されるようになります。

### ポーリング中の再編成

標準の EVM JSON-RPC は、以前に発行されたイベントがチェーンの reorg によって元に戻された場合、WebSocket ログ購読ストリームでのみログオブジェクトに `removed: true` を設定します。`eth_getLogs` を使用して HTTP 経由でポーリングする場合、クエリは正規チェーンからのログを返します。reorg されたログは、その後のクエリに単に現れなくなります。`safe_head` 内でポーリングすることで、十分に確認されたブロックでのみ支払いが処理されます。

## (transactionHash, logIndex) による重複排除

支払いリスナーは厳密な冪等性を適用する必要があります：

1. **1 つのトランザクション内での複数の送金**：単一のトランザクションに、同じ入金先アドレスへの複数の `Transfer` イベントが含まれる場合があります（たとえば、スワップを分割するトークンルーターや複数の支払いを実行するコントラクト）。**重要：** `transactionHash` だけでは支払いごとに一意ではありません。
2. **重複するポーリングと再試行**：ポーリングサービスが再起動した場合、一時的なネットワークエラーから回復した場合、または浅い reorg を処理するために数ブロック巻き戻した場合、同じブロック範囲のログが複数回クエリされます。
3. **ログインデックスの一意性**：`logIndex` は、ブロック内でのイベントログの相対位置を識別します。EVM の仕様では、イベントの正規の複合一意識別子は `(transactionHash, logIndex)` です。

リレーショナルデータベースのスキーマでは、入金記録テーブルに複合ユニークインデックスを宣言します：

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

入金を処理する前に、既存の `(transactionHash, logIndex)` エントリと照合して、各オンチェーントランスファーが正確に 1 回だけ反映されることを保証します。

## 完全なコード例

以下の例は、`/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/en/guides/getlogs-block-range/)を参照してください。
* リアルタイム RPC ノードクエリとインデックス化された過去の送金 API の違いについては、[チェーン先端とインデックス履歴：eth\_getLogs と送金 API の使い分け](https://docs.blockvectra.com/en/guides/logs-vs-transfers/)を参照してください。

## 次のステップ

* [データセット一覧を見る](https://blockvectra.com/en/data/)と、BlockVectra がインデックス化しているすべてのデータセットを確認できます。
* [無料プランと料金を見る](https://blockvectra.com/en/pricing/#free)と、アカウントに含まれる内容を確認できます。
* [コンソールにログイン](https://console.blockvectra.com/login/?next=%2Fkeys%2F)して、API keyを作成します。
