EVM Troubleshooting
Common errors you'll see when sending EVM transactions, and what to do about each.
This page is keyed by the error message you see. Search for the phrase or scan the headings.
Replacement transaction underpriced
What happened. A previous transaction at the same nonce is pending in the mempool with a higher gas price. Your new tx can't replace it without ≥10% higher gas.
What to do. Either wait for the pending tx to clear (usually seconds-to-minutes), or retry with fee: 'fast' to bump the gas.
const r = await evm.sendTransaction({ to, value, fee: 'fast' });The nonce tracker automatically releases your reservation on this error, so your next send picks a fresh nonce. No manual cleanup needed.
Nonce too low
What happened. The nonce you submitted has already been used on-chain. This usually means:
- The server-side tracker drifted from the actual chain state.
- You passed a manual
nonce:override that's stale.
What to do. Just retry. The tracker auto-resyncs on the next prepareEvm call (reads eth_getTransactionCount('pending') from Chainstack). No code change needed on your side.
Insufficient funds for this transaction
What happened. The session address doesn't have enough native token to cover value + (gas × maxFeePerGas).
What to do.
- Check the balance:
await rpc.getBalance({ address: session.address }). - Check the gas cost:
await evm.estimateFeesPerGas()+evm.estimateGas(). - Top up the wallet, or lower
value, or switch tofee: 'slow'.
On testnets, grab faucet ETH from Sepolia faucet or Base Sepolia faucet.
Missing revert data
What happened. An eth_call reverted without a readable reason — almost always because a precondition in the contract failed silently.
Common causes:
- ERC-20 transfer with 0 balance — you're trying to move tokens you don't have. The
transferreverts becausebalanceOf(sender) < amount. - Uninitialized contract — the address has no code.
- Wrong function signature — your ABI doesn't match what's on-chain.
What to do. Read the state first.
// Before transferring:
const { raw } = await evm.getTokenBalance({ tokenAddress });
if (raw < amount) throw new Error('Insufficient token balance');
// Verify the contract exists:
const code = await evm.getCode({ address });
if (code === '0x') throw new Error('Not a contract');Rate limit exceeded
What happened. Your app hit the per-(app, address) rate limit: 60 prepares/min, 30 broadcasts/min on EVM writes.
What to do.
- For normal UI flows, you should never hit this. Investigate why you're calling so often.
- If you genuinely need higher limits (automated system, batch job), contact us via the Developer Dashboard.
- The error includes a
retryAfterhint (seconds). Exponential-backoff on the retry.
Popup blocked by the browser
What happened. Your browser blocked the popup because sendTransaction was called outside a user gesture.
What to do. Always initiate writes from a direct click handler — not in a useEffect, not in a setTimeout, not after an async gap.
// ✅ Good: initiated from a click
<button onClick={() => evm.sendTransaction({ to, value })}>Send</button>
// ❌ Bad: initiated from an async completion
useEffect(() => {
fetchSomething().then(() => evm.sendTransaction({ to, value }));
}, []);If you genuinely need to trigger a write outside a click context (uncommon), use method: 'iframe' — iframes aren't subject to popup blockers.
User closed popup
What happened. The user dismissed the popup without approving.
What to do. Not an exception — it's a normal error result. Just check r.status:
const r = await evm.sendTransaction({ to, value });
if (r.status === 'error') {
// User cancelled, or another error. Handle gracefully.
console.log('Cancelled or failed:', r.message);
}Don't wrap this in try/catch — nothing is thrown.
Invalid nonce. Please try again.
What happened. The bridge received a response with a nonce that doesn't match the one it issued. Usually means the user opened multiple popups, or a race between popup and iframe modes.
What to do. Retry once. If it persists, check that you're not calling sendTransaction from multiple places simultaneously.
Chain mismatch (JWT network claim vs request)
What happened. You called an EVM method while the session is on a UTXO chain (or vice versa). The JWT's network claim doesn't match what the method needs.
What to do. Switch the session first.
import { switchNetwork, Network, getSession, getNetworkType, NetworkType } from '@oviato/connect';
const session = getSession();
if (session && getNetworkType(session.network) !== NetworkType.EVM) {
await switchNetwork(Network.BASEMAINNET); // or whatever EVM chain
}
await evm.sendTransaction({ to, value });Not connected: Please login first.
What happened. The SDK checked for a session before opening a write popup, and there isn't one.
What to do. Require the user to log in first.
import { useOviConnect, ConnectButton } from '@oviato/connect/client';
function SendButton() {
const { session } = useOviConnect();
if (!session) return <ConnectButton text="Connect" />;
return <button onClick={handleSend}>Send</button>;
}SDK not initialized
What happened. You called an SDK method before initialize() or before the <OviConnectProvider> mounted.
What to do. Make sure initialize() runs before any other SDK call. In React, <OviConnectProvider> handles this — just make sure you wrapped your app.
Still stuck?
- Check the Developer Dashboard activity logs — every prepare/broadcast event with error context.
- Report an issue at github.com/oviato-hq.
- Ping the team in the dashboard's support panel.