Как отслеживать платежи USDT / USDC с помощью Webhooks и RPC
Создайте приемник платежей и курсор опроса. Проверяйте контракты токенов, получателей и целочисленные суммы, дедуплицируйте события и согласовывайте пропущенные или замененные блоки.
Для мониторинга платежей в стейблкоинах или обнаружения депозитов на бирже отслеживайте входящие переводы ERC-20 USDT / USDC в EVM-сетях с помощью Webhooks, логов WebSocket или HTTP-опроса. Разработчики и AI Agent используют одинаковые API; перед обработкой платежей выберите сеть, контракт токена, получателя и глубину подтверждений. Выберите рабочий процесс депозитов, уведомлений для продавцов или выплат в решении для мониторинга переводов USDT / USDC.
- Первый шаг: Создайте подписку и отслеживайте получателя, начав с API key и вашего HTTPS-приемника.
- Критерий завершения: подходящий перевод проходит проверку подписи, сети, токена, получателя и целочисленной суммы, однократно сохраняется как кандидат на платеж, а приемник возвращает HTTP
204; проверьте его ончейн в соответствии с вашей политикой подтверждений перед зачислением.
Рабочие процессы платежей в стейблкоинах.
Базовый мониторинг переводов доступен. Фильтрация сумм и токенов выполняется в вашем приемнике. Серверные условия, несколько этапов подтверждения и IM-уведомления появятся скоро.
Для разработчиков и AI Agent: начните с API key и собственного HTTPS-приемника; фильтруйте контракты токенов и суммы в вашем приложении. Скопируйте настройку webhook.
Задачи, которые помогает решить это руководство
- Получение уведомлений о платежах USDT / USDC на ваш HTTPS-эндпоинт после проверки поддержки Push для выбранной сети.
- Валидация кандидата на перевод путем проверки сети, контракта токена, получателя и целочисленной суммы перед применением ончейн-проверки и политики подтверждений.
- Выгрузка пропущенных логов переводов с помощью ограниченных запросов
eth_getLogsи сохраненного курсора.
Выбор Webhook, WebSocket или опроса
| Метод | Для чего использовать | Восстановление |
|---|---|---|
| Webhook | Активность адреса, отправляемая на ваш HTTPS-приемник, включая входящие переводы токенов | Проверка подписей, дедупликация ID событий и обработка subscription.gap / chain.reorg; повтор сохраненных совпадений |
| WebSocket | Отфильтрованные логи logs через постоянное соединение | Переподключение, повторная подписка и довыгрузка пропущенных блоков |
| HTTP-опрос | Регулярный мониторинг или выгрузка исторических логов с собственным курсором | Запрос ограниченных диапазонов eth_getLogs и сохранение прогресса |
Проверьте ws и subscriptions в публичном ответе GET /v1/chains перед выбором WebSocket. Поддержка Push проверяется отдельно: выполните GET /v1/push/chains с вашим API key. Сеть без WebSocket может использовать Webhooks для адресов, если она указана в этом списке. Используйте опрос, когда необходимо просканировать более ранние блоки или работать без постоянного соединения.
Прием платежей с помощью Webhooks
Создание подписки и отслеживание получателя
Получите API key и разверните HTTPS-приемник на порту 443. Выберите CHAIN из авторизованного списка сетей Push, установите RECIPIENT в адрес вашего депозита, а RECEIVER_URL — в URL вашего приемника. Для этого примера оболочки требуется jq; {} использует количество подтверждений по умолчанию для сети. Проверьте min_confirmations, default_confirmations и max_confirmations перед выбором другого значения. Эти запросы определены в Push OpenAPI.
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"Создание возвращает id и secret. Надежно сохраните секрет для приемника; subscription.json содержит учетные данные. Опрашивайте подписку до тех пор, пока applied_version >= change_version из address-change.json, затем зафиксируйте chains[CHAIN].applied_from_block. Новые адреса сопоставляются начиная с этого блока, поэтому продолжайте опрос для любых более ранних интервалов платежей.
Проверка, дедупликация и валидация платежей
Сохраните функцию проверки подписи исходного тела как verify-push.js. Приведенный ниже приемник принимает Web API Request в Node.js и считывает его исходные байты до парсинга JSON. Сформируйте secrets как Map строк ID подписок к сохраненным секретам. Задайте доверенную конфигурацию expected в виде { chain, token, recipient, amountUnits }: token — проверенный контракт стейблкоина в этой сети, а amountUnits — ожидаемая положительная целочисленная сумма в минимальных единицах токена. Сравнивайте суммы с помощью BigInt, ни в коем случае не используя числа с плавающей запятой или символ токена.
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 });
}Реализуйте store.transaction с использованием надежного хранилища. В рамках одной транзакции insertEventOnce вставляет событие с уникальным ключом (subscription_id, event.id) и возвращает false при дубликате; зафиксируйте (commit) его вместе с recordPaymentCandidate или enqueueRecovery. Откатывайте все записи при сбое, чтобы повторная попытка могла обработать событие. Задачи восстановления также должны быть идемпотентными. Возвращайте ответ 2xx в течение 10 секунд только после фиксации; настройте лимит размера тела запроса в 1 МиБ на вашем HTTP-сервере.
Этот пример проверяет одну ожидаемую сумму платежа. Для нескольких заказов находите доверенную конфигурацию платежа по сети, токену и получателю и сопоставляйте частичные или избыточные платежи по собственным правилам. Кандидат на платеж все еще требует ончейн-проверки и соблюдения вашей политики подтверждений перед зачислением. При совместном использовании подписок и опроса сопоставляйте один и тот же перевод по сети, хешу транзакции и индексу лога, чтобы два пути доставки не зачислили его дважды; сохраняйте хеш блока для отслеживания замененных блоков.
Восстановление пропущенных или замененных блоков
Для subscription.gap поставьте в очередь сканирование от from_block до to_block, используя путь опроса ниже или доступные наборы данных Data API. chain.reorg — это бесплатное уведомление о том, что доставленные блоки были заменены, а не о пропуске доставки. Пометьте или отбросьте старые события в этом диапазоне по ref; сверяйте записи платежей по ref и tx_hash с канонической цепочкой перед обработкой автоматически повторно доставленных канонических событий с новыми ID. Дедуплицируйте эти события по id. Уведомление о реорганизации не продвигает завершенный прогресс; фиксируйте complete_through_block для каждой сети, никогда не выводя завершение из наибольшего номера блока событий.
Повторная отправка (Replay) принимает chain и from_block в пределах текущей границы replayable_from_block. Она лишь повторно отправляет сохраненные совпадения; она не сканирует периоды до добавления адреса или сети, а также периоды, когда подписка была отключена. Поддерживайте курсор опроса для покрытия этих интервалов и устаревших пропусков. Ошибки запросов и недопустимые диапазоны replay описаны в справочнике ошибок; плата за доставку, историю и адресо-дни объясняется в правилах биллинга.
В остальных разделах рассматривается реализация фильтрации логов ERC-20 и опрос на основе курсора для мониторинга и восстановления.
Событие Transfer и параметры фильтра
Стандартные контракты токенов ERC-20 генерируют следующее событие при каждом переводе:
event Transfer(address indexed from, address indexed to, uint256 value);При вызове eth_getLogs передайте адрес контракта токена и массив topics для фильтрации подходящих логов:
| Параметр | Значение | Описание |
|---|---|---|
address | Адрес контракта токена (или массив адресов) | Адрес целевого контракта стейблкоина. Можно указать один адрес (например, BSC USDT 0x55d398326f99059fF775485246999027B3197955, Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) или массив адресов для параллельного мониторинга нескольких токенов |
topics[0] | 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef | Хеш сигнатуры события: keccak256("Transfer(address,address,uint256)") |
topics[1] | null | Адрес отправителя (from). Поскольку мониторинг депозитов принимает средства с любого кошелька пользователя, передайте null для сопоставления с любым отправителем |
topics[2] | 32-байтовый адрес получателя, дополненный нулями | Адрес назначения (to). Согласно спецификациям логов EVM, параметры indexed адресов занимают 32 байта (64 шестнадцатеричных символа). Дополните 20-байтовый адрес получателя слева 12 нулевыми байтами (24 шестнадцатеричных нуля) для формирования 32-байтового топика. |
fromBlock | Начальный блок (шестнадцатеричный) | Начало диапазона блоков запроса (включительно) |
toBlock | Конечный блок (шестнадцатеричный) | Конец диапазона блоков запроса (включительно) |
Неиндексированное значение value (сумма перевода) закодировано в поле data объекта лога как 32-байтовое шестнадцатеричное число uint256. Разделите эту исходную сумму на 10^decimals, чтобы получить понятное человеку количество токенов (например, 18 знаков после запятой для BSC USDT; 6 знаков для Base и Ethereum USDC).
Опрос по курсору и ограничения диапазона блоков
Сервис опроса запрашивает новые блоки через регулярные интервалы (например, каждые 3–5 секунд).
Продвижение курсора
Поддерживайте постоянный курсор last_polled_block (наибольший обработанный и зафиксированный блок) в вашей базе данных:
- Для каждого цикла опроса установите
fromBlock = last_polled_block + 1. - Запросите текущую вершину сети через
eth_blockNumberи рассчитайте безопасную целевую высотуsafe_headна основе вашей глубины подтверждений. - Если
fromBlock <= safe_head, запрашивайте логи частями (чанками) вплоть доsafe_head. После успешной обработки каждого чанка продвигайте курсор.
Ограничение диапазона блоков
Диапазон блоков одного вызова eth_getLogs рассчитывается как toBlock − fromBlock + 1. Он не должен превышать max_logs_block_range, опубликованный для этой сети в GET /v1/chains.
Если запрос превышает этот диапазон, сервис отклоняет вызов с кодом ошибки -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
}
}
}Запросы, превышающие диапазон блоков, возвращают ошибку JSON-RPC -32602 (не тарифицируются). В логике вашего приложения считайте max_logs_block_range из GET /v1/chains и ограничивайте каждый интервал опроса: chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head).
Обработка реорганизаций блоков и глубина подтверждений
Вблизи вершины блокчейна могут происходить временные реорганизации блоков (reorgs). Зачисление платежей на блоке latest без необходимой глубины подтверждений создает риск зачисления транзакций в изолированной ветке, которая впоследствии будет отброшена.
Применяйте следующие защитные меры для обеспечения безопасности обработки платежей:
Глубина подтверждений
Вместо запроса вплоть до latest запрашивайте данные до безопасной целевой высоты блока:
safe_head = current_head - CONFIRMATION_DEPTH
Задайте CONFIRMATION_DEPTH в соответствии с допустимым уровнем риска вашего приложения. Запрос только до safe_head гарантирует обработку только тех блоков, которые получили достаточное количество подтверждений.
Реорганизации во время опроса
Стандартный EVM JSON-RPC устанавливает removed: true в объектах логов только в потоках подписок на логи WebSocket, когда ранее сгенерированное событие откатывается из-за реорганизации сети. При опросе по HTTP с помощью eth_getLogs запросы возвращают логи из канонической цепочки; логи из отброшенных блоков просто не появятся в последующих запросах. Опрос в пределах safe_head гарантирует, что платежи будут обработаны только на блоках с достаточным подтверждением.
Дедупликация по (transactionHash, logIndex)
Обработчики платежей должны обеспечивать строгую идемпотентность:
- Несколько переводов в одной транзакции: одна транзакция может содержать несколько событий
Transferна один и тот же адрес депозита (например, маршрутизаторы токенов, разделяющие свопы, или контракты множественных выплат). Важно: один толькоtransactionHashне является уникальным для каждого платежа. - Перекрывающийся опрос и повторные попытки: при перезапуске сервисов опроса, восстановлении после временных сетевых ошибок или откате на несколько блоков для обработки неглубоких реорганизаций логи из одного и того же диапазона блоков запрашиваются повторно.
- Уникальность индекса лога:
logIndexопределяет относительную позицию лога события внутри блока. В спецификациях EVM каноническим составным уникальным идентификатором события является(transactionHash, logIndex).
В схемах реляционных баз данных объявите составной уникальный индекс для таблицы записей депозитов:
CREATE UNIQUE INDEX idx_transfers_tx_log ON deposit_records (transaction_hash, log_index);Перед обработкой депозита выполняйте проверку по существующим записям (transactionHash, logIndex), чтобы гарантировать, что каждый ончейн-перевод будет зачислен ровно один раз.
Полные примеры кода
Приведенные ниже примеры демонстрируют получение возможностей сети из /v1/chains, расчет безопасных диапазонов блоков, опрос логов Transfer стейблкоинов с соблюдением лимитов диапазона и дедупликацию событий.
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Правила биллинга и связанные руководства
- Подробные сведения об учете запросов, весах CU и правилах тарификации кодов ошибок см. в Правилах биллинга: ошибки и нетарифицируемые запросы.
- Подробные рекомендации по ограничениям диапазонов блоков
eth_getLogsи логике разделения на чанки см. в Ограничениях диапазона блоков eth_getLogs и запросах по чанкам. - Различия между запросами к RPC-узлам в реальном времени и API индексированной истории переводов см. в руководстве Вершина цепочки против индексированной истории: когда использовать eth_getLogs, а когда Transfers.
Следующие шаги
- Изучите каталог наборов данных, чтобы увидеть все наборы данных, индексируемые BlockVectra.
- Ознакомьтесь с бесплатным тарифом и ценами, чтобы узнать, что включено в ваш аккаунт.
- Войдите в консоль, чтобы создать API key.
Последнее обновление:
Симуляция транзакций
Выполняйте симуляцию нескольких транзакций и проверяйте изменения состояния перед их отправкой в сеть с помощью eth_simulateV1. Узнайте о политиках методов поддерживаемых сетей, структуре запросов спецификации исполнения, весах CU и интеграции с AI Agent через MCP.
Мультипликатор токенов акций
Узнайте, как работает мультипликатор токенов акций Robinhood Chain, как конвертировать балансы в акции, считывать uiMultiplier через eth_call и отслеживать держателей и ежедневную активность через Data API.