# Как отслеживать платежи USDT / USDC с помощью Webhooks и RPC

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

Для мониторинга платежей в стейблкоинах или обнаружения депозитов на бирже отслеживайте входящие переводы ERC-20 USDT / USDC в EVM-сетях с помощью Webhooks, логов WebSocket или HTTP-опроса. Разработчики и AI Agent используют одинаковые API; перед обработкой платежей выберите сеть, контракт токена, получателя и глубину подтверждений. Выберите рабочий процесс депозитов, уведомлений для продавцов или выплат в [решении для мониторинга переводов USDT / USDC](https://blockvectra.com/ru/use-cases/stablecoin-payments/).

* **Первый шаг:** [Создайте подписку и отслеживайте получателя](#create-a-subscription-and-watch-the-recipient), начав с API key и вашего HTTPS-приемника.
* **Критерий завершения:** подходящий перевод проходит проверку подписи, сети, токена, получателя и целочисленной суммы, однократно сохраняется как кандидат на платеж, а приемник возвращает HTTP `204`; проверьте его ончейн в соответствии с вашей политикой подтверждений перед зачислением.

[Рабочие процессы платежей в стейблкоинах](https://blockvectra.com/ru/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) на ваш HTTPS-эндпоинт после проверки поддержки Push для выбранной сети.
* [Валидация кандидата на перевод](#verify-deduplicate-and-validate-payments) путем проверки сети, контракта токена, получателя и целочисленной суммы перед применением ончейн-проверки и политики подтверждений.
* [Выгрузка пропущенных логов переводов](#cursor-polling-and-block-range-limits) с помощью ограниченных запросов `eth_getLogs` и сохраненного курсора.

## Выбор Webhook, WebSocket или опроса

| Метод                                            | Для чего использовать                                                                    | Восстановление                                                                                                           |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| [Webhook](https://docs.blockvectra.com/ru/guides/webhook-push/)              | Активность адреса, отправляемая на ваш HTTPS-приемник, включая входящие переводы токенов | Проверка подписей, дедупликация ID событий и обработка `subscription.gap` / `chain.reorg`; повтор сохраненных совпадений |
| [WebSocket](https://docs.blockvectra.com/ru/guides/websocket-subscriptions/) | Отфильтрованные логи `logs` через постоянное соединение                                  | Переподключение, повторная подписка и довыгрузка пропущенных блоков                                                      |
| HTTP-опрос                                       | Регулярный мониторинг или выгрузка исторических логов с собственным курсором             | Запрос ограниченных диапазонов `eth_getLogs` и сохранение прогресса                                                      |

Проверьте `ws` и `subscriptions` в публичном ответе `GET /v1/chains` перед выбором WebSocket. Поддержка Push проверяется отдельно: выполните `GET /v1/push/chains` с вашим API key. Сеть без WebSocket может использовать Webhooks для адресов, если она указана в этом списке. Используйте опрос, когда необходимо просканировать более ранние блоки или работать без постоянного соединения.

## Прием платежей с помощью Webhooks

### Создание подписки и отслеживание получателя

[Получите API key](https://blockvectra.com/ru/get-api-key/) и разверните HTTPS-приемник на порту 443. Выберите `CHAIN` из авторизованного списка сетей Push, установите `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` содержит учетные данные. Опрашивайте подписку до тех пор, пока `applied_version >= change_version` из `address-change.json`, затем зафиксируйте `chains[CHAIN].applied_from_block`. Новые адреса сопоставляются начиная с этого блока, поэтому продолжайте опрос для любых более ранних интервалов платежей.

### Проверка, дедупликация и валидация платежей

Сохраните [функцию проверки подписи исходного тела](https://docs.blockvectra.com/ru/guides/webhook-push/#verify-signatures) как `verify-push.js`. Приведенный ниже приемник принимает Web API `Request` в Node.js и считывает его исходные байты до парсинга JSON. Сформируйте `secrets` как `Map` строк ID подписок к сохраненным секретам. Задайте доверенную конфигурацию `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 при дубликате; зафиксируйте (commit) его вместе с `recordPaymentCandidate` или `enqueueRecovery`. Откатывайте все записи при сбое, чтобы повторная попытка могла обработать событие. Задачи восстановления также должны быть идемпотентными. Возвращайте ответ 2xx в течение 10 секунд только после фиксации; настройте лимит размера тела запроса в 1 МиБ на вашем HTTP-сервере.

Этот пример проверяет одну ожидаемую сумму платежа. Для нескольких заказов находите доверенную конфигурацию платежа по сети, токену и получателю и сопоставляйте частичные или избыточные платежи по собственным правилам. Кандидат на платеж все еще требует ончейн-проверки и соблюдения вашей политики подтверждений перед зачислением. При совместном использовании подписок и опроса сопоставляйте один и тот же перевод по сети, хешу транзакции и индексу лога, чтобы два пути доставки не зачислили его дважды; сохраняйте хеш блока для отслеживания замененных блоков.

### Восстановление пропущенных или замененных блоков

Для `subscription.gap` поставьте в очередь сканирование от `from_block` до `to_block`, используя путь опроса ниже или доступные наборы данных Data API. `chain.reorg` — это бесплатное уведомление о том, что доставленные блоки были заменены, а не о пропуске доставки. Пометьте или отбросьте старые события в этом диапазоне по `ref`; сверяйте записи платежей по `ref` и `tx_hash` с канонической цепочкой перед обработкой автоматически повторно доставленных канонических событий с новыми ID. Дедуплицируйте эти события по `id`. Уведомление о реорганизации не продвигает завершенный прогресс; фиксируйте `complete_through_block` для каждой сети, никогда не выводя завершение из наибольшего номера блока событий.

[Повторная отправка (Replay)](https://docs.blockvectra.com/ru/guides/webhook-push/#delivery-retries-and-replay) принимает `chain` и `from_block` в пределах текущей границы `replayable_from_block`. Она лишь повторно отправляет сохраненные совпадения; она не сканирует периоды до добавления адреса или сети, а также периоды, когда подписка была отключена. Поддерживайте курсор опроса для покрытия этих интервалов и устаревших пропусков. Ошибки запросов и недопустимые диапазоны replay описаны в [справочнике ошибок](https://docs.blockvectra.com/ru/errors/); плата за доставку, историю и адресо-дни объясняется в [правилах биллинга](https://docs.blockvectra.com/ru/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-байтового топика. |
| `fromBlock` | Начальный блок (шестнадцатеричный)                                   | Начало диапазона блоков запроса (включительно)                                                                                                                                                                                                                              |
| `toBlock`   | Конечный блок (шестнадцатеричный)                                    | Конец диапазона блоков запроса (включительно)                                                                                                                                                                                                                               |

Неиндексированное значение `value` (сумма перевода) закодировано в поле `data` объекта лога как 32-байтовое шестнадцатеричное число `uint256`. Разделите эту исходную сумму на 10^decimals, чтобы получить понятное человеку количество токенов (например, 18 знаков после запятой для BSC USDT; 6 знаков для Base и Ethereum USDC).

## Опрос по курсору и ограничения диапазона блоков

Сервис опроса запрашивает новые блоки через регулярные интервалы (например, каждые 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`. Он не должен превышать `max_logs_block_range`, опубликованный для этой сети в `GET /v1/chains`.

Если запрос превышает этот диапазон, сервис отклоняет вызов с кодом ошибки `-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` (не тарифицируются). В логике вашего приложения считайте `max_logs_block_range` из `GET /v1/chains` и ограничивайте каждый интервал опроса: `chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head)`.

## Обработка реорганизаций блоков и глубина подтверждений

Вблизи вершины блокчейна могут происходить временные реорганизации блоков (reorgs). Зачисление платежей на блоке `latest` без необходимой глубины подтверждений создает риск зачисления транзакций в изолированной ветке, которая впоследствии будет отброшена.

Применяйте следующие защитные меры для обеспечения безопасности обработки платежей:

### Глубина подтверждений

Вместо запроса вплоть до `latest` запрашивайте данные до безопасной целевой высоты блока:

`safe_head = current_head - CONFIRMATION_DEPTH`

Задайте `CONFIRMATION_DEPTH` в соответствии с допустимым уровнем риска вашего приложения. Запрос только до `safe_head` гарантирует обработку только тех блоков, которые получили достаточное количество подтверждений.

### Реорганизации во время опроса

Стандартный EVM JSON-RPC устанавливает `removed: true` в объектах логов только в потоках подписок на логи WebSocket, когда ранее сгенерированное событие откатывается из-за реорганизации сети. При опросе по HTTP с помощью `eth_getLogs` запросы возвращают логи из канонической цепочки; логи из отброшенных блоков просто не появятся в последующих запросах. Опрос в пределах `safe_head` гарантирует, что платежи будут обработаны только на блоках с достаточным подтверждением.

## Дедупликация по (transactionHash, logIndex)

Обработчики платежей должны обеспечивать строгую идемпотентность:

1. **Несколько переводов в одной транзакции**: одна транзакция может содержать несколько событий `Transfer` на один и тот же адрес депозита (например, маршрутизаторы токенов, разделяющие свопы, или контракты множественных выплат). **Важно:** один только `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/ru/guides/billing-rules/).
* Подробные рекомендации по ограничениям диапазонов блоков `eth_getLogs` и логике разделения на чанки см. в [Ограничениях диапазона блоков eth\_getLogs и запросах по чанкам](https://docs.blockvectra.com/ru/guides/getlogs-block-range/).
* Различия между запросами к RPC-узлам в реальном времени и API индексированной истории переводов см. в руководстве [Вершина цепочки против индексированной истории: когда использовать eth\_getLogs, а когда Transfers](https://docs.blockvectra.com/ru/guides/logs-vs-transfers/).

## Следующие шаги

* [Изучите каталог наборов данных](https://blockvectra.com/ru/data/), чтобы увидеть все наборы данных, индексируемые BlockVectra.
* [Ознакомьтесь с бесплатным тарифом и ценами](https://blockvectra.com/ru/pricing/#free), чтобы узнать, что включено в ваш аккаунт.
* [Войдите в консоль](https://console.blockvectra.com/login/?next=%2Fkeys%2F), чтобы создать API key.
