Como monitorar pagamentos em USDT / USDC com Webhooks e RPC
Crie um receptor de pagamentos e um cursor de polling. Verifique contratos de tokens, destinatários e quantidades inteiras, deduplique eventos e reconcilie blocos ausentes ou substituídos.
Para monitorar pagamentos com stablecoins ou detectar depósitos em exchanges, monitore transferências ERC-20 recebidas de USDT / USDC em redes EVM usando Webhooks, logs via WebSocket ou polling HTTP. Desenvolvedores e agentes de IA usam as mesmas APIs; selecione a rede, o contrato do token, o destinatário e a profundidade de confirmação antes de processar pagamentos. Escolha um fluxo de depósito, notificação para lojistas ou pagamento na solução de monitoramento de transferências USDT / USDC.
O monitoramento básico de transferências está disponível. A filtragem por quantidade e token é executada no seu receptor. Condições no servidor, múltiplas etapas de confirmação e alertas por mensagens instantâneas estarão disponíveis em breve.
Para desenvolvedores e agentes de IA: comece com uma API key e seu próprio receptor HTTPS; filtre contratos de tokens e quantidades na sua aplicação. Copie a configuração de Webhook.
Tarefas que este guia ajuda a concluir
- Receber notificações de pagamentos USDT / USDC no seu endpoint HTTPS após verificar o suporte a Push na rede selecionada.
- Validar uma transferência candidata, verificando rede, contrato do token, destinatário e quantidade inteira antes de aplicar sua verificação on-chain e política de confirmação.
- Recuperar logs de transferências ausentes com consultas limitadas de
eth_getLogse um cursor salvo.
Escolher Webhook, WebSocket ou polling
| Método | Quando usar | Recuperação |
|---|---|---|
| Webhook | Atividade de endereços enviada ao seu receptor HTTPS, incluindo transferências de tokens recebidas | Verifique assinaturas, deduplique IDs de eventos e trate subscription.gap / chain.reorg; execute replay das correspondências retidas |
| WebSocket | logs filtrados por uma conexão persistente | Reconecte, refaça as assinaturas e recupere blocos perdidos |
| Polling HTTP | Monitoramento agendado ou recuperação de logs históricos com seu próprio cursor | Consulte intervalos limitados de eth_getLogs e persista o progresso |
Consulte ws e subscriptions na resposta pública de GET /v1/chains antes de escolher WebSocket. O suporte a Push exige uma verificação separada: consulte GET /v1/push/chains com sua API key. Uma rede sem WebSocket pode usar Webhooks de endereços se estiver listada ali. Use polling quando precisar consultar blocos anteriores ou operar sem conexão persistente.
Receber pagamentos com Webhooks
Criar uma assinatura e monitorar o destinatário
Obtenha uma API key e implante um receptor HTTPS na porta 443. Selecione CHAIN na lista autenticada de redes Push, defina RECIPIENT como seu endereço de depósito e RECEIVER_URL como a URL do receptor. Este exemplo de shell exige jq; {} usa a quantidade padrão de confirmações da rede. Inspecione min_confirmations, default_confirmations e max_confirmations antes de escolher outra quantidade. A OpenAPI de Push define essas requisições.
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"A criação retorna id e secret. Armazene o segredo com segurança para o receptor; subscription.json contém uma credencial. Consulte a assinatura até applied_version >= change_version de address-change.json, depois registre chains[CHAIN].applied_from_block. Novos endereços são associados a partir desse bloco, então continue o polling para qualquer intervalo de pagamentos anterior.
Verificar, deduplicar e validar pagamentos
Salve a função de assinatura no corpo bruto como verify-push.js. O receptor abaixo aceita um Request da Web API em Node.js e lê seus bytes originais antes de interpretar o JSON. Crie secrets como um Map de strings de IDs de assinatura para segredos armazenados. Defina a configuração confiável expected como { chain, token, recipient, amountUnits }: token é o contrato verificado da stablecoin naquela rede e amountUnits é a quantidade inteira positiva esperada em suas menores unidades. Compare quantidades com BigInt, nunca com números de ponto flutuante ou o símbolo do 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 });
}Implemente store.transaction com armazenamento durável. Dentro de uma transação, insertEventOnce insere um evento com uma chave única (subscription_id, event.id) e retorna false para duplicatas; confirme-o junto com recordPaymentCandidate ou enqueueRecovery. Reverta todas as gravações em caso de falha para que uma nova tentativa possa processar o evento. Tarefas de recuperação também devem ser idempotentes. Retorne 2xx em até 10 segundos apenas após confirmar a transação; imponha o limite de corpo de 1 MiB no seu servidor HTTP.
Este exemplo verifica uma quantidade de pagamento esperada. Para vários pedidos, consulte a configuração confiável de pagamento por rede, token e destinatário e reconcilie pagamentos parciais ou excedentes conforme suas próprias regras. Um candidato ainda precisa de verificação on-chain e da sua política de confirmação antes de ser creditado. Entre assinaturas e polling, reconcilie a mesma transferência por rede, hash de transação e índice do log para que dois caminhos de entrega não a creditem duas vezes; retenha o hash do bloco para acompanhar blocos substituídos.
Recuperar blocos ausentes ou substituídos
Para subscription.gap, enfileire uma consulta de from_block até to_block usando o caminho de polling abaixo ou conjuntos de dados disponíveis da Data API. chain.reorg é um aviso gratuito de que blocos entregues foram substituídos, não uma lacuna de entrega. Marque ou descarte eventos antigos nesse intervalo por ref; reconcilie registros de pagamento por ref e tx_hash com a rede canônica antes de processar eventos canônicos reenviados automaticamente com novos IDs. Deduplique esses eventos por id. O aviso de reorg não avança o progresso concluído; registre complete_through_block por rede e nunca deduza conclusão pelo maior número de bloco dos eventos.
Replay aceita chain e from_block dentro do limite atual de replayable_from_block. Apenas reenvia correspondências retidas; não consulta períodos anteriores à adição do endereço ou rede, nem períodos em que a assinatura esteve offline. Mantenha um cursor de polling para cobrir esses intervalos e lacunas expiradas. Falhas de requisição e intervalos de replay inválidos estão na referência de erros; cobranças de entrega, histórico e endereços-dia são explicadas nas regras de cobrança.
As seções restantes implementam filtragem de logs ERC-20 e polling por cursor para monitoramento e recuperação.
Evento Transfer e parâmetros de filtro
Contratos de tokens ERC-20 padrão emitem o seguinte evento em cada transferência:
event Transfer(address indexed from, address indexed to, uint256 value);Ao chamar eth_getLogs, envie o endereço do contrato do token e o array topics para filtrar logs correspondentes:
| Parâmetro | Valor | Descrição |
|---|---|---|
address | Endereço do contrato do token (ou array de endereços) | Endereço do contrato da stablecoin desejada. Você pode especificar um endereço (por exemplo, BSC USDT 0x55d398326f99059fF775485246999027B3197955, Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) ou um array de endereços para monitorar vários tokens simultaneamente |
topics[0] | 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef | Hash da assinatura do evento: keccak256("Transfer(address,address,uint256)") |
topics[1] | null | Endereço do remetente (from). Como o monitoramento de depósitos aceita fundos de qualquer carteira de usuário, envie null para aceitar qualquer remetente |
topics[2] | Endereço do destinatário preenchido com zeros até 32 bytes | Endereço de destino (to). Nas especificações de logs EVM, parâmetros de endereço indexed ocupam 32 bytes (64 caracteres hexadecimais). Complete o endereço do destinatário de 20 bytes à esquerda com 12 bytes zero (24 caracteres hexadecimais zero) para formar um topic de 32 bytes. |
fromBlock | Bloco inicial (hexadecimal) | Início do intervalo de blocos da consulta (inclusivo) |
toBlock | Bloco final (hexadecimal) | Fim do intervalo de blocos da consulta (inclusivo) |
O value não indexado (quantidade transferida) é codificado no campo data do objeto de log como um uint256 hexadecimal de 32 bytes. Divida essa quantidade bruta por 10^decimals para obter a quantidade legível do token (por exemplo, 18 casas decimais para BSC USDT; 6 casas decimais para Base e Ethereum USDC).
Polling por cursor e limites de intervalo de blocos
Um serviço de polling consulta novos blocos em intervalos regulares (por exemplo, a cada 3 a 5 segundos).
Avanço do cursor
Mantenha um cursor persistente last_polled_block (o maior bloco processado e confirmado) no banco de dados:
- Em cada ciclo de polling, defina
fromBlock = last_polled_block + 1. - Consulte o bloco mais recente da rede com
eth_blockNumbere calcule a altura de destino segurasafe_headcom base na profundidade de confirmação. - Se
fromBlock <= safe_head, consulte os logs em partes atésafe_head. Após processar cada parte com sucesso, avance o cursor.
Limite de intervalo de blocos
O intervalo de blocos de uma chamada eth_getLogs é calculado como toBlock − fromBlock + 1. Não pode ultrapassar o max_logs_block_range publicado para aquela rede em GET /v1/chains.
Se uma requisição ultrapassa esse intervalo, o serviço rejeita a chamada com o código de erro -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
}
}
}Requisições que ultrapassam o intervalo de blocos retornam o erro JSON-RPC -32602 (sem cobrança). Na lógica da aplicação, consulte max_logs_block_range em GET /v1/chains e limite cada parte do polling: chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head).
Tratar reorganizações de blocos e profundidade de confirmação
Perto do bloco mais recente da blockchain, podem ocorrer reorganizações temporárias de blocos (reorgs). Creditar pagamentos em latest sem profundidade de confirmação traz o risco de creditar transações de um ramo órfão que será descartado depois.
Aplique as seguintes medidas para proteger o processamento de pagamentos:
Profundidade de confirmação
Em vez de consultar até latest, consulte até uma altura de bloco de destino segura:
safe_head = current_head - CONFIRMATION_DEPTH
Defina CONFIRMATION_DEPTH conforme a tolerância a risco da sua aplicação. Consultar apenas até safe_head garante que somente blocos com confirmações suficientes sejam processados.
Reorganizações durante o polling
O EVM JSON-RPC padrão define removed: true nos objetos de log apenas nos fluxos de assinatura de logs via WebSocket quando um evento emitido anteriormente é revertido por uma reorg da rede. No polling HTTP com eth_getLogs, as consultas retornam logs da rede canônica; logs de blocos reorganizados simplesmente não aparecem em consultas posteriores. Fazer polling dentro de safe_head garante que pagamentos sejam processados apenas em blocos com confirmações suficientes.
Deduplicação por (transactionHash, logIndex)
Monitores de pagamentos devem impor idempotência estrita:
- Várias transferências em uma transação: uma transação pode conter vários eventos
Transferpara o mesmo endereço de depósito (por exemplo, roteadores de tokens que dividem swaps ou contratos de múltiplos pagamentos). Importante:transactionHashsozinho não é único por pagamento. - Sobreposição de polling e novas tentativas: quando serviços de polling reiniciam, recuperam-se de erros temporários de rede ou voltam alguns blocos para tratar reorgs rasas, logs do mesmo intervalo são consultados várias vezes.
- Unicidade do índice de log:
logIndexidentifica a posição relativa do log do evento dentro do bloco. Nas especificações EVM, o identificador único composto canônico de um evento é(transactionHash, logIndex).
Em esquemas de bancos de dados relacionais, declare um índice único composto na tabela de registros de depósitos:
CREATE UNIQUE INDEX idx_transfers_tx_log ON deposit_records (transaction_hash, log_index);Antes de processar um depósito, verifique as entradas (transactionHash, logIndex) existentes para garantir que cada transferência on-chain seja creditada exatamente uma vez.
Exemplos de código completos
Os exemplos abaixo demonstram como consultar recursos da rede em /v1/chains, calcular intervalos seguros de blocos, consultar logs Transfer de stablecoins respeitando os limites de intervalo e deduplicar eventos.
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.mtsRegras de cobrança e guias relacionados
- Para detalhes sobre medição de requisições, pesos de CU e cobrança por código de erro, consulte Regras de cobrança: erros e requisições sem cobrança.
- Para orientações detalhadas sobre limites de intervalo de blocos de
eth_getLogse divisão em partes, consulte Limites de intervalo de blocos do eth_getLogs e consultas em partes. - Para diferenças entre consultas RPC em tempo real ao nó e APIs de transferências históricas indexadas, consulte Dados recentes da rede e histórico indexado: quando usar eth_getLogs ou Transfers.
Próximos passos
- Explore o diretório de conjuntos de dados para ver todos os conjuntos de dados indexados pela BlockVectra.
- Consulte o plano gratuito e os preços para verificar o que sua conta inclui.
- Entre no console para criar uma API key.
Última atualização:
Entender os preços em CU
Consulte os pesos de CU de RPC e Data API e as unidades de cobrança, calcule o preço por milhão de chamadas e estime custos com a API de planos atual.
Página de ativos da carteira
Crie uma página de ativos de carteira com saldos ERC-20 diferentes de zero, histórico de transferências e metadados em lote. Verifique a cobertura da rede, pagine resultados e ajuste quantidades inteiras pelas casas decimais.