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

Escolher Webhook, WebSocket ou polling

MétodoQuando usarRecuperação
WebhookAtividade de endereços enviada ao seu receptor HTTPS, incluindo transferências de tokens recebidasVerifique assinaturas, deduplique IDs de eventos e trate subscription.gap / chain.reorg; execute replay das correspondências retidas
WebSocketlogs filtrados por uma conexão persistenteReconecte, refaça as assinaturas e recupere blocos perdidos
Polling HTTPMonitoramento agendado ou recuperação de logs históricos com seu próprio cursorConsulte 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âmetroValorDescrição
addressEndereç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]0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efHash da assinatura do evento: keccak256("Transfer(address,address,uint256)")
topics[1]nullEndereç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 bytesEndereç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.
fromBlockBloco inicial (hexadecimal)Início do intervalo de blocos da consulta (inclusivo)
toBlockBloco 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:

  1. Em cada ciclo de polling, defina fromBlock = last_polled_block + 1.
  2. Consulte o bloco mais recente da rede com eth_blockNumber e calcule a altura de destino segura safe_head com base na profundidade de confirmação.
  3. 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:

  1. Várias transferências em uma transação: uma transação pode conter vários eventos Transfer para o mesmo endereço de depósito (por exemplo, roteadores de tokens que dividem swaps ou contratos de múltiplos pagamentos). Importante: transactionHash sozinho não é único por pagamento.
  2. 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.
  3. Unicidade do índice de log: logIndex identifica 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.mts

Regras de cobrança e guias relacionados

Próximos passos

Última atualização:

Nesta página