Ethereum and BNB Smart Chain integration
This guide explains how to accept non-custodial crypto payments on Ethereum and BNB Smart Chain using Volet.com payment smart contracts and ethers v6.
Customers connect their wallets, approve transactions and pay directly to your receiving wallet through a smart contract. Funds are not credited to a Volet.com account.
Before proceeding, review Smart contract integration for an overview of the payment flow.
Integration at a glance
- Connect the customer's wallet and confirm the selected blockchain network.
- Configure the payment contracts and your receiving wallet.
- Create an order in your backend before initiating payment.
- Call
previewPayment()to calculate the total payment amount. - Call
transfer()to initiate the payment. For USDT, check the token allowance and request approval if needed. - Save the payment transaction hash (
txid) against the order. - Verify the transaction on your backend before marking the order as paid.
Before you start
You will need:
- Customer wallet: MetaMask or another EIP-1193-compatible wallet for browser payments.
- Library:
ethersversion 6 for interacting with wallets and smart contracts. - Merchant wallet: An Ethereum-format wallet address you control to receive payments.
- Backend and database: To create orders, store transaction details and verify payments.
- RPC provider: To retrieve blockchain transactions and verify their execution.
For mobile wallet connections, you will also need a Reown/WalletConnect Project ID.
Supported networks and assets
| Network | Chain ID | Supported assets |
|---|---|---|
| Ethereum Mainnet | 1 | ETH, USDT (ERC-20) |
| BNB Smart Chain | 56 | BNB, USDT (BEP-20) |
Each asset uses the corresponding payment processor contract. The code examples below contain the processor and token addresses from the integration configuration.
For contract addresses, token decimals and function parameters, see Smart contract reference.
Before using the examples in production, confirm the processor deployment addresses and contract interface with Volet.com.
Step 1. Connect the customer's wallet
Use ethers v6 to connect the customer's browser wallet.
<script src="https://cdn.jsdelivr.net/npm/ethers@6/dist/ethers.umd.min.js"></script>
<button id="connect-wallet">Connect wallet</button>
<script>
let provider;
let signer;
document.getElementById('connect-wallet').addEventListener('click', async () => {
if (!window.ethereum) {
alert('Install a compatible wallet, such as MetaMask.');
return;
}
provider = new ethers.BrowserProvider(window.ethereum);
await provider.send('eth_requestAccounts', []);
signer = await provider.getSigner();
document.getElementById('connect-wallet').textContent =
await signer.getAddress();
});
</script>
The customer must connect to the network selected in your checkout:
- Ethereum Mainnet (
chainId: 1) - BNB Smart Chain (
chainId: 56)
If the wallet is on another network, request a network switch or ask the customer to switch manually.
Always verify the active chain ID immediately before submitting a payment, as customers can change networks in their wallets.
Step 2. Configure the payment contracts
Add the payment processor ABI and token functions to your application.
const PROCESSOR_ABI = [
'function previewPayment(uint256 amount, bool buyerPaysCommission) view returns (uint256 commission, uint256 totalPay)',
'function transfer(address seller, uint256 amount, bool buyerPaysCommission, address optionalCommissionWallet) payable',
];
const ERC20_ABI = [
'function balanceOf(address owner) view returns (uint256)',
'function allowance(address owner, address spender) view returns (uint256)',
'function approve(address spender, uint256 value) returns (bool)',
];
const PROCESSORS = {
native: '0x2fceE245109Cf3c535489a6F52fe11b3BBd644FF',
usdt: '0xe4083ED0A1101615b9092E8cD2164e2f42B4571E',
};
const USDT_BY_CHAIN = {
1: {
address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
decimals: 6,
},
56: {
address: '0x55d398326f99059fF775485246999027B3197955',
decimals: 18,
},
};
The payment processor provides two main functions:
previewPayment(amount, buyerPaysCommission)calculates the processing fee and the total amount the customer must pay.transfer(seller, amount, buyerPaysCommission, optionalCommissionWallet)executes the payment and distributes funds according to the contract's fee settings.
The amount parameter uses the asset's smallest units. Use ethers.parseEther() for ETH and BNB, and ethers.parseUnits() with the correct token decimals for USDT.
The buyerPaysCommission parameter determines who covers the processing fee:
true: The processing fee is added to the customer's payment.false: The processing fee is deducted from the merchant's payment amount.
Use ethers.ZeroAddress for optionalCommissionWallet if no partner commission wallet applies.
Always obtain the total payment amount from previewPayment(). Do not calculate processing fees manually.
For complete parameter definitions, see Smart contract reference.
Step 3. Accept ETH or BNB
Native ETH and BNB payments require one wallet transaction.
The same function works on both networks when the customer is connected to the correct blockchain.
async function payNative(signer, seller, humanAmount, buyerPaysCommission = true) {
if (!ethers.isAddress(seller)) {
throw new Error('Invalid merchant wallet');
}
const network = await signer.provider.getNetwork();
if (![1n, 56n].includes(network.chainId)) {
throw new Error('Unsupported network');
}
const processor = new ethers.Contract(
PROCESSORS.native,
PROCESSOR_ABI,
signer
);
const amount = ethers.parseEther(humanAmount);
const [, totalPay] = await processor.previewPayment(
amount,
buyerPaysCommission
);
const tx = await processor.transfer(
seller,
amount,
buyerPaysCommission,
ethers.ZeroAddress,
{ value: totalPay }
);
return tx.hash;
}
The transaction's value must equal totalPay returned by previewPayment().
The customer must also have enough ETH or BNB to cover the blockchain network fee.
The function returns the payment transaction hash (txid). Save it against the existing order and verify the payment before fulfilment.
Step 4. Accept USDT
USDT payments use an ERC-20 or BEP-20 token contract and may require an additional approval transaction.
Before making a payment, your application must:
- Check the customer's USDT balance using
balanceOf(). - Check the processor's spending allowance using
allowance(). - If the allowance is insufficient, request approval using
approve(). - Call
transfer()to execute the payment.
async function payUsdt(signer, seller, humanAmount, buyerPaysCommission = true) {
if (!ethers.isAddress(seller)) {
throw new Error('Invalid merchant wallet');
}
const chainId = Number((await signer.provider.getNetwork()).chainId);
const tokenConfig = USDT_BY_CHAIN[chainId];
if (!tokenConfig) {
throw new Error('Unsupported network');
}
const processor = new ethers.Contract(
PROCESSORS.usdt,
PROCESSOR_ABI,
signer
);
const token = new ethers.Contract(
tokenConfig.address,
ERC20_ABI,
signer
);
const amount = ethers.parseUnits(humanAmount, tokenConfig.decimals);
const [, totalPay] = await processor.previewPayment(
amount,
buyerPaysCommission
);
const buyer = await signer.getAddress();
const balance = await token.balanceOf(buyer);
if (balance < totalPay) {
throw new Error('Insufficient USDT');
}
const currentAllowance = await token.allowance(
buyer,
PROCESSORS.usdt
);
if (currentAllowance < totalPay) {
// Ethereum USDT may require resetting an existing allowance.
if (chainId === 1 && currentAllowance > 0n) {
const resetTx = await token.approve(PROCESSORS.usdt, 0n);
await resetTx.wait();
}
const approvalTx = await token.approve(
PROCESSORS.usdt,
totalPay
);
await approvalTx.wait();
}
const tx = await processor.transfer(
seller,
amount,
buyerPaysCommission,
ethers.ZeroAddress
);
return tx.hash;
}
USDT approval requirements
Depending on the existing allowance, a USDT payment may require:
| Existing allowance | Required transactions |
|---|---|
| Sufficient | Payment only |
| Zero or insufficient | Approval + payment |
| Existing non-zero allowance on Ethereum USDT | Reset allowance to zero + approval + payment |
Ethereum USDT may reject changing a non-zero allowance directly to another non-zero value. The example handles this by resetting the allowance first when necessary.
Each approval must be confirmed before proceeding to the next transaction.
Important:
- An
approve()transaction is not a payment. - The customer needs enough ETH or BNB to cover network fees, even when paying USDT.
- Use integer token amounts calculated with
ethers.parseUnits(). Do not use floating-point arithmetic for financial amounts. - Track approval transactions separately from the final payment transaction.
Step 5. Save the payment transaction
Your backend must create an order before the customer submits a payment.
Each order should contain the expected:
- Order ID
- Blockchain network and chain ID
- Asset and amount
- Merchant receiving wallet (
seller) - Approved payment processor contract
- Buyer wallet, if required by your checkout
- Processing fee setting (
buyerPaysCommission)
After the customer submits the payment:
- Obtain the payment transaction hash (
txid) returned by the wallet. - Send the hash to your backend.
- Store it together with the order ID and chain ID.
- Start server-side payment verification.
Each payment transaction must be associated with no more than one order. Enforce this using a unique (chainId, txid) constraint in your database.
The smart contract's transfer() function does not accept an order ID. The relationship between your order and its payment transaction must be maintained in your application.
If a customer retries a payment, keep separate payment attempts rather than overwriting transaction records that may still be pending.
Step 6. Verify the payment
Payment verification is required before completing an order.
A returned transaction hash only indicates that a transaction was submitted. It does not prove that the merchant received the correct payment.
Your backend must independently:
- Retrieve the transaction and execution receipt from the blockchain.
- Confirm successful execution and the required blockchain finality.
- Verify that the transaction used the expected network and payment processor.
- Match the asset, merchant wallet, amount and contract call against the order.
- Confirm that the merchant received the expected funds.
- Ensure the transaction has not already been used for another order.
- Mark the order as paid and fulfil it only once.
A successful transaction receipt alone is not sufficient. A transaction may have executed successfully without paying the correct merchant or amount.
Follow Payment verification and order tracking for the backend implementation, order lifecycle, finality requirements and production checklist.
Optional: Support mobile wallets
To support mobile wallets through WalletConnect, integrate @walletconnect/ethereum-provider and configure it with a Project ID from Reown Cloud.
The EVM HTML example demonstrates WalletConnect integration, including network selection and switching.
A WalletConnect Project ID identifies your application. It is not a private key. Restrict the permitted application domains or origins where supported.
Example integration
The example-evm.html demonstrates:
- Connecting MetaMask and WalletConnect-compatible wallets.
- Selecting Ethereum or BNB Smart Chain.
- Calculating payment amounts using
previewPayment(). - Sending ETH, BNB and USDT payments.
- Displaying transaction hashes and tracking blockchain confirmations.
You can open the example in your browser or download the HTML file to study and adapt the code:
- Open the live example — opens the interactive demo. It uses Ethereum and BNB Smart Chain mainnet, so confirming a payment transfers real funds.
- Download example-evm.html — download the HTML file to run locally or adapt for your application.
Browser wallet extensions such as MetaMask may not work correctly with pages opened directly from disk (file://). Serve the downloaded file from a local web server instead. For example, run npx serve in the folder containing the file and open the localhost URL it provides.
Try the example
- Open the live example or serve the downloaded file locally in a browser with MetaMask installed.
- Select Ethereum Mainnet or BNB Smart Chain and click Connect Wallet (MetaMask). For mobile wallets, enter your WalletConnect Project ID and select WalletConnect (QR).
- Select the payment method: ETH, BNB or USDT.
- Enter the receiving wallet in Seller address.
- Enter the payment amount and choose who pays the processing fee.
- Click Preview to see the processing fee and total payment amount. Then click Pay and confirm the transaction in your wallet. USDT payments may require a separate approval first.
- The order and transaction hash appear in the Orders table. The demo tracks up to 15 blockchain confirmations.
Before you pay:
- Use two different wallets. The seller address must differ from the paying wallet. Otherwise, the contract rejects the payment with
SellerIsBuyer.- Transactions use real funds. The example is prefilled with
0.001ETH or BNB. Check the amount, processing fee and network fee before confirming.
Demo limitations: This example stores transaction history in browser
localStorageand is not a production checkout. Tracking 15 confirmations does not establish Ethereum finality or replace backend payment verification. Implement the full payment verification process before accepting customer orders.
Related
- Smart contract integration — overview of the non-custodial payment flow.
- TRON integration — accept TRX and USDT TRC-20.
- Smart contract reference — processor addresses, token decimals and contract parameters.
- Troubleshooting — common wallet and transaction errors.