# MetaMask for Developers — Connect Your dApp to Web3
MetaMask is the gateway to Web3 for millions of users. As the most popular Ethereum wallet browser extension, it allows users to interact with decentralized applications (dApps) directly from their browser. For developers, integrating MetaMask is essential to enable users to connect wallets, sign transactions, and interact with smart contracts. This guide covers everything you need to know to build a seamless MetaMask integration.
What is MetaMask?
MetaMask is a cryptocurrency wallet and gateway to blockchain applications. It functions as:
- Browser extension (Chrome, Firefox, Edge, Brave)
- Mobile app (iOS and Android)
- Web3 provider injecting
window.ethereuminto web pages
- Account manager for Ethereum addresses and private keys
- Network switcher between Ethereum mainnet, testnets, and other EVM chains
With over 30 million monthly active users, MetaMask is the default wallet for most Ethereum dApps.
How MetaMask Works
When installed, MetaMask injects a JavaScript object called window.ethereum (or window.ethereum via EIP-6963) into every webpage. Your dApp uses this object to:
All sensitive operations (signing transactions, messages) happen inside MetaMask's secure UI — your dApp never has access to private keys.
Detecting MetaMask
Basic Detection
if (typeof window.ethereum !== 'undefined') {
console.log('MetaMask is installed!');
} else {
console.log('Please install MetaMask');
// Redirect to https://metamask.io/download
}
EIP-6963: Multi-Wallet Detection
Modern dApps should support multiple wallets. EIP-6963 provides a standard way to detect all installed wallets:
// Listen for wallet providers
const wallets = [];
window.addEventListener('eip6963:announceProvider', (event) => {
wallets.push(event.detail);
});
// Request wallet announcements
window.dispatchEvent(new Event('eip6963:requestProvider'));
// wallets now contains all installed providers (MetaMask, Coinbase Wallet, etc.)
Checking if MetaMask is the Active Provider
const isMetaMask = window.ethereum?.isMetaMask;
if (isMetaMask) {
console.log('MetaMask detected');
}
Connecting to MetaMask
Request Account Access
async function connectWallet() {
try {
// Request account access
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
const account = accounts[0];
console.log('Connected account:', account);
return account;
} catch (error) {
if (error.code === 4001) {
// User rejected the request
console.log('Please connect to MetaMask');
} else {
console.error('Error connecting:', error);
}
}
}
Get Connected Accounts (No Popup)
If the user has already connected, use eth_accounts (doesn't show popup):
async function getConnectedAccounts() {
const accounts = await window.ethereum.request({
method: 'eth_accounts'
});
return accounts; // Empty array if not connected
}
Complete Connection Flow
async function init() {
// Check if already connected
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (accounts.length > 0) {
// Already connected
setAccount(accounts[0]);
} else {
// Show "Connect Wallet" button
document.getElementById('connectButton').addEventListener('click', async () => {
const newAccounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
setAccount(newAccounts[0]);
});
}
}
function setAccount(account) {
document.getElementById('account').textContent = account;
document.getElementById('connectButton').style.display = 'none';
document.getElementById('dapp').style.display = 'block';
}
Sending Transactions
Send ETH
async function sendEther(to, amount) {
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
const from = accounts[0];
// amount in wei (1 ETH = 10^18 wei)
const value = '0x' + (amount * 10**18).toString(16);
try {
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: from,
to: to,
value: value,
// gas: '0x5208', // Optional: 21000 wei for simple transfer
}],
});
console.log('Transaction hash:', txHash);
return txHash;
} catch (error) {
console.error('Transaction failed:', error);
}
}
// Usage
await sendEther('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', 0.1);
Interact with Smart Contracts (Raw)
async function callContractFunction() {
const contractAddress = '0x...';
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
// Function signature: transfer(address,uint256)
const functionSignature = '0xa9059cbb'; // First 4 bytes of keccak256("transfer(address,uint256)")
// Encode parameters (address + amount)
const recipient = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb'.slice(2).padStart(64, '0');
const amount = (100 * 10**18).toString(16).padStart(64, '0');
const data = functionSignature + recipient + amount;
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: accounts[0],
to: contractAddress,
data: data,
}],
});
return txHash;
}
Note: Manual encoding is error-prone. Use ethers.js or web3.js (see below).
Switching Networks
Request Network Switch
async function switchToSepolia() {
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0xaa36a7' }], // Sepolia testnet
});
} catch (error) {
if (error.code === 4902) {
// Network not added to MetaMask
await addSepoliaNetwork();
} else {
console.error('Failed to switch network:', error);
}
}
}
Add Custom Network
async function addPolygonNetwork() {
try {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: '0x89', // 137 in decimal
chainName: 'Polygon Mainnet',
nativeCurrency: {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18
},
rpcUrls: ['https://polygon-rpc.com'],
blockExplorerUrls: ['https://polygonscan.com']
}],
});
} catch (error) {
console.error('Failed to add network:', error);
}
}
Get Current Network
async function getCurrentNetwork() {
const chainId = await window.ethereum.request({ method: 'eth_chainId' });
console.log('Current chain ID:', chainId); // e.g., "0x1" for Ethereum mainnet
const networkMap = {
'0x1': 'Ethereum Mainnet',
'0xaa36a7': 'Sepolia Testnet',
'0x89': 'Polygon Mainnet',
'0xa4b1': 'Arbitrum One',
};
return networkMap[chainId] || 'Unknown Network';
}
Listening for Changes
Account Changes
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
// User disconnected all accounts
console.log('Please connect to MetaMask');
} else {
// User switched account
console.log('Account changed to:', accounts[0]);
setAccount(accounts[0]);
}
});
Network Changes
window.ethereum.on('chainChanged', (chainId) => {
console.log('Network changed to:', chainId);
// Recommended: reload the page to avoid inconsistent state
window.location.reload();
});
Connection/Disconnection
window.ethereum.on('connect', (connectInfo) => {
console.log('Connected to network:', connectInfo.chainId);
});
window.ethereum.on('disconnect', (error) => {
console.log('Disconnected from network:', error);
});
Integrating with Ethers.js
Raw MetaMask API is verbose. Ethers.js provides a cleaner abstraction.
Installation
npm install ethers
Connect Wallet with Ethers.js
import { BrowserProvider } from 'ethers';
async function connectWallet() {
if (typeof window.ethereum === 'undefined') {
alert('Please install MetaMask');
return;
}
// Request account access
await window.ethereum.request({ method: 'eth_requestAccounts' });
// Create ethers provider
const provider = new BrowserProvider(window.ethereum);
// Get signer (connected account)
const signer = await provider.getSigner();
const address = await signer.getAddress();
console.log('Connected:', address);
return { provider, signer, address };
}
Send Transaction with Ethers.js
async function sendEther(to, amount) {
const { signer } = await connectWallet();
const tx = await signer.sendTransaction({
to: to,
value: ethers.parseEther(amount.toString()) // Convert ETH to wei
});
console.log('Transaction sent:', tx.hash);
// Wait for confirmation
const receipt = await tx.wait();
console.log('Transaction confirmed:', receipt);
return receipt;
}
// Usage
await sendEther('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', 0.1);
Interact with Smart Contracts
import { Contract } from 'ethers';
async function interactWithContract() {
const { signer } = await connectWallet();
const contractAddress = '0x...';
const abi = [
"function transfer(address to, uint256 amount) returns (bool)",
"function balanceOf(address owner) view returns (uint256)"
];
const contract = new Contract(contractAddress, abi, signer);
// Read function (no gas cost)
const balance = await contract.balanceOf(await signer.getAddress());
console.log('Balance:', ethers.formatEther(balance));
// Write function (costs gas)
const tx = await contract.transfer('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', ethers.parseEther('10'));
await tx.wait();
console.log('Transfer complete');
}
Read Blockchain Data
async function getBlockchainData() {
const provider = new BrowserProvider(window.ethereum);
// Get block number
const blockNumber = await provider.getBlockNumber();
console.log('Current block:', blockNumber);
// Get gas price
const feeData = await provider.getFeeData();
console.log('Gas price:', ethers.formatUnits(feeData.gasPrice, 'gwei'), 'gwei');
// Get balance
const balance = await provider.getBalance('0x...');
console.log('Balance:', ethers.formatEther(balance), 'ETH');
}
Signing Messages
Personal Sign (Human-Readable Messages)
async function signMessage(message) {
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, accounts[0]],
});
console.log('Signature:', signature);
return signature;
}
// Usage
await signMessage('I agree to the terms of service');
With Ethers.js
async function signMessage(message) {
const { signer } = await connectWallet();
const signature = await signer.signMessage(message);
return signature;
}
EIP-712: Typed Structured Data Signing
For structured data (used in permit functions, gasless transactions):
async function signTypedData() {
const { signer } = await connectWallet();
const domain = {
name: 'MyDApp',
version: '1',
chainId: 1,
verifyingContract: '0x...'
};
const types = {
Transfer: [
{ name: 'to', type: 'address' },
{ name: 'amount', type: 'uint256' }
]
};
const value = {
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
amount: ethers.parseEther('10')
};
const signature = await signer.signTypedData(domain, types, value);
return signature;
}
Error Handling
Common Error Codes
async function handleMetaMaskError() {
try {
await window.ethereum.request({ method: 'eth_requestAccounts' });
} catch (error) {
switch (error.code) {
case 4001:
// User rejected the request
console.log('Please connect to MetaMask');
break;
case -32002:
// Request already pending
console.log('Please open MetaMask to complete the request');
break;
case -32603:
// Internal error
console.error('Internal error:', error.message);
break;
default:
console.error('Unknown error:', error);
}
}
}
Best Practices
1. Always Check for MetaMask
if (typeof window.ethereum === 'undefined') {
showInstallPrompt(); // Guide user to install MetaMask
}
2. Handle Account Changes
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
logout(); // User disconnected
} else {
updateAccount(accounts[0]);
}
});
3. Reload on Network Change
window.ethereum.on('chainChanged', () => {
window.location.reload(); // Prevents inconsistent state
});
4. Request Permissions Once
Don't call eth_requestAccounts on every page load. Use eth_accounts to check if already connected.
5. Provide Clear Error Messages
if (error.code === 4001) {
alert('You need to connect your wallet to use this feature');
}
6. Test on Multiple Networks
Test your dApp on mainnet, testnets (Sepolia), and L2s (Polygon, Arbitrum).
7. Support Mobile
MetaMask Mobile uses WalletConnect for deep linking. Test on mobile browsers.
Complete React Example
import { useState, useEffect } from 'react';
import { BrowserProvider } from 'ethers';
function App() {
const [account, setAccount] = useState(null);
const [provider, setProvider] = useState(null);
useEffect(() => {
if (window.ethereum) {
// Check if already connected
window.ethereum.request({ method: 'eth_accounts' })
.then(accounts => {
if (accounts.length > 0) {
setAccount(accounts[0]);
setProvider(new BrowserProvider(window.ethereum));
}
});
// Listen for account changes
window.ethereum.on('accountsChanged', (accounts) => {
setAccount(accounts[0] || null);
});
// Listen for network changes
window.ethereum.on('chainChanged', () => {
window.location.reload();
});
}
}, []);
const connectWallet = async () => {
if (!window.ethereum) {
alert('Please install MetaMask');
return;
}
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
setAccount(accounts[0]);
setProvider(new BrowserProvider(window.ethereum));
};
return (
<div>
{account ? (
<div>
<p>Connected: {account}</p>
{/* Your dApp UI */}
</div>
) : (
<button onClick={connectWallet}>Connect Wallet</button>
)}
</div>
);
}
Conclusion
MetaMask integration is essential for any Ethereum dApp. By following this guide, you can detect MetaMask, connect wallets, send transactions, and interact with smart contracts seamlessly. Combined with ethers.js, you have a powerful toolkit to build Web3 applications that millions of users can access.
Start building your dApp today — integrate MetaMask and bring your smart contracts to life. Practice wallet integration with Solingo's hands-on Web3 projects to master full-stack blockchain development.
Next steps:
- Build a simple dApp with wallet connection
- Implement transaction history tracking
- Add support for multiple wallets (Coinbase Wallet, WalletConnect)
- Learn about gasless transactions with EIP-2612 permits