Documentation Index
Fetch the complete documentation index at: https://voltaire.tevm.sh/llms.txt
Use this file to discover all available pages before exploring further.
Overview
PendingTransactionFilter represents a filter created by eth_newPendingTransactionFilter that notifies of new pending transaction hashes. Used for monitoring mempool activity, MEV detection, and transaction tracking.
Type Definition
type PendingTransactionFilterType = {
readonly filterId: FilterIdType;
readonly type: "pendingTransaction";
} & {
readonly [brand]: "PendingTransactionFilter";
};
Creating PendingTransactionFilter
from
import * as PendingTransactionFilter from './primitives/PendingTransactionFilter/index.js';
import * as FilterId from './primitives/FilterId/index.js';
// Create filter on node
const filterIdStr = await rpc.eth_newPendingTransactionFilter();
const filterId = FilterId.from(filterIdStr);
// Wrap in PendingTransactionFilter type
const filter = PendingTransactionFilter.from(filterId);
Parameters:
filterId: FilterIdType - Filter identifier from eth_newPendingTransactionFilter
Returns: PendingTransactionFilterType
JSON-RPC Usage
Create Filter
// Returns filter ID
const filterIdStr = await rpc.eth_newPendingTransactionFilter();
const filterId = FilterId.from(filterIdStr);
const filter = PendingTransactionFilter.from(filterId);
Poll for Changes
// Returns array of new pending transaction hashes since last poll
const txHashes = await rpc.eth_getFilterChanges(filter.filterId);
for (const hash of txHashes) {
console.log(`New pending tx: ${hash}`);
}
Uninstall Filter
const success = await rpc.eth_uninstallFilter(filter.filterId);
Example: Mempool Monitor
import * as PendingTransactionFilter from './primitives/PendingTransactionFilter/index.js';
import * as FilterId from './primitives/FilterId/index.js';
import { Hash } from './primitives/Hash/index.js';
// Install pending transaction filter
const filterIdStr = await rpc.eth_newPendingTransactionFilter();
const filterId = FilterId.from(filterIdStr);
const filter = PendingTransactionFilter.from(filterId);
console.log(`Pending tx filter installed: ${filterId.toString()}`);
// Poll every 5 seconds
const interval = setInterval(async () => {
try {
const hashes = await rpc.eth_getFilterChanges(filter.filterId);
if (hashes.length > 0) {
console.log(`New pending txs: ${hashes.length}`);
// Fetch transaction details
for (const hashStr of hashes) {
const hash = Hash.from(hashStr);
const tx = await rpc.eth_getTransactionByHash(hash);
console.log(`From: ${tx.from}, To: ${tx.to}, Value: ${tx.value}`);
}
}
} catch (error) {
console.error('Filter error:', error);
clearInterval(interval);
}
}, 5000);
// Cleanup on exit
process.on('SIGINT', async () => {
await rpc.eth_uninstallFilter(filter.filterId);
clearInterval(interval);
process.exit();
});
Example: High-Value Transaction Alert
import * as PendingTransactionFilter from './primitives/PendingTransactionFilter/index.js';
import * as FilterId from './primitives/FilterId/index.js';
import { Denomination } from './primitives/Denomination/index.js';
const filterId = FilterId.from(await rpc.eth_newPendingTransactionFilter());
const filter = PendingTransactionFilter.from(filterId);
// Alert threshold: 100 ETH
const threshold = Denomination.parseEther("100");
setInterval(async () => {
const hashes = await rpc.eth_getFilterChanges(filter.filterId);
for (const hash of hashes) {
const tx = await rpc.eth_getTransactionByHash(hash);
if (tx.value >= threshold) {
console.log(`HIGH VALUE TX: ${tx.value} wei`);
console.log(`From: ${tx.from} -> To: ${tx.to}`);
console.log(`Hash: ${hash}`);
// Send alert (email, webhook, etc.)
await sendAlert(tx);
}
}
}, 5000);
Example: DEX Frontrun Detector
import * as PendingTransactionFilter from './primitives/PendingTransactionFilter/index.js';
import * as FilterId from './primitives/FilterId/index.js';
const filterId = FilterId.from(await rpc.eth_newPendingTransactionFilter());
const filter = PendingTransactionFilter.from(filterId);
// Track pending swaps
const pendingSwaps = new Map(); // methodId -> txs
// Uniswap swapExactTokensForTokens selector
const swapSelector = "0x38ed1739";
setInterval(async () => {
const hashes = await rpc.eth_getFilterChanges(filter.filterId);
for (const hash of hashes) {
const tx = await rpc.eth_getTransactionByHash(hash);
// Check if swap transaction
if (tx.input.startsWith(swapSelector)) {
const gasPrice = tx.maxFeePerGas || tx.gasPrice;
// Check for frontrunning (same method, higher gas)
const existing = pendingSwaps.get(swapSelector) || [];
const frontruns = existing.filter(ptx => ptx.gasPrice < gasPrice);
if (frontruns.length > 0) {
console.log(`POTENTIAL FRONTRUN DETECTED`);
console.log(`Original: ${frontruns[0].hash}`);
console.log(`Frontrun: ${hash}`);
}
pendingSwaps.set(swapSelector, [...existing, { hash, gasPrice }]);
}
}
// Clean up old txs (keep last 100)
for (const [key, txs] of pendingSwaps) {
if (txs.length > 100) {
pendingSwaps.set(key, txs.slice(-100));
}
}
}, 5000);
Comparison with eth_subscribe
PendingTransactionFilter (eth_newPendingTransactionFilter)
Pros:
- HTTP compatible (no WebSocket required)
- Simple request-response pattern
- Works with all RPC providers
Cons:
- Polling-based (less efficient)
- Delayed notifications (poll interval)
- Filter expiration if not polled
- High volume (mainnet ~150 tx/s)
// HTTP polling
const filterId = FilterId.from(await rpc.eth_newPendingTransactionFilter());
setInterval(async () => {
const hashes = await rpc.eth_getFilterChanges(filterId);
// Process hashes...
}, 5000);
eth_subscribe
Pros:
- Real-time push notifications
- More efficient (no polling)
- No filter expiration
Cons:
- Requires WebSocket connection
- Not supported by all providers
- Still high volume
// WebSocket subscription
const subscription = await ws.eth_subscribe('newPendingTransactions');
subscription.on('data', (txHash) => {
console.log(`New pending tx: ${txHash}`);
});
High Volume
Mainnet mempool produces ~150 transactions/second during busy periods:
// Calculate expected load
const txPerSecond = 150;
const pollInterval = 5; // seconds
const expectedTxsPerPoll = txPerSecond * pollInterval; // ~750 txs
console.log(`Expect ~${expectedTxsPerPoll} txs per poll`);
Filtering Strategies
Don’t fetch all transaction details - filter by criteria:
// INEFFICIENT: Fetch all txs
const hashes = await rpc.eth_getFilterChanges(filterId);
for (const hash of hashes) {
const tx = await rpc.eth_getTransactionByHash(hash); // 750 requests!
}
// EFFICIENT: Filter by hash pattern first
const hashes = await rpc.eth_getFilterChanges(filterId);
const relevantHashes = hashes.filter(h => {
// Filter by some criteria before fetching
return h.startsWith('0xa');
});
// Then fetch only relevant txs
for (const hash of relevantHashes) {
const tx = await rpc.eth_getTransactionByHash(hash);
}
Batch Requests
Use JSON-RPC batching to fetch multiple transactions:
const hashes = await rpc.eth_getFilterChanges(filterId);
// Batch request for all txs
const batch = hashes.map(hash => ({
method: 'eth_getTransactionByHash',
params: [hash],
id: hash
}));
const txs = await rpc.batch(batch);
Node Resource Limits
Some nodes limit or disable pending transaction filters:
- Infura: Disabled (use WebSocket subscriptions)
- Alchemy: Limited rate
- Local node: Configurable
Check provider documentation before using.
Filter Expiration
Pending transaction filters expire after inactivity (typically 5 minutes):
async function pollPendingTxFilter(filterId: FilterIdType) {
try {
const hashes = await rpc.eth_getFilterChanges(filterId);
return hashes;
} catch (error) {
if (error.message.includes('filter not found')) {
// Recreate filter
const newFilterId = FilterId.from(
await rpc.eth_newPendingTransactionFilter()
);
return pollPendingTxFilter(newFilterId);
}
throw error;
}
}
Use Cases
MEV Bot Detection
const filterId = FilterId.from(await rpc.eth_newPendingTransactionFilter());
// Track MEV patterns
const mevSignatures = [
"0x38ed1739", // Uniswap swap
"0x7ff36ab5", // swapExactETHForTokens
"0x18cbafe5" // swapExactTokensForETH
];
setInterval(async () => {
const hashes = await rpc.eth_getFilterChanges(filterId);
for (const hash of hashes) {
const tx = await rpc.eth_getTransactionByHash(hash);
const isMEV = mevSignatures.some(sig =>
tx.input.startsWith(sig)
);
if (isMEV && tx.gasPrice > threshold) {
console.log(`Potential MEV: ${hash}`);
analyzeMEV(tx);
}
}
}, 5000);
Transaction Broadcaster
async function broadcastAndMonitor(signedTx: string) {
// Start monitoring before broadcast
const filterId = FilterId.from(
await rpc.eth_newPendingTransactionFilter()
);
// Broadcast transaction
const txHash = await rpc.eth_sendRawTransaction(signedTx);
// Monitor for inclusion
const interval = setInterval(async () => {
const hashes = await rpc.eth_getFilterChanges(filterId);
if (hashes.includes(txHash)) {
console.log(`Transaction ${txHash} seen in mempool`);
}
// Check if mined
const receipt = await rpc.eth_getTransactionReceipt(txHash);
if (receipt) {
console.log(`Transaction mined in block ${receipt.blockNumber}`);
clearInterval(interval);
await rpc.eth_uninstallFilter(filterId);
}
}, 5000);
}
Gas Price Oracle
const filterId = FilterId.from(await rpc.eth_newPendingTransactionFilter());
// Track gas prices
const gasPrices = [];
setInterval(async () => {
const hashes = await rpc.eth_getFilterChanges(filterId);
// Sample first 50 txs
const sample = hashes.slice(0, 50);
for (const hash of sample) {
const tx = await rpc.eth_getTransactionByHash(hash);
const gasPrice = tx.maxFeePerGas || tx.gasPrice;
gasPrices.push(gasPrice);
}
// Keep last 1000 samples
if (gasPrices.length > 1000) {
gasPrices.splice(0, gasPrices.length - 1000);
}
// Calculate percentiles
const sorted = [...gasPrices].sort((a, b) => Number(a - b));
const p50 = sorted[Math.floor(sorted.length * 0.5)];
const p75 = sorted[Math.floor(sorted.length * 0.75)];
const p90 = sorted[Math.floor(sorted.length * 0.9)];
console.log(`Gas prices - P50: ${p50}, P75: ${p75}, P90: ${p90}`);
}, 5000);
Security Considerations
Privacy
Pending transactions are publicly visible before mining:
- Transaction details (from, to, value, data)
- Gas prices (reveals urgency)
- Nonce (reveals transaction ordering)
MEV Risks
Monitoring the mempool exposes transactions to:
- Frontrunning: Higher gas price to execute first
- Backrunning: Execute after target transaction
- Sandwich attacks: Frontrun + backrun
Mitigation:
- Use private mempools (Flashbots, Eden, etc.)
- Encrypt transaction data
- Use commit-reveal schemes
JSON-RPC Methods
eth_newPendingTransactionFilter - Create pending tx filter
eth_getFilterChanges - Poll for new pending txs
eth_uninstallFilter - Remove filter
eth_getTransactionByHash - Fetch transaction details
eth_sendRawTransaction - Broadcast transaction
See Also