# Cara Memantau Pembayaran USDT / USDC dengan Webhook dan RPC

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

Untuk pemantauan pembayaran stablecoin atau deteksi deposit bursa, pantau transfer ERC-20 USDT / USDC masuk pada chain EVM menggunakan Webhook, log WebSocket, atau polling HTTP. Pengembang dan AI Agent menggunakan API yang sama; pilih chain, kontrak token, penerima, dan kedalaman konfirmasi sebelum memproses pembayaran. Pilih alur deposit, notifikasi pedagang, atau pembayaran keluar dalam [solusi pemantauan transfer USDT / USDC](https://blockvectra.com/en/use-cases/stablecoin-payments/).

* **Langkah pertama:** [Buat langganan dan pantau penerima](#create-a-subscription-and-watch-the-recipient), dimulai dengan API key dan penerima HTTPS Anda.
* **Selesai ketika:** Transfer yang cocok lolos pemeriksaan tanda tangan, chain, token, penerima, dan jumlah bilangan bulat, disimpan sekali sebagai kandidat pembayaran, dan penerima mengembalikan HTTP `204`; verifikasi secara on-chain sesuai kebijakan konfirmasi Anda sebelum mengkreditkannya.

[Alur pembayaran stablecoin](https://blockvectra.com/en/use-cases/stablecoin-payments/).

Pemantauan transfer dasar tersedia. Penyaringan jumlah dan token dilakukan di penerima Anda. Kondisi sisi server, konfirmasi beberapa tahap, dan peringatan IM akan segera tersedia.

Untuk pengembang dan AI Agent: mulai dengan [API key](https://console.blockvectra.com/login/?next=%2Fkeys%2F) dan penerima HTTPS Anda sendiri; saring kontrak token dan jumlah di aplikasi Anda. [Salin konfigurasi Webhook](#create-a-subscription-and-watch-the-recipient).

## Tugas yang dapat Anda selesaikan dengan panduan ini

* [Terima notifikasi pembayaran USDT / USDC](#receive-payments-with-webhooks) di endpoint HTTPS Anda setelah memeriksa dukungan Push untuk chain yang dipilih.
* [Validasi kandidat transfer](#verify-deduplicate-and-validate-payments) dengan memeriksa chain, kontrak token, penerima, dan jumlah bilangan bulat sebelum menerapkan verifikasi on-chain dan kebijakan konfirmasi Anda.
* [Lakukan backfill log transfer yang hilang](#cursor-polling-and-block-range-limits) dengan kueri `eth_getLogs` berbatas dan kursor tersimpan.

## Pilih Webhook, WebSocket, atau polling

| Metode                                           | Kegunaan                                                                            | Pemulihan                                                                                                                                 |
| ------------------------------------------------ | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [Webhook](https://docs.blockvectra.com/en/guides/webhook-push/)              | Aktivitas alamat yang dikirim ke penerima HTTPS Anda, termasuk transfer token masuk | Verifikasi tanda tangan, deduplikasi ID peristiwa, dan tangani `subscription.gap` / `chain.reorg`; lakukan replay kecocokan yang disimpan |
| [WebSocket](https://docs.blockvectra.com/en/guides/websocket-subscriptions/) | `logs` terfilter melalui koneksi persisten                                          | Sambungkan kembali, berlangganan kembali, dan lakukan backfill blok yang terlewat                                                         |
| Polling HTTP                                     | Pemantauan terjadwal atau backfill log historis dengan kursor Anda sendiri          | Kueri rentang `eth_getLogs` berbatas dan simpan kemajuan secara persisten                                                                 |

Baca `ws` dan `subscriptions` dalam respons publik `GET /v1/chains` sebelum memilih WebSocket. Dukungan Push diperiksa secara terpisah: baca `GET /v1/push/chains` dengan API key Anda. Chain tanpa WebSocket dapat menggunakan Webhook alamat jika tercantum di sana. Gunakan polling ketika perlu memindai blok sebelumnya atau berjalan tanpa koneksi persisten.

## Terima pembayaran dengan Webhook

### Buat langganan dan pantau penerima

[Dapatkan API key](https://blockvectra.com/en/get-api-key/) dan deploy penerima HTTPS pada port 443. Pilih `CHAIN` dari daftar chain Push terautentikasi, atur `RECIPIENT` ke alamat deposit Anda dan `RECEIVER_URL` ke URL penerima. Contoh shell ini memerlukan `jq`; `{}` menggunakan jumlah konfirmasi bawaan chain. Periksa `min_confirmations`, `default_confirmations`, dan `max_confirmations` sebelum memilih jumlah berbeda. [Push OpenAPI](https://docs.blockvectra.com/openapi/push.yaml) mendefinisikan permintaan ini.

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

Pembuatan mengembalikan `id` dan `secret`. Simpan secret dengan aman untuk penerima; `subscription.json` berisi kredensial. Lakukan polling langganan hingga `applied_version >= change_version` dari `address-change.json`, lalu catat `chains[CHAIN].applied_from_block`. Alamat baru mulai cocok dari blok tersebut, jadi terus lakukan polling untuk interval pembayaran sebelumnya.

### Verifikasi, deduplikasi, dan validasi pembayaran

Simpan [fungsi tanda tangan body mentah](https://docs.blockvectra.com/en/guides/webhook-push/#verify-signatures) sebagai `verify-push.js`. Penerima di bawah menerima `Request` Web API dalam Node.js dan membaca byte aslinya sebelum parsing JSON. Buat `secrets` sebagai `Map` dari string ID langganan ke secret tersimpan. Atur konfigurasi `expected` tepercaya ke `{ chain, token, recipient, amountUnits }`: `token` adalah kontrak stablecoin terverifikasi pada chain tersebut dan `amountUnits` adalah jumlah bilangan bulat positif yang diharapkan dalam unit terkecilnya. Bandingkan jumlah dengan `BigInt`, jangan pernah menggunakan angka floating-point atau simbol 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 });
}
```

Implementasikan `store.transaction` dengan penyimpanan persisten. Dalam satu transaksi, `insertEventOnce` menyisipkan peristiwa dengan kunci unik `(subscription_id, event.id)` dan mengembalikan false untuk duplikat; commit bersama `recordPaymentCandidate` atau `enqueueRecovery`. Batalkan semua penulisan jika gagal agar percobaan ulang dapat memproses peristiwa. Pekerjaan pemulihan juga harus idempoten. Kembalikan 2xx dalam waktu 10 detik hanya setelah commit; terapkan batas body 1 MiB di server HTTP Anda.

Contoh ini memeriksa satu jumlah pembayaran yang diharapkan. Untuk beberapa pesanan, cari konfigurasi pembayaran tepercaya berdasarkan chain, token, dan penerima, lalu rekonsiliasi pembayaran sebagian atau berlebih sesuai aturan Anda. Kandidat tetap memerlukan verifikasi on-chain dan kebijakan konfirmasi Anda sebelum dikreditkan. Di seluruh langganan dan polling, rekonsiliasi transfer yang sama berdasarkan chain, hash transaksi, dan indeks log agar dua jalur pengiriman tidak mengkreditkannya dua kali; simpan hash blok untuk melacak blok yang diganti.

### Pulihkan blok yang hilang atau diganti

Untuk `subscription.gap`, antrekan pemindaian `from_block` hingga `to_block` menggunakan jalur polling di bawah atau dataset Data API yang tersedia. `chain.reorg` adalah pemberitahuan gratis bahwa blok terkirim telah diganti, bukan celah pengiriman. Tandai atau buang peristiwa lama dalam rentang tersebut berdasarkan `ref`; rekonsiliasi catatan pembayaran berdasarkan `ref` dan `tx_hash` terhadap chain kanonis sebelum memproses peristiwa kanonis yang otomatis dikirim ulang dengan ID baru. Deduplikasi peristiwa tersebut berdasarkan `id`. Pemberitahuan reorg tidak memajukan kemajuan yang telah selesai; catat `complete_through_block` per chain, jangan pernah menyimpulkan penyelesaian dari nomor blok peristiwa terbesar.

[Replay](https://docs.blockvectra.com/en/guides/webhook-push/#delivery-retries-and-replay) menerima `chain` dan `from_block` dalam batas `replayable_from_block` saat ini. Replay hanya mengirim ulang kecocokan yang disimpan; tidak memindai periode sebelum alamat atau chain ditambahkan, atau periode saat langganan offline. Simpan kursor polling untuk mencakup interval tersebut dan celah yang kedaluwarsa. Kegagalan permintaan dan rentang replay tidak valid dijelaskan dalam [referensi error](https://docs.blockvectra.com/en/errors/); biaya pengiriman, riwayat, dan hari-alamat dijelaskan dalam [aturan penagihan](https://docs.blockvectra.com/en/guides/billing-rules/#webhook-push-billing).

Bagian selanjutnya mengimplementasikan penyaringan log ERC-20 dan polling berbasis kursor untuk pemantauan serta pemulihan.

## Peristiwa Transfer dan Parameter Filter

Kontrak token ERC-20 standar menerbitkan peristiwa berikut pada setiap transfer:

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

Saat memanggil `eth_getLogs`, kirim alamat kontrak token dan array `topics` untuk menyaring log yang cocok:

| Parameter   | Nilai                                                                | Deskripsi                                                                                                                                                                                                                                                        |
| ----------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`   | Alamat kontrak token (atau array alamat)                             | Alamat kontrak stablecoin tujuan. Anda dapat menentukan satu alamat (misalnya BSC USDT `0x55d398326f99059fF775485246999027B3197955`, Base USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`), atau array alamat untuk memantau beberapa token secara bersamaan   |
| `topics[0]` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | Hash tanda tangan peristiwa: `keccak256("Transfer(address,address,uint256)")`                                                                                                                                                                                    |
| `topics[1]` | `null`                                                               | Alamat pengirim (`from`). Karena pemantauan deposit menerima dana dari dompet pengguna mana pun, kirim `null` untuk mencocokkan semua pengirim                                                                                                                   |
| `topics[2]` | Alamat penerima dengan padding nol menjadi 32-byte                   | Alamat tujuan (`to`). Menurut spesifikasi log EVM, parameter alamat `indexed` menempati 32 byte (64 karakter heksadesimal). Tambahkan padding kiri pada alamat penerima 20-byte dengan 12 byte nol (24 karakter nol heksadesimal) untuk membentuk topic 32-byte. |
| `fromBlock` | Blok awal (heksadesimal)                                             | Awal rentang blok kueri (inklusif)                                                                                                                                                                                                                               |
| `toBlock`   | Blok akhir (heksadesimal)                                            | Akhir rentang blok kueri (inklusif)                                                                                                                                                                                                                              |

`value` yang tidak diindeks (jumlah transfer) dikodekan dalam bidang `data` objek log sebagai `uint256` heksadesimal 32-byte. Bagi jumlah mentah ini dengan 10^decimals untuk mendapatkan jumlah token yang mudah dibaca manusia (misalnya 18 decimals untuk BSC USDT; 6 decimals untuk Base dan Ethereum USDC).

## Polling Kursor dan Batas Rentang Blok

Layanan polling mengueri blok baru pada interval teratur (seperti setiap 3 hingga 5 detik).

### Memajukan Kursor

Pertahankan kursor persisten `last_polled_block` (blok tertinggi yang telah diproses dan di-commit) dalam database Anda:

1. Untuk setiap siklus polling, atur `fromBlock = last_polled_block + 1`.
2. Kueri head chain saat ini melalui `eth_blockNumber`, lalu hitung tinggi tujuan aman `safe_head` berdasarkan kedalaman konfirmasi Anda.
3. Jika `fromBlock <= safe_head`, kueri log dalam potongan hingga `safe_head`. Setelah berhasil memproses setiap potongan, majukan kursor.

### Batas Rentang Blok

Rentang blok satu panggilan `eth_getLogs` dihitung sebagai `toBlock − fromBlock + 1`. Rentang tersebut tidak boleh melebihi `max_logs_block_range` yang dipublikasikan untuk chain itu dalam `GET /v1/chains`.

Jika permintaan melebihi rentang ini, layanan menolak panggilan dengan kode 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
    }
  }
}
```

Permintaan yang melebihi rentang blok mengembalikan error JSON-RPC `-32602` (tidak ditagih). Dalam logika aplikasi, baca `max_logs_block_range` dari `GET /v1/chains` dan batasi setiap potongan polling: `chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head)`.

## Menangani Reorganisasi Blok dan Kedalaman Konfirmasi

Di dekat ujung blockchain, reorganisasi blok sementara (reorg) dapat terjadi. Mengkreditkan pembayaran pada `latest` tanpa kedalaman konfirmasi berisiko mengkreditkan transaksi pada cabang nonkanonis yang kemudian dibuang.

Terapkan perlindungan berikut untuk melindungi pemrosesan pembayaran:

### Kedalaman Konfirmasi

Alih-alih mengueri hingga `latest`, kueri hingga tinggi blok tujuan yang aman:

`safe_head = current_head - CONFIRMATION_DEPTH`

Atur `CONFIRMATION_DEPTH` sesuai toleransi risiko aplikasi Anda. Mengueri hanya hingga `safe_head` memastikan hanya blok dengan konfirmasi yang cukup diproses.

### Reorganisasi selama Polling

JSON-RPC EVM standar menetapkan `removed: true` pada objek log hanya dalam aliran langganan log WebSocket ketika peristiwa yang sebelumnya diterbitkan dibatalkan akibat reorg chain. Saat polling melalui HTTP dengan `eth_getLogs`, kueri mengembalikan log dari chain kanonis; log yang terkena reorg tidak akan muncul dalam kueri berikutnya. Polling dalam `safe_head` memastikan pembayaran hanya diproses pada blok dengan konfirmasi yang cukup.

## Deduplikasi berdasarkan (transactionHash, logIndex)

Pendengar pembayaran harus menerapkan idempotensi ketat:

1. **Beberapa Transfer dalam Satu Transaksi**: Satu transaksi dapat berisi beberapa peristiwa `Transfer` ke alamat deposit yang sama (misalnya router token yang membagi swap atau kontrak pembayaran keluar berganda). **Penting:** `transactionHash` saja tidak unik untuk setiap pembayaran.
2. **Polling Tumpang Tindih dan Percobaan Ulang**: Ketika layanan polling dimulai ulang, pulih dari error jaringan sementara, atau mundur beberapa blok untuk menangani reorg dangkal, log dari rentang blok yang sama dikueri beberapa kali.
3. **Keunikan Indeks Log**: `logIndex` mengidentifikasi posisi relatif log peristiwa dalam blok. Menurut spesifikasi EVM, pengenal unik gabungan kanonis untuk suatu peristiwa adalah `(transactionHash, logIndex)`.

Dalam skema database relasional, deklarasikan indeks unik gabungan pada tabel catatan deposit Anda:

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

Sebelum memproses deposit, periksa entri `(transactionHash, logIndex)` yang sudah ada untuk menjamin setiap transfer on-chain dikreditkan tepat sekali.

## Contoh Kode Lengkap

Contoh berikut menunjukkan pengambilan kemampuan jaringan dari `/v1/chains`, perhitungan rentang blok aman, polling log `Transfer` stablecoin sesuai batas rentang, dan deduplikasi peristiwa.

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


## Aturan Penagihan dan Panduan Terkait

* Untuk rincian pengukuran permintaan, bobot CU, dan penentuan penagihan kode error, lihat [Aturan Penagihan: Error dan Permintaan yang Tidak Ditagih](https://docs.blockvectra.com/en/guides/billing-rules/).
* Untuk panduan mendalam tentang batas rentang blok `eth_getLogs` dan logika pembagian potongan, lihat [Batas Rentang Blok eth\_getLogs dan Kueri Bertahap](https://docs.blockvectra.com/en/guides/getlogs-block-range/).
* Untuk perbedaan antara kueri node RPC real-time dan API transfer historis terindeks, lihat [Head Chain vs Riwayat Terindeks: Kapan Menggunakan eth\_getLogs vs Transfers](https://docs.blockvectra.com/en/guides/logs-vs-transfers/).

## Langkah selanjutnya

* [Jelajahi direktori dataset](https://blockvectra.com/en/data/) untuk melihat setiap dataset yang diindeks BlockVectra.
* [Lihat paket gratis dan harga](https://blockvectra.com/en/pricing/#free) untuk memeriksa apa yang termasuk dalam akun Anda.
* [Masuk ke konsol](https://console.blockvectra.com/login/?next=%2Fkeys%2F) untuk membuat API key.
