Introduction
Imagine being able to create your own cryptocurrency with just one click. No complex Solidity knowledge, no deployment headaches—just a simple interface that handles everything. That's the power of modern web3 tooling.
In this hands-on guide, you'll build a complete token creation platform from the ground up. We'll harness React for a sleek frontend, wagmi for seamless blockchain interactions, and Hardhat for bulletproof smart contract development. By the time you're done, you'll have a live application that deploys custom tokens with a few clicks.
What you'll learn:
- Setting up a professional Hardhat environment and compiling smart contracts
- Building an intuitive React interface with wagmi for wallet connectivity
- Deploying ERC-20 tokens programmatically straight from your browser
The Architecture: Understanding the System
Before diving into code, let's visualize how the pieces fit together:
User Interface (React) → Wallet Connection (wagmi) → Smart Contract Deployment → ERC-20 Token
The magic happens entirely on-chain. Unlike traditional factory patterns that rely on intermediary contracts, we compile the bytecode locally and deploy it directly from the frontend. This approach delivers:
- No factory contract needed - Deploy directly to reduce complexity
- Lower gas costs - Eliminate middleman contract calls
- Complete control - Full ownership of your contract code
- Zero dependencies - No reliance on third-party services
Part 1: Setting Up the Smart Contract
Step 1: Initialize Hardhat Project
Let's start by creating a new Hardhat project. Open your terminal and execute:
# Create a new directory
mkdir token-creator
cd token-creator
# Initialize Hardhat
npx hardhat --init
When prompted, select these options:
- Which version of Hardhat would you like to use? - hardhat-2
- Where would you like to initialize the project?: . (current)
- Please provide either a relative or an absolute path: . (current)
- What type of project would you like to initialize? · A Javascript project using Mocha and Ethers.js
- Do you want to run it now? (Y/n) · true
Why Hardhat? It's the industry standard for Ethereum development, offering exceptional TypeScript support, comprehensive testing frameworks, and robust deployment tools.
Step 2: Install OpenZeppelin Contracts
OpenZeppelin provides audited, production-grade smart contract implementations trusted by thousands of projects:
npm install @openzeppelin/contracts
Step 3: Create the Custom Token Contract
Create a new file contracts/CustomToken.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract CustomToken is ERC20, Ownable {
uint8 private immutable _decimals;
constructor(
string memory name_,
string memory symbol_,
uint256 initialSupply_,
uint8 decimals_
) ERC20(name_, symbol_) Ownable(msg.sender) {
_decimals = decimals_;
_mint(msg.sender, initialSupply_ * 10 ** decimals_);
}
function decimals() public view override returns (uint8) {
return _decimals;
}
}
Step 4: Compile the Contract
Run the Hardhat compiler:
npx hardhat compile
This generates an artifacts directory containing your compiled contract. Navigate to:
artifacts/contracts/CustomToken.sol/CustomToken.json
This JSON file contains two critical components for frontend deployment:
- ABI (Application Binary Interface): Describes how to interact with the contract
- Bytecode: The compiled contract code to deploy on-chain
Pro tip: The bytecode is what actually gets deployed to the blockchain. The ABI is like an instruction manual for interacting with that deployed code.
Part 2: Building the React Frontend
Step 1: Create React Project
In a new directory (or subdirectory), create a React app:
npm create vite@latest token-creator-frontend -- --template react
cd token-creator-frontend
npm install
Step 2: Install Dependencies
Install the essential Web3 libraries:
npm install wagmi viem @tanstack/react-query
Dependency breakdown:
- wagmi: React hooks for Ethereum interactions
- viem: TypeScript-first Ethereum library with excellent performance
- @tanstack/react-query: Powerful data fetching and caching for seamless UX
Add Tailwind CSS (Optional but Recommended)
Since our components use Tailwind CSS for styling, it’s recommended to set it up in your React project.
Follow the official setup guide here: 👉 Tailwind CSS + Vite Setup Guide
Step 3: Configure wagmi with Chainlist RPCs
Create src/config.js:
import { createConfig, http, injected } from 'wagmi';
import { bscTestnet } from 'wagmi/chains';
import { metaMask } from 'wagmi/connectors';
export const config = createConfig({
chains: [bscTestnet],
connectors: [injected(), metaMask()],
transports: {
[bscTestnet.id]: http(),
},
});
Why BSC Testnet? Lower gas fees make it perfect for development and testing. You can easily switch to Ethereum mainnet, Polygon, or any other EVM-compatible chain by importing from wagmi/chains.
No API key required! The http() transport automatically uses public RPC endpoints. Explore more networks at chainlist.org.
Step 4: Setup wagmi Provider
Update src/main.jsx:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './index.css';
import { WagmiProvider } from 'wagmi';
import { config } from './config.js';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient();
createRoot(document.getElementById('root')).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<WagmiProvider config={config}>
<App />
</WagmiProvider>
</QueryClientProvider>
</StrictMode>
);
Part 3: Building the Wallet Connection
Step 1: Create Connect Component
Create src/components/Connect.jsx:
import { useEffect, useState } from 'react';
import { useChainId, useConnect } from 'wagmi';
export function Connect() {
const chainId = useChainId();
const { connectors, connect } = useConnect();
return (
<div className='buttons'>
{connectors.map((connector) => (
<ConnectorButton
key={connector.uid}
connector={connector}
onClick={() => connect({ connector, chainId })}
/>
))}
</div>
);
}
function ConnectorButton({ connector, onClick }) {
const [ready, setReady] = useState(false);
useEffect(() => {
(async () => {
const provider = await connector.getProvider();
setReady(!!provider);
})();
}, [connector, setReady]);
return (
<button
className='button'
disabled={!ready}
onClick={onClick}
type='button'
>
{connector.name}
</button>
);
}
Step 2: Create ConnectWallet Wrapper
Create src/components/ConnectWallet.jsx:
import { useAccount } from 'wagmi';
import { Connect } from './Connect';
import { TokenDeploy } from './TokenDeploy';
export function ConnectWallet() {
const { isConnected } = useAccount();
return (
<div className='container'>
{isConnected ? <TokenDeploy /> : <Connect />}
</div>
);
}
This conditional rendering ensures users must authenticate their wallet before accessing sensitive token deployment features.
Step 3: The Token Deployment Component
This is where the magic happens. Create src/components/TokenDeploy.jsx:
import React, { useState, useEffect, useMemo } from 'react';
import {
useAccount,
useDeployContract,
useDisconnect,
useEnsAvatar,
useEnsName,
useReadContract,
useWaitForTransactionReceipt,
} from 'wagmi';
export function TokenDeploy() {
const { address, connector } = useAccount();
const { disconnect } = useDisconnect();
const { data: ensName } = useEnsName({ address });
const { data: ensAvatar } = useEnsAvatar({ name: ensName });
const {
deployContract,
data: deployHash,
isPending: isDeploying,
error: deployError,
reset,
} = useDeployContract();
const {
isLoading: isConfirming,
isSuccess: isConfirmed,
data: receipt,
} = useWaitForTransactionReceipt({ hash: deployHash });
const [tokenForm, setTokenForm] = useState({
abi: '',
bytecode: '',
name: '',
symbol: '',
supply: '',
decimals: '18',
});
const [deployedTokens, setDeployedTokens] = useState([]);
const formattedAddress = useMemo(
() => (address ? `${address.slice(0, 6)}…${address.slice(-4)}` : ''),
[address]
);
const handleChange = (key) => (e) =>
setTokenForm((prev) => ({ ...prev, [key]: e.target.value }));
const handleDeploy = async () => {
try {
const abi = JSON.parse(tokenForm.abi);
const bytecode = tokenForm.bytecode.trim();
const args = [
tokenForm.name,
tokenForm.symbol,
BigInt(tokenForm.supply),
parseInt(tokenForm.decimals),
];
await deployContract({ abi, bytecode, args });
} catch (err) {
console.error('Deployment error:', err);
alert('Error deploying contract: ' + err.message);
}
};
useEffect(() => {
if (isConfirmed && receipt?.contractAddress) {
const newToken = {
name: tokenForm.name,
symbol: tokenForm.symbol,
supply: tokenForm.supply,
decimals: tokenForm.decimals,
address: receipt.contractAddress,
timestamp: new Date().toISOString(),
};
setDeployedTokens((prev) => [...prev, newToken]);
setTokenForm((prev) => ({
...prev,
name: '',
symbol: '',
supply: '',
decimals: '18',
}));
reset();
alert(`Token deployed successfully at: ${receipt.contractAddress}`);
}
}, [isConfirmed, receipt]);
const isFormIncomplete =
!tokenForm.abi ||
!tokenForm.bytecode ||
!tokenForm.name ||
!tokenForm.symbol ||
!tokenForm.supply;
return (
<div className='max-w-4xl mx-auto p-6 space-y-8'>
<div className='flex items-center justify-between bg-gray-100 p-4 rounded-md'>
<div className='flex items-center space-x-4'>
{ensAvatar ? (
<img
src={ensAvatar}
alt='ENS Avatar'
className='w-10 h-10 rounded-full'
/>
) : (
<div className='w-10 h-10 bg-gray-300 rounded-full' />
)}
<div>
{address && (
<div className='font-medium'>
{ensName
? `${ensName} (${formattedAddress})`
: formattedAddress}
</div>
)}
<div className='text-sm text-gray-500'>
Connected to {connector?.name}
</div>
</div>
</div>
<button
onClick={disconnect}
className='px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600'
>
Disconnect
</button>
</div>
<div className='bg-white shadow-md rounded-md p-6'>
<h2 className='text-lg font-semibold mb-4'>Deploy Custom Token</h2>
<div className='mb-4'>
<label className='block text-sm font-medium mb-1'>
Contract ABI (from artifacts)
</label>
<textarea
rows={6}
className='w-full border rounded p-2 font-mono text-sm'
value={tokenForm.abi}
onChange={handleChange('abi')}
placeholder='Paste your CustomToken ABI here'
/>
</div>
<div className='mb-4'>
<label className='block text-sm font-medium mb-1'>
Bytecode (0x...)
</label>
<textarea
rows={4}
className='w-full border rounded p-2 font-mono text-sm'
value={tokenForm.bytecode}
onChange={handleChange('bytecode')}
placeholder='Paste your compiled CustomToken bytecode'
/>
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-4 mb-4'>
<div>
<label className='block text-sm font-medium mb-1'>Token Name</label>
<input
type='text'
className='w-full border rounded p-2'
value={tokenForm.name}
onChange={handleChange('name')}
placeholder='My Custom Token'
/>
</div>
<div>
<label className='block text-sm font-medium mb-1'>
Token Symbol
</label>
<input
type='text'
className='w-full border rounded p-2'
value={tokenForm.symbol}
onChange={handleChange('symbol')}
placeholder='MCT'
/>
</div>
<div>
<label className='block text-sm font-medium mb-1'>
Initial Supply
</label>
<input
type='number'
className='w-full border rounded p-2'
value={tokenForm.supply}
onChange={handleChange('supply')}
placeholder='1000000'
/>
</div>
<div>
<label className='block text-sm font-medium mb-1'>Decimals</label>
<input
type='number'
className='w-full border rounded p-2'
value={tokenForm.decimals}
onChange={handleChange('decimals')}
placeholder='18'
min='0'
max='18'
/>
</div>
</div>
<button
onClick={handleDeploy}
disabled={isDeploying || isConfirming || isFormIncomplete}
className={`w-full py-2 px-4 rounded text-white font-semibold ${
isDeploying || isConfirming || isFormIncomplete
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
}`}
>
{isDeploying
? 'Deploying...'
: isConfirming
? 'Confirming...'
: 'Deploy Token Contract'}
</button>
{deployError && (
<div className='mt-4 p-3 bg-red-50 border border-red-200 rounded text-red-700'>
<strong>Error:</strong> {deployError.message}
</div>
)}
</div>
<DeployedTokensList
tokens={deployedTokens}
abi={tokenForm.abi}
userAddress={address}
/>
</div>
);
}
Key implementation details:
- useDeployContract hook: Manages the deployment transaction lifecycle
- useWaitForTransactionReceipt: Tracks blockchain confirmation in real-time
Step 4: Add Live On-Chain Token Data Component
Now, let’s display real-time token data (like name, symbol, and total supply) directly from the blockchain.
Add the following component at the end of your TokenDeploy.jsx file:
function DeployedTokensList({ tokens, abi, userAddress }) {
if (tokens.length === 0) {
return (
<div className='bg-white shadow-md rounded-md p-6'>
<h2 className='text-lg font-semibold mb-4'>Deployed Tokens</h2>
<div className='text-gray-500 text-center py-8'>
No tokens deployed yet. Deploy your first token above.
</div>
</div>
);
}
return (
<div className='bg-white shadow-md rounded-md p-6'>
<div className='flex justify-between items-center mb-6'>
<h2 className='text-lg font-semibold'>Deployed Tokens</h2>
<span className='text-sm text-gray-500'>
{tokens.length} token(s) deployed
</span>
</div>
<div className='space-y-6'>
{tokens.map((token, index) => (
<TokenCard
key={index}
token={token}
abi={abi}
userAddress={userAddress}
/>
))}
</div>
</div>
);
}
function TokenCard({ token, abi, userAddress }) {
const parsedAbi = abi ? JSON.parse(abi) : [];
const { data: name } = useReadContract({
address: token.address,
abi: parsedAbi,
functionName: 'name',
});
const { data: symbol } = useReadContract({
address: token.address,
abi: parsedAbi,
functionName: 'symbol',
});
const { data: totalSupply } = useReadContract({
address: token.address,
abi: parsedAbi,
functionName: 'totalSupply',
});
const { data: decimals } = useReadContract({
address: token.address,
abi: parsedAbi,
functionName: 'decimals',
});
const { data: balance } = useReadContract({
address: token.address,
abi: parsedAbi,
functionName: 'balanceOf',
args: [userAddress],
});
return (
<div className='border rounded-lg p-4'>
<h3 className='font-semibold text-lg mb-4'>
{token.name} ({token.symbol})
</h3>
<div className='grid grid-cols-1 md:grid-cols-3 gap-4 text-sm mb-4'>
<div>
<span className='font-medium'>Initial Supply:</span>
<div>{token.supply}</div>
</div>
<div>
<span className='font-medium'>Decimals:</span>
<div>{token.decimals}</div>
</div>
<div>
<span className='font-medium'>Deployed:</span>
<div>{new Date(token.timestamp).toLocaleString()}</div>
</div>
</div>
<div className='border rounded-lg p-4 bg-gray-50'>
<div className='grid grid-cols-1 md:grid-cols-2 gap-3 text-sm'>
<div>
<span className='font-medium'>On-chain Name:</span>
<div>{name || 'Loading...'}</div>
</div>
<div>
<span className='font-medium'>On-chain Symbol:</span>
<div>{symbol || 'Loading...'}</div>
</div>
<div>
<span className='font-medium'>Total Supply:</span>
<div>{totalSupply ? totalSupply.toString() : 'Loading...'}</div>
</div>
<div>
<span className='font-medium'>Decimals:</span>
<div>
{decimals !== undefined ? decimals.toString() : 'Loading...'}
</div>
</div>
<div>
<span className='font-medium'>Your Balance:</span>
<div>{balance ? balance.toString() : 'Loading...'}</div>
</div>
<div>
<span className='font-medium'>Contract Address:</span>
<div className='font-mono text-xs break-all'>{token.address}</div>
</div>
</div>
</div>
</div>
);
}
Why fetch on-chain data? This verification step confirms successful deployment and displays accurate, real-time information directly from the blockchain—not just what was submitted in the form.
Step 5: Final App Setup
Update src/App.jsx:
import { ConnectWallet } from './components/ConnectWallet';
function App() {
return <ConnectWallet />;
}
export default App;
Part 4: Deploy and Enjoy
Now that everything is ready, it’s time to bring your token to life!
- Connect your wallet (💡 MetaMask is recommended)
- Paste your ABI and Bytecode from the Hardhat compilation. You can find them in the generated JSON file:
artifacts/contracts/CustomToken.sol/CustomToken.json - Enter the token details — Name, Symbol, Total Supply, and Decimals
- Click “Deploy” and confirm the transaction in your wallet
- Voilà! Your custom ERC-20 token is now live on the BSC Testnet — fully viewable and interactable on-chain
Best Practices & Production Tips
1. Always Test on Testnets First
Never deploy directly to mainnet without thorough testing. Start with BSC Testnet, Sepolia, or Goerli. Get free testnet funds from:
- BSC Testnet: testnet.bnbchain.org/faucet-smart
- Sepolia: sepoliafaucet.com
2. Store ABI and Bytecode Securely
For production apps, never ask users to paste ABI/bytecode. Instead:
- Import them directly from compiled artifacts
- Use a secure backend service to provide them
Advanced Enhancements
Enhancement 1: Multi-Chain Support
Expand your app to support multiple blockchains:
import { mainnet, bscTestnet, polygon, arbitrum } from 'wagmi/chains';
export const config = createConfig({
chains: [mainnet, bscTestnet, polygon, arbitrum],
// ... rest of config
});
Enhancement 2: Token Template Selection
Offer preset templates for different token types:
- Standard Token: Basic ERC-20 functionality
- Burnable Token: Users can destroy their tokens
- Mintable Token: Owner can create additional tokens
Real-World Use Cases
Use Case 1: Community Tokens
- Launch tokens for online communities, gaming guilds, or DAOs. Perfect for incentivizing participation and governance.
Use Case 2: Event Tickets & Memberships
- Deploy tokens representing event access or exclusive membership tiers with built-in transferability.
Use Case 3: Educational Purposes
- Ideal for teaching blockchain development, smart contract deployment, and Web3 fundamentals in workshops and bootcamps.
Use Case 4: Fundraising Tokens
- Create tokens for crowdfunding campaigns or community fundraisers with transparent on-chain tracking.
Conclusion
Congratulations! You’ve built a working token-creation tool that demonstrates how ERC-20 contracts are written, tested, and deployed. With this foundation, you can deploy custom ERC-20 tokens in minutes without API keys, factory contracts, or complex setups.
Key Takeaways:
- Hardhat streamlines smart contract development with powerful tooling
- wagmi provides React hooks that make blockchain interactions intuitive
- Direct bytecode deployment gives complete control without intermediary contracts
- Public RPC endpoints eliminate API key requirements and vendor lock-in
- Real-time on-chain data fetching ensures accuracy and builds user trust
The barrier to blockchain development has never been lower. With these tools and techniques, you're equipped to build sophisticated decentralized applications that can scale to production.
Try It Yourself: Live Token Creator (CryptoFusion 🚀)
If you want to skip the setup and see everything in action — try CryptoFusion my live token creation platform.
It’s built using the exact tech stack covered in this guide — React, Hardhat, viem, and wagmi — and lets you create fully customizable tokens, including:
- ✅ Standard Tokens — basic ERC-20s
- 🔥 Burnable Tokens — users can destroy tokens
- ⚡ Mintable Tokens — owners can mint new tokens anytime
Just connect your wallet, choose your options, and deploy directly to the BSC Testnet — all in one click.
🧠 Built by me, Azim Memon — CryptoFusion is a decentralized playground for developers and creators to experiment with tokenomics and smart contract customization.
Check it out here 👉 cryptofusion.vercel.app/generate-token
🔮 Next Steps
Ready to take your skills further? Consider these extensions:
- Add token transfer and allowance management
- Implement multi-signature deployment for team projects
- Build a token dashboard with analytics and charts
- Create automated token vesting schedules
- Integrate with DeFi protocols for instant liquidity
Further Resources
- Wagmi Docs - Complete reference and examples
- Hardhat Tutorial - Official getting started guide
- OpenZeppelin Contracts - Secure contract implementations
- Chainlist RPC Directory - Find RPCs for any EVM chain
- ERC-20 Standard - Official token specification
Ready to deploy your token? Grab the complete code from this guide and start building. Have questions? Get in touch—I'd love to hear from you!

