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
{ sessionId, txHash }
What Coal Verifies
For a payment to be confirmed, all of the following must be true:
| Check | Description |
|---|---|
| Transaction exists | The txHash exists on Base mainnet |
| Not reverted | The transaction status is success (not reverted/failed) |
| Transfer event | The tx emits an ERC-20 Transfer event for the configured settlement token contract |
| Correct recipient | The transfer to address matches the merchant's payoutAddress |
| Correct amount | The 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
| Code | Meaning |
|---|---|
tx_reverted | Transaction was mined but execution failed |
no_transfer_event | No ERC-20 Transfer event found for the configured settlement token |
recipient_mismatch | Transfer went to a different address than the merchant's payout address |
amount_mismatch | Amount transferred differs from session amount by more than the configured tolerance |
tx_not_found | Transaction hash not found after 10 verification attempts |
Timing
| Stage | Typical Duration |
|---|---|
| Base block time | ~2 seconds |
| Cron run interval | 60 seconds |
| Total (best case) | 1–2 minutes after tx submission |
| Timeout | 24 hours (session expiry) |
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
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();56 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`);910 // 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
1GET /api/pay/status/clxxx23{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:
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:
- Fetches all sessions in
verifyingstatus - For each, queries the Base RPC for the
pendingTxHash - Decodes the transaction receipt and checks Transfer events
- Updates the session status and fires the appropriate webhook
In production this runs on Vercel Cron. In development, you can trigger it manually:
1curl -X POST http://localhost:3001/api/cron/verify-payments \2 -H "Authorization: Bearer YOUR_CRON_SECRET"
