Cách giám sát thanh toán USDT / USDC bằng Webhook và RPC
Xây dựng bộ nhận thanh toán và con trỏ thăm dò. Xác minh hợp đồng token, người nhận và số tiền nguyên, loại trùng sự kiện và đối chiếu các khối bị thiếu hoặc thay thế.
Để giám sát thanh toán stablecoin hoặc phát hiện tiền nạp sàn, hãy theo dõi các chuyển khoản ERC-20 USDT / USDC đến trên các chuỗi EVM bằng Webhook, log WebSocket hoặc thăm dò HTTP. Nhà phát triển và AI Agent dùng cùng các API; chọn chuỗi, hợp đồng token, người nhận và độ sâu xác nhận trước khi xử lý thanh toán. Chọn quy trình nạp tiền, thông báo người bán hoặc chi trả trong giải pháp giám sát chuyển USDT / USDC.
- Bước đầu tiên: Tạo đăng ký và theo dõi người nhận, bắt đầu với API key và bộ nhận HTTPS của bạn.
- Hoàn thành khi: Chuyển khoản khớp vượt qua kiểm tra chữ ký, chuỗi, token, người nhận và số tiền nguyên, được lưu một lần thành khoản thanh toán cần xác minh và bộ nhận trả HTTP
204; xác minh trên chuỗi theo chính sách xác nhận của bạn trước khi ghi có.
Quy trình thanh toán stablecoin.
Giám sát chuyển tiền cơ bản đã khả dụng. Việc lọc số tiền và token được thực hiện tại bộ nhận của bạn. Điều kiện phía máy chủ, nhiều giai đoạn xác nhận và cảnh báo IM sẽ sớm ra mắt.
Dành cho nhà phát triển và AI Agent: bắt đầu với API key và bộ nhận HTTPS của bạn; lọc hợp đồng token và số tiền trong ứng dụng. Sao chép cấu hình Webhook.
Các tác vụ hướng dẫn này giúp bạn hoàn thành
- Nhận thông báo thanh toán USDT / USDC tại endpoint HTTPS sau khi kiểm tra hỗ trợ Push cho chuỗi đã chọn.
- Kiểm tra khoản chuyển tiền cần xác minh bằng cách kiểm tra chuỗi, hợp đồng token, người nhận và số tiền nguyên trước khi áp dụng chính sách xác minh trên chuỗi và xác nhận của bạn.
- Truy xuất bổ sung log chuyển tiền bị thiếu bằng truy vấn
eth_getLogscó giới hạn và con trỏ đã lưu.
Chọn Webhook, WebSocket hoặc thăm dò
| Phương thức | Dùng cho | Khôi phục |
|---|---|---|
| Webhook | Hoạt động địa chỉ gửi tới bộ nhận HTTPS, bao gồm chuyển token đến | Xác minh chữ ký, loại trùng ID sự kiện và xử lý subscription.gap / chain.reorg; phát lại sự kiện khớp còn lưu |
| WebSocket | logs đã lọc qua kết nối duy trì liên tục | Kết nối lại, đăng ký lại và truy xuất bổ sung các khối bị bỏ lỡ |
| Thăm dò HTTP | Giám sát theo lịch hoặc truy xuất bổ sung log lịch sử bằng con trỏ của bạn | Truy vấn các khoảng eth_getLogs có giới hạn và lưu tiến độ bền vững |
Đọc ws và subscriptions trong phản hồi công khai GET /v1/chains trước khi chọn WebSocket. Hỗ trợ Push cần kiểm tra riêng: đọc GET /v1/push/chains bằng API key. Chuỗi không có WebSocket có thể dùng Webhook địa chỉ nếu có trong danh sách đó. Dùng thăm dò khi cần quét các khối trước đó hoặc chạy mà không duy trì kết nối liên tục.
Nhận thanh toán bằng Webhook
Tạo đăng ký và theo dõi người nhận
Lấy API key và triển khai bộ nhận HTTPS trên port 443. Chọn CHAIN từ danh sách chuỗi Push có xác thực, đặt RECIPIENT thành địa chỉ nạp tiền và RECEIVER_URL thành URL bộ nhận. Ví dụ shell này cần jq; {} dùng số xác nhận mặc định của chuỗi. Kiểm tra min_confirmations, default_confirmations và max_confirmations trước khi chọn số khác. Push OpenAPI định nghĩa các yêu cầu này.
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"Khi tạo, phản hồi trả id và secret. Lưu secret an toàn cho bộ nhận; subscription.json chứa thông tin xác thực. Thăm dò đăng ký cho đến khi applied_version >= change_version từ address-change.json, rồi ghi lại chains[CHAIN].applied_from_block. Địa chỉ mới bắt đầu khớp từ khối đó, vì vậy tiếp tục thăm dò cho mọi khoảng thanh toán trước đó.
Xác minh, loại trùng và kiểm tra thanh toán
Lưu hàm chữ ký body gốc thành verify-push.js. Bộ nhận bên dưới chấp nhận Web API Request trong Node.js và đọc byte gốc trước khi phân tích JSON. Tạo secrets dưới dạng Map ánh xạ chuỗi ID đăng ký tới secret đã lưu. Đặt cấu hình expected đáng tin cậy thành { chain, token, recipient, amountUnits }: token là hợp đồng stablecoin đã xác minh trên chuỗi đó và amountUnits là số tiền nguyên dương dự kiến theo đơn vị nhỏ nhất. So sánh số tiền bằng BigInt, tuyệt đối không dùng số dấu phẩy động hoặc ký hiệu 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 });
}Triển khai store.transaction bằng lưu trữ bền vững. Trong một giao dịch, insertEventOnce chèn sự kiện theo khóa duy nhất (subscription_id, event.id) và trả false nếu trùng; commit cùng với recordPaymentCandidate hoặc enqueueRecovery. Hoàn tác toàn bộ ghi khi thất bại để lần thử lại có thể xử lý sự kiện. Tác vụ khôi phục cũng phải có tính lũy đẳng. Chỉ trả 2xx trong vòng 10 giây sau khi commit; áp dụng giới hạn body 1 MiB trên máy chủ HTTP.
Ví dụ này kiểm tra một số tiền thanh toán dự kiến. Với nhiều đơn hàng, tra cấu hình thanh toán đáng tin cậy theo chuỗi, token và người nhận, rồi đối chiếu thanh toán thiếu hoặc dư theo quy tắc của bạn. Khoản thanh toán cần xác minh vẫn phải được xác minh trên chuỗi và đáp ứng chính sách xác nhận trước khi ghi có. Giữa các đăng ký và thăm dò, đối chiếu cùng một chuyển khoản theo chuỗi, hash giao dịch và chỉ số log để hai đường gửi không ghi có hai lần; giữ hash khối để theo dõi khối bị thay thế.
Khôi phục các khối bị thiếu hoặc thay thế
Với subscription.gap, đưa tác vụ quét từ from_block đến to_block vào hàng đợi bằng đường thăm dò bên dưới hoặc các bộ dữ liệu Data API khả dụng. chain.reorg là thông báo miễn phí rằng các khối đã gửi bị thay thế, không phải khoảng trống gửi sự kiện. Đánh dấu hoặc loại bỏ sự kiện cũ trong khoảng đó theo ref; đối chiếu bản ghi thanh toán theo ref và tx_hash với chuỗi chuẩn trước khi xử lý sự kiện chuẩn được tự động gửi lại với ID mới. Loại trùng các sự kiện đó theo id. Thông báo tái tổ chức không đẩy tiến độ đã hoàn thành; ghi complete_through_block theo từng chuỗi, tuyệt đối không suy ra hoàn thành từ số khối lớn nhất của sự kiện.
Phát lại chấp nhận chain và from_block trong giới hạn replayable_from_block hiện tại. Nó chỉ gửi lại sự kiện khớp còn lưu; không quét khoảng trước khi địa chỉ hoặc chuỗi được thêm, hay lúc đăng ký ngoại tuyến. Giữ con trỏ thăm dò để bao phủ các khoảng đó và khoảng trống đã hết thời gian lưu. Lỗi yêu cầu và khoảng phát lại không hợp lệ được mô tả trong tham chiếu lỗi; phí gửi sự kiện, lịch sử và địa chỉ-ngày được giải thích trong quy tắc thanh toán.
Các phần còn lại triển khai lọc log ERC-20 và thăm dò bằng con trỏ để giám sát và khôi phục.
Sự kiện Transfer và tham số lọc
Hợp đồng token ERC-20 tiêu chuẩn phát sự kiện sau cho mỗi lần chuyển tiền:
event Transfer(address indexed from, address indexed to, uint256 value);Khi gọi eth_getLogs, truyền địa chỉ hợp đồng token và mảng topics để lọc log khớp:
| Tham số | Giá trị | Mô tả |
|---|---|---|
address | Địa chỉ hợp đồng token (hoặc mảng địa chỉ) | Địa chỉ hợp đồng stablecoin mục tiêu. Có thể chỉ định một địa chỉ (ví dụ BSC USDT 0x55d398326f99059fF775485246999027B3197955, Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913), hoặc mảng địa chỉ để giám sát đồng thời nhiều token |
topics[0] | 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef | Hash chữ ký sự kiện: keccak256("Transfer(address,address,uint256)") |
topics[1] | null | Địa chỉ người gửi (from). Vì giám sát tiền nạp chấp nhận tiền từ bất kỳ ví người dùng nào, truyền null để khớp mọi người gửi |
topics[2] | Địa chỉ người nhận đệm số 0 thành 32 byte | Địa chỉ đích (to). Theo đặc tả log EVM, tham số địa chỉ indexed chiếm 32 byte (64 ký tự hex). Đệm bên trái địa chỉ người nhận 20 byte bằng 12 byte 0 (24 ký tự 0 thập lục phân) để tạo topic 32 byte. |
fromBlock | Khối bắt đầu (thập lục phân) | Đầu khoảng khối truy vấn (bao gồm) |
toBlock | Khối kết thúc (thập lục phân) | Cuối khoảng khối truy vấn (bao gồm) |
value không được đánh chỉ mục (số tiền chuyển) được mã hóa trong trường data của đối tượng log dưới dạng uint256 thập lục phân 32 byte. Chia số tiền thô này cho 10^decimals để được số lượng token dễ đọc (ví dụ 18 chữ số thập phân cho BSC USDT; 6 chữ số cho Base và Ethereum USDC).
Thăm dò bằng con trỏ và giới hạn khoảng khối
Dịch vụ thăm dò truy vấn khối mới theo chu kỳ đều đặn (chẳng hạn mỗi 3 đến 5 giây).
Tiến con trỏ
Duy trì con trỏ bền vững last_polled_block (khối cao nhất đã xử lý và commit) trong cơ sở dữ liệu:
- Với mỗi chu kỳ thăm dò, đặt
fromBlock = last_polled_block + 1. - Truy vấn đầu chuỗi hiện tại bằng
eth_blockNumbervà tính độ cao mục tiêu an toànsafe_headtheo độ sâu xác nhận. - Nếu
fromBlock <= safe_head, truy vấn log theo từng phần đếnsafe_head. Sau khi xử lý thành công mỗi phần, tiến con trỏ.
Giới hạn khoảng khối
Độ rộng khối của một lần gọi eth_getLogs được tính bằng toBlock − fromBlock + 1. Nó không được vượt quá max_logs_block_range công bố cho chuỗi đó trong GET /v1/chains.
Nếu yêu cầu vượt khoảng này, dịch vụ từ chối lệnh gọi với mã lỗi -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
}
}
}Yêu cầu vượt khoảng khối trả lỗi JSON-RPC -32602 (không tính phí). Trong logic ứng dụng, đọc max_logs_block_range từ GET /v1/chains và giới hạn mỗi phần thăm dò: chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head).
Xử lý tái tổ chức khối và độ sâu xác nhận
Gần đầu chuỗi, có thể xảy ra tái tổ chức khối tạm thời (reorg). Ghi có thanh toán tại latest mà không có độ sâu xác nhận có nguy cơ ghi có giao dịch trên nhánh khối mồ côi sau đó bị loại bỏ.
Áp dụng các biện pháp sau để bảo vệ việc xử lý thanh toán:
Độ sâu xác nhận
Thay vì truy vấn đến latest, truy vấn đến độ cao khối mục tiêu an toàn:
safe_head = current_head - CONFIRMATION_DEPTH
Đặt CONFIRMATION_DEPTH theo mức chấp nhận rủi ro của ứng dụng. Chỉ truy vấn đến safe_head đảm bảo chỉ xử lý khối có đủ xác nhận.
Tái tổ chức trong khi thăm dò
JSON-RPC EVM tiêu chuẩn chỉ đặt removed: true trên đối tượng log trong luồng đăng ký log WebSocket khi sự kiện đã phát bị đảo ngược do tái tổ chức chuỗi. Khi thăm dò qua HTTP bằng eth_getLogs, truy vấn trả log từ chuỗi chuẩn; log bị tái tổ chức đơn giản sẽ không xuất hiện trong các truy vấn tiếp theo. Thăm dò trong safe_head đảm bảo thanh toán chỉ được xử lý trên các khối đủ xác nhận.
Loại trùng theo (transactionHash, logIndex)
Bộ theo dõi thanh toán phải bảo đảm tính lũy đẳng nghiêm ngặt:
- Nhiều chuyển khoản trong một giao dịch: Một giao dịch có thể chứa nhiều sự kiện
Transferđến cùng địa chỉ nạp (ví dụ bộ định tuyến token chia giao dịch hoán đổi hoặc hợp đồng chi trả nhiều lần). Quan trọng: Chỉ riêngtransactionHashkhông duy nhất cho từng khoản thanh toán. - Thăm dò chồng lấn và thử lại: Khi dịch vụ thăm dò khởi động lại, khôi phục sau lỗi mạng tạm thời hoặc lùi vài khối để xử lý tái tổ chức nông, log từ cùng khoảng khối được truy vấn nhiều lần.
- Tính duy nhất của chỉ số log:
logIndexxác định vị trí tương đối của log sự kiện trong khối. Theo đặc tả EVM, định danh duy nhất kết hợp chuẩn của sự kiện là(transactionHash, logIndex).
Trong lược đồ cơ sở dữ liệu quan hệ, khai báo chỉ mục duy nhất kết hợp trên bảng bản ghi tiền nạp:
CREATE UNIQUE INDEX idx_transfers_tx_log ON deposit_records (transaction_hash, log_index);Trước khi xử lý tiền nạp, đối chiếu các mục (transactionHash, logIndex) hiện có để bảo đảm mỗi chuyển khoản trên chuỗi chỉ được ghi có đúng một lần.
Ví dụ mã hoàn chỉnh
Các ví dụ bên dưới minh họa việc lấy khả năng mạng từ /v1/chains, tính khoảng khối an toàn, thăm dò log Transfer stablecoin tuân thủ giới hạn khoảng và loại trùng sự kiện.
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.mtsQuy tắc thanh toán và hướng dẫn liên quan
- Để biết chi tiết về đo lường yêu cầu, trọng số CU và xác định tính phí theo mã lỗi, xem Quy tắc thanh toán: lỗi và yêu cầu không tính phí.
- Để tìm hiểu sâu về giới hạn khoảng khối
eth_getLogsvà logic chia phần, xem Giới hạn khoảng khối eth_getLogs và truy vấn theo phần. - Để biết khác biệt giữa truy vấn nút RPC thời gian thực và API chuyển tiền lịch sử được lập chỉ mục, xem Đầu chuỗi và lịch sử được lập chỉ mục: khi nào dùng eth_getLogs hay Transfers.
Các bước tiếp theo
- Duyệt danh mục bộ dữ liệu để xem mọi bộ dữ liệu BlockVectra lập chỉ mục.
- Xem gói miễn phí và giá để kiểm tra những gì tài khoản bao gồm.
- Đăng nhập bảng điều khiển để tạo API key.
Cập nhật lần cuối:
Bắt đầu với Robinhood Chain Testnet
Bắt đầu với RPC Robinhood Chain Testnet: URL RPC công khai, đọc không cần key, log WebSocket bằng API key, truy cập faucet và chuyển cùng key sang mainnet.
Cổ phiếu token hóa
Truy vấn bảng xếp hạng cổ phiếu token hóa hàng ngày và các chỉ số lịch sử bằng Data API, bao gồm các trường, quy ước mã hóa, phân trang và ước tính mức sử dụng.