Skip to main content

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}`);
});

Performance Considerations

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