# Comment surveiller les paiements USDT / USDC avec les Webhooks et le RPC

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

Pour la surveillance des paiements en stablecoins ou la détection des dépôts sur les plateformes d'échange, surveillez les transferts entrants ERC-20 USDT / USDC sur les chaînes EVM à l'aide de Webhooks, de logs WebSocket ou du polling HTTP. Les développeurs et les agents IA utilisent les mêmes API ; sélectionnez la chaîne, le contrat de jeton, le destinataire et la profondeur de confirmation avant de traiter les paiements. Choisissez un flux de travail de dépôt, de notification commerçant ou de paiement sortant dans la [solution de surveillance des transferts USDT / USDC](https://blockvectra.com/fr/use-cases/stablecoin-payments/).

* **Première étape :** [Créer un abonnement et surveiller le destinataire](#create-a-subscription-and-watch-the-recipient), en commençant par une API key et votre récepteur HTTPS.
* **Terminé quand :** Un transfert correspondant passe avec succès les vérifications de signature, chaîne, jeton, destinataire et montant entier, est enregistré une seule fois comme candidat au paiement, et le récepteur renvoie HTTP `204` ; vérifiez-le on-chain selon votre politique de confirmation avant de le créditer.

[Flux de paiement en stablecoins](https://blockvectra.com/fr/use-cases/stablecoin-payments/).

La surveillance basique des transferts est disponible. Le filtrage des montants et des jetons s'exécute dans votre récepteur. Les conditions côté serveur, les étapes de confirmation multiples et les alertes par messagerie instantanée seront bientôt disponibles.

Pour les développeurs et les agents IA : commencez avec une [API key](https://console.blockvectra.com/login/?next=%2Fkeys%2F) et votre propre récepteur HTTPS ; filtrez les contrats de jetons et les montants dans votre application. [Copier la configuration du webhook](#create-a-subscription-and-watch-the-recipient).

## Tâches que ce guide vous aide à accomplir

* [Recevoir des notifications de paiement USDT / USDC](#receive-payments-with-webhooks) sur votre point de terminaison HTTPS après avoir vérifié la prise en charge de Push pour la chaîne sélectionnée.
* [Valider un candidat au transfert](#verify-deduplicate-and-validate-payments) en vérifiant sa chaîne, son contrat de jeton, son destinataire et son montant entier avant d'appliquer votre vérification on-chain et votre politique de confirmation.
* [Rattraper les logs de transfert manquants](#cursor-polling-and-block-range-limits) avec des requêtes `eth_getLogs` bornées et un curseur sauvegardé.

## Choisir entre Webhook, WebSocket ou polling

| Méthode                                          | Cas d'usage                                                                                      | Récupération                                                                                                                                  |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| [Webhook](https://docs.blockvectra.com/fr/guides/webhook-push/)              | Activité d'adresses envoyée à votre récepteur HTTPS, y compris les transferts de jetons entrants | Vérifier les signatures, dédupliquer les ID d'événements et gérer `subscription.gap` / `chain.reorg` ; rejouer les correspondances conservées |
| [WebSocket](https://docs.blockvectra.com/fr/guides/websocket-subscriptions/) | `logs` filtrés sur une connexion persistante                                                     | Se reconnecter, se réabonner et rattraper les blocs manqués                                                                                   |
| Polling HTTP                                     | Surveillance planifiée ou rattrapage de logs historiques avec votre propre curseur               | Interroger des plages `eth_getLogs` bornées et persister la progression                                                                       |

Lisez `ws` et `subscriptions` dans la réponse publique `GET /v1/chains` avant de choisir WebSocket. La prise en charge de Push fait l'objet d'une vérification distincte : lisez `GET /v1/push/chains` avec votre API key. Une chaîne sans WebSocket peut utiliser les Webhooks d'adresses si elle y est répertoriée. Utilisez le polling lorsque vous devez analyser des blocs antérieurs ou fonctionner sans connexion persistante.

## Recevoir des paiements avec des Webhooks

### Créer un abonnement et surveiller le destinataire

[Obtenez une API key](https://blockvectra.com/fr/get-api-key/) et déployez un récepteur HTTPS sur le port 443. Sélectionnez `CHAIN` dans la liste authentifiée des chaînes Push, définissez `RECIPIENT` sur votre adresse de dépôt et `RECEIVER_URL` sur l'URL de votre récepteur. Cet exemple shell nécessite `jq` ; `{}` utilise le nombre de confirmations par défaut de la chaîne. Inspectez `min_confirmations`, `default_confirmations` et `max_confirmations` avant de choisir un nombre différent. L'[OpenAPI Push](https://docs.blockvectra.com/openapi/push.yaml) définit ces requêtes.

```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 création renvoie `id` et `secret`. Conservez le secret en toute sécurité pour le récepteur ; `subscription.json` contient un identifiant d'authentification. Interrogez l'abonnement par polling jusqu'à ce que `applied_version >= change_version` de `address-change.json`, puis enregistrez `chains[CHAIN].applied_from_block`. Les nouvelles adresses correspondent à partir de ce bloc, continuez donc le polling pour tout intervalle de paiement antérieur.

### Vérifier, dédupliquer et valider les paiements

Enregistrez la [fonction de vérification de signature sur corps brut](https://docs.blockvectra.com/fr/guides/webhook-push/#verify-signatures) sous le nom `verify-push.js`. Le récepteur ci-dessous accepte une `Request` Web API dans Node.js et lit ses octets d'origine avant l'analyse JSON. Construisez `secrets` sous forme de `Map` associant les chaînes d'ID d'abonnement aux secrets enregistrés. Définissez la configuration de confiance `expected` sur `{ chain, token, recipient, amountUnits }` : `token` est le contrat de stablecoin vérifié sur cette chaîne et `amountUnits` est le montant entier positif attendu dans ses plus petites unités. Comparez les montants avec `BigInt`, jamais avec des nombres à virgule flottante ou le symbole du jeton.

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

Implémentez `store.transaction` avec un stockage durable. Au sein d'une même transaction, `insertEventOnce` insère un événement sous une clé unique `(subscription_id, event.id)` et renvoie false en cas de doublon ; validez-le (commit) conjointement avec `recordPaymentCandidate` ou `enqueueRecovery`. Annulez (roll back) toutes les écritures en cas d'échec afin qu'une nouvelle tentative puisse traiter l'événement. Les tâches de récupération doivent également être idempotentes. Ne renvoyez 2xx dans un délai de 10 secondes qu'après validation ; appliquez la limite de taille de corps de 1 MiB sur votre serveur HTTP.

Cet exemple vérifie un seul montant de paiement attendu. Pour plusieurs commandes, recherchez la configuration de paiement de confiance par chaîne, jeton et destinataire, et réconciliez les paiements partiels ou excédentaires selon vos propres règles. Un candidat nécessite encore une vérification on-chain et votre politique de confirmation avant d'être crédité. Entre les abonnements et le polling, réconciliez le même transfert par chaîne, hash de transaction et index de log afin que deux chemins de distribution ne le créditent pas deux fois ; conservez le hash de bloc pour suivre les blocs remplacés.

### Récupérer les blocs manquants ou réorganisés

Pour `subscription.gap`, mettez en file d'attente une analyse de `from_block` jusqu'à `to_block` en utilisant le chemin de polling ci-dessous ou les jeux de données disponibles de la Data API. `chain.reorg` est une notification gratuite indiquant que des blocs livrés ont été remplacés, et non une lacune de distribution. Marquez ou rejetez les anciens événements dans cette plage par `ref` ; réconciliez les enregistrements de paiement par `ref` et `tx_hash` par rapport à la chaîne canonique avant de traiter les événements canoniques automatiquement redistribués avec de nouveaux identifiants. Dédupliquez ces événements par `id`. La notification de réorganisation n'avance pas la progression achevée ; enregistrez `complete_through_block` par chaîne, ne déduisez jamais l'achèvement à partir du plus grand numéro de bloc d'événement.

Le [rejeu (replay)](https://docs.blockvectra.com/fr/guides/webhook-push/#delivery-retries-and-replay) accepte `chain` et `from_block` à l'intérieur de la limite actuelle `replayable_from_block`. Il renvoie uniquement les correspondances conservées ; il n'analyse pas les périodes antérieures à l'ajout de l'adresse ou de la chaîne, ni les périodes où l'abonnement était hors ligne. Conservez un curseur de polling pour couvrir ces intervalles et les lacunes expirées. Les échecs de requête et les plages de rejeu non valides sont traités dans la [référence des erreurs](https://docs.blockvectra.com/fr/errors/) ; les frais de distribution, d'historique et de jours-adresses sont expliqués dans les [règles de facturation](https://docs.blockvectra.com/fr/guides/billing-rules/#webhook-push-billing).

Les sections suivantes implémentent le filtrage des logs ERC-20 et le polling basé sur curseur pour la surveillance et la récupération.

## Événement Transfer et paramètres de filtre

Les contrats de jetons ERC-20 standard émettent l'événement suivant à chaque transfert :

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

Lors de l'appel à `eth_getLogs`, transmettez l'adresse du contrat de jeton et le tableau `topics` pour filtrer les logs correspondants :

| Paramètre   | Valeur                                                               | Description                                                                                                                                                                                                                                                                                        |
| ----------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`   | Adresse du contrat de jeton (ou tableau d'adresses)                  | Adresse du contrat de stablecoin cible. Vous pouvez spécifier une adresse unique (ex. BSC USDT `0x55d398326f99059fF775485246999027B3197955`, Base USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`), ou un tableau d'adresses pour surveiller plusieurs jetons simultanément                      |
| `topics[0]` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | Hash de la signature de l'événement : `keccak256("Transfer(address,address,uint256)")`                                                                                                                                                                                                             |
| `topics[1]` | `null`                                                               | Adresse de l'expéditeur (`from`). Comme la surveillance des dépôts accepte les fonds provenant de n'importe quel portefeuille d'utilisateur, transmettez `null` pour faire correspondre n'importe quel expéditeur                                                                                  |
| `topics[2]` | Adresse du destinataire sur 32 octets complétée par des zéros        | Adresse de destination (`to`). Selon les spécifications des logs EVM, les paramètres d'adresse `indexed` occupent 32 octets (64 caractères hexadécimaux). Complétez à gauche l'adresse de destinataire de 20 octets avec 12 octets nuls (24 zéros hexadécimaux) pour former un topic de 32 octets. |
| `fromBlock` | Bloc de début (hexadécimal)                                          | Début de la plage de blocs de la requête (inclus)                                                                                                                                                                                                                                                  |
| `toBlock`   | Bloc de fin (hexadécimal)                                            | Fin de la plage de blocs de la requête (inclus)                                                                                                                                                                                                                                                    |

La `value` non indexée (montant du transfert) est encodée dans le champ `data` de l'objet de log sous forme d'un `uint256` hexadécimal de 32 octets. Divisez ce montant brut par 10^décimales pour obtenir le montant de jetons lisible par l'humain (par exemple 18 décimales pour BSC USDT ; 6 décimales pour Base et Ethereum USDC).

## Polling par curseur et limites de plage de blocs

Un service de polling interroge les nouveaux blocs à intervalles réguliers (par exemple toutes les 3 à 5 secondes).

### Avancement du curseur

Maintenez un curseur persistant `last_polled_block` (le bloc le plus élevé traité et validé) dans votre base de données :

1. Pour chaque cycle de polling, définissez `fromBlock = last_polled_block + 1`.
2. Interrogez la tête de chaîne actuelle via `eth_blockNumber`, et calculez la hauteur cible sécurisée `safe_head` en fonction de votre profondeur de confirmation.
3. Si `fromBlock <= safe_head`, interrogez les logs par morceaux (chunks) jusqu'à `safe_head`. Après avoir traité chaque morceau avec succès, avancez le curseur.

### Limite de plage de blocs

L'étendue de blocs d'un appel `eth_getLogs` unique est calculée par `toBlock − fromBlock + 1`. Elle ne doit pas dépasser la valeur `max_logs_block_range` publiée pour cette chaîne dans `GET /v1/chains`.

Si une requête dépasse cette plage, le service rejette l'appel avec le code d'erreur `-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
    }
  }
}
```

Les requêtes dépassant la plage de blocs renvoient l'erreur JSON-RPC `-32602` (non facturée). Dans la logique de votre application, lisez `max_logs_block_range` depuis `GET /v1/chains` et bornez chaque tranche de polling : `chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head)`.

## Gestion des réorganisations de blocs et profondeur de confirmation

Près du sommet de la blockchain, des réorganisations temporaires de blocs (reorgs) peuvent se produire. Créditer des paiements au niveau de `latest` sans profondeur de confirmation risque de créditer des transactions sur une branche orpheline qui sera ultérieurement rejetée.

Appliquez les mesures de protection suivantes pour sécuriser le traitement des paiements :

### Profondeur de confirmation

Au lieu d'interroger jusqu'à `latest`, interrogez jusqu'à une hauteur de bloc cible sécurisée :

`safe_head = current_head - CONFIRMATION_DEPTH`

Définissez `CONFIRMATION_DEPTH` en fonction de la tolérance au risque de votre application. N'interroger que jusqu'à `safe_head` garantit que seuls les blocs disposant d'un nombre suffisant de confirmations sont traités.

### Réorganisations pendant le polling

Le JSON-RPC EVM standard définit `removed: true` sur les objets de log uniquement dans les flux d'abonnements WebSocket de logs lorsqu'un événement émis précédemment est annulé suite à une réorganisation de la chaîne. Lors d'un polling HTTP avec `eth_getLogs`, les requêtes renvoient les logs de la chaîne canonique ; les logs réorganisés n'apparaîtront tout simplement pas dans les requêtes ultérieures. Le polling en deçà de `safe_head` garantit que les paiements ne sont traités que sur des blocs suffisamment confirmés.

## Déduplication par (transactionHash, logIndex)

Les écouteurs de paiement doivent imposer une idempotence stricte :

1. **Transferts multiples dans une seule transaction** : une transaction unique peut contenir plusieurs événements `Transfer` vers la même adresse de dépôt (par exemple, des routeurs de jetons divisant des swaps ou des contrats de paiements multiples). **Important :** `transactionHash` seul n'est pas unique par paiement.
2. **Chevauchement du polling et nouvelles tentatives** : lorsque les services de polling redémarrent, récupèrent d'erreurs réseau temporaires ou reculent de plusieurs blocs pour gérer des réorganisations peu profondes, les logs de la même plage de blocs sont interrogés plusieurs fois.
3. **Unicité de l'index de log** : le `logIndex` identifie la position relative du log d'événement dans le bloc. Selon les spécifications EVM, l'identifiant composite canonique unique pour un événement est `(transactionHash, logIndex)`.

Dans les schémas de base de données relationnelle, déclarez un index unique composite sur votre table d'enregistrements de dépôts :

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

Avant de traiter un dépôt, vérifiez la présence d'entrées existantes `(transactionHash, logIndex)` afin de garantir que chaque transfert on-chain n'est crédité qu'une seule et unique fois.

## Exemples de code complets

Les exemples ci-dessous illustrent la récupération des capacités réseau depuis `/v1/chains`, le calcul des plages de blocs sécurisées, le polling des logs `Transfer` de stablecoins dans le respect des limites de plage et la déduplication des événements.

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


## Règles de facturation et guides associés

* Pour des détails sur la mesure des requêtes, les pondérations en CU et la détermination de facturation des codes d'erreur, consultez [Règles de facturation : erreurs et requêtes non facturées](https://docs.blockvectra.com/fr/guides/billing-rules/).
* Pour un guide approfondi sur les limites de plage de blocs et la logique de découpage d'eth\_getLogs, consultez [Limites de plage de blocs et requêtes découpées eth\_getLogs](https://docs.blockvectra.com/fr/guides/getlogs-block-range/).
* Pour les différences entre requêtes de nœuds RPC en temps réel et API de transferts historiques indexés, consultez [Sommet de chaîne vs historique indexé : quand utiliser eth\_getLogs vs Transfers](https://docs.blockvectra.com/fr/guides/logs-vs-transfers/).

## Étapes suivantes

* [Parcourir le répertoire des jeux de données](https://blockvectra.com/fr/data/) pour voir chaque jeu de données indexé par BlockVectra.
* [Consulter le forfait gratuit et les tarifs](https://blockvectra.com/fr/pricing/#free) pour vérifier ce que comprend votre compte.
* [Se connecter à la console](https://console.blockvectra.com/login/?next=%2Fkeys%2F) pour créer une API key.
