coal
coal

Settlement Verification

This page explains how Coal verifies an on-chain ERC-20 payment after a customer submits a transaction hash. Historical examples use MNEE, but the same flow applies to whichever settlement token the merchant has configured.

The Verification Pipeline

Payment Submitted
User / BrowserSigns & broadcasts ERC-20 transfer in wallet
User / BrowserCoal
POST /api/pay/confirm

{ sessionId, txHash }

CoalSession status: pending → verifying
On-Chain Verification
CoalBackground cron job runs every 60 seconds
CoalBase Chain
getTransactionReceipt(txHash) on Base
Base ChainCoal
Transfer event logs + receipt
Outcome
CoalChecks pass → status: "confirmed", webhook fired
CoalChecks fail → status: "failed", webhook fired

What Coal Verifies

For a payment to be confirmed, all of the following must be true:

CheckDescription
Transaction existsThe txHash exists on Base mainnet
Not revertedThe transaction status is success (not reverted/failed)
Transfer eventThe tx emits an ERC-20 Transfer event for the configured settlement token contract
Correct recipientThe transfer to address matches the merchant's payoutAddress
Correct amountThe transferred amount matches the session amount within the configured token decimals

If any check fails, the session transitions to failed with a reason code stored internally.

Failure Reasons

CodeMeaning
tx_revertedTransaction was mined but execution failed
no_transfer_eventNo ERC-20 Transfer event found for the configured settlement token
recipient_mismatchTransfer went to a different address than the merchant's payout address
amount_mismatchAmount transferred differs from session amount by more than the configured tolerance
tx_not_foundTransaction hash not found after 10 verification attempts

Timing

StageTypical Duration
Base block time~2 seconds
Cron run interval60 seconds
Total (best case)1–2 minutes after tx submission
Timeout24 hours (session expiry)
Tip:

Don't poll /api/pay/status every second. Poll every 5–10 seconds during the first 2 minutes, then fall back to relying on webhooks for the confirmation event.

Polling the Status

typescript
1async function pollUntilConfirmed(sessionId: string, maxAttempts = 24) {
2 for (let i = 0; i < maxAttempts; i++) {
3 const res = await fetch(`/api/pay/status/${sessionId}`);
4 const { data } = await res.json();
5
6 if (data.status === 'confirmed') return data;
7 if (data.status === 'failed') throw new Error(`Payment failed`);
8 if (data.status === 'expired') throw new Error(`Session expired`);
9
10 // Exponential backoff: 5s, 5s, 10s, 10s, 20s, ...
11 const delay = i < 2 ? 5000 : i < 4 ? 10000 : 20000;
12 await new Promise(r => setTimeout(r, delay));
13 }
14 throw new Error('Timed out waiting for confirmation');
15}

Status Response

json
1GET /api/pay/status/clxxx
2
3{
4 "data": {
5 "status": "confirmed",
6 "txHash": "0xabc123...",
7 "amount": "49.99",
8 "currency": "USDC",
9 "confirmedAt": "2026-03-22T12:01:45.000Z"
10 }
11}

Block Explorer

Confirmed transactions can be verified on Base:

text
1https://basescan.org/tx/0xYOUR_TX_HASH

The transaction will show an ERC-20 token transfer for the configured settlement token contract to the merchant's payout address.

Cron Job Architecture

Coal runs a cron job every minute (*/1 * * * *) that:

  1. Fetches all sessions in verifying status
  2. For each, queries the Base RPC for the pendingTxHash
  3. Decodes the transaction receipt and checks Transfer events
  4. Updates the session status and fires the appropriate webhook

In production this runs on Vercel Cron. In development, you can trigger it manually:

bash
1curl -X POST http://localhost:3001/api/cron/verify-payments \
2 -H "Authorization: Bearer YOUR_CRON_SECRET"