So überwachen Sie USDT- / USDC-Zahlungen mit Webhooks und RPC

Erstellen Sie einen Zahlungsempfänger und einen Polling-Cursor. Verifizieren Sie Token-Verträge, Empfänger und ganzzahlige Beträge, deduplizieren Sie Ereignisse und gleichen Sie fehlende oder ersetzte Blöcke ab.

Für die Überwachung von Stablecoin-Zahlungen oder die Erkennung von Börseneinzahlungen überwachen Sie eingehende ERC-20 USDT- / USDC-Transfers auf EVM-Chains mithilfe von Webhooks, WebSocket-Logs oder HTTP-Polling. Entwickler und KI-Agenten nutzen dieselben APIs; wählen Sie vor der Zahlungsverarbeitung die Chain, den Token-Vertrag, den Empfänger und die Bestätigungstiefe aus. Wählen Sie einen Einzahlungs-, Händlerbenachrichtigungs- oder Auszahlungs-Workflow in der USDT- / USDC-Transferüberwachungslösung.

  • Erster Schritt: Abonnement erstellen und Empfänger überwachen, beginnend mit einem API-Schlüssel und Ihrem HTTPS-Empfänger.
  • Abgeschlossen, wenn: Ein passender Transfer die Prüfungen von Signatur, Chain, Token, Empfänger und ganzzahligem Betrag besteht, einmalig als Zahlungskandidat gespeichert wird und der Empfänger HTTP 204 zurückgibt; verifizieren Sie ihn on-chain gemäß Ihrer Bestätigungsrichtlinie, bevor Sie ihn gutschreiben.

Workflows für Stablecoin-Zahlungen.

Grundlegende Transferüberwachung ist verfügbar. Die Filterung von Beträgen und Token erfolgt in Ihrem Empfänger. Serverseitige Bedingungen, mehrere Bestätigungsstufen und IM-Benachrichtigungen folgen in Kürze.

Für Entwickler und KI-Agenten: Starten Sie mit einem API-Schlüssel und Ihrem eigenen HTTPS-Empfänger; filtern Sie Token-Verträge und Beträge in Ihrer Anwendung. Webhook-Einrichtung kopieren.

Aufgaben, die dieser Leitfaden abdeckt

Webhook, WebSocket oder Polling wählen

MethodeVerwendungWiederherstellung
WebhookAn Ihren HTTPS-Empfänger gesendete Adressaktivität, einschließlich eingehender Token-TransfersSignaturen verifizieren, Ereignis-IDs deduplizieren und subscription.gap / chain.reorg behandeln; gespeicherte Treffer erneut abspielen
WebSocketGefilterte logs über eine persistente VerbindungErneut verbinden, neu abonnieren und verpasste Blöcke nacherfassen
HTTP-PollingGeplante Überwachung oder historische Log-Nacherfassung mit eigenem CursorBegrenzte eth_getLogs-Bereiche abfragen und Fortschritt dauerhaft speichern

Prüfen Sie ws und subscriptions in der öffentlichen Antwort von GET /v1/chains, bevor Sie sich für WebSocket entscheiden. Die Push-Unterstützung ist eine separate Prüfung: Rufen Sie GET /v1/push/chains mit Ihrem API-Schlüssel ab. Eine Chain ohne WebSocket kann Adress-Webhooks nutzen, sofern sie dort aufgeführt ist. Verwenden Sie Polling, wenn Sie frühere Blöcke durchsuchen müssen oder ohne persistente Verbindung arbeiten möchten.

Zahlungen mit Webhooks empfangen

Abonnement erstellen und Empfänger überwachen

API-Schlüssel anfordern und einen HTTPS-Empfänger auf Port 443 bereitstellen. Wählen Sie CHAIN aus der authentifizierten Push-Chain-Liste, setzen Sie RECIPIENT auf Ihre Einzahlungsadresse und RECEIVER_URL auf Ihre Empfänger-URL. Dieses Shell-Beispiel setzt jq voraus; {} verwendet die standardmäßige Bestätigungsanzahl der Chain. Prüfen Sie min_confirmations, default_confirmations und max_confirmations, bevor Sie eine abweichende Anzahl wählen. Die Push-OpenAPI definiert diese Anfragen.

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"

Die Erstellung gibt id und secret zurück. Bewahren Sie das Secret sicher für den Empfänger auf; subscription.json enthält Zugangsdaten. Rufen Sie das Abonnement wiederholt ab, bis applied_version >= change_version aus address-change.json erreicht ist, und notieren Sie anschließend chains[CHAIN].applied_from_block. Neue Adressen erfassen Übereinstimmungen erst ab diesem Block; führen Sie daher für frühere Zahlungsintervalle weiterhin Polling durch.

Zahlungen verifizieren, deduplizieren und validieren

Speichern Sie die Signaturfunktion für den Raw-Body als verify-push.js. Der folgende Empfänger nimmt einen Web-API-Request in Node.js entgegen und liest dessen ursprüngliche Bytes vor dem JSON-Parsing. Erstellen Sie secrets als eine Map, die Abonnement-ID-Strings den gespeicherten Secrets zuordnet. Setzen Sie die vertrauenswürdige expected-Konfiguration auf { chain, token, recipient, amountUnits }: token ist der verifizierte Stablecoin-Vertrag auf dieser Chain und amountUnits ist der erwartete positive ganzzahlige Betrag in seinen kleinsten Einheiten. Vergleichen Sie Beträge immer mit BigInt, niemals mit Gleitkommazahlen oder dem Token-Symbol.

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 });
}

Implementieren Sie store.transaction mit persistentem Speicher. Innerhalb einer Transaktion fügt insertEventOnce ein Ereignis unter einem eindeutigen Schlüssel (subscription_id, event.id) ein und gibt bei einem Duplikat false zurück; committen Sie dies gemeinsam mit recordPaymentCandidate oder enqueueRecovery. Rollen Sie bei Fehlern alle Schreibvorgänge zurück, damit ein erneuter Versuch das Ereignis verarbeiten kann. Wiederherstellungsjobs müssen ebenfalls idempotent sein. Geben Sie den HTTP-Status 2xx innerhalb von 10 Sekunden erst nach erfolgreichem Commit zurück; erzwingen Sie das Body-Limit von 1 MiB in Ihrem HTTP-Server.

Dieses Beispiel prüft einen einzelnen erwarteten Zahlungsbetrag. Bei mehreren Bestellungen ermitteln Sie die vertrauenswürdige Zahlungskonfiguration anhand von Chain, Token und Empfänger und gleichen Teil- oder Überzahlungen nach Ihren eigenen Regeln ab. Ein Kandidat erfordert vor der Gutschrift weiterhin eine On-Chain-Verifizierung und die Anwendung Ihrer Bestätigungsrichtlinie. Gleichen Sie über Abonnements und Polling hinweg denselben Transfer anhand von Chain, Transaktions-Hash und Log-Index ab, damit zwei Übertragungswege denselben Betrag nicht doppelt gutschreiben; behalten Sie den Block-Hash bei, um ersetzte Blöcke nachzuverfolgen.

Fehlende oder ersetzte Blöcke wiederherstellen

Reihen Sie bei subscription.gap einen Suchlauf von from_block bis to_block über den unten beschriebenen Polling-Pfad oder verfügbare Data API-Datensätze in eine Warteschlange ein. chain.reorg ist eine kostenlose Benachrichtigung darüber, dass zugestellte Blöcke ersetzt wurden, und keine Zustellungslücke. Markieren oder verwerfen Sie alte Ereignisse in diesem Bereich anhand von ref; gleichen Sie Zahlungsdatensätze anhand von ref und tx_hash mit der kanonischen Chain ab, bevor Sie automatisch neu zugestellte kanonische Ereignisse mit neuen IDs verarbeiten. Deduplizieren Sie diese Ereignisse anhand von id. Die Reorg-Benachrichtigung rückt den abgeschlossenen Fortschritt nicht vor; protokollieren Sie complete_through_block pro Chain und schließen Sie niemals aus der höchsten Ereignis-Blocknummer auf den Abschluss.

Replay akzeptiert chain und from_block innerhalb der aktuellen replayable_from_block-Grenze. Es sendet nur gespeicherte Treffer erneut; es durchsucht keine Zeiträume vor dem Hinzufügen der Adresse oder Chain oder Zeiträume, in denen das Abonnement offline war. Führen Sie einen Polling-Cursor, um diese Intervalle und abgelaufene Lücken abzudecken. Anforderungsfehler und ungültige Replay-Bereiche werden in der Fehlerreferenz behandelt; Gebühren für Zustellung, Verlauf und Adresstage werden in den Abrechnungsregeln erläutert.

Die folgenden Abschnitte implementieren die ERC-20-Log-Filterung und das cursorbasierte Polling für Überwachung und Wiederherstellung.

Transfer-Event und Filterparameter

Standardmäßige ERC-20-Token-Verträge emittieren bei jedem Transfer folgendes Event:

event Transfer(address indexed from, address indexed to, uint256 value);

Übergeben Sie beim Aufruf von eth_getLogs die Adresse des Token-Vertrags und das topics-Array, um passende Logs zu filtern:

ParameterWertBeschreibung
addressAdresse des Token-Vertrags (oder Array von Adressen)Zieladresse des Stablecoin-Vertrags. Sie können eine einzelne Adresse angeben (z. B. BSC USDT 0x55d398326f99059fF775485246999027B3197955, Base USDC 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) oder ein Array von Adressen, um mehrere Token gleichzeitig zu überwachen
topics[0]0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efEvent-Signatur-Hash: keccak256("Transfer(address,address,uint256)")
topics[1]nullAbsenderadresse (from). Da die Einzahlungsüberwachung Gelder von jedem Benutzer-Wallet akzeptiert, übergeben Sie null, um jeden Absender zu erfassen
topics[2]Mit Nullen auf 32 Bytes aufgefüllte EmpfängeradresseZieladresse (to). Gemäß EVM-Log-Spezifikationen belegen indexed Adressparameter 32 Bytes (64 Hex-Zeichen). Füllen Sie die 20-Byte-Empfängeradresse links mit 12 Null-Bytes (24 hexadezimalen Null-Zeichen) auf, um ein 32-Byte-Topic zu bilden.
fromBlockStartblock (hexadezimal)Beginn des Abfrage-Blockbereichs (einschließlich)
toBlockEndblock (hexadezimal)Ende des Abfrage-Blockbereichs (einschließlich)

Der nicht-indizierte value (Transferbetrag) ist im data-Feld des Log-Objekts als 32-Byte hexadezimaler uint256 codiert. Teilen Sie diesen Rohbetrag durch 10^decimals, um den menschenlesbaren Token-Betrag zu erhalten (z. B. 18 Dezimalstellen für BSC USDT; 6 Dezimalstellen für Base und Ethereum USDC).

Cursor-Polling und Blockbereichs-Limits

Ein Polling-Dienst fragt neue Blöcke in regelmäßigen Abständen ab (z. B. alle 3 bis 5 Sekunden).

Weiterschalten des Cursors

Führen Sie einen persistenten Cursor last_polled_block (den höchsten verarbeiteten und committeten Block) in Ihrer Datenbank:

  1. Setzen Sie für jeden Polling-Zyklus fromBlock = last_polled_block + 1.
  2. Fragen Sie die aktuelle Chain-Spitze über eth_blockNumber ab und berechnen Sie die sichere Zielhöhe safe_head basierend auf Ihrer Bestätigungstiefe.
  3. Wenn fromBlock <= safe_head, fragen Sie Logs in Abschnitten (Chunks) bis zu safe_head ab. Nach erfolgreicher Verarbeitung jedes Chunks rücken Sie den Cursor vor.

Blockbereichs-Limit

Die Blockspanne eines einzelnen eth_getLogs-Aufrufs berechnet sich als toBlock − fromBlock + 1. Sie darf den für diese Chain in GET /v1/chains veröffentlichten Wert max_logs_block_range nicht überschreiten.

Wenn eine Anfrage diesen Bereich überschreitet, weist der Dienst den Aufruf mit dem Fehlercode -32602 ab:

{
  "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
    }
  }
}

Anfragen, die den Blockbereich überschreiten, geben den JSON-RPC-Fehler -32602 zurück (werden nicht berechnet). Lesen Sie in Ihrer Anwendungslogik max_logs_block_range aus GET /v1/chains aus und begrenzen Sie jeden Polling-Abschnitt: chunk_end = min(fromBlock + max_logs_block_range - 1, safe_head).

Umgang mit Block-Reorganisationen und Bestätigungstiefe

Nahe der Spitze der Blockchain können temporäre Block-Reorganisationen (Reorgs) auftreten. Werden Zahlungen bei latest ohne Bestätigungstiefe gutgeschrieben, besteht das Risiko, Transaktionen auf einem verwaisten Zweig gutzuschreiben, der anschließend verworfen wird.

Wenden Sie folgende Schutzmaßnahmen an, um die Zahlungsverarbeitung abzusichern:

Bestätigungstiefe

Fragen Sie statt bis latest bis zu einer sicheren Zielblockhöhe ab:

safe_head = current_head - CONFIRMATION_DEPTH

Legen Sie CONFIRMATION_DEPTH entsprechend der Risikotoleranz Ihrer Anwendung fest. Die Abfrage ausschließlich bis safe_head stellt sicher, dass nur Blöcke mit ausreichenden Bestätigungen verarbeitet werden.

Reorganisationen während des Pollings

Standardmäßiges EVM-JSON-RPC setzt removed: true bei Log-Objekten nur in WebSocket-Logs-Abonnement-Streams, wenn ein zuvor emittiertes Event aufgrund eines Chain-Reorgs zurückgesetzt wird. Beim Polling über HTTP mit eth_getLogs geben Abfragen Logs aus der kanonischen Chain zurück; reorganisierte Logs tauchen in nachfolgenden Abfragen schlichtweg nicht auf. Polling innerhalb von safe_head stellt sicher, dass Zahlungen nur auf ausreichend bestätigten Blöcken verarbeitet werden.

Deduplizierung nach (transactionHash, logIndex)

Zahlungs-Listener müssen strikte Idempotenz erzwingen:

  1. Mehrere Transfers in einer Transaktion: Eine einzelne Transaktion kann mehrere Transfer-Events an dieselbe Einzahlungsadresse enthalten (z. B. Token-Router, die Swaps aufteilen, oder Multi-Auszahlungsverträge). Wichtig: transactionHash allein ist nicht eindeutig pro Zahlung.
  2. Überlappendes Polling und Wiederholungsversuche: Wenn Polling-Dienste neu starten, sich von vorübergehenden Netzwerkfehlern erholen oder einige Blöcke zurückspringen, um flache Reorgs zu verarbeiten, werden Logs aus demselben Blockbereich mehrfach abgefragt.
  3. Eindeutigkeit des Log-Index: Der logIndex identifiziert die relative Position des Event-Logs innerhalb des Blocks. Gemäß EVM-Spezifikationen ist der kanonische zusammengesetzte eindeutige Bezeichner für ein Ereignis (transactionHash, logIndex).

Deklarieren Sie in relationalen Datenbankschemata einen zusammengesetzten eindeutigen Index auf Ihrer Tabelle für Einzahlungsdatensätze:

CREATE UNIQUE INDEX idx_transfers_tx_log ON deposit_records (transaction_hash, log_index);

Prüfen Sie vor der Verarbeitung einer Einzahlung vorhandene Einträge für (transactionHash, logIndex), um zu gewährleisten, dass jeder On-Chain-Transfer exakt einmal gutgeschrieben wird.

Vollständige Codebeispiele

Die folgenden Beispiele veranschaulichen das Abrufen von Netzwerkfunktionen aus /v1/chains, das Berechnen sicherer Blockbereiche, das Polling von Stablecoin-Transfer-Logs unter Einhaltung von Bereichslimits und die Deduplizierung von Ereignissen.

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

Abrechnungsregeln und verwandte Leitfäden

Nächste Schritte

Zuletzt aktualisiert:

Auf dieser Seite