Skip to main content

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​

  1. Create an order. Store the expected network, asset, receiving wallet (seller), processor contract, expected merchant amount and, where applicable, buyer wallet.
  2. Record the payment attempt. Associate the payment txid with the order. Store USDT approve() transaction hashes separately.
  3. Check execution and finality. Retrieve blockchain data using a backend RPC connection or hosted blockchain service.
  4. Verify payment events. Find PaymentProcessed emitted by the approved processor. For USDT, also verify the actual token Transfer to the merchant.
  5. Match the order. Compare the event's merchant address, buyer (if required) and net amount with immutable backend order details.
  6. 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.

AssetRequired on-chain evidence
ETH, BNBSuccessful finalized execution and a matching PaymentProcessed log emitted by the approved native-asset processor
USDT ERC-20, BEP-20Matching processor PaymentProcessed log and USDT Transfer log from the approved token contract: processor → merchant for receivedAmount
TRXSuccessful solidified execution and a matching PaymentProcessed log emitted by the approved TRX processor
USDT TRC-20Matching 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​

NetworkCompletion rule
EthereumTransaction in a finalized block; successful receipt and matching payment events
BNB Smart ChainTransaction meets the configured, provider-supported finality policy; successful receipt and matching payment events
TRONTransaction 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​

StateMeaningFulfil?
createdOrder saved, no payment submittedNo
approval_pendingUSDT approval transaction submittedNo
pendingPayment txid saved; awaiting inclusion or finalityNo
verifyingExecution, finality and events are being checkedNo
paidVerified payment matches the orderYes, once
failedConfirmed execution failure or conclusive validation failureNo
expiredCheckout window expired; submitted payments still require reconciliationNo, until reconciled
reviewIncomplete, ambiguous or conflicting verification dataNo

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 txid under 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​

SituationAction
Transaction not yet mined or solidifiedKeep the attempt pending and retry
RPC timeout, 429, or transient network errorRetry with backoff; do not infer a failed payment
Approval transaction succeededRecord the approval, then await the separate payment transaction
Finalized transaction revertedMark the attempt failed; do not fulfil
Successful receipt but unmatched eventKeep under review; inspect processor, token, seller and expected amount
Browser closes or checkout expiresContinue backend reconciliation
Payment event already claimed by an orderDo 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.
  • PaymentProcessed event 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.
  • PaymentProcessed verified against the emitting processor address, seller and expected net amount.
  • USDT Transfer verified 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.