Accept Crypto Payments With Zero Blockchain Knowledge

Coinley handles all the complexity of cryptocurrency payments. You just call our API. Automatic fee splitting, multi-wallet support, and real-time verification included.

What is Coinley?

For Developers

Coinley is a payment processor API that lets you accept cryptocurrency payments (USDT, USDC, etc.) without learning blockchain development, smart contracts, or Web3.

  • No blockchain experience needed
  • Simple REST API integration
  • Automatic smart contract interaction
  • Built-in payment verification

For Your Customers

Your customers connect their crypto wallet (MetaMask, Coinbase Wallet, etc.), approve the transaction, and payment is processed automatically.

  • Works with any Web3 wallet
  • Secure, non-custodial payments
  • Instant settlement
  • Supports 8+ blockchain networks

Real-World Example: SportsPass Ticket Sales

This documentation uses SportsPass - a live ticket sales platform built with Coinley - as the primary example. You'll see exactly how a production application integrates crypto payments without any blockchain expertise required.

→ View the complete SportsPass implementation

Understanding Coinley's Payment Flow

Here's exactly what happens when a customer pays with cryptocurrency using Coinley. We handle the blockchain complexity - you just call our API.

1

Create Payment

Your backend calls Coinley API to create payment request

2

Connect Wallet

Customer connects MetaMask or any other crypto Wallet to your site

3

Execute & Verify

Payment executes via smart contract, Coinley verifies on-chain

1

Step 1: Create Payment Request

Your backend calls POST /api/payments/create with payment details (amount, currency, network). Coinley returns:

  • Payment ID: Unique identifier for this transaction
  • Smart Contract Address: Where payment will be sent
  • Fee Split Details: Merchant (98.5%) + Coinley (1.5%) wallets and percentages
  • Token Details: Contract address and decimals for the selected currency
POST /api/payments/create → Returns payment contract details
2

Step 2: Connect Wallet & Switch Network

Your frontend uses Web3.js to connect the customer's crypto wallet and switch to the correct blockchain network:

Connect Wallet

Use window.ethereum.request() to connect MetaMask, Coinbase Wallet, or any Web3 wallet.

Switch Network

Use wallet_switchEthereumChain to switch to Polygon, BSC, Optimism, etc.

3

Step 3: Execute Payment Transaction

The customer approves and executes the payment. Coinley uses smart contracts to automatically split funds:

Why Two Wallet Popups?

Customers will see two MetaMask popups. This is normal and required for ERC20 token payments:

  1. 1. Approve Transaction: Give the smart contract permission to spend tokens (security feature)
  2. 2. Execute Payment: The smart contract transfers tokens and splits them automatically
98.5%
Goes to your wallet
1.5%
Coinley platform fee
100%
Automatic split
4

Step 4: Verify Payment On-Chain

After the customer completes payment, your frontend sends the transaction hash to Coinley. We verify the payment on the blockchain and update the payment status:

  • Verify transaction exists on blockchain
  • Confirm correct amount was sent
  • Validate correct recipient addresses
  • Mark payment as completed
POST /api/payments/process → Verifies transaction on-chain

That's It!

Coinley handles all the blockchain complexity: smart contract deployment, fee splitting, network configuration, gas optimization, and payment verification. You just make API calls.

Supported Networks & Stablecoins

Accept USDT, USDC, and other stablecoins across 8 major blockchains

Ethereum

High fees

Polygon

~$0.01 fees

BSC

~$0.02 fees

Base

~$0.03 fees

Optimism

~$0.60 fees

Arbitrum

~$0.50 fees

Avalanche

Low fees

Solana

Coming soon

Pro Tip: Use Polygon or BSC for transactions under $100 to minimize gas fees (~$0.01 vs $3-5 on Ethereum/Optimism)

SDK Integration (Recommended)

Use the Coinley SDK for the fastest and easiest integration. The SDK handles all the complexity including wallet connection, network switching, payment execution, and verification.

Why Use the SDK?

  • 5-minute setup - Just import and configure
  • Beautiful UI included - Pre-built payment modals with modern design
  • Automatic error handling - User-friendly error messages built-in
  • Works with React or vanilla JS - Use in any project
  • Production-ready - Battle-tested in real applications

React Integration

Step 1: Install the SDK

npm install coinley-pay
# or
yarn add coinley-pay

Step 2: Import Components and Styles

import { EnhancedSimpleCoinleyPayment } from 'coinley-pay';
import 'coinley-pay/dist/style.css';

Step 3: Add the Payment Component

Here's a complete example from a real e-commerce checkout:

import { useState } from 'react';
import { EnhancedSimpleCoinleyPayment } from 'coinley-pay';
import 'coinley-pay/dist/style.css';

function CheckoutPage() {
    const [isPaymentOpen, setIsPaymentOpen] = useState(false);
    const [customerEmail, setCustomerEmail] = useState('');
    const [orderTotal] = useState(49.99);

    // Handle successful payment
    const handleSuccess = (paymentId, transactionHash, paymentDetails) => {
        console.log('Payment successful!', { paymentId, transactionHash });
        // Update your order status, redirect to success page, etc.
        alert('Payment completed successfully!');
    };

    // Handle payment errors
    const handleError = (error) => {
        console.error('Payment error:', error);
        alert('Payment failed: ' + error);
    };

    // Handle modal close
    const handleClose = () => {
        setIsPaymentOpen(false);
    };

    return (
        <div>
            {/* Your checkout form */}
            <button onClick={() => setIsPaymentOpen(true)}>
                Pay with Crypto
            </button>

            {/* Coinley Payment Modal */}
            <EnhancedSimpleCoinleyPayment
                apiKey="your_api_key"
                apiSecret="your_api_secret"
                apiUrl="https://hub.coinley.io"
                config={{
                    amount: orderTotal,
                    customerEmail: customerEmail,
                    merchantName: "Your Store Name",
                    callbackUrl: `${window.location.origin}/api/webhooks/coinley`,
                    merchantWalletAddresses: {
                        polygon: "0xYourPolygonWallet...",
                        bsc: "0xYourBSCWallet...",
                        // Add other networks as needed
                    },
                    metadata: {
                        orderId: "ORDER_123",
                        // Add any custom data
                    }
                }}
                onSuccess={handleSuccess}
                onError={handleError}
                onClose={handleClose}
                isOpen={isPaymentOpen}
                theme="light"
            />
        </div>
    );
}

Configuration Options

Property Type Required Description
apiKey string Yes Your Coinley API key
apiSecret string Yes Your Coinley API secret
apiUrl string Yes Coinley API base URL
config.amount number Yes Payment amount in USD
config.customerEmail string Yes Customer's email address
config.merchantName string Yes Your business name
config.merchantWalletAddresses object Yes Your wallet addresses per network
isOpen boolean Yes Controls modal visibility
theme string No "light" or "dark" (default: "light")

Vanilla JavaScript Integration

Step 1: Include the SDK

Add the SDK script to your HTML:

<script src="https://unpkg.com/coinley-pay@latest/dist/coinley-vanilla.min.js"></script>

Step 2: Initialize and Use

<script>
    // Initialize Coinley
    const coinley = new CoinleyVanilla({
        apiKey: 'your_api_key',
        apiSecret: 'your_api_secret',
        apiUrl: 'https://hub.coinley.io',
        theme: 'light',
        debug: false
    });

    // Open payment when button is clicked
    document.getElementById('payButton').addEventListener('click', function() {
        coinley.open(
            {
                amount: 49.99,
                customerEmail: 'customer@example.com',
                merchantName: 'Your Store',
                merchantWalletAddresses: {
                    polygon: '0xYourPolygonWallet...',
                    bsc: '0xYourBSCWallet...'
                },
                metadata: {
                    orderId: 'ORDER_123'
                }
            },
            {
                onSuccess: function(paymentId, transactionHash, paymentDetails) {
                    console.log('Payment successful!', paymentId);
                    alert('Payment completed! Transaction: ' + transactionHash);
                },
                onError: function(error) {
                    console.error('Payment failed:', error);
                    alert('Payment failed: ' + error);
                },
                onClose: function() {
                    console.log('Payment modal closed');
                }
            }
        );
    });
</script>

Auto-Initialization (Optional)

You can auto-initialize the SDK using script tag attributes:

<script
    src="https://unpkg.com/coinley-pay@latest/dist/coinley-vanilla.min.js"
    data-api-key="your_api_key"
    data-api-secret="your_api_secret"
    data-api-url="https://hub.coinley.io"
    data-theme="light"
></script>

<script>
    // SDK is already initialized as window.coinley
    window.coinley.open({ ... });
</script>

Complete Vanilla JS Example

Here's a complete working HTML page:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Coinley Payment Example</title>
</head>
<body>
    <h1>Buy Product - $49.99</h1>

    <input type="email" id="customerEmail" placeholder="Enter your email" />
    <button id="payButton">Pay with Crypto</button>

    <!-- Include Coinley SDK -->
    <script src="https://unpkg.com/coinley-pay@latest/dist/coinley-vanilla.min.js"></script>

    <script>
        // Initialize Coinley
        const coinley = new CoinleyVanilla({
            apiKey: 'your_api_key',
            apiSecret: 'your_api_secret',
            apiUrl: 'https://hub.coinley.io'
        });

        // Handle payment button click
        document.getElementById('payButton').addEventListener('click', () => {
            const email = document.getElementById('customerEmail').value;

            if (!email) {
                alert('Please enter your email');
                return;
            }

            coinley.open(
                {
                    amount: 49.99,
                    customerEmail: email,
                    merchantName: 'My Store',
                    merchantWalletAddresses: {
                        polygon: '0xYourWallet...'
                    }
                },
                {
                    onSuccess: (paymentId, txHash) => {
                        alert('Payment successful!\\nTransaction: ' + txHash);
                        // Redirect to success page or update order
                    },
                    onError: (error) => {
                        alert('Payment failed: ' + error);
                    }
                }
            );
        });
    </script>
</body>
</html>

SDK vs Manual API Integration

Using SDK (Recommended)

  • 5 minutes - Quick setup with pre-built components
  • Beautiful UI - Professional payment modal included
  • Error handling - User-friendly messages built-in
  • Maintained - Updates and improvements included
  • Less code - ~20 lines vs 200+ lines

Manual API Integration

  • 1-2 hours - Manual implementation required
  • Build your own UI - Need to create payment interface
  • Handle errors - Custom error handling needed
  • Self-maintained - You handle all updates
  • More code - Full Web3 integration logic
  • Maximum control - Full customization possible

Ready to integrate in 5 minutes?

Get started with the SDK for the fastest crypto payment integration

Manual API Integration Guide

This section shows how to manually integrate with the Coinley API using Web3.js and custom code.

Looking for a faster option? Check out the SDK Integration above for a 5-minute setup with pre-built UI components.

Prerequisites

  • Coinley API Credentials

    Sign up at Coinley to get your API key and secret

  • Basic JavaScript/HTML Knowledge

    You don't need blockchain experience

  • Web3 Libraries

    We'll use Web3.js (auto-loaded from CDN) and Axios

Installation

<!-- Include required libraries in your HTML -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/web3@1.10.0/dist/web3.min.js"></script>

Configuration

Set up your Coinley API credentials. Keep these secure on your backend.

Security Warning

In production, store API credentials on your backend. The frontend should call your backend API, not Coinley directly.

// Coinley Configuration
const COINLEY_CONFIG = {
    baseURL: 'https://hub.coinley.io',
    apiKey: 'your_api_key_here',
    apiSecret: 'your_api_secret_here'
};

// Store cart and payment state
let cart = [];
let currentPayment = null;
let web3 = null;
let userAccount = null;
let availableNetworks = [];
let networkTokens = {};

Load Available Networks & Tokens

Fetch the list of supported blockchain networks and their tokens from Coinley:

async function loadNetworks() {
    const response = await axios.get(`${COINLEY_CONFIG.baseURL}/api/networks`, {
        headers: {
            'X-API-Key': COINLEY_CONFIG.apiKey,
            'X-API-Secret': COINLEY_CONFIG.apiSecret
        }
    });

    availableNetworks = response.data.networks.filter(n => !n.isTestnet);

    // Sort networks by cost-effectiveness
    const sortedNetworks = availableNetworks.sort((a, b) => {
        const costRanking = {
            'polygon': 1, 'bsc': 2, 'base': 3,
            'arbitrum': 4, 'optimism': 5, 'ethereum': 7
        };
        return (costRanking[a.shortName] || 99) - (costRanking[b.shortName] || 99);
    });

    // Populate network dropdown
    const networkSelect = document.getElementById('paymentNetwork');
    networkSelect.innerHTML = sortedNetworks.map(network => {
        const costIndicator = network.shortName === 'polygon' || network.shortName === 'bsc'
            ? ' 💚 (Low fees)' : network.shortName === 'optimism' ? ' ⚠️ (Higher fees)' : '';
        return `<option value="${network.shortName}">${network.name}${costIndicator}</option>`;
    }).join('');
}

async function loadTokensForNetwork(networkShortName) {
    const network = availableNetworks.find(n => n.shortName === networkShortName);

    const response = await axios.get(
        `${COINLEY_CONFIG.baseURL}/api/networks/${network.id}/tokens`,
        {
            headers: {
                'X-API-Key': COINLEY_CONFIG.apiKey,
                'X-API-Secret': COINLEY_CONFIG.apiSecret
            }
        }
    );

    networkTokens[networkShortName] = response.data.tokens;

    // Populate currency dropdown
    const currencySelect = document.getElementById('paymentCurrency');
    currencySelect.innerHTML = response.data.tokens.map(token =>
        `<option value="${token.symbol}">${token.symbol} - ${token.name}</option>`
    ).join('');
}

Step 1: Create Payment

Call the Coinley API to create a payment request. You'll get back smart contract details and fee split configuration.

POST /api/payments/create

SportsPass Example

In the SportsPass ticket platform, this function is called when a customer clicks "Connect MetaMask & Pay" with items in their cart. It creates a payment for the total cart value.

async function connectMetaMaskAndPay() {
    const email = document.getElementById('customerEmail').value;
    const network = document.getElementById('paymentNetwork').value;
    const currency = document.getElementById('paymentCurrency').value;

    if (!email || cart.length === 0) {
        alert('Please enter your email and add items to cart');
        return;
    }

    const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);

    // Create payment via Coinley API
    const response = await axios.post(
        `${COINLEY_CONFIG.baseURL}/api/payments/create`,
        {
            amount: total.toFixed(2),        // e.g., "19.99"
            currency: currency,               // e.g., "USDT"
            network: network,                 // e.g., "polygon"
            customerEmail: email,
            orderId: `TICKET_${Date.now()}`,
            metadata: {
                tickets: cart,
                platform: 'SportsPass'
            }
        },
        {
            headers: {
                'X-API-Key': COINLEY_CONFIG.apiKey,
                'X-API-Secret': COINLEY_CONFIG.apiSecret,
                'Content-Type': 'application/json'
            }
        }
    );

    if (response.data.success) {
        currentPayment = response.data.payment;
        console.log('Payment created:', currentPayment);
        // Continue to wallet connection...
    }
}

What You Get Back

{
  "success": true,
  "payment": {
    "id": "21870d6e-d53d-4e72-8761-e0208a8ceacb",
    "amount": "19.99",
    "currency": "USDT",
    "network": "polygon",
    "status": "pending",
    "contractAddress": "0x48E967d182F744EdB18AcA7092814f1FE17fcB2d",
    "splitterPaymentId": "21870d6e-d53d-4e72-8761-e0208a8ceacb",
    "merchantWallet": "0x9d961e093FFeC1577d124Cfe65233fE140E88Fc4",
    "coinleyWallet": "0xEe9025Cc02c060C03ba5dba3d19C7ea2e752f44d",
    "merchantPercentage": 9700,  // 98.5%
    "coinleyPercentage": 300,    // 1.5%
    "expiresAt": "2025-10-15T14:30:00.000Z"
  }
}

What's happening: Coinley creates a unique payment ID and tells you which smart contract to use. The contract will automatically split funds 98.5% to you, 1.5% to Coinley.

Step 2: Connect Wallet & Switch Network

Connect the user's crypto wallet (MetaMask, Coinbase Wallet, etc.) and switch to the correct blockchain network.

Connect Wallet

async function connectWallet() {
    // Check if user has a Web3 wallet installed
    if (typeof window.ethereum === 'undefined') {
        alert('Please install MetaMask or another Web3 wallet');
        window.open('https://metamask.io/download/', '_blank');
        return;
    }

    // Initialize Web3
    web3 = new Web3(window.ethereum);

    // Request account access
    const accounts = await window.ethereum.request({
        method: 'eth_requestAccounts'
    });

    if (!accounts || accounts.length === 0) {
        throw new Error('No wallet accounts found');
    }

    userAccount = accounts[0];
    console.log('Connected wallet:', userAccount);
    return userAccount;
}

Switch to Correct Network

Ensure the customer's wallet is on the correct blockchain network for the payment:

async function switchToNetwork(networkShortName) {
    const network = availableNetworks.find(n => n.shortName === networkShortName);
    if (!network) throw new Error('Network not found');

    // Convert chainId to hex format (required by MetaMask)
    const chainIdHex = '0x' + parseInt(network.chainId).toString(16);

    try {
        // Ask wallet to switch networks
        await window.ethereum.request({
            method: 'wallet_switchEthereumChain',
            params: [{ chainId: chainIdHex }]
        });

        console.log(`Switched to ${network.name}`);
    } catch (error) {
        if (error.code === 4902) {
            // Network not added to wallet yet
            alert(`Please add ${network.name} to your wallet manually`);
        }
        throw error;
    }
}

User Experience: The wallet will show a popup asking the user to switch networks. This is automatic - you don't need to worry about network configurations.

Balance Validation (Critical!)

Always check if the user has enough tokens BEFORE attempting payment. This prevents confusing blockchain errors.

Common Error

"ERC20: transfer amount exceeds balance" means the user doesn't have enough tokens.

Solution: Check balance first and show a friendly error message before attempting payment.

async function checkTokenBalance(tokenAddress, requiredAmount, decimals) {
    // ERC20 standard ABI for balanceOf
    const erc20ABI = [{
        "constant": true,
        "inputs": [{"name": "_owner", "type": "address"}],
        "name": "balanceOf",
        "outputs": [{"name": "balance", "type": "uint256"}],
        "type": "function"
    }];

    const tokenContract = new web3.eth.Contract(erc20ABI, tokenAddress);

    // Get user's token balance
    const balanceWei = await tokenContract.methods
        .balanceOf(userAccount)
        .call();

    // Convert to decimal (e.g., USDT uses 6 decimals)
    const balanceDecimal = parseFloat(balanceWei) / Math.pow(10, decimals);

    console.log(`Balance: ${balanceDecimal} | Required: ${requiredAmount}`);

    // Check if sufficient
    if (balanceDecimal < requiredAmount) {
        const shortfall = (requiredAmount - balanceDecimal).toFixed(2);
        throw new Error(
            `Insufficient balance!\n\n` +
            `Required: ${requiredAmount}\n` +
            `Available: ${balanceDecimal.toFixed(2)}\n` +
            `Shortfall: ${shortfall}\n\n` +
            `Please add more funds to your wallet.`
        );
    }

    return true;
}

Best Practice: Display the user's current balance in your UI so they know if they need to add funds.

Step 3: Execute Payment

Execute the smart contract payment. This involves two transactions: approve and pay.

Why Two Transactions?

  1. 1. Approve: Give the smart contract permission to spend your tokens (ERC20 security standard)
  2. 2. Pay: The contract transfers tokens and automatically splits payment to merchant + Coinley

This is a security feature of ERC20 tokens - contracts can't take your tokens without explicit permission.

Complete Payment Function from SportsPass

async function executePayment() {
    const networkShortName = currentPayment.network;
    const currency = currentPayment.currency;
    const tokenData = networkTokens[networkShortName].find(t => t.symbol === currency);

    const decimals = tokenData.decimals || 6;
    const amount = parseFloat(currentPayment.amount);
    const amountWei = (amount * Math.pow(10, decimals)).toString();

    // ERC20 Token ABI
    const erc20ABI = [
        {
            "constant": false,
            "inputs": [
                {"name": "_spender", "type": "address"},
                {"name": "_value", "type": "uint256"}
            ],
            "name": "approve",
            "outputs": [{"name": "", "type": "bool"}],
            "type": "function"
        },
        {
            "constant": true,
            "inputs": [{"name": "_owner", "type": "address"}],
            "name": "balanceOf",
            "outputs": [{"name": "balance", "type": "uint256"}],
            "type": "function"
        }
    ];

    const tokenContract = new web3.eth.Contract(erc20ABI, tokenData.contractAddress);

    // ✅ CRITICAL: Check balance first
    const balance = await tokenContract.methods.balanceOf(userAccount).call();
    if (BigInt(balance) < BigInt(amountWei)) {
        throw new Error('Insufficient token balance');
    }

    // ✅ STEP 1: Approve token spending
    console.log('Requesting token approval...');
    const approveTx = await tokenContract.methods
        .approve(currentPayment.contractAddress, amountWei)
        .send({ from: userAccount });

    console.log('Approved! Tx:', approveTx.transactionHash);

    // ✅ STEP 2: Execute payment via splitter contract
    const splitterABI = [{
        "inputs": [{
            "components": [
                {"name": "token", "type": "address"},
                {"name": "amount", "type": "uint256"},
                {"name": "paymentId", "type": "string"},
                {"name": "recipient1", "type": "address"},
                {"name": "recipient2", "type": "address"},
                {"name": "recipient3", "type": "address"},
                {"name": "recipient1Percentage", "type": "uint256"},
                {"name": "recipient2Percentage", "type": "uint256"},
                {"name": "recipient3Percentage", "type": "uint256"}
            ],
            "name": "params",
            "type": "tuple"
        }],
        "name": "splitPayment",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    }];

    const splitterContract = new web3.eth.Contract(
        splitterABI,
        currentPayment.contractAddress
    );

    const splitParams = {
        token: tokenData.contractAddress,
        amount: amountWei,
        paymentId: currentPayment.splitterPaymentId,
        recipient1: currentPayment.merchantWallet,
        recipient2: currentPayment.coinleyWallet,
        recipient3: '0x0000000000000000000000000000000000000000',
        recipient1Percentage: currentPayment.merchantPercentage,
        recipient2Percentage: currentPayment.coinleyPercentage,
        recipient3Percentage: 0
    };

    console.log('Executing payment...');
    const paymentTx = await splitterContract.methods
        .splitPayment(splitParams)
        .send({ from: userAccount });

    console.log('Payment successful! Tx:', paymentTx.transactionHash);

    // ✅ STEP 3: Verify payment with Coinley
    await verifyPayment(paymentTx.transactionHash);
}

What happens: The wallet shows two popups - one to approve tokens, one to execute payment. The smart contract automatically splits funds between merchant (98.5%) and Coinley (1.5%).

Step 4: Verify Payment

Send the transaction hash to Coinley for verification. We check the blockchain to confirm payment.

POST /api/payments/process
async function verifyPayment(txHash) {
    const response = await axios.post(
        `${COINLEY_CONFIG.baseURL}/api/payments/process`,
        {
            paymentId: currentPayment.id,
            transactionHash: txHash,
            network: currentPayment.network,
            senderAddress: userAccount
        },
        {
            headers: {
                'X-API-Key': COINLEY_CONFIG.apiKey,
                'X-API-Secret': COINLEY_CONFIG.apiSecret,
                'Content-Type': 'application/json'
            }
        }
    );

    if (!response.data.success) {
        throw new Error(response.data.error || 'Verification failed');
    }

    console.log('Payment verified!', response.data);
}

Verification Response

{
  "success": true,
  "message": "Payment processed successfully",
  "payment": {
    "id": "21870d6e-d53d-4e72-8761-e0208a8ceacb",
    "status": "completed",
    "amount": "19.99",
    "transactionHash": "0xabcdef1234567890...",
    "senderAddress": "0x581c333..."
  },
  "onChainData": {
    "blockNumber": 18745123,
    "gasUsed": "65432",
    "confirmed": true
  }
}

What Coinley verifies: Transaction exists on blockchain, correct amount was sent, correct recipients received funds, and fee split was executed correctly.

Gas Fee Optimization

Minimize transaction costs for your users with these strategies (as implemented in SportsPass).

✅ Low Fee Networks

  • Polygon: ~$0.01 per transaction
  • BSC: ~$0.02 per transaction
  • Base: ~$0.03 per transaction

⚠️ Higher Fee Networks

  • Optimism: $0.50-$0.70 per transaction
  • Arbitrum: $0.40-$0.60 per transaction
  • Ethereum: $10-$30+ per transaction

Show Fee Warnings to Users

SportsPass displays warnings when users select high-fee networks. Here's how:

// Show fee warning based on selected network
networkSelect.addEventListener('change', (e) => {
    const selectedNetwork = e.target.value;
    const networkWarning = document.getElementById('networkWarning');
    const highFeeNetworks = ['optimism', 'arbitrum', 'ethereum'];
    const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);

    if (highFeeNetworks.includes(selectedNetwork)) {
        let estimatedFee = 0;

        if (selectedNetwork === 'optimism') {
            estimatedFee = 0.61;
            networkWarning.innerHTML = `
                ⚠️ OPTIMISM ALERT: Expected fee ~$${estimatedFee}
                (${(estimatedFee / total * 100).toFixed(0)}% of transaction).
                Switch to Polygon for ~$0.01 fees.
            `;
        } else if (selectedNetwork === 'ethereum') {
            estimatedFee = 15;
            networkWarning.innerHTML = `
                🚨 ETHEREUM: Fees typically $10-20.
                DO NOT USE for small transactions! Use Polygon instead.
            `;
        }

        networkWarning.classList.remove('hidden');
    } else {
        networkWarning.classList.add('hidden');
    }
});

Optimism/Arbitrum Direct Transfer Optimization

For Optimism and Arbitrum, SportsPass offers a "direct transfer" option that bypasses the splitter contract, reducing fees by 90%. Set isDirectTransfer: true when verifying payment.

Error Handling

Handle common errors gracefully to improve user experience.

❌ "User rejected transaction"

User clicked "Reject" in wallet popup

Show: "Payment cancelled. No charges were made."

❌ "Insufficient funds"

User doesn't have enough tokens

Show: "Insufficient USDT balance. You need X more USDT."

❌ "Network mismatch"

User is on wrong blockchain network

Show: "Please switch to Polygon network in your wallet."

Complete Error Handling

try {
    await connectWallet();
    await switchToNetwork('polygon');
    await executePayment();
    alert('✅ Payment successful!');
} catch (error) {
    console.error('Payment error:', error);

    // User-friendly error messages
    if (error.message.includes('User denied')) {
        alert('Payment cancelled. No charges were made.');
    } else if (error.message.includes('Insufficient')) {
        alert(error.message); // Our custom balance error
    } else if (error.code === 4902) {
        alert('Please add this network to your wallet.');
    } else {
        alert('Payment failed: ' + error.message);
    }
}

Complete Working Example: SportsPass

Here's the complete ticket sales platform built with Coinley. This is a real, production-ready implementation.

SportsPass: Crypto Ticket Sales Platform

A complete e-commerce platform for selling sports tickets with cryptocurrency payments. Features include:

  • Shopping cart functionality
  • Multi-network support
  • Real-time balance checking
  • Gas fee warnings
  • Payment status tracking
  • Error handling
Open Live Demo

Key Implementation Highlights

1. Network Selection with Fee Indicators

See loadNetworks() function - Line 246

Automatically sorts networks by cost-effectiveness and shows visual fee indicators (💚 Low fees, ⚠️ Higher fees)

2. Dynamic Gas Fee Warnings

See network change handler - Line 280

Calculates and displays estimated fees as a percentage of transaction value

3. Optimism Direct Transfer Optimization

See executeDirectTransfer() - Line 537

Reduces Optimism fees by 90% using direct transfer instead of splitter contract

4. Real-time Payment Status UI

See updateStep() and payment modal - Line 932

Shows users exactly what's happening: approve tokens → execute payment → verify

API Reference

POST /api/payments/create

Create a new payment request

Request Body

{
  "amount": "10.00",
  "currency": "USDT",
  "network": "polygon",
  "customerEmail": "user@example.com",
  "orderId": "ORDER_123",
  "metadata": {
    "items": [...],
    "platform": "YourApp"
  }
}

Headers Required

X-API-Key: your_api_key
X-API-Secret: your_api_secret
Content-Type: application/json

Response

{
  "success": true,
  "payment": {
    "id": "uuid",
    "amount": "10.00",
    "contractAddress": "0x...",
    "merchantWallet": "0x...",
    "merchantPercentage": 9700
  }
}
POST /api/payments/process

Verify and complete a payment

Request Body

{
  "paymentId": "uuid",
  "transactionHash": "0xabc...",
  "network": "polygon",
  "senderAddress": "0x123...",
  "isDirectTransfer": false
}

Response

{
  "success": true,
  "message": "Payment processed successfully",
  "payment": {
    "status": "completed",
    "transactionHash": "0xabc..."
  }
}
GET /api/networks

Get list of supported blockchain networks

Response

{
  "success": true,
  "networks": [
    {
      "id": "1",
      "name": "Polygon",
      "shortName": "polygon",
      "chainId": "137",
      "isTestnet": false
    }
  ]
}
GET /api/networks/:networkId/tokens

Get supported tokens for a specific network

Response

{
  "success": true,
  "tokens": [
    {
      "symbol": "USDT",
      "name": "Tether USD",
      "contractAddress": "0x...",
      "decimals": 6
    }
  ]
}
GET /api/payments/public/:paymentId

Check payment status publicly (no authentication required)

Public Endpoint: No API credentials needed. Perfect for polling payment status from frontend.

Response

{
  "success": true,
  "payment": {
    "id": "uuid",
    "amount": "10.00",
    "status": "completed",
    "Network": {
      "name": "Polygon",
      "chainId": "137"
    }
  }
}