Payment verification and order tracking
This guide explains how to verify non-custodial payments made through Volet.com smart contracts on Ethereum, BNB Smart Chain and TRON.
A transaction hash (txid) is not proof of payment. Your backend must independently check execution, finality and the payment events before marking an order as paid. Volet.com does not send Hosted Checkout Status URL callbacks for this integration.
How payment verification works
- Create an order. Store the expected network, asset, receiving wallet (
seller), processor contract, expected merchant amount and, where applicable, buyer wallet. - Record the payment attempt. Associate the payment
txidwith the order. Store USDTapprove()transaction hashes separately. - Check execution and finality. Retrieve blockchain data using a backend RPC connection or hosted blockchain service.
- Verify payment events. Find
PaymentProcessedemitted by the approved processor. For USDT, also verify the actual tokenTransferto the merchant. - Match the order. Compare the event's merchant address, buyer (if required) and net amount with immutable backend order details.
- Complete the order once. Atomically record the verified payment and trigger idempotent fulfilment.
You can use a hosted provider. Running your own blockchain node is not required.
Payment events and amounts
The documented Volet.com processors emit PaymentProcessed after making the merchant payout:
event PaymentProcessed(
address indexed buyer,
address indexed seller,
uint256 commission,
uint256 totalPay
);
For these deployments, calculate the merchant's net receipt as:
receivedAmount = totalPay - commission
Both values use the asset's smallest units. The calculation applies with either setting of buyerPaysCommission.
| Asset | Required on-chain evidence |
|---|---|
| ETH, BNB | Successful finalized execution and a matching PaymentProcessed log emitted by the approved native-asset processor |
| USDT ERC-20, BEP-20 | Matching processor PaymentProcessed log and USDT Transfer log from the approved token contract: processor → merchant for receivedAmount |
| TRX | Successful solidified execution and a matching PaymentProcessed log emitted by the approved TRX processor |
| USDT TRC-20 | Matching processor PaymentProcessed log and USDT TRC-20 Transfer log from the approved token contract: processor → merchant for receivedAmount |
Always check the address that emitted each event. A log with the right event name but emitted by an unrelated contract is not payment evidence. Direct wallet-to-wallet transfers are outside this processor-based payment flow.
The processor integration materials specify that native payouts revert when the merchant transfer fails; therefore, the authentic PaymentProcessed event in a successful finalized transaction serves as the native payout evidence. Internal-transfer explorer views can be used for investigation but are not required by the verification method documented here.
These event checks are authoritative for the deployed processor implementations described here. Validate the ABI and processor addresses against the deployment you use.
Finality requirements
| Network | Completion rule |
|---|---|
| Ethereum | Transaction in a finalized block; successful receipt and matching payment events |
| BNB Smart Chain | Transaction meets the configured, provider-supported finality policy; successful receipt and matching payment events |
| TRON | Transaction is available from a SolidityNode, receipt reports SUCCESS, and payment events match |
The developer examples use 15 confirmations on EVM networks. This is a confirmation-count policy, not Ethereum finality. The examples below retain a finalized-block check; use a provider that supports the finality semantics required for your network. If it cannot supply the information, keep the payment under review rather than treating it as final.
If a provider is unavailable or a receipt has not yet appeared, retry. An empty response is not proof that a payment failed.
Order lifecycle
| State | Meaning | Fulfil? |
|---|---|---|
created | Order saved, no payment submitted | No |
approval_pending | USDT approval transaction submitted | No |
pending | Payment txid saved; awaiting inclusion or finality | No |
verifying | Execution, finality and events are being checked | No |
paid | Verified payment matches the order | Yes, once |
failed | Confirmed execution failure or conclusive validation failure | No |
expired | Checkout window expired; submitted payments still require reconciliation | No, until reconciled |
review | Incomplete, ambiguous or conflicting verification data | No |
A page refresh, expired checkout or closed browser tab must not stop the backend verification worker.
Store the expected payment
Create the payment record before asking the buyer to submit the transaction. Example:
function createPendingPaymentRecord(order) {
return {
orderId: order.orderId,
chain: order.chain, // e.g. 'ethereum', 'bsc', 'tron'
chainId: order.chainId ?? null,
asset: order.asset,
tokenAddress: order.tokenAddress ?? null,
processorAddress: order.processorAddress,
sellerAddress: order.sellerAddress,
buyerAddress: order.buyerAddress ?? null,
amountBaseUnits: order.amountBaseUnits.toString(),
expectedSellerAmount: order.expectedSellerAmount.toString(),
buyerPaysCommission: order.buyerPaysCommission,
approvalTxids: [],
paymentAttempts: [],
status: 'created'
};
}
amountBaseUnits is the order's requested amount. expectedSellerAmount is the net amount the merchant must receive, in smallest units. Determine and persist it from your validated server-side order and the approved fee configuration, using the processor's previewPayment() results. Do not derive it from arbitrary browser inputs or a hard-coded fee percentage.
For USDT, use 6 decimals on Ethereum and TRON and 18 decimals on BNB Smart Chain. ETH and BNB use 18 decimals; TRX uses 6 (SUN). Never use floating-point arithmetic to calculate token amounts.
Store approved network, token and processor addresses in a backend allowlist. The transfer() function has no orderId parameter, so your application must link payment attempts to orders.
Use unique payment-attempt records and enforce that an on-chain payment event cannot be assigned to more than one order. An event identity is (chain, txid, logIndex). Also make order fulfilment idempotent. A customer retry must create a new attempt rather than overwrite an earlier pending hash.
Ethereum and BNB Smart Chain verification
Use ethers v6 on the backend. The following example verifies execution and finality, then checks an authentic PaymentProcessed log. For USDT it additionally checks the real token transfer.
The order argument must contain trusted backend values. Pass the provider for the same chain as order.chainId.
import { ethers } from 'ethers';
const events = new ethers.Interface([
'event PaymentProcessed(address indexed buyer, address indexed seller, uint256 commission, uint256 totalPay)',
'event Transfer(address indexed from, address indexed to, uint256 value)'
]);
function sameEvmAddress(a, b) {
return ethers.getAddress(a) === ethers.getAddress(b);
}
async function verifyEvmPayment(provider, txid, order) {
const chainId = (await provider.getNetwork()).chainId;
if (chainId !== BigInt(order.chainId)) {
return { state: 'review', reason: 'Unexpected network' };
}
const receipt = await provider.getTransactionReceipt(txid);
if (!receipt) return { state: 'pending' };
if (receipt.status !== 1) return { state: 'failed', reason: 'Execution reverted' };
let finalized;
try {
finalized = await provider.getBlock('finalized');
} catch {
return { state: 'review', reason: 'Finality data unavailable' };
}
if (!finalized || receipt.blockNumber > finalized.number) {
return { state: 'pending' };
}
// Make sure the receipt still belongs to the canonical block.
const block = await provider.getBlock(receipt.blockNumber);
if (!block || block.hash !== receipt.blockHash) {
return { state: 'review', reason: 'Block mismatch' };
}
const processor = ethers.getAddress(order.processorAddress);
const seller = ethers.getAddress(order.sellerAddress);
const token = order.tokenAddress
? ethers.getAddress(order.tokenAddress)
: null;
const expectedNet = BigInt(order.expectedSellerAmount);
for (const log of receipt.logs) {
if (!sameEvmAddress(log.address, processor)) continue;
let event;
try { event = events.parseLog(log); } catch { continue; }
if (event?.name !== 'PaymentProcessed') continue;
if (!sameEvmAddress(event.args.seller, seller)) continue;
if (order.buyerAddress &&
!sameEvmAddress(event.args.buyer, order.buyerAddress)) continue;
const net = event.args.totalPay - event.args.commission;
if (net < 0n || net !== expectedNet) continue;
if (token) {
const tokenPaid = receipt.logs.some((otherLog) => {
if (!sameEvmAddress(otherLog.address, token)) return false;
let transfer;
try { transfer = events.parseLog(otherLog); } catch { return false; }
return transfer?.name === 'Transfer' &&
sameEvmAddress(transfer.args.from, processor) &&
sameEvmAddress(transfer.args.to, seller) &&
transfer.args.value === net;
});
if (!tokenPaid) continue;
}
return {
state: 'matched',
buyer: event.args.buyer,
sellerReceived: net.toString(),
eventId: `${order.chain}:${txid.toLowerCase()}:${log.index}`
};
}
return { state: 'review', reason: 'No matching payment event' };
}
How to use this result: matched means the payment evidence passed this function's checks. Before changing the order to paid, claim its eventId uniquely in your database and complete the order through an idempotent server-side transaction.
The code intentionally matches authenticated receipt logs rather than relying on transaction calldata. A direct transaction to the merchant or an unrelated Transfer event does not qualify.
For native ETH and BNB payments, the processor's authenticated PaymentProcessed event is the documented payout evidence. For USDT, the additional Transfer must be emitted by the approved token contract and transfer tokens from the processor to the seller.
For production, configure your own reliable RPC endpoints and handle provider errors and retries around this function.
TRON verification
On TRON, query a solidified transaction receipt using the SolidityNode endpoint:
POST https://api.trongrid.io/walletsolidity/gettransactioninfobyid
The receipt includes raw event logs. The following implementation reads them directly, avoiding dependence on an indexer's event-list availability or address formatting. It uses ethers v6 to decode the EVM-compatible event data and tronweb to normalise TRON addresses.
import { ethers } from 'ethers';
import { TronWeb } from 'tronweb';
const tronEvents = new ethers.Interface([
'event PaymentProcessed(address indexed buyer, address indexed seller, uint256 commission, uint256 totalPay)',
'event Transfer(address indexed from, address indexed to, uint256 value)'
]);
function tronAddress20(address) {
// Base58 addresses use 41 + 20-byte account; EVM event addresses use 20 bytes.
if (typeof address !== 'string') throw new Error('Invalid address');
if (TronWeb.isAddress(address)) {
return TronWeb.address.toHex(address).slice(-40).toLowerCase();
}
const hex = address.replace(/^0x/, '').toLowerCase();
if (/^[0-9a-f]{40}$/.test(hex)) return hex;
if (/^41[0-9a-f]{40}$/.test(hex)) return hex.slice(2);
throw new Error('Invalid TRON address format');
}
function decodeTronLog(log) {
return tronEvents.parseLog({
topics: (log.topics || []).map((topic) => `0x${topic.replace(/^0x/, '')}`),
data: `0x${(log.data || '').replace(/^0x/, '')}`
});
}
async function verifyTronPayment(txid, order, tronGridApiKey) {
if (!/^[0-9a-fA-F]{64}$/.test(txid)) {
throw new Error('Invalid TRON transaction ID');
}
const response = await fetch(
'https://api.trongrid.io/walletsolidity/gettransactioninfobyid',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'TRON-PRO-API-KEY': tronGridApiKey
},
body: JSON.stringify({ value: txid })
}
);
if (!response.ok) throw new Error(`TronGrid HTTP ${response.status}`);
const info = await response.json();
if (info.Error) throw new Error(`TronGrid: ${info.Error}`);
if (!info.id) return { state: 'pending' };
if (info.id.toLowerCase() !== txid.toLowerCase()) {
return { state: 'review', reason: 'Transaction ID mismatch' };
}
if (!info.receipt?.result) {
return { state: 'review', reason: 'Receipt status unavailable' };
}
if (info.receipt.result !== 'SUCCESS') {
return { state: 'failed', reason: info.receipt.result };
}
const processor = tronAddress20(order.processorAddress);
const seller = tronAddress20(order.sellerAddress);
const token = order.tokenAddress ? tronAddress20(order.tokenAddress) : null;
const expectedNet = BigInt(order.expectedSellerAmount);
const logs = info.log || [];
for (const [index, log] of logs.entries()) {
if (tronAddress20(log.address) !== processor) continue;
let event;
try { event = decodeTronLog(log); } catch { continue; }
if (event?.name !== 'PaymentProcessed') continue;
if (tronAddress20(event.args.seller) !== seller) continue;
if (order.buyerAddress &&
tronAddress20(event.args.buyer) !== tronAddress20(order.buyerAddress)) continue;
const net = event.args.totalPay - event.args.commission;
if (net < 0n || net !== expectedNet) continue;
if (token) {
const tokenPaid = logs.some((other) => {
if (tronAddress20(other.address) !== token) return false;
let transfer;
try { transfer = decodeTronLog(other); } catch { return false; }
return transfer?.name === 'Transfer' &&
tronAddress20(transfer.args.from) === processor &&
tronAddress20(transfer.args.to) === seller &&
transfer.args.value === net;
});
if (!tokenPaid) continue;
}
return {
state: 'matched',
buyer: TronWeb.address.fromHex(`41${tronAddress20(event.args.buyer)}`),
sellerReceived: net.toString(),
eventId: `tron:${txid.toLowerCase()}:${index}`
};
}
return { state: 'review', reason: 'No matching payment event' };
}
The function verifies the solidified execution receipt, authentic PaymentProcessed event and, for USDT, the matching token Transfer event. A missing solidified receipt is pending, not a payment failure. For errors or incomplete event information, keep the payment under review and retry.
Use the approved TRON mainnet processor and token addresses from Smart contract reference. Do not accept a different network or processor address supplied by the browser.
See TRON confirmation semantics for details about SolidityNode finality.
Match payments to orders and prevent replay
The transfer() function does not include an orderId. It is your backend's responsibility to maintain a reliable order-to-payment association.
- When the checkout submits a payment: save its
txidunder the existing order and record an immutable payment attempt. - When processing a payment event: check that the event's seller, optional buyer and net amount match the expected order.
- For idempotency: use a unique
(chain, txid, logIndex)key for each payment event. Prevent the same event from completing multiple orders. Do not fulfil an order twice even if the verification worker retries. - For retries and late payments: retain previous payment attempts and reconcile transactions that finalize after the checkout expires.
Multiple payments may share the same seller and amount. Do not rely on matching only seller, amount or time window, or on modifying small decimal digits to manufacture unique invoice amounts. Keep a direct transaction-to-order association wherever your checkout submits the transaction.
Retries and operational errors
| Situation | Action |
|---|---|
| Transaction not yet mined or solidified | Keep the attempt pending and retry |
| RPC timeout, 429, or transient network error | Retry with backoff; do not infer a failed payment |
| Approval transaction succeeded | Record the approval, then await the separate payment transaction |
| Finalized transaction reverted | Mark the attempt failed; do not fulfil |
| Successful receipt but unmatched event | Keep under review; inspect processor, token, seller and expected amount |
| Browser closes or checkout expires | Continue backend reconciliation |
| Payment event already claimed by an order | Do not assign it to another order |
A browser-side confirmation counter, block explorer status badge or customer screenshot is not sufficient to complete an order.
Manual verification
Use Etherscan, BscScan or TronScan when investigating a payment. Check the transaction on the correct network, successful execution, the approved processor's PaymentProcessed event, the merchant's address and the net amount. For USDT, also inspect the genuine token Transfer from processor to merchant.
Manual inspection can help support teams reconcile payments, but a production checkout must implement automated, server-side verification.
Go-live checklist
- Approved mainnet processor and USDT token addresses configured server-side.
-
PaymentProcessedevent ABI matches the deployed processor contracts. - Correct asset precision: ETH/BNB 18, Ethereum/TRON USDT 6, BSC USDT 18, TRX 6.
- Orders and expected net seller amounts recorded before payment submission.
- Token approval hashes kept separate from payment hashes.
- Correct receipt status and chain-specific finality checked.
-
PaymentProcessedverified against the emitting processor address, seller and expected net amount. - USDT
Transferverified against the emitting token address, processor, seller and amount. - Optional fixed buyer wallet matched when required by the checkout.
- Each event uniquely claimed; fulfilment is idempotent.
- Failed, duplicated, underpaid and wrong-recipient payments tested.
- RPC outages, missing logs, late confirmations and expired checkouts handled.
- Verification tested against known successful and failed transactions for every supported asset.
Related
- Smart contract integration — overview of non-custodial payment integration.
- Ethereum and BNB Smart Chain integration — submit ETH, BNB and USDT payments.
- TRON integration — submit TRX and USDT TRC-20 payments.
- Smart contract reference — processor addresses, token decimals and event interface.
- Troubleshooting — common wallet and payment errors.