
Description
Send ETH and contract transactions with MetaMask using eth_sendTransaction via the EIP-1193 provider, gas estimation, receipt polling, and the connectWith shortcut
SKILL.md
Send EVM Transactions with MetaMask Connect
When to use
Use this skill when:
- Sending ETH transfers via
eth_sendTransaction - Calling smart contract functions by encoding
datain the transaction - Estimating gas with
eth_estimateGasbefore sending - Polling for transaction confirmation with
eth_getTransactionReceipt - Using the
connectWithshortcut to connect and send in a single approval
Workflow
Step 1: Get the provider and connected account
import { createEVMClient, getInfuraRpcUrls } from '@metamask/connect-evm';
import type { Hex, Address } from '@metamask/connect-evm';
const client = await createEVMClient({
dapp: { name: 'My DApp', url: window.location.href },
api: {
supportedNetworks: {
...getInfuraRpcUrls({ infuraApiKey: 'YOUR_INFURA_KEY', chainIds: ['0x1', '0x89'] }),
},
},
});
const { accounts } = await client.connect({ chainIds: ['0x1'] });
const provider = client.getProvider();
const from = accounts[0] as Address;
Step 2: Convert ETH to hex wei
All value fields in transactions must be hex-encoded wei. 1 ETH = 10^18 wei:
function ethToHexWei(ethAmount: string): Hex {
const wei = BigInt(Math.round(parseFloat(ethAmount) * 1e18));
return `0x${wei.toString(16)}` as Hex;
}
// Examples
ethToHexWei('0.01'); // '0x2386f26fc10000'
ethToHexWei('0.001'); // '0x38d7ea4c68000'
ethToHexWei('1'); // '0xde0b6b3a7640000'
Step 3: Send an ETH transfer
Build the transaction params and call eth_sendTransaction:
const txParams = {
from: from,
to: '0xRecipientAddress' as Address,
value: ethToHexWei('0.01'),
};
try {
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [txParams],
}) as Hex;
console.log('Transaction hash:', txHash);
} catch (err: any) {
if (err.code === 4001) {
console.log('User rejected the transaction');
return;
}
if (err.code === -32002) {
console.log('A transaction request is already pending');
return;
}
throw err;
}
Step 4: Estimate gas before sending
Use eth_estimateGas to get a gas estimate, then optionally add a buffer:
const txParams = {
from: from,
to: '0xRecipientAddress' as Address,
value: ethToHexWei('0.01'),
data: '0x', // empty for plain ETH transfer
};
const estimatedGas = await provider.request({
method: 'eth_estimateGas',
params: [txParams],
}) as Hex;
// Add 20% buffer to the estimate
const gasWithBuffer = BigInt(estimatedGas) * 120n / 100n;
const gasHex = `0x${gasWithBuffer.toString(16)}` as Hex;
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [{
...txParams,
gas: gasHex,
}],
}) as Hex;
Step 5: Send a contract interaction
Encode the function call as the data field. For an ERC-20 transfer(address,uint256):
// ERC-20 transfer function selector: 0xa9059cbb
// Encode: transfer(0xRecipient, 1000000) for USDC (6 decimals)
const recipient = '0xRecipientAddress'.slice(2).padStart(64, '0');
const amount = (1000000).toString(16).padStart(64, '0'); // 1 USDC
const data = `0xa9059cbb${recipient}${amount}` as Hex;
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [{
from: from,
to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as Address, // USDC contract
data: data,
value: '0x0', // no ETH sent with token transfer
}],
}) as Hex;
Step 6: Poll for transaction receipt
After sending, poll eth_getTransactionReceipt until the transaction is confirmed:
async function waitForReceipt(
provider: any,
txHash: Hex,
intervalMs = 2000,
timeoutMs = 120000,
): Promise<any> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const receipt = await provider.request({
method: 'eth_getTransactionReceipt',
params: [txHash],
});
if (receipt !== null) {
// receipt.status: '0x1' = success, '0x0' = revert
return receipt;
}
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error(`Transaction ${txHash} not confirmed within ${timeoutMs}ms`);
}
// Usage
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [txParams],
}) as Hex;
const receipt = await waitForReceipt(provider, txHash);
if (receipt.status === '0x1') {
console.log('Transaction confirmed in block:', parseInt(receipt.blockNumber, 16));
} else {
console.error('Transaction reverted');
}
Step 7: Use connectWith for single-approval flow
connectWith connects the wallet and sends a transaction in one user interaction:
const { accounts, chainId, result } = await client.connectWith({
method: 'eth_sendTransaction',
// The params function receives the FIRST connected account (a single
// Address), not the accounts array
params: (account: Address) => [
{
from: account,
to: '0xRecipientAddress' as Address,
value: ethToHexWei('0.01'),
},
],
chainIds: ['0x1'],
});
// result is the transaction hash
const txHash = result as Hex;
console.log('Connected as:', accounts[0]);
console.log('Transaction hash:', txHash);
The params field accepts a function that receives the first connected account ((account: Address) => unknown[]), letting you use the connected address as from without knowing it ahead of time.
Step 8: Handle errors
try {
const txHash = await provider.request({
method: 'eth_sendTransaction',
params: [txParams],
});
} catch (err: any) {
switch (err.code) {
case 4001:
// User rejected the transaction — offer retry
break;
case -32002:
// Request already pending — wait for user action in MetaMask
break;
case -32000:
// Execution error (insufficient funds, gas too low, etc.)
console.error('Execution error:', err.message);
break;
default:
console.error('Transaction failed:', err);
}
}
Important Notes
valuemust be hex-encoded wei —'0xde0b6b3a7640000'is 1 ETH. Never pass decimal strings or ETH-denominated numbers directly.frommust match the connected account — MetaMask rejects transactions wherefromdoesn't match the active account.eth_sendTransactionreturns a transaction hash, not a receipt — polleth_getTransactionReceiptto confirm the transaction was mined.- Receipt
statusis hex —'0x1'means success,'0x0'means the transaction was mined but reverted. eth_estimateGascan throw — if the transaction would revert, estimation fails. Wrap it in a try/catch and show the error to the user.connectWithparams can be a function —params: (account) => [{ from: account, ... }]— it receives the first connected account (a singleAddress, not an array).- Chain IDs are always hex strings in SDK calls —
'0x1','0x89','0xaa36a7'. ThechainIdin transaction objects follows the same convention when present. - Error code 4001 means the user deliberately rejected — handle gracefully.
- Error code -32002 means a request is pending — do not send another transaction.
0x1is auto-included in everyconnect()/connectWith()call.
More skills from the metamask-connect-cursor-plugin repository
View all 18 skillsmigrate-from-sdk
migrate MetaMask SDK to connect-evm and connect-solana
Jul 13EthereumMigrationSDKWeb3migrate-wagmi-metamask-connector
migrate wagmi apps to MetaMask connector
Jul 13EthereumMigrationwagmiWeb3send-solana-transaction
send Solana transactions via MetaMask
Jul 13PaymentsSolanaWeb3setup-evm-browser-app
scaffold EVM browser applications with MetaMask
Jul 13EthereumFrontendJavaScriptTypeScript +1setup-evm-react-app
scaffold React apps with MetaMask integration
Jul 13API DevelopmentEthereumFrontendReact +1setup-evm-react-native-app
scaffold React Native apps with MetaMask
Jul 13EthereumMobileReact NativeWeb3
More from MetaMask
View publishersmart-accounts-kit
build dApps with MetaMask Smart Accounts
smart-accounts-kit
Jul 13API DevelopmentAuthEthereumPermissions +1x402-payments
build x402 payment flows
smart-accounts-kit
Jul 13API DevelopmentPaymentsWeb3x402discovery
discover and use services
ocap-kernel
Jul 13API DevelopmentSearchmetamask
interact with MetaMask wallet capabilities
ocap-kernel
Jul 13API DevelopmentEthereumWeb3wallet
manage wallet balances and transactions
ocap-kernel
Jul 13EthereumPaymentsWeb3metamask-connect
integrate MetaMask into dApps
connect-monorepo
Jul 13API DevelopmentEthereumSolanawagmi +1