Skip to content
onchain-engineeringusdcinvoice-matching
Joshua C.

How to Verify Onchain USDC Payments to Your Address

Step-by-step guide for engineers: verify USDC payment settled to your address and matches an invoice on Base, Ethereum, or Polygon. Includes example and code.

How to Verify Onchain USDC Payments to Your Address

When you accept USDC onchain, you need to know when a payment arrives and whether it matches an invoice. This is not the same as checking an offchain database or waiting for a webhook. Onchain, payment verification means reading the blockchain, matching a transaction to your invoice, and confirming the details are correct.

What You Need to Verify

Before you start, decide what you need to check. Most teams want to confirm:

  • The payment settled to the right address (your settlement address).
  • The amount matches the invoice, or at least meets a minimum.
  • The correct payer sent the funds (optional, but common).
  • The payment happened within the invoice window (timestamp check).
  • The payment used the correct asset: USDC, not a lookalike.

You do not need to check for reversals. Once the transaction has enough confirmations, it is final. There is no chargeback. If the payment is for a subscription, you may want to check that the spend did not exceed a cap, but for one-time invoices, these are the core checks.

Step 1: Collect Invoice and Payment Data

For each invoice, you should store:

  • Unique invoice ID (string or UUID).
  • Amount due, in USDC’s smallest unit (6 decimals).
  • Settlement address (your wallet or contract address).
  • Optional: intended payer address.
  • Invoice creation and expiry timestamps.

When a customer claims to have paid, or when you scan the chain, you need:

  • The transaction hash (if provided).
  • The block explorer link (optional, for review).
  • Alternatively, you may scan all incoming transfers to your address within the invoice window.

Step 2: Fetch USDC Transfers to Your Address

USDC is an ERC-20 token. On Ethereum, Base, and Polygon, each USDC transfer emits a Transfer event. You can use a node, an archive provider, or an API like Alchemy, Infura, or QuickNode.

To filter incoming payments:

  • Contract address: USDC contract for the relevant chain.
  • Event signature: Transfer(address indexed from, address indexed to, uint256 value)
  • Filter: to = your settlement address, block.timestamp within your invoice window.

Example: Querying USDC Transfers with ethers.js

Suppose your address is 0xMerchant1... on Base. Your invoice is for 250 USDC. The customer says they paid at block 9,876,543. USDC on Base: 0x...A0b86991c6218b36c1d19d4a2e9eb0ce3606eb48.

const { ethers } = require('ethers');

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const usdcAddress = '0xA0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
const merchant = '0xMerchant1...';
const invoiceAmount = ethers.parseUnits('250', 6); // USDC has 6 decimals

const usdcAbi = [
  'event Transfer(address indexed from, address indexed to, uint256 value)'
];

const contract = new ethers.Contract(usdcAddress, usdcAbi, provider);

const filter = contract.filters.Transfer(null, merchant);

// Optionally, filter by block range
const fromBlock = 9876500;
const toBlock = 9876543;

const logs = await contract.queryFilter(filter, fromBlock, toBlock);

for (const log of logs) {
  const { from, to, value } = log.args;
  if (value.eq(invoiceAmount)) {
    console.log(`Matched payment from ${from} for ${ethers.formatUnits(value, 6)} USDC in tx ${log.transactionHash}`);
  }
}

This finds any transfer of exactly 250 USDC to your address in the given block range. Adjust the fromBlock and toBlock to your invoice’s valid window.

Step 3: Cross-Check the Asset and Contract

Onchain, anyone can deploy a lookalike token. Your payment must use the canonical USDC contract for the chain. Get the correct contract address from the official source:

  • Circle Docs
  • Double-check the transfer’s address field matches the correct USDC contract.

If you accept payment on multiple chains, check that the payment settled to your address on the intended chain.

Step 4: Confirm the Amount Matches

USDC uses 6 decimals. An invoice for 250 USDC is 250_000000. Compare the value in the event to the expected amount. If you allow overpayment or underpayment, set a tolerance. For strict matching, require exact value equality.

Step 5: Optional, Check the Payer Address

If your invoice is for a specific wallet, check the event’s from field. This is the address that sent the payment. For open invoices, you may not care who pays. For metered or recurring billing, or if you map payments to customer accounts, matching from is important.

Step 6: Check the Block Timestamp

Each block has a timestamp. To verify the payment arrived before expiry, compare the block’s timestamp to your invoice’s expiration. With ethers.js:

const tx = await provider.getTransactionReceipt(log.transactionHash);
const block = await provider.getBlock(tx.blockNumber);
if (block.timestamp > invoiceExpiry) {
  console.log('Payment arrived after invoice expired');
}

Step 7: Confirm Settlement (Confirmations)

A transfer is only final after the block is confirmed. For Base, Ethereum, and Polygon, most merchants accept 1 to 12 confirmations. To check:

  • Get the current block height.
  • Ensure the payment’s block is deep enough.
  • Only deliver goods or update access after this point.

Full Example: Verifying a USDC Invoice Payment

Suppose:

  • Invoice: #INV-1001
  • Amount: 250 USDC
  • Merchant address: 0xMerchant1...
  • Intended payer: 0xCust0...
  • Invoice created: block 9,876,500
  • Expiry: block 9,876,560
  • Payment claimed in tx 0xPayTx... at block 9,876,543

Summary verification process:

  1. Fetch the transfer events to 0xMerchant1... in blocks 9,876,500-9,876,560.
  2. Filter for those with value == 250_000000 and from == 0xCust0....
  3. Confirm the event’s contract address is the canonical USDC for Base.
  4. Check the payment’s block has at least 6 confirmations.
  5. Confirm the payment’s timestamp or block is before expiry.

If all conditions pass, the payment matches the invoice. If not, flag for review.

Handling Edge Cases

  • Partial payments: If the amount is less than required, decide whether to accept, prompt for the remainder, or refund.
  • Overpayments: Some teams refund the excess, others credit it to future invoices.
  • Multiple payments for one invoice: Require idempotency at the application layer. Mark invoices as paid after a settlement.
  • No matching transfer: Ask the customer for a transaction hash, or scan again after a few blocks.
  • Wrong asset or contract: Reject, and explain the supported USDC contract addresses.

Automating Onchain Payment Reconciliation

You can script these checks to run on every new block or upon webhook notification from your node provider. Store the mapping of invoice IDs to payment hashes and mark invoices as settled only after all checks pass.

How We Handle USDC Payment Verification at Reclack

At Reclack, we automate all these steps. Our billing engine scans the relevant chain for incoming USDC transfers to your settlement address. We verify the contract address, amount, payer (if required), and time window before marking an invoice paid. No funds ever touch our infrastructure. You control your address, and every payment is visible onchain. See how we handle stablecoin billing at reclack.com.

onchain-engineeringusdcinvoice-matching