Token sale landing page development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Token sale landing page development
Medium
~3-5 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1215
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1043
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Token Sale Landing Page Development

Token sale landing—not just marketing page. It's a web3 app interacting with token sale smart contract, handling crypto payments, managing whitelist, and must work reliably at high load (when thousands of users arrive simultaneously at round opening).

Smart Contract

Landing is frontend to contract. Design contract first.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";

contract TokenSale is Ownable2Step, ReentrancyGuard, Pausable {
    using SafeERC20 for IERC20;
    
    IERC20 public immutable saleToken;
    IERC20 public immutable paymentToken;  // USDC
    
    uint256 public immutable tokenPrice;    // USDC per token, 6 decimals
    uint256 public immutable hardCap;       // total tokens for sale
    uint256 public immutable minPurchase;   // per wallet min
    uint256 public immutable maxPurchase;   // per wallet max
    
    uint256 public saleStart;
    uint256 public saleEnd;
    
    bytes32 public whitelistMerkleRoot;
    bool public whitelistRequired;
    
    uint256 public totalSold;
    mapping(address => uint256) public purchased;
    
    event TokensPurchased(address indexed buyer, uint256 usdcAmount, uint256 tokenAmount);
    event SaleConfigured(uint256 start, uint256 end, bool whitelistRequired);
    
    constructor(
        address _saleToken,
        address _paymentToken,
        uint256 _tokenPrice,
        uint256 _hardCap,
        uint256 _minPurchase,
        uint256 _maxPurchase,
        address _owner
    ) Ownable2Step() {
        saleToken = IERC20(_saleToken);
        paymentToken = IERC20(_paymentToken);
        tokenPrice = _tokenPrice;
        hardCap = _hardCap;
        minPurchase = _minPurchase;
        maxPurchase = _maxPurchase;
        _transferOwnership(_owner);
    }
    
    function configureSale(
        uint256 _start,
        uint256 _end,
        bytes32 _merkleRoot,
        bool _whitelistRequired
    ) external onlyOwner {
        saleStart = _start;
        saleEnd = _end;
        whitelistMerkleRoot = _merkleRoot;
        whitelistRequired = _whitelistRequired;
        emit SaleConfigured(_start, _end, _whitelistRequired);
    }
    
    function buy(
        uint256 usdcAmount,
        bytes32[] calldata merkleProof
    ) external nonReentrant whenNotPaused {
        require(block.timestamp >= saleStart, "Sale not started");
        require(block.timestamp <= saleEnd, "Sale ended");
        
        if (whitelistRequired) {
            bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
            require(
                MerkleProof.verify(merkleProof, whitelistMerkleRoot, leaf),
                "Not whitelisted"
            );
        }
        
        uint256 tokenAmount = usdcAmount * 10**18 / tokenPrice;
        
        require(usdcAmount >= minPurchase, "Below min purchase");
        require(purchased[msg.sender] + tokenAmount <= maxPurchase, "Exceeds max per wallet");
        require(totalSold + tokenAmount <= hardCap, "Hard cap reached");
        
        purchased[msg.sender] += tokenAmount;
        totalSold += tokenAmount;
        
        paymentToken.safeTransferFrom(msg.sender, address(this), usdcAmount);
        saleToken.safeTransfer(msg.sender, tokenAmount);
        
        emit TokensPurchased(msg.sender, usdcAmount, tokenAmount);
    }
    
    function withdrawFunds(address to) external onlyOwner {
        uint256 balance = paymentToken.balanceOf(address(this));
        paymentToken.safeTransfer(to, balance);
    }
    
    function withdrawUnsoldTokens(address to) external onlyOwner {
        require(block.timestamp > saleEnd, "Sale not ended");
        uint256 balance = saleToken.balanceOf(address(this));
        saleToken.safeTransfer(to, balance);
    }
}

Merkle Tree whitelist: instead of storing each address on-chain (expensive), store only merkle root. Proof generated off-chain and passed on purchase. For 10,000 whitelist addresses—~$1000+ gas savings on deployment.

Frontend: Web3 Integration

Wallet Connect and Sale Status

import { useReadContracts, useWriteContract, useWaitForTransactionReceipt } from "wagmi";
import { formatUnits, parseUnits } from "viem";

const SALE_ABI = [...] as const;

function useSaleData(saleAddress: `0x${string}`) {
  const result = useReadContracts({
    contracts: [
      { address: saleAddress, abi: SALE_ABI, functionName: "saleStart" },
      { address: saleAddress, abi: SALE_ABI, functionName: "saleEnd" },
      { address: saleAddress, abi: SALE_ABI, functionName: "totalSold" },
      { address: saleAddress, abi: SALE_ABI, functionName: "hardCap" },
      { address: saleAddress, abi: SALE_ABI, functionName: "tokenPrice" },
      { address: saleAddress, abi: SALE_ABI, functionName: "whitelistRequired" },
    ],
    query: { refetchInterval: 10_000 }, // update every 10 sec
  });
  
  const [start, end, totalSold, hardCap, tokenPrice, whitelistRequired] = 
    result.data ?? [];
  
  const now = Date.now() / 1000;
  const saleStatus = !start?.result ? "loading" :
    now < Number(start.result) ? "upcoming" :
    now > Number(end?.result) ? "ended" :
    "active";
  
  const progress = totalSold?.result && hardCap?.result
    ? Number(totalSold.result * 100n / hardCap.result)
    : 0;
  
  return { saleStatus, progress, tokenPrice: tokenPrice?.result, whitelistRequired: whitelistRequired?.result };
}

Purchase with USDC Approve

USDC requires approve before buy. Pattern: first check allowance, if insufficient—first step approve, second step—buy:

function BuyFlow({ saleAddress, usdcAddress, amount }) {
  const { address } = useAccount();
  const [step, setStep] = useState<"approve" | "buy" | "done">("approve");
  
  const { data: allowance } = useReadContract({
    address: usdcAddress,
    abi: ERC20_ABI,
    functionName: "allowance",
    args: [address, saleAddress],
    query: { enabled: !!address },
  });
  
  const needsApprove = !allowance || allowance < parseUnits(amount, 6);
  
  const { writeContract: approve, data: approveTx } = useWriteContract();
  const { writeContract: buy, data: buyTx } = useWriteContract();
  
  const { isSuccess: approveSuccess } = useWaitForTransactionReceipt({ hash: approveTx });
  
  useEffect(() => {
    if (approveSuccess) setStep("buy");
  }, [approveSuccess]);
  
  // Merkle proof for whitelist (if needed)
  const merkleProof = useMerkleProof(address);
  
  const handleApprove = () => {
    approve({
      address: usdcAddress,
      abi: ERC20_ABI,
      functionName: "approve",
      args: [saleAddress, parseUnits(amount, 6)],
    });
  };
  
  const handleBuy = () => {
    buy({
      address: saleAddress,
      abi: SALE_ABI,
      functionName: "buy",
      args: [parseUnits(amount, 6), merkleProof ?? []],
    });
  };
  
  if (needsApprove && step === "approve") {
    return <Button onClick={handleApprove}>Allow USDC (step 1/2)</Button>;
  }
  
  return <Button onClick={handleBuy}>Buy tokens (step 2/2)</Button>;
}

Merkle Tree Whitelist: Generation and Management

import { MerkleTree } from "merkletreejs";
import { keccak256, encodePacked } from "viem";

function generateMerkleTree(addresses: string[]) {
  const leaves = addresses.map((addr) =>
    keccak256(encodePacked(["address"], [addr as `0x${string}`]))
  );
  
  const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
  const root = tree.getHexRoot();
  
  return { tree, root };
}

function getMerkleProof(tree: MerkleTree, address: string): `0x${string}`[] {
  const leaf = keccak256(encodePacked(["address"], [address as `0x${string}`]));
  return tree.getHexProof(leaf) as `0x${string}`[];
}

// API endpoint: GET /api/whitelist/proof?address=0x...
async function getProofForAddress(req, res) {
  const { address } = req.query;
  const whitelist = await loadWhitelistFromDB(); // your logic
  const { tree } = generateMerkleTree(whitelist);
  const proof = getMerkleProof(tree, address);
  
  if (proof.length === 0) {
    return res.status(403).json({ error: "Not whitelisted" });
  }
  
  res.json({ proof, address });
}

Real-time Updates and Queue

On round opening—load spike. Frontend shouldn't RPC node query every second for thousands of users.

Solution: WebSocket or SSE from backend → clients. Backend subscribes to contract events, on TokensPurchased pushes updated data to all connected clients.

// Backend: pusher or own WebSocket
import { WebSocket } from "ws";

const wss = new WebSocket.Server({ port: 3001 });
const clients = new Set<WebSocket>();

// Listen to contract events
publicClient.watchContractEvent({
  address: SALE_ADDRESS,
  abi: SALE_ABI,
  eventName: "TokensPurchased",
  onLogs: async (logs) => {
    const totalSold = await publicClient.readContract({
      address: SALE_ADDRESS,
      abi: SALE_ABI,
      functionName: "totalSold",
    });
    
    const update = JSON.stringify({ type: "sale_update", totalSold: totalSold.toString() });
    clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) client.send(update);
    });
  },
});

Important UX Details

Countdown to start: reverse countdown to saleStart. Don't use server time—synchronize with block.timestamp via contract.

Progress bar: totalSold / hardCap * 100%. Updates real-time via WebSocket.

Amount calculation: user enters USDC—show how many tokens they get, vice versa. Real price from contract, not hardcoded.

Gasless approve via Permit: if token supports EIP-2612, can combine approve + buy into one transaction via permit + buy pattern—improves UX.

Mobile responsive: most crypto users buy from phone. Large buttons, MetaMask Mobile deep link.

Stack

Component Technology
Frontend Next.js 14 + TypeScript
Web3 wagmi v2 + viem + RainbowKit
Whitelist Merkle Tree + API endpoint
Real-time WebSocket or Pusher
Analytics custom events + Dune Dashboard
Hosting Vercel + Cloudflare

Development timeline: 2–3 weeks for full-stack landing with smart contract, whitelist, real-time updates. Design and marketing content—separately.