> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sei.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Nonce Lanes: Concurrent Submission from One Account

> Keep many independent transactions in flight from one funded Sei address without a sequential nonce queue. This guide explains how ERC-4337 nonce lanes, EIP-7702 delegation, an in-process bundling queue, and gas-only relayers fit together, and walks you through running the reference implementation.

export const NonceLaneFailureIsolation = () => {
  const ink = 'currentColor';
  const ok = '#10b981';
  const bad = '#ef4444';
  const warn = '#f59e0b';
  const lane = (x, y, label, sub, c, dashed, key) => <g key={key}>
      <rect x={x} y={y} width={86} height={62} rx={7} fill={c} fillOpacity={dashed ? 0.04 : 0.14} stroke={c} strokeOpacity={dashed ? 0.6 : 0.95} strokeWidth="1.2" strokeDasharray={dashed ? '4 3' : undefined} />
      <text x={x + 43} y={y + 24} fontSize="11" fontWeight="600" textAnchor="middle" fill={ink}>{label}</text>
      <text x={x + 43} y={y + 42} fontSize="9.5" textAnchor="middle" fill={c}>{sub}</text>
    </g>;
  return <div className="not-prose w-full my-5">
      <div className="overflow-x-auto rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/40 p-4 text-neutral-800 dark:text-neutral-200">
        <svg viewBox="0 0 900 320" role="img" aria-label="Execution reverts are isolated to one lane inside a bundle; validation failures revert the whole bundle without consuming anything; never-submitted operations consume nothing" style={{
    width: '100%',
    minWidth: 640,
    height: 'auto',
    display: 'block'
  }}>
          <text x={30} y={28} fontSize="13" fontWeight="600" fill={ink}>Execution revert: isolated to its lane</text>
          <rect x={30} y={44} width={400} height={118} rx={9} fill="none" stroke={ink} strokeOpacity="0.35" strokeWidth="1" strokeDasharray="5 4" />
          <text x={230} y={60} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.6">one handleOps transaction, one block</text>
          {lane(46, 70, 'lane 29', 'filled', ok, false, 'a29')}
          {lane(142, 70, 'lane 30', 'reverted', bad, false, 'a30')}
          {lane(238, 70, 'lane 31', 'filled', ok, false, 'a31')}
          {lane(334, 70, 'lane 32', 'filled', ok, false, 'a32')}
          <text x={230} y={152} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.7">all four sequences advance; lane 30 emits a failed UserOperationEvent and pays gas</text>
          <text x={30} y={186} fontSize="10" fill={ink} fillOpacity="0.75">the EntryPoint treats an account call failure as a per-operation result</text>
          <text x={30} y={202} fontSize="10" fill={ink} fillOpacity="0.75">and continues with the next operation in the bundle</text>

          <line x1={450} y1={20} x2={450} y2={210} stroke={ink} strokeOpacity="0.15" strokeWidth="1" />

          <text x={470} y={28} fontSize="13" fontWeight="600" fill={ink}>Validation failure: the whole bundle</text>
          <rect x={470} y={44} width={400} height={118} rx={9} fill="none" stroke={bad} strokeOpacity="0.6" strokeWidth="1" strokeDasharray="5 4" />
          <text x={670} y={60} fontSize="9.5" textAnchor="middle" fill={bad} fillOpacity="0.9">handleOps reverts, nothing in it is consumed</text>
          {lane(486, 70, 'lane 29', 'not consumed', ink, true, 'b29')}
          {lane(582, 70, 'lane 30', 'AA25 stale seq', bad, false, 'b30')}
          {lane(678, 70, 'lane 31', 'not consumed', ink, true, 'b31')}
          {lane(774, 70, 'lane 32', 'not consumed', ink, true, 'b32')}
          <text x={670} y={152} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.7">bad signature, stale sequence, or insufficient prefund fails before any execution</text>
          <text x={470} y={186} fontSize="10" fill={ink} fillOpacity="0.75">every bundle is simulated before broadcast, so this is normally caught</text>
          <text x={470} y={202} fontSize="10" fill={ink} fillOpacity="0.75">for free; MAX_OPS_PER_BUNDLE sets the size of this failure domain</text>

          <line x1={30} y1={222} x2={870} y2={222} stroke={ink} strokeOpacity="0.15" strokeWidth="1" />
          <rect x={30} y={236} width={840} height={72} rx={9} fill={warn} fillOpacity="0.08" stroke={warn} strokeOpacity="0.8" strokeWidth="1" />
          <text x={48} y={258} fontSize="11.5" fontWeight="600" fill={ink}>Never submitted: nothing consumed</text>
          <text x={48} y={277} fontSize="10" fill={ink} fillOpacity="0.8">a signed operation whose outer transaction was dropped, evicted, or never broadcast</text>
          <text x={48} y={293} fontSize="10" fill={ink} fillOpacity="0.8">leaves its lane sequence untouched; neighboring lanes stay valid and the journal requeues it</text>
        </svg>
      </div>
      <div className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">The three failures have different blast radii. An execution revert costs one lane sequence and nothing else. A validation failure costs the outer transaction attempt but no sequences. A dropped bundle costs nothing on chain, which is exactly the case that strands a sequential account.</div>
    </div>;
};

export const NonceLaneBundleLifecycle = () => {
  const ink = 'currentColor';
  const accent = 'var(--sei-maroon-50)';
  const ok = '#10b981';
  const warn = '#f59e0b';
  const bad = '#ef4444';
  const steps = [{
    t: 'take bundle',
    s: 'from the queue'
  }, {
    t: 'simulate',
    s: 'eth_estimateGas'
  }, {
    t: 'sign at nonce n',
    s: 'EIP-1559 outer tx'
  }, {
    t: 'journal',
    s: 'bytes hit disk first'
  }, {
    t: 'broadcast',
    s: 'sendRawTransaction'
  }, {
    t: 'wait for receipt',
    s: 'poll until the timeout'
  }, {
    t: 'settle lanes',
    s: 'then nonce n + 1'
  }];
  return <div className="not-prose w-full my-5">
      <div className="overflow-x-auto rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/40 p-4 text-neutral-800 dark:text-neutral-200">
        <svg viewBox="0 0 920 300" role="img" aria-label="Lifecycle of one bundle inside a relayer worker, including the same-nonce replacement loop and the simulation guard" style={{
    width: '100%',
    minWidth: 640,
    height: 'auto',
    display: 'block'
  }}>
          <defs>
            <marker id="bundle-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
              <path d="M 0 1 L 9 5 L 0 9 z" fill={ink} fillOpacity="0.6" />
            </marker>
            <marker id="bundle-warn" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
              <path d="M 0 1 L 9 5 L 0 9 z" fill={warn} />
            </marker>
            <marker id="bundle-bad" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
              <path d="M 0 1 L 9 5 L 0 9 z" fill={bad} />
            </marker>
          </defs>

          <text x={30} y={28} fontSize="13" fontWeight="600" fill={ink}>One relayer worker, one bundle at a time</text>
          {steps.map((st, i) => {
    const x = 30 + i * 124;
    const last = i === steps.length - 1;
    return <g key={st.t}>
                <rect x={x} y={52} width={112} height={54} rx={7} fill={last ? ok : ink} fillOpacity={last ? 0.14 : 0.05} stroke={last ? ok : ink} strokeOpacity={last ? 0.9 : 0.35} strokeWidth="1.1" />
                <text x={x + 56} y={74} fontSize="11" fontWeight="600" textAnchor="middle" fill={ink}>{st.t}</text>
                <text x={x + 56} y={92} fontSize="8.5" textAnchor="middle" fill={ink} fillOpacity="0.6">{st.s}</text>
                {i < steps.length - 1 ? <line x1={x + 114} y1={79} x2={x + 122} y2={79} stroke={ink} strokeOpacity="0.5" strokeWidth="1.2" markerEnd="url(#bundle-arrow)" /> : null}
              </g>;
  })}

          <line x1={210} y1={108} x2={210} y2={160} stroke={bad} strokeWidth="1.2" strokeDasharray="4 3" markerEnd="url(#bundle-bad)" />
          <rect x={60} y={164} width={300} height={62} rx={7} fill={bad} fillOpacity="0.08" stroke={bad} strokeOpacity="0.8" strokeWidth="1.1" />
          <text x={210} y={184} fontSize="10.5" fontWeight="600" textAnchor="middle" fill={bad}>simulation fails (AA24, AA25, prefund, AA95)</text>
          <text x={210} y={201} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.75">nothing is broadcast, no lane sequence is consumed,</text>
          <text x={210} y={216} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.75">and the relayer nonce is not spent</text>

          <line x1={706} y1={108} x2={706} y2={160} stroke={warn} strokeWidth="1.2" strokeDasharray="4 3" markerEnd="url(#bundle-warn)" />
          <rect x={430} y={164} width={440} height={62} rx={7} fill={warn} fillOpacity="0.1" stroke={warn} strokeOpacity="0.9" strokeWidth="1.1" />
          <text x={650} y={184} fontSize="10.5" fontWeight="600" textAnchor="middle" fill={warn}>no receipt before the timeout</text>
          <text x={650} y={201} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.75">check every earlier attempt for a receipt, bump fees by REPLACEMENT_FEE_BUMP_PERCENT,</text>
          <text x={650} y={216} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.75">sign a replacement at the same nonce n, journal it, rebroadcast (up to BUNDLE_MAX_ATTEMPTS)</text>
          <line x1={582} y1={162} x2={582} y2={110} stroke={warn} strokeWidth="1.2" strokeDasharray="4 3" markerEnd="url(#bundle-warn)" />

          <text x={450} y={262} fontSize="10.5" textAnchor="middle" fill={accent} fontWeight="600">the worker never sends nonce n + 1 while a transaction at n might still land</text>
          <text x={450} y={282} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.6">on restart, the last exact raw transaction is rebroadcast first, then reconciled against lane sequences and the relayer's confirmed nonce</text>
        </svg>
      </div>
      <div className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">Write-ahead ordering is deliberate: the signed outer transaction is journaled before it reaches the network, so a crash between signing and broadcast cannot lose or duplicate work. Replacement always reuses the relayer nonce, which is why a stuck bundle cannot create a nonce gap.</div>
    </div>;
};

export const NonceLanePipeline = () => {
  const ink = 'currentColor';
  const accent = 'var(--sei-maroon-50)';
  const gold = 'var(--sei-gold-25)';
  const box = {
    fill: ink,
    fillOpacity: 0.05,
    stroke: ink,
    strokeOpacity: 0.35,
    strokeWidth: 1
  };
  const relayers = ['relayer 0', 'relayer 1', 'relayer 2', 'relayer N'];
  return <div className="not-prose w-full my-5">
      <div className="overflow-x-auto rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/40 p-4 text-neutral-800 dark:text-neutral-200">
        <svg viewBox="0 0 900 340" role="img" aria-label="Submission pipeline: the funded account signs UserOperations into an in-process bundling queue, gas-only relayers wrap bundles in handleOps transactions, and the EntryPoint executes them through LaneAccount at the venue" style={{
    width: '100%',
    minWidth: 640,
    height: 'auto',
    display: 'block'
  }}>
          <defs>
            <marker id="pipeline-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
              <path d="M 0 1 L 9 5 L 0 9 z" fill={ink} fillOpacity="0.6" />
            </marker>
          </defs>

          <line x1={450} y1={18} x2={450} y2={322} stroke={accent} strokeWidth="1.3" strokeDasharray="6 4" />
          <text x={440} y={14} fontSize="10" textAnchor="end" fill={accent} fontWeight="600">holds the funds, signs intents</text>
          <text x={460} y={14} fontSize="10" fill={accent} fontWeight="600">holds gas only, cannot forge an operation</text>

          <rect x={30} y={110} width={180} height={124} rx={9} fill={accent} fillOpacity="0.1" stroke={accent} strokeWidth="1.3" />
          <text x={120} y={134} fontSize="12.5" fontWeight="600" textAnchor="middle" fill={ink}>Funded EOA</text>
          <text x={120} y={154} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.8">delegated to LaneAccount</text>
          <text x={120} y={170} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.8">signs one UserOperation per lane</text>
          <text x={120} y={186} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.8">EIP-712 digest, no nonce RPCs</text>
          <text x={120} y={210} fontSize="10" textAnchor="middle" fill={accent} fontWeight="600">EVM nonce does not move</text>
          <line x1={212} y1={172} x2={246} y2={172} stroke={ink} strokeOpacity="0.5" strokeWidth="1.3" markerEnd="url(#pipeline-arrow)" />

          <rect x={250} y={110} width={170} height={124} rx={9} {...box} />
          <text x={335} y={134} fontSize="12.5" fontWeight="600" textAnchor="middle" fill={ink}>Bundling queue</text>
          <text x={335} y={154} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.8">in-process FIFO of signed ops</text>
          <text x={335} y={170} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.8">one op per lane per bundle</text>
          <text x={335} y={186} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.8">no ERC-7562 4-op sender cap</text>
          <text x={335} y={210} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.6">bundles ≤ MAX_OPS_PER_BUNDLE</text>

          {relayers.map((r, i) => {
    const y = 40 + i * 72;
    return <g key={r}>
                <path d={`M 422 172 C 460 172, 470 ${y + 24}, 486 ${y + 24}`} fill="none" stroke={ink} strokeOpacity="0.45" strokeWidth="1.1" markerEnd="url(#pipeline-arrow)" />
                <rect x={490} y={y} width={150} height={48} rx={7} {...box} />
                <text x={565} y={y + 19} fontSize="11.5" fontWeight="600" textAnchor="middle" fill={ink}>{r}</text>
                <text x={565} y={y + 35} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.65">own sequential nonce, 1 tx in flight</text>
                <path d={`M 642 ${y + 24} C 660 ${y + 24}, 664 172, 686 172`} fill="none" stroke={ink} strokeOpacity="0.45" strokeWidth="1.1" markerEnd="url(#pipeline-arrow)" />
              </g>;
  })}
          <text x={565} y={334} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.6">the sequential constraint moved here, away from the funds</text>

          <rect x={690} y={126} width={190} height={92} rx={9} {...box} stroke={accent} strokeOpacity="0.9" strokeWidth="1.3" />
          <text x={785} y={149} fontSize="12.5" fontWeight="600" textAnchor="middle" fill={ink}>EntryPoint v0.8</text>
          <text x={785} y={168} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.8" fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">handleOps(ops[], relayer)</text>
          <text x={785} y={185} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.8">validates every op, then executes</text>
          <text x={785} y={201} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.8">each one, refunds gas to the relayer</text>
          <line x1={785} y1={220} x2={785} y2={254} stroke={ink} strokeOpacity="0.55" strokeWidth="1.3" markerEnd="url(#pipeline-arrow)" />

          <rect x={690} y={258} width={190} height={56} rx={9} fill={gold} fillOpacity="0.25" stroke={gold} strokeWidth="1.1" />
          <text x={785} y={281} fontSize="11.5" fontWeight="600" textAnchor="middle" fill={ink}>LaneAccount.execute → venue</text>
          <text x={785} y={299} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.7">msg.sender is the funded EOA</text>
        </svg>
      </div>
      <div className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">UserOperations are not transactions. Gas-only relayers wrap each bundle in an EntryPoint.handleOps transaction and pay for it. Each relayer still has one sequential EVM nonce, but a relayer key can only lose its own gas: it cannot create a valid operation without the funded account's signature.</div>
    </div>;
};

export const NonceLaneDelegation = () => {
  const ink = 'currentColor';
  const accent = 'var(--sei-maroon-50)';
  const gold = 'var(--sei-gold-25)';
  const box = {
    fill: ink,
    fillOpacity: 0.05,
    stroke: ink,
    strokeOpacity: 0.35,
    strokeWidth: 1
  };
  return <div className="not-prose w-full my-5">
      <div className="overflow-x-auto rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/40 p-4 text-neutral-800 dark:text-neutral-200">
        <svg viewBox="0 0 880 350" role="img" aria-label="EIP-7702 installs a delegation designator in the funded EOA's code slot so the EntryPoint can execute LaneAccount logic at the same address" style={{
    width: '100%',
    minWidth: 620,
    height: 'auto',
    display: 'block'
  }}>
          <defs>
            <marker id="delegation-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
              <path d="M 0 1 L 9 5 L 0 9 z" fill={ink} fillOpacity="0.6" />
            </marker>
            <marker id="delegation-accent" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
              <path d="M 0 1 L 9 5 L 0 9 z" fill={accent} />
            </marker>
          </defs>

          <rect x={40} y={40} width={330} height={216} rx={9} {...box} stroke={accent} strokeOpacity="0.9" strokeWidth="1.3" />
          <text x={205} y={64} fontSize="13" fontWeight="600" textAnchor="middle" fill={ink}>Funded EOA, same address</text>
          <text x={60} y={92} fontSize="10.5" fill={ink} fillOpacity="0.8">same native SEI balance</text>
          <text x={60} y={110} fontSize="10.5" fill={ink} fillOpacity="0.8">same token balances and venue approvals</text>
          <text x={60} y={128} fontSize="10.5" fill={ink} fillOpacity="0.8">same private key signs every UserOperation</text>
          <text x={60} y={146} fontSize="10.5" fill={ink} fillOpacity="0.8">EVM nonce: a self-sponsored delegation spends two,</text>
          <text x={60} y={161} fontSize="10.5" fill={ink} fillOpacity="0.8">then it is frozen on the submission path</text>
          <rect x={56} y={180} width={298} height={58} rx={6} fill={accent} fillOpacity="0.12" stroke={accent} strokeWidth="1" />
          <text x={68} y={198} fontSize="9.5" fill={accent} fontWeight="600">code slot (delegation designator)</text>
          <text x={68} y={220} fontSize="11" fill={ink} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">0xef0100 || LaneAccount address</text>

          <rect x={530} y={40} width={310} height={78} rx={9} {...box} />
          <text x={685} y={62} fontSize="12.5" fontWeight="600" textAnchor="middle" fill={ink}>EntryPoint v0.8 singleton</text>
          <text x={685} y={80} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.7" fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108</text>
          <text x={685} y={100} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.65">checks signature, lane sequence, and prefund</text>
          <line x1={528} y1={79} x2={374} y2={79} stroke={ink} strokeOpacity="0.55" strokeWidth="1.3" markerEnd="url(#delegation-arrow)" />
          <text x={451} y={70} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.75">handleOps calls</text>
          <text x={451} y={92} fontSize="8.5" textAnchor="middle" fill={ink} fillOpacity="0.75" fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">execute(target, value, data)</text>

          <rect x={530} y={168} width={310} height={88} rx={9} fill={accent} fillOpacity="0.08" stroke={accent} strokeWidth="1.2" />
          <text x={685} y={191} fontSize="12.5" fontWeight="600" textAnchor="middle" fill={ink}>LaneAccount implementation</text>
          <text x={685} y={210} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.75">inherits the reference Simple7702Account</text>
          <text x={685} y={226} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.75">adds one rule: lane 0 is rejected</text>
          <text x={685} y={242} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.75">ADMIN_LANE = max uint192 for ordered admin calls</text>
          <line x1={356} y1={209} x2={526} y2={209} stroke={accent} strokeWidth="1.3" markerEnd="url(#delegation-accent)" />
          <text x={441} y={202} fontSize="9.5" textAnchor="middle" fill={accent} fontWeight="600">runs this code</text>

          <line x1={205} y1={258} x2={205} y2={296} stroke={ink} strokeOpacity="0.55" strokeWidth="1.3" markerEnd="url(#delegation-arrow)" />
          <rect x={90} y={298} width={230} height={40} rx={7} fill={gold} fillOpacity="0.25" stroke={gold} strokeWidth="1.1" />
          <text x={205} y={323} fontSize="12" fontWeight="600" textAnchor="middle" fill={ink}>venue contract</text>
          <text x={340} y={312} fontSize="10" fill={ink} fillOpacity="0.8">msg.sender = the funded EOA, not a proxy</text>
          <text x={340} y={328} fontSize="10" fill={ink} fillOpacity="0.8">tx.origin = the gas-paying relayer</text>
        </svg>
      </div>
      <div className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">One type-4 transaction writes the designator into the EOA's code slot. The address, balances, and approvals do not move. When the EntryPoint calls the account, the EVM runs LaneAccount's code in the EOA's context, so the venue sees the funded address as msg.sender.</div>
    </div>;
};

export const NonceLanes = () => {
  const ink = 'currentColor';
  const accent = 'var(--sei-maroon-50)';
  const ok = '#10b981';
  const bad = '#ef4444';
  const chip = (x, y, label, state, key) => {
    const c = state === 'ok' ? ok : state === 'bad' ? bad : ink;
    return <g key={key}>
        <rect x={x} y={y} width={44} height={26} rx={5} fill={c} fillOpacity={state === 'next' ? 0 : 0.14} stroke={c} strokeOpacity={state === 'next' ? 0.5 : 0.9} strokeWidth="1.1" strokeDasharray={state === 'next' ? '3 3' : undefined} />
        <text x={x + 22} y={y + 17} fontSize="10.5" textAnchor="middle" fill={ink} fillOpacity={state === 'next' ? 0.6 : 1}>{label}</text>
      </g>;
  };
  return <div className="not-prose w-full my-5">
      <div className="overflow-x-auto rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/40 p-4 text-neutral-800 dark:text-neutral-200">
        <svg viewBox="0 0 880 342" role="img" aria-label="ERC-4337 two-dimensional nonce: a 192-bit lane key and a 64-bit sequence, with one sequence counter per lane" style={{
    width: '100%',
    minWidth: 620,
    height: 'auto',
    display: 'block'
  }}>
          <defs>
            <marker id="lanes-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
              <path d="M 0 1 L 9 5 L 0 9 z" fill={accent} />
            </marker>
          </defs>

          <text x={40} y={24} fontSize="13" fontWeight="600" fill={ink}>One uint256 nonce, two dimensions</text>
          <text x={40} y={44} fontSize="9.5" fill={ink} fillOpacity="0.55">bit 255</text>
          <text x={640} y={44} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.55">bit 64</text>
          <text x={840} y={44} fontSize="9.5" textAnchor="end" fill={ink} fillOpacity="0.55">bit 0</text>
          <rect x={40} y={50} width={600} height={44} rx={6} fill={accent} fillOpacity="0.14" stroke={accent} strokeWidth="1.2" />
          <text x={340} y={68} fontSize="12" fontWeight="600" textAnchor="middle" fill={ink}>uint192 key: the lane</text>
          <text x={340} y={84} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.65">any value from 1 to 2^192 - 1, chosen by the caller</text>
          <rect x={640} y={50} width={200} height={44} rx={6} fill={ink} fillOpacity="0.06" stroke={ink} strokeOpacity="0.4" strokeWidth="1.2" />
          <text x={740} y={68} fontSize="12" fontWeight="600" textAnchor="middle" fill={ink}>uint64 sequence</text>
          <text x={740} y={84} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.65">kept by the EntryPoint per lane</text>
          <text x={440} y={118} fontSize="11" textAnchor="middle" fill={ink} fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">nonce = (lane &lt;&lt; 64) | sequence</text>

          <line x1={40} y1={134} x2={840} y2={134} stroke={ink} strokeOpacity="0.15" strokeWidth="1" />
          <text x={40} y={158} fontSize="13" fontWeight="600" fill={ink}>The EntryPoint keeps one sequence counter per lane</text>

          <text x={40} y={191} fontSize="11" fill={bad} fontWeight="600">lane 0</text>
          <line x1={38} y1={187} x2={78} y2={187} stroke={bad} strokeWidth="1.2" />
          <text x={120} y={191} fontSize="10" fill={bad}>rejected by LaneAccount: SDKs default to key 0, and work on key 0 is one queue again</text>

          <text x={40} y={226} fontSize="11" fill={ink} fontWeight="600">lane 1</text>
          {chip(120, 210, 'seq 0', 'ok', 'l1s0')}
          {chip(172, 210, 'seq 1', 'ok', 'l1s1')}
          {chip(224, 210, 'seq 2', 'ok', 'l1s2')}
          {chip(276, 210, 'next 3', 'next', 'l1n')}
          <text x={340} y={226} fontSize="10" fill={ink} fillOpacity="0.65">three operations landed in order</text>

          <text x={40} y={262} fontSize="11" fill={ink} fontWeight="600">lane 2</text>
          {chip(120, 246, 'seq 0', 'ok', 'l2s0')}
          {chip(172, 246, 'seq 1', 'bad', 'l2s1')}
          {chip(224, 246, 'next 2', 'next', 'l2n')}
          <text x={340} y={262} fontSize="10" fill={ink} fillOpacity="0.65">seq 1 executed and reverted: it still consumed its sequence</text>

          <text x={40} y={298} fontSize="11" fill={ink} fontWeight="600">lane 3</text>
          {chip(120, 282, 'next 0', 'next', 'l3n')}
          <text x={340} y={298} fontSize="10" fill={ink} fillOpacity="0.65">a fresh lane starts at 0 and is valid immediately</text>

          <line x1={700} y1={206} x2={700} y2={302} stroke={accent} strokeWidth="1.2" strokeDasharray="4 3" markerStart="url(#lanes-arrow)" markerEnd="url(#lanes-arrow)" />
          <text x={712} y={250} fontSize="10" fill={accent} fontWeight="600">no ordering between lanes</text>
          <text x={712} y={264} fontSize="9.5" fill={ink} fillOpacity="0.6">whatever happens on one lane,</text>
          <text x={712} y={277} fontSize="9.5" fill={ink} fillOpacity="0.6">the others stay valid</text>

          <line x1={120} y1={324} x2={200} y2={324} stroke={accent} strokeWidth="1.2" markerEnd="url(#lanes-arrow)" />
          <text x={210} y={328} fontSize="9.5" fill={accent} fontWeight="600">strictly ordered within a lane, left to right</text>
        </svg>
      </div>
      <div className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">EntryPoint v0.8 stores a UserOperation nonce as a 192-bit key plus a 64-bit sequence and tracks one sequence per key. Operations on different keys never queue behind each other; operations on the same key stay sequential. The repository calls a key a lane and forbids lane 0.</div>
    </div>;
};

export const SequentialNonceQueue = () => {
  const ink = 'currentColor';
  const ok = '#10b981';
  const bad = '#ef4444';
  const warn = '#f59e0b';
  const nonces = [{
    n: 5,
    s: 'landed',
    c: ok
  }, {
    n: 6,
    s: 'dropped, never landed',
    c: bad
  }, {
    n: 7,
    s: 'stranded',
    c: warn
  }, {
    n: 8,
    s: 'stranded',
    c: warn
  }];
  return <div className="not-prose w-full my-5">
      <div className="overflow-x-auto rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/60 dark:bg-neutral-900/40 p-4 text-neutral-800 dark:text-neutral-200">
        <svg viewBox="0 0 880 310" role="img" aria-label="A missing EVM nonce strands every later nonce; the usual workaround is more hot wallets" style={{
    width: '100%',
    minWidth: 620,
    height: 'auto',
    display: 'block'
  }}>
          <defs>
            <marker id="queue-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
              <path d="M 0 1 L 9 5 L 0 9 z" fill={ink} fillOpacity="0.6" />
            </marker>
          </defs>

          <text x={40} y={28} fontSize="13" fontWeight="600" fill={ink}>One account, one queue</text>
          {nonces.map((it, i) => {
    const x = 40 + i * 190;
    return <g key={'n' + it.n}>
                <rect x={x} y={46} width={160} height={54} rx={7} fill={it.c} fillOpacity="0.12" stroke={it.c} strokeWidth="1.2" />
                <text x={x + 80} y={68} fontSize="12" fontWeight="600" textAnchor="middle" fill={ink}>nonce {it.n}</text>
                <text x={x + 80} y={86} fontSize="10" textAnchor="middle" fill={it.c}>{it.s}</text>
                {i < nonces.length - 1 ? <line x1={x + 162} y1={73} x2={x + 186} y2={73} stroke={ink} strokeOpacity="0.45" strokeWidth="1.2" markerEnd="url(#queue-arrow)" /> : null}
              </g>;
  })}
          <text x={40} y={124} fontSize="10.5" fill={ink} fillOpacity="0.7">nonces 7 and 8 are signed and valid, but nothing at or above 7 can execute until 6 is filled or replaced</text>
          <text x={40} y={142} fontSize="10.5" fill={ink} fillOpacity="0.7">on Sei under Giga the producer mempool rejects 7 and 8 with a bad nonce error rather than queueing them: the same stall</text>

          <line x1={40} y1={162} x2={840} y2={162} stroke={ink} strokeOpacity="0.15" strokeWidth="1" />

          <text x={40} y={190} fontSize="13" fontWeight="600" fill={ink}>The usual workaround: more hot wallets</text>
          {['A', 'B', 'C', 'N'].map((w, i) => {
    const x = 40 + i * 190;
    return <g key={'w' + w}>
                <rect x={x} y={206} width={160} height={68} rx={7} fill={ink} fillOpacity="0.05" stroke={ink} strokeOpacity="0.35" strokeWidth="1" />
                <text x={x + 80} y={226} fontSize="11.5" fontWeight="600" textAnchor="middle" fill={ink}>hot wallet {w}</text>
                <text x={x + 80} y={243} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.65">own balance, own approvals</text>
                <text x={x + 80} y={258} fontSize="9.5" textAnchor="middle" fill={bad} fillOpacity="0.9">own key that can move funds</text>
              </g>;
  })}
          <text x={440} y={298} fontSize="10.5" textAnchor="middle" fill={ink} fillOpacity="0.6">throughput scales with wallets, and so do fragmented balances, duplicated approvals, and keys that hold funds</text>
        </svg>
      </div>
      <div className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">A transaction that lands and reverts consumes its nonce and blocks nothing. A transaction that never lands leaves a gap, and every later nonce waits behind it. On Sei a pending-nonce read comes from the mempool rather than aliasing latest, and it is too unreliable to rebuild an in-flight queue from. Splitting funds across hot wallets buys width at the cost of custody surface.</div>
    </div>;
};

One funded account on Sei EVM has one sequential nonce. That suits a wallet, where each action follows the last. It does not suit a stream of independent work: one transaction that never lands blocks every transaction signed after it. The usual fix is a fleet of hot wallets, which multiplies balances, approvals, and keys.

[sei-nonce-lanes](https://github.com/sei-protocol/sei-nonce-lanes) is a reference implementation that removes the queue instead. One funded externally owned account (EOA) keeps its address, balance, and approvals while holding many mutually independent operations in flight at the same time.

The pattern applies whenever one funded address needs to do many things that do not depend on each other: market making and order flow, liquidation and keeper bots, oracle updates, payout and claim batching, or game and backend transactions.

<Info>
  The repository is a runnable engineering demonstration, not a production service. It uses a mock venue, plaintext development keys in `.env`, an in-process queue, and console output. Read [Before you adapt this](#before-you-adapt-this) before pointing it at real funds.
</Info>

<Note>
  This page is about submission concurrency. If you want a hosted bundler, a paymaster, and gas sponsorship for consumer wallets, start with [Pimlico](/evm/wallet-integrations/pimlico) or [Thirdweb EIP-7702](/evm/wallet-integrations/thirdweb-7702) instead. Nonce lanes solve the opposite problem: throughput from one address you control, with no third party in the submission path.
</Note>

## Why one account is one queue

An EVM account's transaction nonces are strictly sequential. If nonce `n` has not executed, nonce `n + 1` cannot execute first. Two failures that look similar behave differently:

| Event                                      | Blocks later nonces?                                              |
| ------------------------------------------ | ----------------------------------------------------------------- |
| The transaction lands and its call reverts | No. The transaction consumed its nonce.                           |
| The transaction never lands                | Yes. Every later nonce waits until the gap is filled or replaced. |

The second case is the submission bottleneck. It covers transactions that are dropped, underpriced, rejected at admission, lost before broadcast, or stranded after a process crash.

<SequentialNonceQueue />

Two properties of Sei make this sharper than on Ethereum:

* **Strict nonce admission.** Under Giga, the Autobahn producer mempool admits EVM transactions in per-sender nonce order and rejects a gap with a `bad nonce` error instead of holding it for later. See [Giga mode behavior](/node/technical-reference#giga-mode-behavior-and-per-block-limits). What you observe through an RPC endpoint can differ, because the node in front of the producer may hold a gapped transaction and release it later, or accept it and then drop it. The repository's `npm run baseline` command probes the RPC path you configure; in one session it returned [two different verdicts](#optional-see-the-baseline-you-are-escaping) for two Atlantic-2 endpoints.
* **No dependable pending view.** Sei does not expose Ethereum-style pending state, and [Finality and block tags](/evm/evm-parity/finality#pending-state) tells you not to rely on a pending nonce differing from the confirmed nonce. `txpool_content` is also [truncated and collapses the pending/queued distinction](/evm/reference). Even where a node answers a pending-nonce query, the value cannot be used to rebuild an in-flight queue.

<Warning>
  Do not design around a pending nonce on Sei. `eth_getTransactionCount(address, "pending")` is [documented](/evm/reference) as returning `EvmNextPendingNonce` from the mempool, so it is not an alias for `"latest"`. The finality guidance still marks the pending view as unreliable, and the value varies by node and by whether the node runs Giga. The design below does not depend on the answer: its hot path reads no nonces.
</Warning>

Splitting work across hot wallets raises throughput, but every wallet is another balance to rebalance, another set of approvals to maintain, and another key that can move funds.

## How it works

The design combines four mechanisms. Each solves one part of the problem; none is sufficient alone.

| Mechanism                                    | What it contributes                                                                |
| -------------------------------------------- | ---------------------------------------------------------------------------------- |
| ERC-4337 v0.8 two-dimensional nonces         | Independent nonce lanes for one account                                            |
| EIP-7702 delegation                          | The existing funded address keeps its balance and approvals                        |
| An in-process bundling queue                 | No per-sender pending cap and no third-party bundler                               |
| Gas-only relayers with a write-ahead journal | Sequential transactions still exist, but the accounts that send them hold only gas |

### ERC-4337 nonce lanes

EntryPoint v0.8 stores a UserOperation nonce as a 192-bit key plus a 64-bit sequence:

```text theme={"dark"}
nonce = (uint192 key << 64) | uint64 sequence
```

The EntryPoint keeps one `sequence` counter for each `key`. The specification calls this a two-dimensional nonce; the repository calls a key a **lane**.

<NonceLanes />

Four rules follow from that layout:

* Operations on different lanes have no ordering relationship.
* Operations on the same lane stay strictly sequential, so the implementation allows at most one in-flight operation per lane.
* An operation that executes and reverts still consumes its lane sequence.
* An operation that never reaches a successful `handleOps` transaction consumes nothing.

`LaneAccount` rejects lane `0`. Most SDKs pick key `0` when you do not pass one, and work that lands entirely on key `0` is a single queue again. Rejecting it converts a silent fallback into an explicit validation failure. `ADMIN_LANE` (the maximum `uint192`) is reserved for integrations that need one explicitly ordered lane for administrative calls.

### EIP-7702 keeps the funded address

An EIP-7702 authorization writes a delegation designator into the EOA's code slot:

```text theme={"dark"}
0xef0100 || <LaneAccount implementation address>
```

The address does not change. Its native balance, token balances, protocol state, and approvals stay attached to the same account. When the EntryPoint calls the account, the EVM runs `LaneAccount`'s code in the EOA's context, so `LaneAccount.execute` reaches the target with the funded EOA as `msg.sender`.

<NonceLaneDelegation />

`LaneAccount` inherits the reference `Simple7702Account` from the eth-infinitism [account-abstraction](https://github.com/eth-infinitism/account-abstraction) repository and adds a single policy check:

```solidity theme={"dark"}
contract LaneAccount is Simple7702Account {
    uint192 public constant ADMIN_LANE = type(uint192).max;

    error LaneZeroReserved();

    function _validateNonce(uint256 nonce) internal pure override {
        if (nonce >> 64 == 0) revert LaneZeroReserved();
    }
}
```

Installing the delegation takes one type-4 transaction. Sei requires a non-empty authorization list on type-4 transactions; see [Transaction types](/evm/evm-parity/transaction-types#set-code-eip-7702-auth-list-requirement).

The nonce cost depends on who sends that transaction. The EVM applies the authorization list after incrementing the sender's nonce for the transaction itself, so when the authority is also the sender, the authorization must be signed over `nonce + 1` and applying it increments the account a second time. A self-sponsored delegation advances the account's nonce by two; `npm run delegate` takes this path. A delegation sponsored by a different sender advances the funded account by one, the increment the authorization itself performs. In both cases, once the designator is in place, the submission path signs UserOperations only and the funded account's EVM nonce stops moving.

<Note>
  EIP-7702 alone does not create independent nonces; it preserves the account. ERC-4337 supplies the nonce model. The design needs both.
</Note>

### Gas-only relayers carry what is left of the queue

UserOperations are not transactions. Something still has to wrap them in `EntryPoint.handleOps` transactions and pay for them. The repository uses a pool of gas-only relayers fed by an in-process bundling queue.

<NonceLanePipeline />

The sequential constraint moves rather than disappears. Each relayer has one sequential EVM nonce and keeps one outer transaction in flight at a time. The custody boundary is what changes: relayers hold native SEI for gas and nothing else. A compromised relayer key can lose its own gas balance or rebroadcast operations the funded account already signed. It cannot create a new operation, because every UserOperation carries an EIP-712 signature from the funded account over the EntryPoint's `PackedUserOperation` digest.

The bundling queue matters for a different reason. It is an in-process queue, not a mempool: nothing is gossiped, nothing arrives from another party, and it is gone when the process exits. It therefore never enters the canonical ERC-4337 alt-mempool, and the [ERC-7562](https://eips.ethereum.org/EIPS/eip-7562) validation rules that govern that mempool, including `SAME_SENDER_MEMPOOL_COUNT = 4` for an unstaked sender, do not apply to it. A cap of four pending operations per sender is sized for wallets, not for a submission pipeline. Those rules exist so competing bundlers can safely pack operations from unrelated senders into one bundle. Here every operation comes from one account you control, so that threat does not arise. The EntryPoint still enforces everything that protects funds: the signature, per-lane nonce uniqueness, and prefund solvency.

### One bundle, start to finish

Each relayer runs one asynchronous worker. The worker takes a bundle of up to `MAX_OPS_PER_BUNDLE` operations (never two from the same lane), and then works through a fixed sequence.

<NonceLaneBundleLifecycle />

Two rules provide most of the safety:

* **Write-ahead ordering.** The signed outer transaction is written to the journal before it is broadcast. Each journal snapshot is fsynced before it is renamed into place, and the directory is fsynced after it, so a written record survives a kernel panic or host loss, not only a process crash. On restart, the exact raw bytes are rebroadcast first, so a crash between signing and sending cannot lose or duplicate work. The journal is still one file on one disk, not a replicated database.
* **Same-nonce replacement.** If no receipt arrives within `BUNDLE_RECEIPT_TIMEOUT_MS`, the worker checks earlier attempts for a receipt, bumps fees by `REPLACEMENT_FEE_BUMP_PERCENT`, and signs a replacement at the same relayer nonce. It never sends nonce `n + 1` while a transaction at `n` might still land, which is the gap this design exists to avoid.

The receipt wait polls every `RECEIPT_POLLING_INTERVAL_MS`, 250 ms by default. Sei produces blocks in well under a second. With viem's 4-second default, most of the timeout passes idle between polls, and a bundle that has already landed can be reported as timed out, which triggers an unnecessary fee-bumped replacement.

Two further guards cover the simulation and the journal. The `eth_estimateGas` call passes the relayer's address string as `account`, not the viem `Account` object. Given a local account object, viem prepares a full transaction request before estimating, which adds a chain ID read, a fee lookup, and an `eth_getTransactionCount(address, "pending")` call; passing the address skips all three, including the pending-nonce read this hot path is built to avoid. A file lock keeps a second process off the same account. The lock identifies its holder by inode rather than by the existence of a file, and the process re-checks that it still holds the lock immediately before it signs and before every journal write, so a process that lost a lock race stops instead of signing.

Before any new work is created, a restarted process reconciles every incomplete journal entry against the EntryPoint. If the chain sequence is ahead of the journal, the operation was consumed. If they match, the lane is reserved and the operation is recovered or requeued. If the chain is behind the journal, the state is inconsistent and the process stops rather than guessing.

### What fails alone and what fails together

The EntryPoint treats an account execution failure as a per-operation result: it emits a failed `UserOperationEvent`, charges gas, advances that lane, and continues with the next operation. A validation failure (bad signature, stale sequence, insufficient prefund) is different: it reverts the whole `handleOps` transaction and nothing in it is consumed.

<NonceLaneFailureIsolation />

| Situation                                     | Lane sequence            | Other lanes in the bundle       | Recovery                                                  |
| --------------------------------------------- | ------------------------ | ------------------------------- | --------------------------------------------------------- |
| Operation executes successfully               | Consumed                 | Continue                        | None                                                      |
| Operation execution reverts                   | Consumed                 | Continue                        | Optionally retry the intent on the lane's next sequence   |
| Operation validation fails                    | Not consumed             | Entire `handleOps` reverts      | Fix the cause and resubmit                                |
| Signed operation never reached a mined bundle | Not consumed             | Independent lanes stay valid    | The journal requeues it                                   |
| Outer transaction times out or is evicted     | Unknown until reconciled | Bundle stays intact             | Rebroadcast and fee-bump at the same relayer nonce        |
| Outer transaction mines and reverts           | Not consumed             | Nothing in that bundle executes | Relayer nonce is consumed; operations can be requeued     |
| Process exits after journaling                | Determined at restart    | No new work starts first        | Reconcile receipts, lane sequences, and the relayer nonce |

Every bundle is simulated with `eth_estimateGas` before broadcast, so validation failures are normally caught before any gas is spent. `MAX_OPS_PER_BUNDLE` sets the size of the shared validation domain; keep it small when isolation matters more than amortized cost.

## Why this is hard to replicate

Faster hardware and better RPC routing help every submission strategy. The difference here is structural: one funded address holds many independent operations in flight while keeping the custody surface of a single wallet.

The relevant comparison is a fleet of hot wallets, which is what most teams run. A fleet can match the width. It cannot do so from one balance, one approval set, and one key.

The baseline is not one transaction at a time. A single address can sign and broadcast nonces `n`, `n+1`, `n+2`, and onward and have all of them unresolved, and [the producer mempool admits them](/node/technical-reference#giga-mode-behavior-and-per-block-limits) as long as they arrive in order. The constraint is ordering: the outstanding transactions form one queue, and one that never lands strands every later nonce behind it.

|                                            | One hot wallet              | Fleet of N hot wallets                   | Public ERC-4337 bundler       | Nonce lanes                                       |
| ------------------------------------------ | --------------------------- | ---------------------------------------- | ----------------------------- | ------------------------------------------------- |
| Submissions signed and unresolved at once  | Many                        | Many per wallet                          | At most 4 per unstaked sender | `LANE_POOL_SIZE` (32 by default, 4096 maximum)    |
| Outer transactions broadcast at once       | Many                        | Many per wallet                          | Set by the bundler's policy   | `RELAYER_COUNT`                                   |
| Are those submissions independent?         | No: one ordered queue       | Only across wallets, ordered within each | Yes                           | Yes: one sequence per lane                        |
| Balances and approvals to maintain         | One address                 | N addresses, approved N times            | One address                   | One address                                       |
| Keys that can move funds                   | 1                           | N                                        | 1                             | 1; relayers hold gas only                         |
| Recovering the in-flight set after a crash | Re-derive it from the chain | Re-derive it per wallet                  | Handled by the bundler        | Durable journal and deterministic reconciliation  |
| Failure blast radius                       | Everything behind the gap   | Everything behind the gap, per wallet    | One operation                 | One lane for execution, one bundle for validation |

<Note>
  Read the first two rows together. `LANE_POOL_SIZE` caps how many operations can be signed and unresolved at once; `RELAYER_COUNT` caps how many outer transactions are broadcast at once. Lanes provide a large pool of independent intents rather than a large number of simultaneous transactions. The approximate per-block submission width is `RELAYER_COUNT × MAX_OPS_PER_BUNDLE`.
</Note>

Measured on Atlantic-2 against the same venue call, one address with sequential nonces landed 1 operation per second sending serially and 7 per second pipelining one request at a time, and reached 51 per second only by sending all 1,024 transactions in one JSON-RPC batch. A fleet of 8 hot wallets also landed 51 per second. Nonce lanes landed 60 to 64 per second from one address whose EVM nonce never moved, against a block-gas ceiling of about 73 for that call shape. The method, the run shapes, and the caveats are in [Benchmarks](#benchmarks).

Six properties of the design produce those rows:

1. Capital, approvals, and protocol state stay on one address. Width comes from lanes, not from splitting funds. A new lane costs nothing to open and is valid at sequence `0` immediately.
2. The only key that can create a valid operation is the funded account's. Relayer keys can be rotated, replaced, or lost at a cost bounded by their gas balances.
3. After the one-time delegation, the funded account's EVM nonce does not move while submitting. Nothing an RPC drops or a producer rejects can strand the account.
4. The in-process bundling queue removes the ERC-7562 `SAME_SENDER_MEMPOOL_COUNT` limit and the dependency on a third-party bundler's inclusion policy, while every EntryPoint check that protects funds still runs.
5. Signed operations and signed outer transactions are journaled and fsynced before broadcast, replacement reuses the relayer nonce, and restart reconciliation is deterministic: it refuses to create new work when the recorded state is ambiguous.
6. The signing path performs no nonce reads, so an unreliable pending view costs nothing. Fast blocks and instant finality keep each relayer's receipt wait short, which is what makes `RELAYER_COUNT × MAX_OPS_PER_BUNDLE` a usable per-block width.

<Warning>
  Submission concurrency is not execution parallelism. Independent lanes remove ordering between submissions. They do not make conflicting storage writes execute in parallel. A contract that funnels everything through one hot storage slot still serializes on that slot. See [Optimizing for parallelization](/evm/best-practices/optimizing-for-parallelization) and the [parallelization engine](/learn/parallelization-engine).
</Warning>

<Note>
  Block time and gas limits differ between today's [Twin Turbo Consensus](/learn/twin-turbo-consensus) and [Sei Giga](/learn/sei-giga). Size a relayer pool against the network you submit to, and measure rather than assume.
</Note>

## Tutorial: run the reference implementation

The walkthrough below deploys the demo contracts, delegates a throwaway account, funds a relayer pool, and submits 24 operations across 32 lanes. One order is deliberately given an unfillable limit price so you can watch a revert land without disturbing its neighbors.

### Prerequisites

* Git with submodule support
* [Foundry](https://getfoundry.sh/) with `forge`, `anvil`, and `cast`
* Node.js 22 or newer, and npm
* For the Atlantic-2 path: a fresh throwaway key funded from the [Sei faucet](/learn/faucet)

<Steps>
  <Step title="Clone and verify">
    Clone with submodules so the pinned `account-abstraction`, OpenZeppelin, and `forge-std` dependencies are checked out:

    ```bash theme={"dark"}
    git clone --recurse-submodules https://github.com/sei-protocol/sei-nonce-lanes.git
    cd sei-nonce-lanes
    ```

    For an existing clone, run `git submodule update --init --recursive`.

    Install the Node dependencies and run every local check:

    ```bash theme={"dark"}
    cd app
    npm ci
    npm run check
    cd ..

    forge fmt --check
    forge test -vv
    ```

    The Foundry suite runs against the real EntryPoint v0.8 bytecode from the pinned dependency, placed at the canonical address inside the test VM. It verifies that different lanes can land in any order, that an execution revert affects only its own lane, that an operation that is never submitted blocks nothing, that a gap on one shared lane reproduces sequential blocking, that one validation failure reverts the whole bundle, that lane `0` is rejected, and that a 50-lane bundle fits in one outer transaction.
  </Step>

  <Step title="Choose a target network">
    Start with a local Prague fork. It carries the deployed EntryPoint bytecode from Atlantic-2 but spends only Anvil funds.

    <Danger>
      Never use Anvil, Hardhat, tutorial, or shared test mnemonics on Atlantic-2 or Pacific-1. Their addresses and keys are public, and some are already delegated to sweeper code, so a successful funding transaction can still leave a zero balance. The fork below also runs as chain `1328`, so an authorization signed against it is replayable on Atlantic-2 itself whenever the account's nonce lines up. Regenerate every key and mnemonic before you point a fork's `.env` at a public network.
    </Danger>

    <Tabs>
      <Tab title="Local Anvil fork (recommended first)">
        EIP-7702 requires a Prague-capable node. Keep this terminal running:

        ```bash theme={"dark"}
        anvil \
          --fork-url https://evm-rpc-testnet.sei-apis.com \
          --chain-id 1328 \
          --hardfork prague
        ```

        The repository compiles its contracts for Cancun because they use no Prague-only opcodes, but the local node must run Prague to accept the type-4 delegation transaction.

        Create the configuration and point it at the fork:

        ```bash theme={"dark"}
        cp .env.example .env
        ```

        ```dotenv title=".env" theme={"dark"}
        SEI_CHAIN_ID=1328
        SEI_RPC_URL=http://127.0.0.1:8545

        RELAYER_COUNT=4
        RELAYER_START_INDEX=1
        ```

        Set `TRADER_PRIVATE_KEY` to account `0`'s private key from the Anvil startup output, and `RELAYER_MNEMONIC` to the mnemonic printed by that same Anvil process. Starting relayers at index `1` keeps the funded account and relayer identities distinct; the application rejects overlapping identities.

        Those are Anvil's published credentials, so this `.env` must only ever point at the fork.

        Deploy the demo contracts from the repository root using Anvil's unlocked account:

        ```bash theme={"dark"}
        forge script script/Deploy.s.sol:Deploy \
          --rpc-url http://127.0.0.1:8545 \
          --broadcast \
          --unlocked \
          --sender 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
        ```
      </Tab>

      <Tab title="Atlantic-2 testnet">
        Generate fresh credentials. Never reuse a key or mnemonic that has appeared in a tutorial, a test framework, or a shared document:

        ```bash theme={"dark"}
        cast wallet new            # funded account key
        cast wallet new-mnemonic   # relayer mnemonic
        ```

        Fund the address from the [Sei faucet](/learn/faucet), then configure:

        ```bash theme={"dark"}
        cp .env.example .env
        ```

        ```dotenv title=".env" theme={"dark"}
        SEI_CHAIN_ID=1328
        SEI_RPC_URL=https://evm-rpc-testnet.sei-apis.com

        TRADER_PRIVATE_KEY=0x...
        RELAYER_MNEMONIC="word word word ..."
        RELAYER_COUNT=4
        RELAYER_START_INDEX=0
        ```

        Deploy with a funded key. The deployer can be the throwaway account, but it does not have to be:

        ```bash theme={"dark"}
        forge script script/Deploy.s.sol:Deploy \
          --rpc-url https://evm-rpc-testnet.sei-apis.com \
          --broadcast \
          --interactives 1
        ```

        `--interactives 1` prompts for the key and does not echo it, so it never reaches your shell history, the process table, or an environment variable.
      </Tab>
    </Tabs>

    Copy the two printed addresses into `.env`:

    ```dotenv title=".env" theme={"dark"}
    LANE_ACCOUNT_IMPL=0x...
    VENUE=0x...
    ```

    <Note>
      Mutating commands refuse to write to a remote Pacific-1 RPC unless `ALLOW_MAINNET=1` is set explicitly, and they refuse any remote-chain write when `TRADER_PRIVATE_KEY` is Anvil account `0` or `RELAYER_MNEMONIC` is the Anvil mnemonic. Those guards catch only an accidental `SEI_CHAIN_ID=1329` run and the two most common leaked credentials. They do not cover the separate Forge deployment command, and they do not make the demo production-ready.
    </Note>
  </Step>

  <Step title="Check the preflight">
    `status` is read-only. Run it before anything that writes:

    ```bash theme={"dark"}
    cd app
    npm run status
    ```

    It prints the chain ID, whether code exists at the EntryPoint address `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108`, the account's current delegation, balances, its EntryPoint deposit, each relayer's confirmed nonce and gas balance, the lane sequences, and the venue state.
  </Step>

  <Step title="Delegate the account">
    ```bash theme={"dark"}
    npm run delegate
    ```

    This sends one type-4 transaction with an authorization for `LANE_ACCOUNT_IMPL`, signed and paid for by the account itself, so the account's EVM nonce advances by two: once for the transaction, once for the authorization. The command reads the account's confirmed nonce once and pins both the transaction nonce and the authorization nonce (`nonce + 1`) to that read, instead of letting the client fill them from a pending-nonce lookup. That matters because a wrong authorization nonce is not rejected: the EVM skips the authorization, the transaction still succeeds, and nothing is installed. The command therefore re-reads the designator after the receipt and fails if the delegation did not take effect.

    The command is idempotent: if the account already delegates to the configured implementation, it does nothing. If the account delegates to something else, it tells you before replacing it.
  </Step>

  <Step title="Fund the relayers and the EntryPoint deposit">
    ```bash theme={"dark"}
    npm run fund
    npm run status
    ```

    `fund` uses ordinary transactions to top each relayer up to `RELAYER_FUNDING` SEI and to bring the account's `EntryPoint.depositTo` balance up to `ENTRYPOINT_DEPOSIT` SEI. The deposit is what the EntryPoint draws prefund from when it validates each operation. On a public network you can instead send SEI to relayer `0` and run `npm run dispense`, which waits for the balance and splits it across the pool. Use one bootstrapping path or the other, not both.
  </Step>

  <Step title="Submit the run">
    ```bash theme={"dark"}
    npm run submit
    ```

    One `submit` process performs the complete run and exits. It runs the preflight, estimates the delegated call's gas, reads each lane's sequence once, signs 24 operations concurrently with no nonce RPCs, journals them, bundles them, drains the bundles through the relayer pool, and prints a report. By default, order `2` receives a limit price below the mark, so its operation reverts during execution while the neighboring lanes continue.
  </Step>
</Steps>

### Read the report

The output below is illustrative; your addresses, blocks, and timings will differ.

```text theme={"dark"}
=== preflight ===
chain          Sei Testnet (1328)
rpc            http://127.0.0.1:8545
entryPoint     0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 (… bytes)
trader         0xf39F…2266
delegated to   0x5FbD…0aa3
…
trader nonce   9  <- watch this, it must not move
journal        0 pending op(s)
relayer        0x7099…79C8  0.5 SEI
…

=== build ===
call gas       78125 (estimate 62500)
lane pool      32 lanes, 32 idle
signed         24 new ops in 52ms, without nonce RPCs
hash check     local digest matches EntryPoint.getUserOpHash

=== submit ===
24 ops -> bundles of <=4 -> 4 relayers

  mined   lanes [32,31,30,29]  block 187213  gas 612345
  mined   lanes [28,27,26,25]  block 187213  gas 598120
  …

=== per-order outcome ===
  #  lane  seq  exec      filled  land#  block     note
   0    32    0  ok           yes      1    187213
   1    31    0  ok           yes      2    187213
   2    30    0  reverted     no       0    187213  expected revert (limit under mark)
   3    29    0  ok           yes      3    187213
  …

=== summary ===
ops submitted        24
ops landed           24
  executed ok        23
  reverted on chain  1  (each consumed only its own lane)
bundles              6 across 2 block(s)
  pending recovery   0
  failed safely      0
journal pending      0
distinct lanes       24
relayers used        4
sign time            52ms
submit time          1846ms
throughput           13.0 landed ops/sec

trader EVM nonce     9 -> 9  UNCHANGED
total wall time      4210ms
```

What to look for:

* **`trader nonce` before and `trader EVM nonce` after** must be identical. The tool calls the funded account the trader. Its key never entered a queue.
* **`call gas`** is the measured delegated-call estimate plus 25 percent. A `CALL_GAS_LIMIT floor` appears in that line only when you set one; leave it unset unless you have a reason, because the EntryPoint reserves the declared limit before running each operation.
* **`exec`** combines the bundle receipt with the venue's `isFilled` read. `reverted` means the outer transaction landed, so the operation consumed its lane sequence, but the call failed inside the venue. `not mined` would mean the outer transaction never landed and nothing was consumed.
* **`land#`** is the venue's global landing counter. It shows the order in which operations actually executed, which has nothing to do with lane number. Lane acquisition is last-in, first-out, so a fresh 32-lane pool starts at lane `32`; lane numbers carry no priority.
* **`hash check`** confirms that the locally computed EIP-712 digest matches `EntryPoint.getUserOpHash` for the first operation, so client-side hashing matches consensus.

If a bundle is reported as `PENDING` or `FAILED`, the command exits non-zero and leaves the journal intact. Run `npm run submit` again once the RPC can answer receipt and nonce queries; the process recovers or replaces at the same relayer nonce before it creates new work. Do not delete the journal and do not send the relayer's next nonce by hand.

### Optional: real swaps on Atlantic-2

The repository includes a real-target path that routes tiny native SEI and native USDC swaps through the documented DragonSwap V1 deployment on Atlantic-2. It is hard-blocked on every other chain. Get testnet USDC from the [Circle faucet](https://faucet.circle.com/), then:

```bash theme={"dark"}
# from the app directory, where the walkthrough left off
npm run swap:setup
npm run swap:submit
```

`swap:setup` uses ordinary transactions to approve a limited amount of USDC and to create and seed the WSEI/USDC pair if the factory has no live pair. `swap:submit` alternates SEI to USDC and USDC to SEI swaps through independent lanes and reports outcomes from EntryPoint events rather than one RPC read per swap. The same knobs apply:

```bash theme={"dark"}
ORDERS=3000 \
LANE_POOL_SIZE=3000 \
MAX_OPS_PER_BUNDLE=4 \
REVERT_ORDER_INDEX=2 \
npm run swap:submit
```

The account must hold enough of both assets for every input-side swap to execute regardless of landing order. Set `REVERT_ORDER_INDEX=-1` when you want to measure maximum throughput.

### Optional: see the baseline you are escaping

```bash theme={"dark"}
# from the app directory, where the walkthrough left off
npm run baseline
```

Using a gas-only relayer key so nothing of value is at risk, `baseline` measures the constraint directly. It sends nonce `n + 1` while deliberately skipping `n`, waits to see whether that transaction can be included above a gap, fills the gap with nonce `n`, then checks whether the skipped-ahead transaction lands once the gap is closed. The verdict is one of four outcomes:

| Verdict                                     | What the RPC path did                                | What happened to the out-of-order transaction                                     |
| ------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------- |
| `REJECTED at admission`                     | Refused the gapped nonce, typically with `bad nonce` | Refused synchronously, so the sender learns at once and no gap forms on this path |
| `ADMITTED then dropped`                     | Returned a hash, then discarded the transaction      | Lost without an error; it did not land even after the gap was filled              |
| `QUEUED, then included once the gap filled` | Held it and released it in nonce order               | Waited, then landed after nonce `n` arrived                                       |
| `ACCEPTED and included above a gap`         | Included it out of order                             | Landed before nonce `n`; this path does not enforce nonce order                   |

The probe measures gap handling only. How many consecutive nonces one sender can have outstanding is a separate question, answered in [Why this is hard to replicate](#why-this-is-hard-to-replicate): many, as long as they arrive in order.

The probe samples both the `latest` and `pending` nonce tags before, during, and after, and prints any rejection message verbatim, because the exact message is the finding. If the two tags already disagree before the probe starts, it warns you: either the account has work in flight or the node reports a mempool-derived pending nonce, and in both cases the gap it is about to create may not be a gap at all, so the verdict cannot be attributed to nonce ordering. Compare the result with a `submit` run, where 24 operations from one account are mutually independent and one failure strands nothing.

The verdict depends on the path. In one session, two Atlantic-2 RPC endpoints answered differently: a dedicated provider queued the gapped transaction and released it once the gap filled, while the public endpoint returned a hash and then dropped it, so it never landed even after the gap was filled. Neither rejected it at admission. Probe the path you will actually use.

## Tuning

Three knobs shape a run. They interact, so change one at a time and measure.

<AccordionGroup>
  <Accordion title="Lane pool size: the ceiling on in-flight operations">
    One lane holds at most one in-flight operation, so `LANE_POOL_SIZE` is the hard ceiling on unresolved UserOperations in a process. Larger pools permit more concurrently unresolved intents, add startup `getNonce` reads (batched with bounded concurrency so a public RPC does not rate-limit you), and increase the recovery state you must understand after a failure. `ORDERS` must not exceed `LANE_POOL_SIZE`; the application rejects that configuration instead of silently submitting fewer operations.
  </Accordion>

  <Accordion title="Bundle width: gas efficiency against isolation">
    `MAX_OPS_PER_BUNDLE` trades amortized outer-transaction overhead for the size of the shared validation failure domain. A width of `1` gives maximum isolation and the highest overhead. Execution reverts stay per-operation at any width. The relayer caps signed transaction gas below the block gas limit it read at startup and rejects a bundle whose estimate cannot fit.

    A bundle does not need the sum of its operations' declared call gas. The EntryPoint checks before each operation that enough gas remains to honor that operation's `callGasLimit`, and whatever an operation leaves unspent passes to the next one, so the outer transaction needs the gas the bundle actually consumes plus about one operation's declared limit in reserve. Over-declaring still has a cost: v0.8 charges 10 percent of unused call gas beyond a 40,000-gas threshold, and each operation reserves prefund from the deposit against its declared limits rather than its measured cost, so an inflated limit ties up deposit that is only refunded afterwards.

    That is why `CALL_GAS_LIMIT` is unset by default. Both submit paths measure the real call gas for the chain and call shape and add 25 percent; the variable is only a floor, and it has an effect only when you set it above that measurement. Because the EntryPoint reserves each operation's declared call gas before running it, a floor above the measurement costs block gas, and therefore operations per block, without changing the gas actually used.

    As a reference point, on Atlantic-2 with `CALL_GAS_LIMIT=500000`, the default in an earlier revision of the repository, the real-swap path sustained 77 operations per bundle; 78 no longer fit the 12,500,000 block gas limit in effect during that run and failed safely during simulation. Width 76 produced the best observed submission rate for that call shape, 47.7 landed swaps per second. These are measurements for one call shape, one configuration, and one network state, not protocol limits.

    The mock venue is a heavier call. In the [benchmark session](#benchmarks), with `CALL_GAS_LIMIT` unset, each `place` operation used about 331,000 gas of outer transaction gas, so 36 operations was the widest bundle that fit and 37 failed safely in simulation. Widths 8, 16, and 36 packed 4, 2, and 1 bundles into a 12,500,000-gas block; width 9, which should fit 4, landed 3. Wide bundles also queue behind each other, because only one 11,700,000-gas bundle fits a block. With more than about 8 relayers at width 32 or 36, receipt waits exceed the default `BUNDLE_RECEIPT_TIMEOUT_MS`; raise the timeout for those shapes instead of paying for fee-bumped replacements of bundles that land anyway.
  </Accordion>

  <Accordion title="Relayer count: concurrent outer transaction streams">
    Each relayer has one sequential outer transaction stream. Under favorable admission and inclusion conditions, the immediate submission width is roughly `RELAYER_COUNT × MAX_OPS_PER_BUNDLE` per block. That is a planning heuristic, not a throughput guarantee: RPC latency, block limits, state contention, gas, and producer policy still apply. Public RPC endpoints have [rate limits](/evm/networks); use a [dedicated provider](/learn/rpc-providers) or your own node for anything beyond a demo.

    One relayer's bundle cycle is five sequential RPC round trips (gas estimate, fee estimate, block number, broadcast, receipt poll) plus inclusion, about 1.5 seconds on a 120 ms endpoint. Below roughly 120 operations in flight the pool is the bottleneck and throughput scales with `RELAYER_COUNT`; above that, block gas is. In the [benchmark session](#benchmarks), 4 relayers landed 2.7, 8.9, 15.3, 24.3, 45.4, and 50.5 operations per second at widths 1, 4, 8, 16, 32, and 36, and width 4 landed 2.2, 8.9, 15.4, 27.9, and 47.2 per second with 1, 4, 8, 16, and 32 relayers.
  </Accordion>
</AccordionGroup>

### Configuration reference

The application always loads `.env` from the repository root. The variables you are most likely to change:

| Variable                       | Default                       | Meaning                                                                                                      |
| ------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `SEI_CHAIN_ID`                 | `1328`                        | `1328` (Atlantic-2) or `1329` (Pacific-1)                                                                    |
| `SEI_RPC_URL`                  | viem chain default            | HTTP endpoint; its reported chain ID must match before any write                                             |
| `ALLOW_MAINNET`                | `0`                           | Must be `1` for writes to a remote Pacific-1 RPC                                                             |
| `TRADER_PRIVATE_KEY`           | required                      | The funded account that signs every UserOperation                                                            |
| `RELAYER_MNEMONIC`             | required                      | Fresh BIP-39 mnemonic used only for gas-paying relayers                                                      |
| `RELAYER_COUNT`                | `4`                           | Relayer workers, `1..256`                                                                                    |
| `RELAYER_START_INDEX`          | `0`                           | First derivation index; offset it if the mnemonic also holds the funded account                              |
| `RELAYER_FUNDING`              | `0.5`                         | Target SEI balance per relayer for `fund`                                                                    |
| `ENTRYPOINT_DEPOSIT`           | `1`                           | Target deposit in the EntryPoint for `fund`                                                                  |
| `LANE_ACCOUNT_IMPL`            | unset                         | Deployed `LaneAccount`; required by `delegate` and `submit`                                                  |
| `VENUE`                        | unset                         | Deployed demo target; required by `submit`                                                                   |
| `ORDERS`                       | `24`                          | Operations per run; must not exceed `LANE_POOL_SIZE`                                                         |
| `LANE_POOL_SIZE`               | `32`                          | `1..4096` lanes and the in-flight ceiling                                                                    |
| `MAX_OPS_PER_BUNDLE`           | `4`                           | `1..LANE_POOL_SIZE` operations sharing one validation domain                                                 |
| `REVERT_ORDER_INDEX`           | `2`                           | Order that receives an unfillable limit price; `-1` disables                                                 |
| `CALL_GAS_LIMIT`               | unset                         | Optional floor for each operation's declared call gas; when unset, the live estimate plus 25 percent is used |
| `BUNDLE_RECEIPT_TIMEOUT_MS`    | `12000`                       | Receipt wait before same-nonce replacement                                                                   |
| `RECEIPT_POLLING_INTERVAL_MS`  | `250`                         | Receipt poll interval, `10..60000`; viem's own default is `4000`                                             |
| `BUNDLE_MAX_ATTEMPTS`          | `3`                           | Same-nonce attempts per process invocation                                                                   |
| `REPLACEMENT_FEE_BUMP_PERCENT` | `25`                          | Fee increase per replacement, `10..1000`                                                                     |
| `OPERATION_JOURNAL_PATH`       | `app/.state/pending-ops.json` | Durable signed-operation journal                                                                             |

The [repository README](https://github.com/sei-protocol/sei-nonce-lanes#configuration) documents the full set, including the real-swap variables.

## Benchmarks

`app/bench/` measures the lane path against the baseline it replaces: one EOA sending ordinary transactions with sequential nonces. Every mode calls the same `MockPerpVenue.place` on the configured `VENUE`, so the numbers differ only in how the calls were submitted. The commands read the root `.env`, and the shell environment overrides it:

```bash theme={"dark"}
cd app

# Lane throughput for one run shape. Same safety path as submit, outcomes read
# from the bundle receipts, one JSON line appended to .state/bench/lanes-results.jsonl.
ORDERS=1024 LANE_POOL_SIZE=1024 MAX_OPS_PER_BUNDLE=16 RELAYER_COUNT=16 \
REVERT_ORDER_INDEX=-1 BENCH_LABEL="16x16" npm run bench:lanes

# The same over several "RELAYER_COUNT MAX_OPS_PER_BUNDLE" combinations.
BUNDLE_RECEIPT_TIMEOUT_MS=60000 npm run bench:sweep -- sat "32 8" "16 16" "16 36"

# One EOA with sequential nonces, every nonce taken from one confirmed read.
BENCH_MODE=serial        BENCH_TXS=30   npm run bench:baseline
BENCH_MODE=pipelined     BENCH_TXS=200  npm run bench:baseline
BENCH_MODE=batch         BENCH_TXS=1024 npm run bench:baseline
BENCH_MODE=pipelined-gap BENCH_TXS=100  BENCH_GAP_INDEX=50 npm run bench:baseline
BENCH_MODE=fleet         BENCH_TXS=64   BENCH_FLEET_SIZE=8 BENCH_FUND_SEI=4 npm run bench:baseline

# What the chain included, block by block, independent of any client clock.
REPORT_FROM_BLOCK=<first block> REPORT_TO_BLOCK=<last block> npm run bench:report
npm run bench:latency
```

The `bench:baseline` modes are the ways one address can drive a sequential queue. `serial` sends one transaction and waits for its receipt before the next. `pipelined` signs everything up front and broadcasts in nonce order without waiting. `pipelined-gap` does the same but never broadcasts the transaction at `BENCH_GAP_INDEX`, then repairs the gap. `batch` sends everything in one JSON-RPC batch request. `fleet` derives `BENCH_FLEET_SIZE` wallets from `RELAYER_MNEMONIC`, funds them from the account, and runs `pipelined` on each at once.

`bench:lanes` shares the account-wide run lock with `submit` and `swap:submit` but keeps its own journal under `app/.state/bench/`, so a benchmark never replays the tutorial's pending operations. If a `submit` run was interrupted, recover it with `npm run submit` first. The remaining `BENCH_*` and `REPORT_*` knobs are documented in the [repository README](https://github.com/sei-protocol/sei-nonce-lanes#benchmarks).

### Measured against the alternatives

Every row below comes from one measurement session on Atlantic-2. Conditions during the session: block gas limit 12,500,000; base fee 50 gwei plus a 1 gwei tip; about 2.0 blocks per second under load; 120 ms warm request latency to the configured endpoint. Each direct `place` transaction used 328,425 gas and each lane operation about 331,000 gas of outer transaction gas, so the block admits 36 to 38 operations however they are submitted: roughly 73 landed operations per second for this call shape. Chain-side rates divide landed operations by the block-timestamp span, which Sei stamps in whole seconds. Client-side rates divide by wall time from first broadcast to last receipt.

| Submission                                         | Landed ops/s, chain-side | Client-side | Block gas used, average |
| -------------------------------------------------- | ------------------------ | ----------- | ----------------------- |
| One EOA, serial send and wait (30 tx)              | 1.0                      | 1.0         | not recorded            |
| One EOA, pipelined, one request in flight (200 tx) | 6.9                      | 6.7         | not recorded            |
| Tutorial lanes, 4 relayers × 4 per bundle (24 ops) | 8.0                      | 5.7         | 11%                     |
| Fleet of 4 wallets × 100 tx                        | 26.7                     | 24.8        | not recorded            |
| Fleet of 8 wallets × 64 tx                         | 51.2                     | 49.7        | 54%                     |
| One EOA, 1,024 tx in one JSON-RPC batch            | 51.2                     | 47.3        | 68%                     |
| Lanes, 32 relayers × 8 per bundle (1,024 ops)      | 60.2                     | 55.3        | 83%                     |
| Lanes, 16 relayers × 16 per bundle (1,024 ops)     | 64.0                     | 54.6        | 81%                     |
| Lanes, 16 relayers × 36 per bundle (1,024 ops)     | 64.0                     | 55.1        | 88%                     |

What the numbers show:

* Block gas, not the nonce model, set the ceiling. Lanes came closest to it, with 28 of 31 blocks at least 90 percent full in the 16 × 36 run, from one address whose EVM nonce never moved. A single ordered queue matched the fleet only when every transaction left in one JSON-RPC batch; one request at a time, it is bounded by the round trip, about 7 per second.
* Small relayer pools are client-bound. Throughput scales with `RELAYER_COUNT` until roughly 120 operations are in flight, then block gas takes over. The relayer and width series are in [Tuning](#tuning).
* A gap costs the whole queue. One lost transaction in the sequential queue (`pipelined-gap`, nonce 50 of 100) left the 49 transactions behind it accepted but unmined until the client resent it 15 seconds later. In the lane runs, the deliberately reverting order consumed only its own lane while the rest of its bundle landed.
* RPC paths differ. `npm run baseline` gave [two different verdicts](#optional-see-the-baseline-you-are-escaping) for two Atlantic-2 endpoints in the same session.

<Note>
  These are measurements of one call shape, one network state, and one client machine, not protocol limits. Cost was about 0.02 SEI per landed operation at the effective 52 gwei. Rerun the benchmarks against the network and endpoint you will actually use; block time and gas limits differ between [Twin Turbo Consensus](/learn/twin-turbo-consensus) and [Sei Giga](/learn/sei-giga). The raw records under `app/.state/bench/` include a journal for `bench:lanes`, which you should treat like `pending-ops.json`.
</Note>

## Adapting it to your contract

The demo's `MockPerpVenue` is a stand-in that reverts on slippage so a failure is observable. Swapping it for a real target means encoding a different call. `LaneAccount.execute(target, value, data)` forwards any call, and the target sees the funded EOA as `msg.sender`:

```ts theme={"dark"}
import { encodeFunctionData } from 'viem';
import { buildOp, signUserOp, userOpHash } from './userop.js';

// Any call your contract accepts from the funded EOA.
const data = encodeFunctionData({
  abi: targetAbi,
  functionName: 'placeOrder',
  args: [market, side, size, limitPrice],
});

// One lane per in-flight intent. The pool tracks sequences locally.
const slot = lanePool.acquire();
if (!slot) throw new Error('lane pool exhausted; raise LANE_POOL_SIZE');

const unsigned = buildOp({
  sender: account.address, // the delegated EOA
  lane: slot.lane,
  seq: slot.seq,
  target: TARGET,
  value: 0n, // native SEI to forward, if the call needs it
  data,
  verificationGasLimit,
  callGasLimit,
  preVerificationGas,
  maxFeePerGas,
  maxPriorityFeePerGas,
});

const op = await signUserOp(account, unsigned, chain.id, ENTRY_POINT);
bundlingQueue.add({
  op,
  hash: userOpHash(op, chain.id, ENTRY_POINT),
  lane: slot.lane,
  seq: slot.seq,
  orderId,
  label: '',
});
```

The real-swap path in `app/src/swap-submit.ts` is a complete example of this against a live router, including forwarding native SEI as `value`.

Check these before you trust a new target:

* **`msg.sender` and `tx.origin`.** At the target, `msg.sender` is the funded EOA and `tx.origin` is the gas-paying relayer. Contracts that require `tx.origin == msg.sender` are incompatible. Audit each router, approval path, callback, reentrancy assumption, and authorization rule.
* **Gas on Sei.** Storage writes cost materially more than on Ethereum. The application estimates the delegated call live and declares that estimate plus 25 percent, raised to `CALL_GAS_LIMIT` only when you set that floor higher. Do not copy Ethereum-sized static limits in either direction: too low runs out of gas, and too high reserves block gas your operations never use. See [Gas and fees](/evm/evm-parity/gas-and-fees).
* **Storage contention.** Lanes remove submission ordering, not execution conflicts. Analyze which storage slots your calls touch; see [Optimizing for parallelization](/evm/best-practices/optimizing-for-parallelization).
* **Lane policy.** One lane per in-flight intent is the simplest correct policy. If some calls must stay ordered relative to each other, put them on one lane (or on `ADMIN_LANE`) rather than falling back to lane `0`.

### Before you adapt this

The repository is explicit about what it leaves out. Before this design touches real funds, add at least:

* audited account and integration contracts;
* hardware-backed or remote signing;
* a real risk engine with an idempotent intent model;
* durable, replicated queue and reconciliation storage;
* metrics, tracing, alerting, and structured logs;
* controlled deployment and delegation procedures;
* RPC redundancy and chain-specific fee policy;
* graceful shutdown and operator runbooks; and
* load, fault-injection, and live-chain recovery testing.

<Warning>
  EIP-7702 changes the code that executes at your address. Before delegating, verify the implementation source and deployed address, verify the target chain, inspect any existing delegation, and use a throwaway account for this demo. `submit` refuses to run if the current designator does not exactly match `LANE_ACCOUNT_IMPL`.
</Warning>

Treat `.env`, `app/.state/`, signed raw transactions, and RPC URLs containing credentials as sensitive. The journal does not contain private keys, but it contains signed UserOperations and replayable raw transactions until their nonces are consumed. Clone the repository for a teammate and create fresh keys; do not copy a working directory.

## Troubleshooting

<AccordionGroup>
  <Accordion title="RPC chain ID does not match">
    Check both `SEI_CHAIN_ID` and `SEI_RPC_URL`. For a local fork, pass `--chain-id 1328` to Anvil. The configured chain ID is part of the EIP-712 signature domain and cannot be guessed safely.
  </Accordion>

  <Accordion title="EntryPoint v0.8 … MISSING">
    The RPC has no code at `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108`. Confirm the chain and the fork source before deploying anything. The preflight checks for code presence, not byte-for-byte identity; verify canonical addresses independently before a real deployment.
  </Accordion>

  <Accordion title="Delegation mismatch">
    Run `npm run status` and compare `delegated to` with `LANE_ACCOUNT_IMPL`. Do not blindly replace an unexpected designator. Confirm the account, chain, and implementation first, then run `npm run delegate` deliberately.
  </Accordion>

  <Accordion title="Relayer has no gas">
    Run `npm run fund`, or send SEI to relayer `0` and run `npm run dispense`.
  </Accordion>

  <Accordion title="simulation failed: AA…">
    Common validation causes: `AA24` is an invalid signature or the wrong EIP-712 chain or domain; `AA25` is a stale or incorrect lane sequence; an insufficient EntryPoint prefund; or delegation to the wrong account implementation. Run `npm run status` and resolve the cause before widening bundles or retrying.
  </Accordion>

  <Accordion title="AA95 out of gas while widening bundles">
    `AA95` is the EntryPoint's report that the outer transaction had less gas left than an operation's declared limits require. In general it points to an outer gas limit or estimation headroom that is too low, not to network capacity. While widening bundles, the usual cause is the block gas limit: simulation cannot find an outer gas amount under that limit that satisfies the check, and the relayer never signs above the limit it read at startup. Nothing in a bundle that fails simulation is broadcast or consumed. Rerun with a smaller `MAX_OPS_PER_BUNDLE`; the durable queue is repacked at the smaller width.
  </Accordion>

  <Accordion title="Journal lock is owned by another process">
    Only one lane-based process may use an account at a time, even when `submit` and `swap:submit` use different journals. Stop the other process. A lock whose recorded PID is no longer alive is removed automatically on the next run.

    `could not acquire the lock … within 2000ms` is different. The lock file exists but its contents could not be parsed, so the process cannot tell whether a live holder owns it, and it refuses to remove a lock it cannot attribute. A torn or truncated file left by a dead process and a live holder caught mid-publish produce the same message, and the message repeating across runs does not distinguish between them. Removing a lock that a live process holds lets two processes sign for the same account, which produces duplicate operations on the same lane sequences and conflicting relayer nonces.

    Before deleting a lock file by hand, confirm that no `submit`, `swap:submit`, or `bench:lanes` process is running against this account. Read the `pid` from the file if it is legible, and check the process table on every host that shares the state directory. Both locks live under `app/.state/`: the account-wide run lock (`sender-<chainId>-<address>.lock`, or `SENDER_RUN_LOCK_PATH`) and one `.lock` file next to each journal. Delete the file only after that check.
  </Accordion>

  <Accordion title="A bundle remains in pending recovery">
    Do not delete the journal and do not send the relayer's next nonce manually. Run `npm run submit` again once the RPC can answer receipt and nonce queries. If the application reports partial lane consumption or a state it cannot reconcile, stop and inspect the EntryPoint events, every attempted transaction hash, the relayer's confirmed nonce, and each lane sequence.
  </Accordion>
</AccordionGroup>

## Resources

<CardGroup cols={2}>
  <Card title="sei-nonce-lanes" icon="github" href="https://github.com/sei-protocol/sei-nonce-lanes">
    Source, tests, benchmarks, and the full configuration reference.
  </Card>

  <Card title="EIP-7702: Set EOA account code" icon="file-lines" href="https://eips.ethereum.org/EIPS/eip-7702">
    The delegation mechanism that keeps the funded address.
  </Card>

  <Card title="ERC-4337: Account abstraction" icon="file-lines" href="https://eips.ethereum.org/EIPS/eip-4337">
    UserOperations, the EntryPoint, and two-dimensional nonces.
  </Card>

  <Card title="ERC-7562: Validation and mempool rules" icon="file-lines" href="https://eips.ethereum.org/EIPS/eip-7562">
    The alt-mempool rules the in-process bundling queue sidesteps.
  </Card>

  <Card title="Finality and block tags" icon="clock" href="/evm/evm-parity/finality">
    Why one confirmation is final and the pending view is unreliable.
  </Card>

  <Card title="Transaction types" icon="list" href="/evm/evm-parity/transaction-types">
    Type-4 support and authorization list requirements on Sei.
  </Card>

  <Card title="Pimlico" icon="rocket" href="/evm/wallet-integrations/pimlico">
    A hosted ERC-4337 bundler and paymaster, if you want sponsorship instead of throughput.
  </Card>

  <Card title="Thirdweb EIP-7702" icon="wallet" href="/evm/wallet-integrations/thirdweb-7702">
    EIP-7702 delegation for consumer wallet flows.
  </Card>
</CardGroup>
