Offline Transaction Signing with web3.js v4 and EIP‑1559 Fees
Learn how to create, sign, and later broadcast an EIP‑1559 transaction with web3.js v4 entirely offline, using native BigInt values and modular imports.
19 Dec 2025, 04:52 UTC

The problem: signing without a live node
Many teams need to create a signed transaction on an air‑gapped machine or in a CI pipeline, then broadcast it later from a separate environment. web3.js v4’s modular ESM build and first‑class web3.eth.accounts API make this workflow explicit, while the library’s native EIP‑1559 support lets you set maxFeePerGas and maxPriorityFeePerGas instead of the legacy gasPrice.
Thesis
With web3.js v4 you can assemble a complete transaction object, sign it deterministically using a private key, and hand the raw hex to any broadcaster — all without a WebSocket connection or a running Ethereum client on the signing host.
1. Install the v4 package and create a minimal HttpProvider
# Run on the machine that will *only* sign npm install web3@4.9.0
The HttpProvider is enough for reading chain data (nonce, gas estimate) because it uses JSON‑RPC over HTTP. No WebSocket is required for offline signing.
import { Web3 } from 'web3';
import { HttpProvider } from 'web3/providers/http';
const rpcUrl = process.env.RPC_URL || 'https://mainnet.infura.io/v3/YOUR_KEY';
const web3 = new Web3(new HttpProvider(rpcUrl));
2. Build the transaction payload
Fetch the sender’s nonce and a gas estimate, then fill the EIP‑1559 fields. All values are bigint in v4 (native BigInt), so arithmetic is straightforward.
const sender = '0xYourAddress';
const privateKey = process.env.PRIVATE_KEY; // 0x‑prefixed 32‑byte hex
const nonce = await web3.eth.getTransactionCount(sender);
const gasEstimate = await web3.eth.estimateGas({
from: sender,
to: '0xRecipientAddress',
value: 10000000000000000n, // 0.01 ETH in wei
data: '0x'
});
// Choose a max priority fee (tip) and a max fee per gas (base + tip)
const maxPriorityFeePerGas = 2_000_000_000n; // 2 gwei
const maxFeePerGas = 30_000_000_000n; // 30 gwei
const tx = {
from: sender,
to: '0xRecipientAddress',
value: 10000000000000000n,
gas: gasEstimate,
maxPriorityFeePerGas,
maxFeePerGas,
nonce,
chainId: 1, // mainnet
data: '0x'
};
3. Sign offline
The web3.eth.accounts.signTransaction method does not contact the network; it only uses the private key and the transaction object.
import { Account } from 'web3/accounts';
const signed = await Account.signTransaction(tx, privateKey);
// signed.rawTransaction is a 0x‑prefixed hex string ready for broadcast
console.log('Signed raw tx:', signed.rawTransaction);
Check: verify the hash matches an independent signer (e.g., ethereumjs-tx) before you hand the payload to the broadcaster.
4. Broadcast from a connected host
On a machine with network access, send the raw transaction via sendSignedTransaction. This call returns a promise that resolves to the receipt once mined.
import { Web3 } from 'web3';
import { HttpProvider } from 'web3/providers/http';
const web3 = new Web3(new HttpProvider(process.env.RPC_URL));
const receipt = await web3.eth.sendSignedTransaction(signed.rawTransaction);
console.log('Mined in block:', receipt.blockNumber);
Trade‑off: stale nonce and gas estimates
- Offline signing does not validate the nonce against the current chain state. If another transaction from the same account is mined before you broadcast, the signed nonce becomes invalid and the broadcast will revert.
- Gas estimates can change between estimation and inclusion. Using a generous
maxFeePerGasmitigates this, but you still risk over‑paying or having the transaction stuck if the base fee spikes.
Mitigation: run a quick eth_call simulation (or estimateGas again) on the broadcasting host just before sendSignedTransaction, and maintain a nonce manager (e.g., a small Redis counter) if you sign many transactions in parallel.
Actionable closing
Adopt the pattern: fetch → build → sign → verify → broadcast. Keep the signing step on an isolated machine, store only the raw hex, and let a separate service handle the final RPC call. This separation reduces the attack surface of private keys while still giving you full control over EIP‑1559 fee parameters.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.