Cara Memantau Pembayaran USDT / USDC dengan Webhook dan RPC
Bangun penerima pembayaran dan kursor polling. Verifikasi kontrak token, penerima, dan jumlah bilangan bulat, deduplikasi peristiwa, serta rekonsiliasi blok yang hilang atau diganti.
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.
- Langkah pertama: Buat langganan dan pantau penerima, 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.
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 dan penerima HTTPS Anda sendiri; saring kontrak token dan jumlah di aplikasi Anda. Salin konfigurasi Webhook.
Tugas yang dapat Anda selesaikan dengan panduan ini
- Terima notifikasi pembayaran USDT / USDC di endpoint HTTPS Anda setelah memeriksa dukungan Push untuk chain yang dipilih.
- Validasi kandidat transfer 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 dengan kueri
eth_getLogsberbatas dan kursor tersimpan.
Pilih Webhook, WebSocket, atau polling
| Metode | Kegunaan | Pemulihan |
|---|---|---|
| Webhook | 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 | 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 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 mendefinisikan permintaan ini.
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 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.
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 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; biaya pengiriman, riwayat, dan hari-alamat dijelaskan dalam aturan penagihan.
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:
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:
- Untuk setiap siklus polling, atur
fromBlock = last_polled_block + 1. - Kueri head chain saat ini melalui
eth_blockNumber, lalu hitung tinggi tujuan amansafe_headberdasarkan kedalaman konfirmasi Anda. - Jika
fromBlock <= safe_head, kueri log dalam potongan hinggasafe_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:
{
"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:
- Beberapa Transfer dalam Satu Transaksi: Satu transaksi dapat berisi beberapa peristiwa
Transferke alamat deposit yang sama (misalnya router token yang membagi swap atau kontrak pembayaran keluar berganda). Penting:transactionHashsaja tidak unik untuk setiap pembayaran. - 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.
- Keunikan Indeks Log:
logIndexmengidentifikasi 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:
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.
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.mtsAturan Penagihan dan Panduan Terkait
- Untuk rincian pengukuran permintaan, bobot CU, dan penentuan penagihan kode error, lihat Aturan Penagihan: Error dan Permintaan yang Tidak Ditagih.
- Untuk panduan mendalam tentang batas rentang blok
eth_getLogsdan logika pembagian potongan, lihat Batas Rentang Blok eth_getLogs dan Kueri Bertahap. - 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.
Langkah selanjutnya
- Jelajahi direktori dataset untuk melihat setiap dataset yang diindeks BlockVectra.
- Lihat paket gratis dan harga untuk memeriksa apa yang termasuk dalam akun Anda.
- Masuk ke konsol untuk membuat API key.
Terakhir diperbarui:
Panduan awal Robinhood Chain Testnet
Mulai menggunakan RPC Robinhood Chain Testnet: URL RPC publik, pembacaan tanpa API key, log WebSocket dengan API key, akses faucet, dan penggunaan API key yang sama di mainnet.
Saham tertokenisasi
Kueri papan peringkat saham tertokenisasi harian dan metrik historis dengan Data API, mencakup bidang, konvensi pengodean, paginasi, dan estimasi penggunaan.