# วิธีตรวจสอบการชำระเงิน USDT / USDC ด้วย Webhook และ RPC

> Source: https://docs.blockvectra.com/th/guides/stablecoin-payments/

สำหรับการตรวจสอบการชำระเงินด้วย stablecoin หรือการตรวจจับการฝากเงินของกระดานเทรด ให้ตรวจสอบการโอน ERC-20 USDT / USDC ขาเข้าบนเชน EVM โดยใช้ Webhook, WebSocket log หรือ HTTP polling ทั้งนักพัฒนาและ AI agent ต่างใช้ API เดียวกัน ให้เลือกเชน, สัญญาโทเค็น, ผู้รับ และระดับการยืนยันบล็อกก่อนประมวลผลการชำระเงิน เลือกลำดับขั้นตอนสำหรับการฝากเงิน การแจ้งเตือนร้านค้า หรือการจ่ายเงินได้ใน [โซลูชันการตรวจสอบการโอน USDT / USDC](https://blockvectra.com/en/use-cases/stablecoin-payments/)

* **ขั้นตอนแรก:** [สร้างการสมัครรับข้อมูลและเฝ้าดูผู้รับ](#create-a-subscription-and-watch-the-recipient) โดยเริ่มต้นด้วย API key และตัวรับ HTTPS ของคุณ
* **เสร็จสมบูรณ์เมื่อ:** การโอนที่ตรงกันผ่านการตรวจสอบลายเซ็น, เชน, โทเค็น, ผู้รับ และจำนวนเต็ม แล้วถูกบันทึกเพียงครั้งเดียวในฐานะรายการชำระเงินที่รอยืนยัน และตัวรับส่งคืน HTTP `204`; ตรวจสอบบนเชนตามนโยบายการยืนยันบล็อกของคุณก่อนทำการเพิ่มยอดเครดิต

[เวิร์กโฟลว์การชำระเงินด้วย Stablecoin](https://blockvectra.com/en/use-cases/stablecoin-payments/)

การตรวจสอบการโอนขั้นพื้นฐานพร้อมใช้งานแล้ว การกรองจำนวนเงินและโทเค็นจะทำงานในตัวรับของคุณ ส่วนเงื่อนไขฝั่งเซิร์ฟเวอร์ ขั้นตอนการยืนยันหลายระดับ และการแจ้งเตือนผ่าน IM จะพร้อมใช้งานเร็วๆ นี้

สำหรับนักพัฒนาและ AI Agent: เริ่มต้นด้วย [API key](https://console.blockvectra.com/login/?next=%2Fkeys%2F) และตัวรับ HTTPS ของคุณเอง กรองสัญญาโทเค็นและจำนวนเงินในแอปพลิเคชันของคุณ [คัดลอกการตั้งค่า Webhook](#create-a-subscription-and-watch-the-recipient)

## งานที่คู่มือนี้จะช่วยคุณดำเนินการ

* [รับการแจ้งเตือนการชำระเงิน USDT / USDC](#receive-payments-with-webhooks) ที่ HTTPS endpoint ของคุณหลังจากตรวจสอบการรองรับ Push สำหรับเชนที่เลือก
* [ตรวจสอบความถูกต้องของรายการโอนที่รอยืนยัน](#verify-deduplicate-and-validate-payments) โดยตรวจสอบเชน, สัญญาโทเค็น, ผู้รับ และจำนวนเต็ม ก่อนใช้นโยบายการตรวจสอบบนเชนและการยืนยันบล็อกของคุณ
* [ดึงข้อมูล log การโอนที่ขาดหายย้อนหลัง](#cursor-polling-and-block-range-limits) ด้วยคำค้นหา `eth_getLogs` ที่จำกัดขอบเขตและเคอร์เซอร์ที่บันทึกไว้

## เลือกระหว่าง Webhook, WebSocket หรือ polling

| เมธอด                                            | เหมาะสำหรับ                                                              | การกู้คืนข้อมูล                                                                                                              |
| ------------------------------------------------ | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| [Webhook](https://docs.blockvectra.com/en/guides/webhook-push/)              | กิจกรรมของแอดเดรสที่ส่งไปยังตัวรับ HTTPS ของคุณ รวมถึงการโอนโทเค็นขาเข้า | ตรวจสอบลายเซ็น, ขจัด event ID ที่ซ้ำซ้อน และจัดการ `subscription.gap` / `chain.reorg`; เล่นซ้ำข้อมูลที่ตรงกันที่เก็บรักษาไว้ |
| [WebSocket](https://docs.blockvectra.com/en/guides/websocket-subscriptions/) | กรอง `logs` ผ่านการเชื่อมต่อแบบต่อเนื่อง                                 | เชื่อมต่อใหม่, สมัครรับข้อมูลซ้ำ และดึงข้อมูลบล็อกที่พลาดไปย้อนหลัง                                                          |
| HTTP polling                                     | การตรวจสอบตามกำหนดเวลาหรือการดึง log ย้อนหลังด้วยเคอร์เซอร์ของคุณเอง     | ค้นหาช่วง `eth_getLogs` ที่มีขอบเขตและบันทึกความคืบหน้าอย่างถาวร                                                             |

อ่าน `ws` และ `subscriptions` ในการตอบกลับสาธารณะของ `GET /v1/chains` ก่อนเลือก WebSocket การรองรับ Push เป็นการตรวจสอบแยกต่างหาก: ให้อ่าน `GET /v1/push/chains` โดยใช้ API key ของคุณ เชนที่ไม่มี WebSocket สามารถใช้ Webhook ของแอดเดรสได้หากมีระบุไว้ที่นั่น ใช้ polling เมื่อคุณต้องการสแกนบล็อกก่อนหน้าหรือทำงานโดยไม่มีการเชื่อมต่อแบบต่อเนื่อง

## รับการชำระเงินด้วย Webhook

### สร้างการสมัครรับข้อมูลและเฝ้าดูผู้รับ

[รับ API key](https://blockvectra.com/en/get-api-key/) และดีพลอยตัวรับ HTTPS บนพอร์ต 443 เลือก `CHAIN` จากรายการเชนของ Push ที่ยืนยันตัวตนแล้ว กำหนดค่า `RECIPIENT` เป็นแอดเดรสรับเงินฝากของคุณ และกำหนด `RECEIVER_URL` เป็น URL ตัวรับของคุณ ตัวอย่างเชลล์นี้ต้องใช้ `jq`; `{}` จะใช้จำนวนการยืนยันเริ่มต้นของเชน ตรวจสอบ `min_confirmations`, `default_confirmations` และ `max_confirmations` ก่อนเลือกจำนวนอื่น โดย [Push OpenAPI](https://docs.blockvectra.com/openapi/push.yaml) ได้กำหนดคำขอเหล่านี้ไว้

```bash
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` ให้จัดเก็บ secret ไว้อย่างปลอดภัยสำหรับตัวรับ; `subscription.json` มีข้อมูลประจำตัวอยู่ ให้โพลล์การสมัครรับข้อมูลจนกระทั่ง `applied_version >= change_version` จาก `address-change.json` แล้วบันทึก `chains[CHAIN].applied_from_block` แอดเดรสใหม่จะจับคู่ตั้งแต่บล็อกนั้นเป็นต้นไป ดังนั้นให้ทำการโพลล์ต่อไปสำหรับช่วงการชำระเงินที่เกิดขึ้นก่อนหน้านั้น

### ตรวจสอบลายเซ็น ขจัดรายการซ้ำ และตรวจสอบความถูกต้องของการชำระเงิน

บันทึก [ฟังก์ชันลายเซ็นจาก raw-body](https://docs.blockvectra.com/en/guides/webhook-push/#verify-signatures) เป็น `verify-push.js` ตัวรับด้านล่างนี้รับ `Request` ของ Web API ใน Node.js และอ่านไบต์ดั้งเดิมก่อนทำการแปลง JSON สร้าง `secrets` เป็น `Map` ของสตริง subscription ID ไปยัง secret ที่จัดเก็บไว้ กำหนดค่าคอนฟิกที่เชื่อถือได้ `expected` เป็น `{ chain, token, recipient, amountUnits }`: โดย `token` คือสัญญา stablecoin ที่ได้รับการตรวจสอบแล้วบนเชนนั้น และ `amountUnits` คือจำนวนเต็มบวกที่คาดหวังในหน่วยที่เล็กที่สุด เปรียบเทียบจำนวนเงินด้วย `BigInt` ห้ามใช้ตัวเลขทศนิยมหรือสัญลักษณ์โทเค็นโดยเด็ดขาด

```js
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 หากซ้ำ; ให้คอมมิตพร้อมกันกับ `recordPaymentCandidate` หรือ `enqueueRecovery` ทำการย้อนกลับการเขียนทั้งหมดหากล้มเหลวเพื่อให้การลองใหม่สามารถประมวลผลเหตุการณ์ได้ งานกู้คืนข้อมูลจะต้องทำงานแบบ idempotent ด้วยเช่นกัน ส่งคืนรหัส 2xx ภายใน 10 วินาทีหลังจากคอมมิตแล้วเท่านั้น; บังคับใช้ขีดจำกัดขนาด body ที่ 1 MiB บนเซิร์ฟเวอร์ HTTP ของคุณ

ตัวอย่างนี้ตรวจสอบจำนวนเงินที่คาดหวังรายการเดียว สำหรับคำสั่งซื้อหลายรายการ ให้ค้นหาการกำหนดค่าการชำระเงินที่เชื่อถือได้ตามเชน โทเค็น และผู้รับ แล้วทำการกระทบยอดการชำระเงินบางส่วนหรือเงินส่วนเกินตามกฎของคุณเอง รายการที่รอยืนยันยังคงต้องผ่านการตรวจสอบบนเชนและนโยบายการยืนยันบล็อกของคุณก่อนเพิ่มเครดิต ข้ามระหว่างการสมัครรับข้อมูลและ polling ให้กระทบยอดการโอนเดียวกันตามเชน แฮชธุรกรรม และดัชนี log เพื่อไม่ให้เส้นทางการนำส่งสองเส้นทางเพิ่มเครดิตซ้ำกันสองครั้ง; เก็บแฮชของบล็อกไว้เพื่อติดตามบล็อกที่ถูกแทนที่

### กู้คืนบล็อกที่ขาดหายหรือถูกแทนที่

สำหรับ `subscription.gap` ให้จัดคิวการสแกนตั้งแต่ `from_block` ถึง `to_block` โดยใช้เส้นทาง polling ด้านล่างหรือชุดข้อมูล Data API ที่มีอยู่ ส่วน `chain.reorg` คือการแจ้งเตือนฟรีว่าบล็อกที่นำส่งไปแล้วถูกแทนที่ ไม่ใช่ช่องว่างในการนำส่ง ให้ทำเครื่องหมายหรือละทิ้งเหตุการณ์เก่าในช่วงนั้นตาม `ref`; กระทบยอดบันทึกการชำระเงินตาม `ref` และ `tx_hash` กับเชนมาตรฐานก่อนที่จะประมวลผลเหตุการณ์มาตรฐานที่ส่งมอบใหม่อัตโนมัติด้วย ID ใหม่ ขจัดเหตุการณ์เหล่านั้นที่ซ้ำซ้อนตาม `id` การแจ้งเตือน reorg ไม่ได้เลื่อนความคืบหน้าที่เสร็จสมบูรณ์ไปข้างหน้า; ให้บันทึก `complete_through_block` แยกตามเชน ห้ามอนุมานความสมบูรณ์จากหมายเลขบล็อกของเหตุการณ์ที่ใหญ่ที่สุดเด็ดขาด

[การเล่นซ้ำ (Replay)](https://docs.blockvectra.com/en/guides/webhook-push/#delivery-retries-and-replay) รับ `chain` และ `from_block` ภายในขอบเขต `replayable_from_block` ปัจจุบัน โดยจะส่งเฉพาะรายการที่ตรงกันที่เก็บรักษาไว้เท่านั้น; จะไม่สแกนช่วงเวลาก่อนที่จะเพิ่มแอดเดรสหรือเชน หรือช่วงเวลาที่การสมัครรับข้อมูลออฟไลน์ ให้รักษาเคอร์เซอร์ polling ไว้เพื่อครอบคลุมช่วงเวลาเหล่านั้นและช่องว่างที่หมดอายุ ข้อผิดพลาดของคำขอและช่วง replay ที่ไม่ถูกต้องมีอธิบายไว้ใน [เอกสารอ้างอิงข้อผิดพลาด](https://docs.blockvectra.com/en/errors/); ค่าบริการการนำส่ง ประวัติ และ address-day มีอธิบายไว้ใน [กฎการเรียกเก็บเงิน](https://docs.blockvectra.com/en/guides/billing-rules/#webhook-push-billing)

ส่วนที่เหลือจะนำเสนอการกรอง log ของ ERC-20 และการทำ polling แบบอิงตามเคอร์เซอร์สำหรับการตรวจสอบและการกู้คืนข้อมูล

## Event การโอนและพารามิเตอร์การกรอง

สัญญาโทเค็น ERC-20 มาตรฐานจะส่ง event ต่อไปนี้ในทุกการโอน:

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

เมื่อเรียกใช้ `eth_getLogs` ให้ส่งแอดเดรสสัญญาโทเค็นและอาร์เรย์ `topics` เพื่อกรอง log ที่ตรงกัน:

| พารามิเตอร์ | ค่า                                                                  | คำอธิบาย                                                                                                                                                                                                                                 |
| ----------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`   | แอดเดรสสัญญาโทเค็น (หรืออาร์เรย์ของแอดเดรส)                          | แอดเดรสสัญญา stablecoin เป้าหมาย คุณสามารถระบุแอดเดรสเดียว (เช่น BSC USDT `0x55d398326f99059fF775485246999027B3197955`, Base USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`) หรืออาร์เรย์ของแอดเดรสเพื่อตรวจสอบหลายโทเค็นพร้อมกัน     |
| `topics[0]` | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` | แฮชของลายเซ็น event: `keccak256("Transfer(address,address,uint256)")`                                                                                                                                                                    |
| `topics[1]` | `null`                                                               | แอดเดรสผู้ส่ง (`from`) เนื่องจากการตรวจสอบการฝากเงินยอมรับเงินจากกระเป๋าเงินของผู้ใช้รายใดก็ได้ ให้ส่ง `null` เพื่อจับคู่ผู้ส่งทุกราย                                                                                                    |
| `topics[2]` | แอดเดรสผู้รับขนาด 32 ไบต์ที่เติมศูนย์ด้านหน้า                        | แอดเดรสปลายทาง (`to`) ตามข้อกำหนด EVM log พารามิเตอร์แอดเดรสที่เป็น `indexed` จะใช้พื้นที่ 32 ไบต์ (64 อักขระฐานสิบหก) ให้เติมศูนย์ 12 ไบต์ (อักขระศูนย์ฐานสิบหก 24 ตัว) ด้านหน้าแอดเดรสผู้รับขนาด 20 ไบต์ เพื่อสร้าง topic ขนาด 32 ไบต์ |
| `fromBlock` | บล็อกเริ่มต้น (เลขฐานสิบหก)                                          | จุดเริ่มต้นของช่วงบล็อกการค้นหา (นับรวม)                                                                                                                                                                                                 |
| `toBlock`   | บล็อกสิ้นสุด (เลขฐานสิบหก)                                           | จุดสิ้นสุดของช่วงบล็อกการค้นหา (นับรวม)                                                                                                                                                                                                  |

ฟิลด์ `data` ของออบเจกต์ log จะเข้ารหัส `value` ที่ไม่ได้ทำดัชนี (จำนวนเงินที่โอน) เป็น `uint256` ฐานสิบหกขนาด 32 ไบต์ ให้หารจำนวนเงินดิบนี้ด้วย 10^decimals เพื่อให้ได้จำนวนโทเค็นในรูปแบบที่มนุษย์อ่านได้ (เช่น 18 decimals สำหรับ BSC USDT; 6 decimals สำหรับ Base และ Ethereum USDC)

## การโพลล์ด้วยเคอร์เซอร์และขีดจำกัดช่วงบล็อก

บริการโพลล์จะสอบถามบล็อกใหม่เป็นระยะสม่ำเสมอ (เช่น ทุกๆ 3 ถึง 5 วินาที)

### การเลื่อนเคอร์เซอร์ไปข้างหน้า

รักษาเคอร์เซอร์แบบคงทน `last_polled_block` (บล็อกสูงสุดที่ประมวลผลและคอมมิตแล้ว) ไว้ในฐานข้อมูลของคุณ:

1. สำหรับแต่ละรอบการโพลล์ ให้ตั้งค่า `fromBlock = last_polled_block + 1`
2. สอบถามส่วนหัวของเชนปัจจุบันผ่าน `eth_blockNumber` และคำนวณความสูงเป้าหมายที่ปลอดภัย `safe_head` ตาม confirmation depth ของคุณ
3. หาก `fromBlock <= safe_head` ให้สืบค้น log เป็นส่วนๆ จนถึง `safe_head` หลังจากประมวลผลแต่ละ chunk สำเร็จแล้ว ให้เลื่อนเคอร์เซอร์ไปข้างหน้า

### ขีดจำกัดช่วงบล็อก

ช่วงบล็อกของการเรียก `eth_getLogs` ครั้งเดียวจะคำนวณเป็น `toBlock − fromBlock + 1` โดยต้องไม่เกิน `max_logs_block_range` ที่เผยแพร่สำหรับเชนนั้นใน `GET /v1/chains`

หากคำขอเกินช่วงนี้ บริการจะปฏิเสธคำขอด้วยรหัสข้อผิดพลาด `-32602`:

```json
{
  "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)`

## การจัดการ Block Reorganization และ Confirmation Depth

บริเวณส่วนปลายของบล็อกเชน อาจเกิดการปรับโครงสร้างบล็อกชั่วคราว (reorgs) ได้ การเพิ่มเครดิตการชำระเงินที่ `latest` โดยไม่มี confirmation depth มีความเสี่ยงที่จะเพิ่มเครดิตให้ธุรกรรมบนกิ่งที่ถูกทิ้งในภายหลัง

ใช้มาตรการป้องกันต่อไปนี้เพื่อปกป้องกระบวนการประมวลผลการชำระเงิน:

### Confirmation Depth

แทนที่จะสืบค้นไปจนถึง `latest` ให้สืบค้นไปจนถึงความสูงของบล็อกเป้าหมายที่ปลอดภัย:

`safe_head = current_head - CONFIRMATION_DEPTH`

กำหนดค่า `CONFIRMATION_DEPTH` ตามการยอมรับความเสี่ยงของแอปพลิเคชันของคุณ การสืบค้นจนถึง `safe_head` เท่านั้นจะช่วยให้มั่นใจได้ว่ามีเพียงบล็อกที่มีการยืนยันเพียงพอเท่านั้นที่จะได้รับการประมวลผล

### การ Reorganize ระหว่างการทำ Polling

EVM JSON-RPC มาตรฐานจะตั้งค่า `removed: true` บนออบเจกต์ log เฉพาะในสตรีมการสมัครรับข้อมูล WebSocket logs เมื่อเหตุการณ์ที่ส่งออกมาก่อนหน้าเกิด revert เนื่องจากเกิด chain reorg เมื่อทำ polling ผ่าน HTTP ด้วย `eth_getLogs` คำค้นหาจะส่งคืน log จากเชนมาตรฐาน; log ที่เกิด reorg จะไม่ปรากฏในการสืบค้นครั้งถัดไป การทำ polling ภายใน `safe_head` จะช่วยรับประกันว่าการชำระเงินจะได้รับการประมวลผลเฉพาะบนบล็อกที่ได้รับการยืนยันอย่างเพียงพอแล้วเท่านั้น

## การขจัดรายการซ้ำด้วย (transactionHash, logIndex)

ตัวรับฟังการชำระเงินต้องบังคับใช้คุณสมบัติ idempotency อย่างเข้มงวด:

1. **การโอนหลายรายการในธุรกรรมเดียว**: ธุรกรรมเดียวสามารถมี event `Transfer` หลายรายการไปยังแอดเดรสรับเงินฝากเดียวกัน (เช่น ตัวเราเตอร์โทเค็นที่แบ่งการสวอป หรือสัญญาจ่ายเงินหลายรายการ) **ข้อสำคัญ:** `transactionHash` เพียงอย่างเดียวไม่ถือเป็นค่าเฉพาะสำหรับการชำระเงินแต่ละรายการ
2. **การโพลล์ซ้ำซ้อนและการลองใหม่**: เมื่อบริการโพลล์เริ่มต้นใหม่ กู้คืนจากข้อผิดพลาดของเครือข่ายชั่วคราว หรือกรอย้อนกลับหลายบล็อกเพื่อจัดการกับ reorg แบบตื้น log จากช่วงบล็อกเดียวกันจะถูกสอบถามหลายครั้ง
3. **ความเป็นเอกลักษณ์ของ Log Index**: `logIndex` ระบุตำแหน่งสัมพัทธ์ของ event log ภายในบล็อก ตามข้อกำหนดของ EVM ตัวระบุเฉพาะแบบผสมที่เป็นมาตรฐานสำหรับเหตุการณ์คือ `(transactionHash, logIndex)`

ในสคีมาฐานข้อมูลเชิงสัมพันธ์ ให้ประกาศ composite unique index บนตารางบันทึกการฝากเงินของคุณ:

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

ก่อนประมวลผลการฝากเงิน ให้ตรวจสอบกับรายการ `(transactionHash, logIndex)` ที่มีอยู่ เพื่อรับประกันว่าการโอนบนเชนแต่ละรายการจะได้รับการเพิ่มเครดิตเพียงครั้งเดียวเท่านั้น

## ตัวอย่างโค้ดฉบับสมบูรณ์

ตัวอย่างด้านล่างแสดงการดึงข้อมูลความสามารถของเครือข่ายจาก `/v1/chains`, การคำนวณช่วงบล็อกที่ปลอดภัย, การทำ polling log `Transfer` ของ stablecoin ตามขีดจำกัดของช่วง และการขจัดเหตุการณ์ที่ซ้ำซ้อน

**TypeScript**

```ts
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
```


  **Python**

```python
from decimal import Decimal
import os
import requests

api_key = os.environ.get("BLOCKVECTRA_API_KEY")
if not api_key:
    raise RuntimeError("BLOCKVECTRA_API_KEY environment variable is not set")

CHAIN = "bsc_mainnet"
RPC_URL = "https://api.blockvectra.com/v1/bsc_mainnet"
CHAINS_URL = "https://api.blockvectra.com/v1/chains"

# Target stablecoin contract address (BSC USDT used in this example)
TOKEN_CONTRACT = "0x55d398326f99059fF775485246999027B3197955"
TOKEN_DECIMALS = 18

# Monitored deposit address
RECIPIENT_ADDRESS = "0xdded13D555B6DA811103cC1794D3d4330F69632C"

# Transfer(address,address,uint256) signature hash
TRANSFER_TOPIC0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

# Left-pad 20-byte address to 32 bytes (64 hex characters)
padded_recipient = f"0x{RECIPIENT_ADDRESS.lower()[2:].rjust(64, '0')}"

# Confirmation depth to guard against chain reorgs
CONFIRMATION_DEPTH = 15

# 1. Fetch chain capabilities from public metadata endpoint (unauthenticated, unbilled)
chains_res = requests.get(CHAINS_URL, timeout=10)
chains_res.raise_for_status()
chain_list = chains_res.json().get("chains", [])

chain_config = next((c for c in chain_list if c["chain"] == CHAIN), None)
if not chain_config:
    raise RuntimeError(f"Chain {CHAIN} not found in /v1/chains")

max_logs_range = chain_config.get("max_logs_block_range", 1000)
ws_supported = chain_config.get("ws", False)
print(f"Chain: {CHAIN} | WebSocket supported: {ws_supported} | Max logs range: {max_logs_range}")

def rpc_request(method: str, params: list):
    res = requests.post(
        RPC_URL,
        headers={
            "Content-Type": "application/json",
            "x-api-key": api_key,
        },
        json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
        timeout=15,
    )
    res.raise_for_status()
    payload = res.json()
    if "error" in payload:
        err = payload["error"]
        raise RuntimeError(f"JSON-RPC error {err.get('code')}: {err.get('message')}")
    return payload["result"]

# 2. Query latest block number and calculate safe head
current_head_hex = rpc_request("eth_blockNumber", [])
current_head = int(current_head_hex, 16)
safe_head = max(0, current_head - CONFIRMATION_DEPTH)

# For demonstration, start cursor 10 blocks before safe_head
cursor = max(0, safe_head - 10)
print(f"Current head: {current_head} | Safe head: {safe_head} | Polling cursor: {cursor}")

# In-memory deduplication set using (transactionHash, logIndex)
processed_logs = set()

while cursor <= safe_head:
    chunk_end = min(cursor + max_logs_range - 1, safe_head)

    logs = rpc_request(
        "eth_getLogs",
        [
            {
                "address": TOKEN_CONTRACT,
                "fromBlock": hex(cursor),
                "toBlock": hex(chunk_end),
                "topics": [
                    TRANSFER_TOPIC0,
                    None,  # match any sender
                    padded_recipient,  # match monitored recipient
                ],
            }
        ],
    )

    for log in logs:
        tx_hash = log["transactionHash"]
        log_index = int(log["logIndex"], 16)
        dedup_key = (tx_hash, log_index)

        if dedup_key in processed_logs:
            continue
        processed_logs.add(dedup_key)

        raw_amount = int(log["data"], 16)
        token_amount = Decimal(raw_amount) / (Decimal(10) ** TOKEN_DECIMALS)
        block_number = int(log["blockNumber"], 16)

        print(
            f"[Payment Received] Amount: {token_amount} | "
            f"Tx: {tx_hash} | Log: {log_index} | Block: {block_number}"
        )

    cursor = chunk_end + 1

# Run with: python example.py
```


## กฎการเรียกเก็บเงินและคู่มือที่เกี่ยวข้อง

* สำหรับรายละเอียดเกี่ยวกับการวัดปริมาณคำขอ, ค่าน้ำหนัก CU และการพิจารณาการเรียกเก็บเงินตามรหัสข้อผิดพลาด โปรดดู [กฎการเรียกเก็บเงิน: ข้อผิดพลาดและคำขอที่ไม่คิดค่าบริการ](https://docs.blockvectra.com/en/guides/billing-rules/)
* สำหรับคำแนะนำเชิงลึกเกี่ยวกับขีดจำกัดช่วงบล็อกของ `eth_getLogs` และตรรกะการแบ่ง chunk โปรดดู [ขีดจำกัดช่วงบล็อกและการสืบค้นแบบ Chunk ของ eth\_getLogs](https://docs.blockvectra.com/en/guides/getlogs-block-range/)
* สำหรับความแตกต่างระหว่างการสืบค้นโหนด RPC แบบเรียลไทม์และ API ประวัติการโอนที่ทำดัชนีไว้ โปรดดู [ข้อมูลส่วนหัวของเชนเทียบกับประวัติที่ทำดัชนี: เมื่อใดควรใช้ eth\_getLogs เทียบกับ Transfers](https://docs.blockvectra.com/en/guides/logs-vs-transfers/)

## ขั้นตอนถัดไป

* [เลือกดูไดเรกทอรีชุดข้อมูล](https://blockvectra.com/en/data/) เพื่อดูชุดข้อมูลทั้งหมดที่ BlockVectra จัดทำดัชนี
* [ดูแพ็กเกจฟรีและการกำหนดราคา](https://blockvectra.com/en/pricing/#free) เพื่อตรวจสอบสิทธิประโยชน์ในบัญชีของคุณ
* [เข้าสู่ระบบคอนโซล](https://console.blockvectra.com/login/?next=%2Fkeys%2F) เพื่อสร้าง API key
