diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index f4e3a688..2e3a9e60 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -323,3 +323,19 @@ textarea {
.lesson-viewer .token.url { color: var(--prism-operator); }
.lesson-viewer .token.punctuation { color: var(--prism-punctuation); }
+
+/* Thin themed scrollbar shared by simulator-style panels */
+.custom-scrollbar::-webkit-scrollbar {
+ width: 4px;
+ height: 4px;
+}
+.custom-scrollbar::-webkit-scrollbar-track {
+ background: transparent;
+}
+.custom-scrollbar::-webkit-scrollbar-thumb {
+ background: #333;
+ border-radius: 10px;
+}
+.custom-scrollbar::-webkit-scrollbar-thumb:hover {
+ background: #ef4444;
+}
diff --git a/frontend/src/app/mempool-auction/page.tsx b/frontend/src/app/mempool-auction/page.tsx
new file mode 100644
index 00000000..edc8621b
--- /dev/null
+++ b/frontend/src/app/mempool-auction/page.tsx
@@ -0,0 +1,169 @@
+'use client';
+
+import { BlockHistory } from '@/components/mempool-auction/BlockHistory';
+import { MempoolGrid } from '@/components/mempool-auction/MempoolGrid';
+import { useMempoolSimulator } from '@/hooks/useMempoolSimulator';
+import { MAX_FEE_BID, MIN_FEE_BID, selectForBlock, totalGas } from '@/lib/mempool';
+import { useMemo } from 'react';
+
+const GAS_LIMIT_MIN = 500_000;
+const GAS_LIMIT_MAX = 5_000_000;
+
+export default function MempoolAuctionPage() {
+ const {
+ pool,
+ blocks,
+ settings,
+ autoFlow,
+ addTransaction,
+ removeTransaction,
+ setFeeBid,
+ updateSettings,
+ mineBlock,
+ reset,
+ setAutoFlow,
+ } = useMempoolSimulator();
+
+ // Preview which transactions the next block would settle, so the grid can
+ // highlight winning bids live as parameters change.
+ const nextBlock = useMemo(
+ () => selectForBlock(pool, settings.gasLimit, settings.baseFee),
+ [pool, settings.gasLimit, settings.baseFee],
+ );
+ const nextBlockIds = useMemo(() => new Set(nextBlock.map((tx) => tx.id)), [nextBlock]);
+ const projectedFill = Math.round((totalGas(nextBlock) / settings.gasLimit) * 100);
+
+ return (
+
+ {/* Background grid accent */}
+
+
+
+ {/* Header */}
+
+
+
+ Gas Fee Auction
+
+
+ Mempool Simulator — Highest Bidder Settles First
+
+
+
+
+
+
+
+
+
+
+ {/* Network parameters */}
+
+
+
+ Base Fee
+ {settings.baseFee} gwei
+
+
updateSettings({ baseFee: Number(e.target.value) })}
+ className="w-full accent-red-600"
+ aria-label="Network base fee in gwei"
+ />
+
Bids below the base fee are excluded.
+
+
+
+
+ Block Gas Limit
+
+ {settings.gasLimit.toLocaleString()}
+
+
+
updateSettings({ gasLimit: Number(e.target.value) })}
+ className="w-full accent-red-600"
+ aria-label="Block gas limit"
+ />
+
Caps how many bids fit per block.
+
+
+
+
+ Next Block
+
+ {nextBlock.length} tx · {projectedFill}%
+
+
+
+
+ Highest bidders that clear the base fee and fit the gas limit.
+
+
+
+
+ {/* Auction view */}
+
+
+
+ );
+}
diff --git a/frontend/src/components/mempool-auction/BlockHistory.tsx b/frontend/src/components/mempool-auction/BlockHistory.tsx
new file mode 100644
index 00000000..25bd05cc
--- /dev/null
+++ b/frontend/src/components/mempool-auction/BlockHistory.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import { MinedBlock } from '@/lib/mempool';
+
+interface BlockHistoryProps {
+ blocks: MinedBlock[];
+}
+
+export function BlockHistory({ blocks }: BlockHistoryProps) {
+ return (
+
+
+ Mined Blocks
+ History [{blocks.length}]
+
+
+
+ {blocks.length === 0 ? (
+
+ No blocks yet — mine one to settle the top bids.
+
+ ) : (
+ blocks.map((block) => {
+ const fill = Math.round((block.gasUsed / block.gasLimit) * 100);
+ return (
+
+
+ #{block.height}
+
+ {new Date(block.minedAt).toLocaleTimeString()}
+
+
+
+
+
+
Txs
+
{block.transactions.length}
+
+
+
Base
+
{block.baseFee} gwei
+
+
+
Reward
+
+ {(block.totalFees / 1e9).toFixed(4)} Ξ
+
+
+
+
+
+
+ {block.gasUsed.toLocaleString()} / {block.gasLimit.toLocaleString()} gas ({fill}%)
+
+
+ );
+ })
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/mempool-auction/MempoolGrid.tsx b/frontend/src/components/mempool-auction/MempoolGrid.tsx
new file mode 100644
index 00000000..6a8b428b
--- /dev/null
+++ b/frontend/src/components/mempool-auction/MempoolGrid.tsx
@@ -0,0 +1,138 @@
+'use client';
+
+import {
+ MAX_FEE_BID,
+ MIN_FEE_BID,
+ PendingTx,
+ sortByFee,
+} from '@/lib/mempool';
+import { useMemo } from 'react';
+
+interface MempoolGridProps {
+ pool: PendingTx[];
+ baseFee: number;
+ /** Ids the next block would include — used to highlight winning bids. */
+ nextBlockIds: Set;
+ onFeeBid: (id: string, feeBid: number) => void;
+ onRemove: (id: string) => void;
+}
+
+const typeColor: Record = {
+ TRANSFER: 'text-sky-400',
+ SWAP: 'text-violet-400',
+ MINT: 'text-amber-400',
+ CONTRACT: 'text-emerald-400',
+};
+
+export function MempoolGrid({
+ pool,
+ baseFee,
+ nextBlockIds,
+ onFeeBid,
+ onRemove,
+}: MempoolGridProps) {
+ const sorted = useMemo(() => sortByFee(pool), [pool]);
+
+ return (
+
+
+ Mempool
+
+ Pending [{sorted.length}]
+
+
+
+
+ {sorted.length === 0 ? (
+
+ Pool is empty — broadcast a transaction to start the auction.
+
+ ) : (
+
+ {sorted.map((tx, index) => {
+ const winning = nextBlockIds.has(tx.id);
+ const underpriced = tx.feeBid < baseFee;
+ return (
+ -
+
+
+
+ #{index + 1}
+
+
+ {tx.id}
+
+
+ {tx.type}
+
+
+
+ {winning && (
+
+ NEXT BLOCK
+
+ )}
+ {underpriced && !winning && (
+
+ UNDERPRICED
+
+ )}
+
+
+
+
+
+
+
+ Fee Bid
+
+ {tx.feeBid} gwei
+
+
+
onFeeBid(tx.id, Number(e.target.value))}
+ className="w-full accent-red-600"
+ aria-label={`Fee bid for transaction ${tx.id}, in gwei`}
+ />
+
+
+
Gas
+
+ {tx.gasUnits.toLocaleString()}
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/hooks/useMempoolSimulator.ts b/frontend/src/hooks/useMempoolSimulator.ts
new file mode 100644
index 00000000..7e215ecc
--- /dev/null
+++ b/frontend/src/hooks/useMempoolSimulator.ts
@@ -0,0 +1,148 @@
+'use client';
+
+import { getItem, setItem } from '@/lib/localStorage';
+import {
+ MAX_FEE_BID,
+ MIN_FEE_BID,
+ MinedBlock,
+ PendingTx,
+ randomTx,
+ selectForBlock,
+ totalGas,
+ txPriorityFee,
+} from '@/lib/mempool';
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+const STORAGE_KEY = 'mempool_auction_state';
+
+export interface MempoolSettings {
+ /** Gas ceiling per block. */
+ gasLimit: number;
+ /** Network base fee (gwei); bids below it are ineligible. */
+ baseFee: number;
+}
+
+interface PersistedState {
+ pool: PendingTx[];
+ blocks: MinedBlock[];
+ settings: MempoolSettings;
+ nextHeight: number;
+}
+
+export const DEFAULT_SETTINGS: MempoolSettings = {
+ gasLimit: 1_500_000,
+ baseFee: 12,
+};
+
+function defaultState(): PersistedState {
+ return { pool: [], blocks: [], settings: DEFAULT_SETTINGS, nextHeight: 1_000_001 };
+}
+
+function clampFee(value: number): number {
+ return Math.min(MAX_FEE_BID, Math.max(MIN_FEE_BID, Math.round(value)));
+}
+
+export function useMempoolSimulator() {
+ const [pool, setPool] = useState([]);
+ const [blocks, setBlocks] = useState([]);
+ const [settings, setSettings] = useState(DEFAULT_SETTINGS);
+ const [autoFlow, setAutoFlow] = useState(false);
+ const [hydrated, setHydrated] = useState(false);
+ const nextHeight = useRef(defaultState().nextHeight);
+
+ // Load persisted state once on mount (client only).
+ useEffect(() => {
+ const stored = getItem(STORAGE_KEY, defaultState());
+ setPool(stored.pool);
+ setBlocks(stored.blocks);
+ setSettings({ ...DEFAULT_SETTINGS, ...stored.settings });
+ nextHeight.current = stored.nextHeight ?? defaultState().nextHeight;
+ setHydrated(true);
+ }, []);
+
+ // Persist whenever the simulation changes (after hydration to avoid clobbering).
+ useEffect(() => {
+ if (!hydrated) return;
+ setItem(STORAGE_KEY, {
+ pool,
+ blocks,
+ settings,
+ nextHeight: nextHeight.current,
+ });
+ }, [hydrated, pool, blocks, settings]);
+
+ const addTransaction = useCallback((tx?: PendingTx) => {
+ setPool((prev) => [tx ?? randomTx(Date.now()), ...prev].slice(0, 60));
+ }, []);
+
+ const removeTransaction = useCallback((id: string) => {
+ setPool((prev) => prev.filter((tx) => tx.id !== id));
+ }, []);
+
+ /** Update a single transaction's fee bid — the lever that re-sorts the pool. */
+ const setFeeBid = useCallback((id: string, feeBid: number) => {
+ setPool((prev) =>
+ prev.map((tx) => (tx.id === id ? { ...tx, feeBid: clampFee(feeBid) } : tx)),
+ );
+ }, []);
+
+ const updateSettings = useCallback((patch: Partial) => {
+ setSettings((prev) => ({ ...prev, ...patch }));
+ }, []);
+
+ /** Build the next block: pack the highest bidders that fit, then drop them. */
+ const mineBlock = useCallback(() => {
+ setPool((prevPool) => {
+ const included = selectForBlock(prevPool, settings.gasLimit, settings.baseFee);
+ if (included.length === 0) return prevPool;
+
+ const includedIds = new Set(included.map((tx) => tx.id));
+ const block: MinedBlock = {
+ height: nextHeight.current,
+ transactions: included,
+ gasUsed: totalGas(included),
+ gasLimit: settings.gasLimit,
+ baseFee: settings.baseFee,
+ totalFees: included.reduce((sum, tx) => sum + txPriorityFee(tx, settings.baseFee), 0),
+ minedAt: Date.now(),
+ };
+ nextHeight.current += 1;
+ setBlocks((prev) => [block, ...prev].slice(0, 12));
+
+ return prevPool.filter((tx) => !includedIds.has(tx.id));
+ });
+ }, [settings.gasLimit, settings.baseFee]);
+
+ const reset = useCallback(() => {
+ const fresh = defaultState();
+ setPool(fresh.pool);
+ setBlocks(fresh.blocks);
+ setSettings(fresh.settings);
+ nextHeight.current = fresh.nextHeight;
+ setAutoFlow(false);
+ }, []);
+
+ // Auto-flow: simulate organic mempool influx.
+ useEffect(() => {
+ if (!autoFlow) return;
+ const interval = setInterval(() => {
+ if (Math.random() > 0.4) addTransaction();
+ }, 1200);
+ return () => clearInterval(interval);
+ }, [autoFlow, addTransaction]);
+
+ return {
+ pool,
+ blocks,
+ settings,
+ autoFlow,
+ hydrated,
+ addTransaction,
+ removeTransaction,
+ setFeeBid,
+ updateSettings,
+ mineBlock,
+ reset,
+ setAutoFlow,
+ };
+}
diff --git a/frontend/src/lib/mempool.ts b/frontend/src/lib/mempool.ts
new file mode 100644
index 00000000..f82ae9f9
--- /dev/null
+++ b/frontend/src/lib/mempool.ts
@@ -0,0 +1,119 @@
+/**
+ * Mempool & Gas Fee Auction — domain logic
+ *
+ * Educational model of an Ethereum-style fee market. Pending transactions sit in
+ * a mempool and bid a priority fee (gwei per unit of gas). When a block is built,
+ * the highest bidders are packed greedily until the block's gas limit is reached,
+ * which is exactly the "auction" learners are meant to observe.
+ *
+ * Everything here is a pure function so it can be unit-tested and reused by both
+ * the simulator hook and the visual components.
+ */
+
+export type TxType = 'TRANSFER' | 'SWAP' | 'MINT' | 'CONTRACT';
+
+export interface PendingTx {
+ id: string;
+ /** Shortened pseudo address of the sender. */
+ from: string;
+ /** Operation kind — drives the gas estimate. */
+ type: TxType;
+ /** Gas units the transaction consumes when executed. */
+ gasUnits: number;
+ /** Priority fee bid in gwei per gas unit. The lever users tune. */
+ feeBid: number;
+ /** Epoch millis when the tx entered the pool — tiebreaker for equal bids. */
+ addedAt: number;
+}
+
+export interface MinedBlock {
+ /** Block height. */
+ height: number;
+ /** Transactions included, in execution order (highest bid first). */
+ transactions: PendingTx[];
+ /** Gas actually consumed by the included transactions. */
+ gasUsed: number;
+ /** Block gas ceiling at mining time. */
+ gasLimit: number;
+ /** Base fee (gwei) that bids had to clear to be eligible. */
+ baseFee: number;
+ /** Total priority fees paid to the validator, in gwei. */
+ totalFees: number;
+ minedAt: number;
+}
+
+/** Typical gas footprint per operation kind. */
+export const GAS_BY_TYPE: Record = {
+ TRANSFER: 21_000,
+ SWAP: 180_000,
+ MINT: 90_000,
+ CONTRACT: 250_000,
+};
+
+export const TX_TYPES: TxType[] = ['TRANSFER', 'SWAP', 'MINT', 'CONTRACT'];
+
+/** Bounds for the fee-bid control, in gwei. */
+export const MIN_FEE_BID = 1;
+export const MAX_FEE_BID = 200;
+
+/**
+ * Priority fee a transaction pays the validator if mined, in gwei.
+ * (priority bid above the base fee) × gas consumed.
+ */
+export function txPriorityFee(tx: PendingTx, baseFee: number): number {
+ return Math.max(0, tx.feeBid - baseFee) * tx.gasUnits;
+}
+
+/**
+ * Order the pool by the auction rule: highest bid wins, earliest arrival breaks
+ * ties. Returns a new array; the input is left untouched.
+ */
+export function sortByFee(pool: PendingTx[]): PendingTx[] {
+ return [...pool].sort((a, b) => b.feeBid - a.feeBid || a.addedAt - b.addedAt);
+}
+
+/**
+ * Greedily select the transactions a validator would include: walk the pool from
+ * the highest bid down, skipping anything that fails to clear the base fee or no
+ * longer fits under the remaining gas. Mirrors a simple block builder.
+ */
+export function selectForBlock(
+ pool: PendingTx[],
+ gasLimit: number,
+ baseFee: number,
+): PendingTx[] {
+ const included: PendingTx[] = [];
+ let gasLeft = gasLimit;
+
+ for (const tx of sortByFee(pool)) {
+ if (tx.feeBid < baseFee) continue;
+ if (tx.gasUnits > gasLeft) continue;
+ included.push(tx);
+ gasLeft -= tx.gasUnits;
+ }
+
+ return included;
+}
+
+let txCounter = 0;
+
+/** Build a pseudo-random pending transaction for the simulated network. */
+export function randomTx(now: number): PendingTx {
+ const type = TX_TYPES[Math.floor(Math.random() * TX_TYPES.length)];
+ const gasJitter = Math.floor((Math.random() - 0.5) * 10_000);
+ txCounter += 1;
+
+ return {
+ id: `0x${(now.toString(16) + txCounter.toString(16)).slice(-10)}`,
+ from: `0x${Math.random().toString(16).slice(2, 6)}…${Math.random().toString(16).slice(2, 6)}`,
+ type,
+ gasUnits: Math.max(21_000, GAS_BY_TYPE[type] + gasJitter),
+ feeBid: Math.floor(Math.random() * 60) + MIN_FEE_BID,
+ addedAt: now,
+ };
+}
+
+/** Sum the gas of a set of transactions. */
+export function totalGas(txs: PendingTx[]): number {
+ return txs.reduce((sum, tx) => sum + tx.gasUnits, 0);
+}
diff --git a/frontend/src/lib/site-data.ts b/frontend/src/lib/site-data.ts
index 031e6e4c..5d1ebe19 100644
--- a/frontend/src/lib/site-data.ts
+++ b/frontend/src/lib/site-data.ts
@@ -3,6 +3,7 @@ import {
BrainCircuit,
CheckCircle2,
Compass,
+ Fuel,
LayoutDashboard,
Rocket,
Calculator,
@@ -35,6 +36,11 @@ export const primaryNav: NavItem[] = [
href: '/yield-calculator',
description: 'Estimate compounding yield returns based on APY, frequency, and lock-up terms.',
},
+ {
+ label: 'Gas Auction',
+ href: '/mempool-auction',
+ description: 'Simulate a mempool fee market where the highest bidders are mined first.',
+ },
{
label: 'Dashboard',
href: '/dashboard',
@@ -61,6 +67,12 @@ export const spotlightTools = [
summary: 'Estimate compounding yield returns based on APY, frequency, and lock-up terms.',
icon: Calculator,
},
+ {
+ title: 'Gas Fee Auction',
+ href: '/mempool-auction',
+ summary: 'Watch a live mempool sort by fee bids and mine the highest bidders into blocks.',
+ icon: Fuel,
+ },
{
title: 'Verification Center',
href: '/verify',