# Cómo monitorear pagos USDT / USDC con Webhooks y RPC

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

Para monitorear pagos con stablecoins o detectar depósitos en un exchange, supervise las transferencias entrantes de USDT / USDC ERC-20 en cadenas EVM mediante Webhooks, logs de WebSocket o sondeo HTTP. Los desarrolladores y agentes de IA utilizan las mismas APIs; seleccione la cadena, el contrato del token, el destinatario y la profundidad de confirmación antes de procesar los pagos. Elija un flujo de depósitos, avisos de cobro para comercios o pagos salientes en la [solución de monitoreo de transferencias USDT / USDC](https://blockvectra.com/es/use-cases/stablecoin-payments/).

* **Primer paso:** [Cree una suscripción y supervise el destinatario](#create-a-subscription-and-watch-the-recipient), comenzando con una API key y su receptor HTTPS.
* **Se completa cuando:** Una transferencia coincidente supera las comprobaciones de firma, cadena, token, destinatario e importe entero, se almacena una sola vez como candidata a pago y el receptor devuelve HTTP `204`; verifíquela on-chain según su política de confirmaciones antes de acreditarla.

[Flujos de pagos con stablecoins](https://blockvectra.com/es/use-cases/stablecoin-payments/).

El monitoreo básico de transferencias está disponible. El filtrado de importes y tokens se ejecuta en su receptor. Próximamente habrá condiciones en el servidor, múltiples etapas de confirmación y alertas IM.

Para desarrolladores y agentes de IA: comience con una [API key](https://console.blockvectra.com/login/?next=%2Fkeys%2F) y su propio receptor HTTPS; filtre los contratos de tokens y los importes en su aplicación. [Copie la configuración del webhook](#create-a-subscription-and-watch-the-recipient).

## Tareas que esta guía le ayuda a completar

* [Recibir avisos de pagos USDT / USDC](#receive-payments-with-webhooks) en su endpoint HTTPS después de comprobar la compatibilidad de Push con la cadena seleccionada.
* [Validar una transferencia candidata](#verify-deduplicate-and-validate-payments) comprobando su cadena, contrato del token, destinatario e importe entero antes de aplicar su verificación on-chain y política de confirmaciones.
* [Recuperar logs de transferencias faltantes](#cursor-polling-and-block-range-limits) con consultas `eth_getLogs` acotadas y un cursor guardado.

## Elegir Webhook, WebSocket o sondeo

| Método                                           | Uso                                                                                                    | Recuperación                                                                                                                     |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| [Webhook](https://docs.blockvectra.com/es/guides/webhook-push/)              | Actividad de direcciones enviada a su receptor HTTPS, incluidas las transferencias entrantes de tokens | Verificar firmas, deduplicar IDs de eventos y gestionar `subscription.gap` / `chain.reorg`; reproducir coincidencias conservadas |
| [WebSocket](https://docs.blockvectra.com/en/guides/websocket-subscriptions/) | `logs` filtrados mediante una conexión persistente                                                     | Reconectar, volver a suscribirse y recuperar los bloques omitidos                                                                |
| Sondeo HTTP                                      | Monitoreo programado o recuperación de logs históricos con su propio cursor                            | Consultar rangos `eth_getLogs` acotados y guardar el progreso                                                                    |

Lea `ws` y `subscriptions` en la respuesta pública de `GET /v1/chains` antes de elegir WebSocket. La compatibilidad de Push se comprueba por separado: consulte `GET /v1/push/chains` con su API key. Una cadena sin WebSocket puede utilizar Webhooks de direcciones si figura en esa lista. Use sondeo cuando necesite explorar bloques anteriores o trabajar sin una conexión persistente.

## Recibir pagos con Webhooks

### Crear una suscripción y supervisar el destinatario

[Obtenga una API key](https://blockvectra.com/es/get-api-key/) y despliegue un receptor HTTPS en el puerto 443. Seleccione `CHAIN` en la lista autenticada de cadenas Push, establezca `RECIPIENT` como su dirección de depósito y `RECEIVER_URL` como la URL de su receptor. Este ejemplo de shell requiere `jq`; `{}` utiliza el número de confirmaciones predeterminado de la cadena. Revise `min_confirmations`, `default_confirmations` y `max_confirmations` antes de elegir un número diferente. La [OpenAPI de Push](https://docs.blockvectra.com/openapi/push.yaml) define estas solicitudes.

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

La creación devuelve `id` y `secret`. Guarde el secret de forma segura para el receptor; `subscription.json` contiene una credencial. Consulte la suscripción periódicamente hasta que `applied_version >= change_version` de `address-change.json`, y registre después `chains[CHAIN].applied_from_block`. Las nuevas direcciones generan coincidencias a partir de ese bloque, así que continúe el sondeo para cualquier intervalo de pagos anterior.

### Verificar, deduplicar y validar pagos

Guarde la [función de firma del body original](https://docs.blockvectra.com/es/guides/webhook-push/#verify-signatures) como `verify-push.js`. El receptor siguiente acepta una `Request` de Web API en Node.js y lee sus bytes originales antes de analizar el JSON. Construya `secrets` como un `Map` que asocie cadenas de texto de IDs de suscripción con los secrets almacenados. Establezca una configuración `expected` de confianza como `{ chain, token, recipient, amountUnits }`: `token` es el contrato de la stablecoin verificado en esa cadena y `amountUnits` es el importe entero positivo esperado en sus unidades mínimas. Compare los importes con `BigInt`, nunca con números de coma flotante ni con el símbolo del 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 });
}
```

Implemente `store.transaction` con almacenamiento duradero. Dentro de una transacción, `insertEventOnce` inserta un evento bajo una clave única `(subscription_id, event.id)` y devuelve false si está duplicado; confírmelo junto con `recordPaymentCandidate` o `enqueueRecovery`. Revierta todas las escrituras si se produce un fallo para que un reintento pueda procesar el evento. Los trabajos de recuperación también deben ser idempotentes. Devuelva 2xx en un máximo de 10 segundos solo después de confirmar la transacción; aplique el límite de body de 1 MiB en su servidor HTTP.

Este ejemplo comprueba un único importe de pago esperado. Para varios pedidos, busque la configuración de pago de confianza por cadena, token y destinatario, y concilie los pagos parciales o excesivos según sus propias reglas. Una candidata aún necesita verificación on-chain y su política de confirmaciones antes de acreditarse. Entre las suscripciones y el sondeo, concilie la misma transferencia por cadena, hash de transacción e índice del log para que dos vías de entrega no la acrediten dos veces; conserve el hash del bloque para rastrear bloques sustituidos.

### Recuperar bloques faltantes o sustituidos

Para `subscription.gap`, encole una exploración desde `from_block` hasta `to_block` mediante el sondeo siguiente o los conjuntos de datos disponibles de la Data API. `chain.reorg` es un aviso gratuito de que los bloques entregados fueron sustituidos, no una laguna de entrega. Marque o descarte los eventos antiguos de ese rango por `ref`; concilie los registros de pagos por `ref` y `tx_hash` con la cadena canónica antes de procesar los eventos canónicos reenviados automáticamente con nuevos IDs. Deduplique esos eventos por `id`. El aviso de reorganización no avanza el progreso completado; registre `complete_through_block` por cadena y nunca deduzca la finalización a partir del mayor número de bloque de los eventos.

[Replay](https://docs.blockvectra.com/es/guides/webhook-push/#delivery-retries-and-replay) acepta `chain` y `from_block` dentro del límite actual `replayable_from_block`. Solo reenvía las coincidencias conservadas; no explora períodos anteriores a la incorporación de la dirección o cadena ni períodos en que la suscripción estaba desconectada. Mantenga un cursor de sondeo para cubrir esos intervalos y las lagunas caducadas. Los fallos de solicitudes y los rangos de replay inválidos se describen en la [referencia de errores](https://docs.blockvectra.com/es/errors/); los cargos por entrega, historial y dirección-día se explican en las [reglas de facturación](https://docs.blockvectra.com/en/guides/billing-rules/#webhook-push-billing).

Las secciones restantes implementan el filtrado de logs ERC-20 y el sondeo con cursor para el monitoreo y la recuperación.

## Evento Transfer y parámetros de filtrado

Los contratos de tokens ERC-20 estándar emiten el siguiente evento en cada transferencia:

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

Al llamar a `eth_getLogs`, pase la dirección del contrato del token y el array `topics` para filtrar los logs coincidentes:

| Parámetro   | Valor                                                                | Descripción                                                                                                                                                                                                                                                                                                       |
| ----------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`   | Dirección del contrato del token (o array de direcciones)            | Dirección del contrato de la stablecoin de destino. Puede especificar una sola dirección (p. ej., BSC USDT `0x55d398326f99059fF775485246999027B3197955`, Base USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`) o un array de direcciones para monitorear varios tokens simultáneamente                          |
| `topics[0]` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | Hash de la firma del evento: `keccak256("Transfer(address,address,uint256)")`                                                                                                                                                                                                                                     |
| `topics[1]` | `null`                                                               | Dirección del remitente (`from`). Como el monitoreo de depósitos acepta fondos de cualquier billetera de usuario, pase `null` para aceptar cualquier remitente                                                                                                                                                    |
| `topics[2]` | Dirección del destinatario rellenada con ceros hasta 32 bytes        | Dirección de destino (`to`). Según las especificaciones de logs EVM, los parámetros de dirección `indexed` ocupan 32 bytes (64 caracteres hexadecimales). Rellene a la izquierda la dirección del destinatario de 20 bytes con 12 bytes cero (24 caracteres hexadecimales cero) para formar un topic de 32 bytes. |
| `fromBlock` | Bloque inicial (hexadecimal)                                         | Inicio del rango de bloques de la consulta (incluido)                                                                                                                                                                                                                                                             |
| `toBlock`   | Bloque final (hexadecimal)                                           | Fin del rango de bloques de la consulta (incluido)                                                                                                                                                                                                                                                                |

El `value` no indexado (importe de la transferencia) se codifica en el campo `data` del objeto log como un `uint256` hexadecimal de 32 bytes. Divida este importe bruto por 10^decimals para obtener el importe del token legible por humanos (p. ej., 18 decimales para BSC USDT; 6 decimales para USDC de Base y Ethereum).

## Sondeo con cursor y límites de rango de bloques

Un servicio de sondeo consulta los nuevos bloques a intervalos regulares (como cada 3 a 5 segundos).

### Avance del cursor

Mantenga un cursor persistente `last_polled_block` (el bloque más alto procesado y confirmado) en su base de datos:

1. En cada ciclo de sondeo, establezca `fromBlock = last_polled_block + 1`.
2. Consulte la cabecera actual de la cadena mediante `eth_blockNumber` y calcule la altura de destino segura `safe_head` según su profundidad de confirmación.
3. Si `fromBlock <= safe_head`, consulte los logs por tramos hasta `safe_head`. Tras procesar correctamente cada tramo, avance el cursor.

### Límite de rango de bloques

La amplitud de bloques de una llamada `eth_getLogs` se calcula como `toBlock − fromBlock + 1`. No debe superar el `max_logs_block_range` publicado para esa cadena en `GET /v1/chains`.

Si una solicitud supera ese rango, el servicio rechaza la llamada con el código de error `-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
    }
  }
}
```

Las solicitudes que superan el rango de bloques devuelven el error JSON-RPC `-32602` (no se facturan). En la lógica de su aplicación, lea `max_logs_block_range` de `GET /v1/chains` y acote cada tramo de sondeo: `chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head)`.

## Gestionar reorganizaciones de bloques y profundidad de confirmación

Cerca de la cabecera de la blockchain pueden producirse reorganizaciones temporales de bloques (reorgs). Acreditar pagos en `latest` sin profundidad de confirmación conlleva el riesgo de acreditar transacciones de una rama huérfana que después se descarta.

Aplique las siguientes medidas para proteger el procesamiento de pagos:

### Profundidad de confirmación

En lugar de consultar hasta `latest`, consulte hasta una altura de bloque de destino segura:

`safe_head = current_head - CONFIRMATION_DEPTH`

Establezca `CONFIRMATION_DEPTH` según la tolerancia al riesgo de su aplicación. Consultar solo hasta `safe_head` garantiza que se procesen únicamente bloques con suficientes confirmaciones.

### Reorganizaciones durante el sondeo

El JSON-RPC EVM estándar establece `removed: true` en los objetos log solo en los flujos de suscripción a logs de WebSocket cuando un evento previamente emitido se revierte por una reorganización de la cadena. Al sondear por HTTP con `eth_getLogs`, las consultas devuelven logs de la cadena canónica; los logs reorganizados simplemente no aparecerán en consultas posteriores. Sondear dentro de `safe_head` garantiza que los pagos se procesen solo en bloques suficientemente confirmados.

## Deduplicación por (transactionHash, logIndex)

Los receptores de pagos deben aplicar una idempotencia estricta:

1. **Varias transferencias en una transacción**: Una sola transacción puede contener varios eventos `Transfer` hacia la misma dirección de depósito (por ejemplo, routers de tokens que dividen intercambios o contratos con múltiples pagos). **Importante:** `transactionHash` por sí solo no es único para cada pago.
2. **Sondeo superpuesto y reintentos**: Cuando los servicios de sondeo se reinician, se recuperan de errores de red transitorios o retroceden varios bloques para gestionar reorganizaciones poco profundas, los logs del mismo rango de bloques se consultan varias veces.
3. **Unicidad del índice del log**: `logIndex` identifica la posición relativa del log del evento dentro del bloque. Según las especificaciones EVM, el identificador único compuesto canónico de un evento es `(transactionHash, logIndex)`.

En esquemas de bases de datos relacionales, declare un índice único compuesto en su tabla de registros de depósitos:

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

Antes de procesar un depósito, compruebe las entradas `(transactionHash, logIndex)` existentes para garantizar que cada transferencia on-chain se acredite exactamente una vez.

## Ejemplos de código completos

Los ejemplos siguientes muestran cómo obtener las capacidades de red de `/v1/chains`, calcular rangos de bloques seguros, sondear logs `Transfer` de stablecoins respetando los límites de rango y deduplicar eventos.

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


## Reglas de facturación y guías relacionadas

* Para conocer la medición de solicitudes, los pesos CU y los criterios de facturación de códigos de error, consulte [Reglas de facturación: errores y solicitudes no facturadas](https://docs.blockvectra.com/en/guides/billing-rules/).
* Para obtener información detallada sobre los límites de rango de bloques de `eth_getLogs` y la lógica de división en tramos, consulte [Límites de rango de bloques de eth\_getLogs y consultas por tramos](https://docs.blockvectra.com/es/guides/getlogs-block-range/).
* Para conocer las diferencias entre las consultas en tiempo real a nodos RPC y las APIs de transferencias históricas indexadas, consulte [Cabecera de la cadena frente a historial indexado: cuándo utilizar eth\_getLogs o Transfers](https://docs.blockvectra.com/es/guides/logs-vs-transfers/).

## Próximos pasos

* [Explore el directorio de conjuntos de datos](https://blockvectra.com/es/data/) para ver todos los conjuntos de datos indexados por BlockVectra.
* [Consulte el plan gratuito y los precios](https://blockvectra.com/es/pricing/#free) para comprobar lo que incluye su cuenta.
* [Inicie sesión en la consola](https://console.blockvectra.com/login/?next=%2Fkeys%2F) para crear una API key.
