> ## Documentation Index
> Fetch the complete documentation index at: https://seilabs-docs-evm-cookbook.mintlify.site/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 NonceLaneScalingSeries = () => {
  const ink = 'currentColor';
  const accent = 'var(--sei-maroon-50)';
  const warn = 'var(--sei-diagram-warning, #b45309)';
  const yTop = 62;
  const yBottom = 252;
  const yFor = v => yBottom - v / 80 * (yBottom - yTop);
  const ceiling = 73;
  const panels = [{
    title: '4 relayers, by bundle width',
    axis: 'MAX_OPS_PER_BUNDLE',
    x0: 78,
    step: 70,
    cats: ['1', '4', '8', '16', '32', '36'],
    chain: [3.2, 9.8, 17.1, 28.4, 51.2, 56.9],
    client: [2.7, 8.9, 15.3, 24.3, 45.4, 50.5]
  }, {
    title: 'width 4, by relayer count',
    axis: 'RELAYER_COUNT',
    x0: 518,
    step: 84,
    cats: ['1', '4', '8', '16', '32'],
    chain: [2.7, 9.8, 17.1, 32.0, 51.2],
    client: [2.2, 8.9, 15.4, 27.9, 47.2]
  }];
  const points = (p, series) => series.map((v, i) => `${p.x0 + i * p.step},${yFor(v)}`).join(' ');
  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 312" role="img" aria-label="Landed operations per second as bundle width grows at 4 relayers, and as the relayer count grows at width 4. Both series rise toward the block-gas ceiling of about 73 per second" style={{
    width: '100%',
    minWidth: 640,
    height: 'auto',
    display: 'block'
  }}>
          {panels.map(p => {
    const left = p.x0 - 40;
    const right = p.x0 + (p.cats.length - 1) * p.step + 30;
    return <g key={p.title}>
                <text x={left} y={24} fontSize="13" fontWeight="600" fill={ink}>{p.title}</text>
                <text x={left} y={44} fontSize="9.5" fill={ink} fillOpacity="0.6">landed operations per second</text>
                {[0, 20, 40, 60, 80].map(v => <g key={p.title + v}>
                    <line x1={left + 22} y1={yFor(v)} x2={right} y2={yFor(v)} stroke={ink} strokeOpacity="0.12" strokeWidth="1" />
                    <text x={left + 16} y={yFor(v) + 3.5} fontSize="9" textAnchor="end" fill={ink} fillOpacity="0.6">{v}</text>
                  </g>)}
                <line x1={left + 22} y1={yFor(ceiling)} x2={right} y2={yFor(ceiling)} stroke={warn} strokeWidth="1.2" strokeDasharray="4 3" />
                <text x={right} y={yFor(ceiling) - 5} fontSize="9" textAnchor="end" fill={ink} fontWeight="600">block-gas ceiling ≈ 73</text>
                <polyline points={points(p, p.client)} fill="none" stroke={ink} strokeOpacity="0.45" strokeWidth="1.4" strokeDasharray="5 3" />
                <polyline points={points(p, p.chain)} fill="none" stroke={accent} strokeWidth="1.8" />
                {p.cats.map((c, i) => {
      const x = p.x0 + i * p.step;
      const low = yFor(p.client[i]) + 15 > yBottom - 4;
      return <g key={p.title + c}>
                      <circle cx={x} cy={yFor(p.client[i])} r={3} fill={ink} fillOpacity="0.45" />
                      <circle cx={x} cy={yFor(p.chain[i])} r={3.4} fill={accent} />
                      <text x={x} y={yFor(p.chain[i]) - 8} fontSize="9" textAnchor="middle" fill={accent} fontWeight="600">{p.chain[i].toFixed(1)}</text>
                      <text x={low ? x + 8 : x} y={low ? yFor(p.client[i]) + 3.5 : yFor(p.client[i]) + 15} fontSize="8.5" textAnchor={low ? 'start' : 'middle'} fill={ink} fillOpacity="0.65">{p.client[i].toFixed(1)}</text>
                      <text x={x} y={yBottom + 16} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.8">{c}</text>
                    </g>;
    })}
                <text x={(left + 22 + right) / 2} y={yBottom + 32} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.6" fontFamily="ui-monospace, SFMono-Regular, Menlo, monospace">{p.axis}</text>
              </g>;
  })}
          <line x1={330} y1={302} x2={352} y2={302} stroke={accent} strokeWidth="1.8" />
          <circle cx={341} cy={302} r={3.4} fill={accent} />
          <text x={358} y={305.5} fontSize="9.5" fill={ink} fillOpacity="0.8">chain-side</text>
          <line x1={430} y1={302} x2={452} y2={302} stroke={ink} strokeOpacity="0.45" strokeWidth="1.4" strokeDasharray="5 3" />
          <circle cx={441} cy={302} r={3} fill={ink} fillOpacity="0.45" />
          <text x={458} y={305.5} fontSize="9.5" fill={ink} fillOpacity="0.8">client-side</text>
        </svg>
      </div>
      <div className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">Categories are evenly spaced, not to scale. While the pool is the bottleneck, both series climb almost linearly: each relayer's bundle cycle is about 1.5 seconds of sequential RPC round trips plus inclusion. They flatten as the in-flight work approaches what a 12,500,000-gas block can hold. Runs used 32 to 1,024 operations, so the shorter ones read higher on the chain-side scale than they would sustain. The sweep point with 4 relayers and a bundle width of 4 used 32 operations. The tutorial comparison used 24, so their rates differ despite the same bundle shape.</div>
    </div>;
};

export const NonceLaneBlockGasUtilization = () => {
  const ink = 'currentColor';
  const accent = 'var(--sei-maroon-50)';
  const warn = 'var(--sei-diagram-warning, #b45309)';
  const rows = [{
    label: 'tutorial lanes, 4 relayers × 4 per bundle',
    avg: 10.6,
    max: 31.1,
    full: '0 of 7'
  }, {
    label: 'fleet of 8 hot wallets × 64 tx',
    avg: 54.4,
    max: 66.7,
    full: '0 of 25'
  }, {
    label: '1 EOA, 1,024 tx in one JSON-RPC batch',
    avg: 68.1,
    max: 97.2,
    full: '24 of 40'
  }, {
    label: 'lanes, 16 relayers × 16 per bundle',
    avg: 81.1,
    max: 91.4,
    full: '1 of 34'
  }, {
    label: 'lanes, 32 relayers × 8 per bundle',
    avg: 82.5,
    max: 94.6,
    full: '1 of 34'
  }, {
    label: 'lanes, 16 relayers × 36 per bundle',
    avg: 88.4,
    max: 98.1,
    full: '28 of 31'
  }];
  const x0 = 306;
  const scale = 4.3;
  const rowTop = i => 62 + i * 40;
  const plotBottom = rowTop(rows.length) - 6;
  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 350" role="img" aria-label="Average and maximum block gas used as a percentage of the 12,500,000 limit during each benchmark run, with the number of blocks at least 90 percent full" style={{
    width: '100%',
    minWidth: 640,
    height: 'auto',
    display: 'block'
  }}>
          <text x={20} y={24} fontSize="13" fontWeight="600" fill={ink}>Block gas used during each run, percent of the 12,500,000 limit</text>
          <rect x={x0} y={37} width={10} height={10} rx={2} fill={accent} fillOpacity="0.9" />
          <text x={x0 + 15} y={45} fontSize="9.5" fill={ink} fillOpacity="0.8">average over the run's blocks</text>
          <line x1={x0 + 190} y1={36} x2={x0 + 190} y2={48} stroke={ink} strokeWidth="1.6" />
          <text x={x0 + 197} y={45} fontSize="9.5" fill={ink} fillOpacity="0.8">fullest block</text>
          <text x={770} y={45} fontSize="9.5" fill={ink} fillOpacity="0.8" fontWeight="600">blocks ≥ 90% full</text>

          {[0, 25, 50, 75, 100].map(v => <g key={'g' + v}>
              <line x1={x0 + v * scale} y1={58} x2={x0 + v * scale} y2={plotBottom} stroke={ink} strokeOpacity="0.12" strokeWidth="1" />
              <text x={x0 + v * scale} y={plotBottom + 14} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.6">{v}%</text>
            </g>)}
          <line x1={x0 + 90 * scale} y1={58} x2={x0 + 90 * scale} y2={plotBottom} stroke={warn} strokeWidth="1.2" strokeDasharray="4 3" />
          <text x={x0 + 90 * scale} y={plotBottom + 28} fontSize="9.5" textAnchor="middle" fill={ink} fontWeight="600">90%: the chain, not the client, is the limit</text>

          {rows.map((r, i) => {
    const y = rowTop(i);
    const xMax = x0 + r.max * scale;
    const inside = r.avg >= 60;
    return <g key={r.label}>
                <text x={x0 - 12} y={y + 20} fontSize="10.5" textAnchor="end" fill={ink}>{r.label}</text>
                <rect x={x0} y={y + 8} width={r.avg * scale} height={16} rx={2} fill={accent} fillOpacity="0.9" />
                <text x={inside ? x0 + r.avg * scale - 5 : x0 + r.avg * scale + 4} y={y + 20} fontSize="9.5" textAnchor={inside ? 'end' : 'start'} fill={inside ? '#ffffff' : ink} fontWeight={inside ? '600' : '400'}>{r.avg.toFixed(1)}%</text>
                <line x1={xMax} y1={y + 5} x2={xMax} y2={y + 27} stroke={ink} strokeWidth="1.6" />
                <text x={xMax + 4} y={y + 20} fontSize="9" fill={ink} fillOpacity="0.65">{r.max.toFixed(1)}%</text>
                <text x={770} y={y + 20} fontSize="10.5" fill={ink} fillOpacity="0.85">{r.full}</text>
              </g>;
  })}
        </svg>
      </div>
      <div className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">Read from the chain with the repository's block report over each run's block range. The tutorial shape leaves blocks almost empty, so its rate is the client's, not the chain's. The lanes run with 16 relayers and 36 operations per bundle filled 28 of 31 blocks past 90 percent. The batch run's fullest block came within one percentage point of it, 97.2 percent compared with 98.1 percent, but averaged 68.1 percent compared with 88.4 percent for lanes.</div>
    </div>;
};

export const NonceLaneThroughputComparison = () => {
  const ink = 'currentColor';
  const accent = 'var(--sei-maroon-50)';
  const warn = 'var(--sei-diagram-warning, #b45309)';
  const rows = [{
    label: '1 EOA, serial: send, wait, send (30 tx)',
    chain: 1.0,
    client: 1.0,
    lanes: false
  }, {
    label: '1 EOA, pipelined, 1 request in flight (200 tx)',
    chain: 6.9,
    client: 6.7,
    lanes: false
  }, {
    label: 'tutorial lanes, 4 relayers × 4 per bundle (24 ops)',
    chain: 8.0,
    client: 5.7,
    lanes: true
  }, {
    label: 'fleet of 4 hot wallets × 100 tx',
    chain: 26.7,
    client: 24.8,
    lanes: false
  }, {
    label: 'fleet of 8 hot wallets × 64 tx',
    chain: 51.2,
    client: 49.7,
    lanes: false
  }, {
    label: '1 EOA, 1,024 tx in one JSON-RPC batch',
    chain: 51.2,
    client: 47.3,
    lanes: false
  }, {
    label: 'lanes, 32 relayers × 8 per bundle (1,024 ops)',
    chain: 60.2,
    client: 55.3,
    lanes: true
  }, {
    label: 'lanes, 16 relayers × 16 per bundle (1,024 ops)',
    chain: 64.0,
    client: 54.6,
    lanes: true
  }, {
    label: 'lanes, 16 relayers × 36 per bundle (1,024 ops)',
    chain: 64.0,
    client: 55.1,
    lanes: true
  }];
  const ceiling = 73;
  const x0 = 316;
  const scale = 6.5;
  const rowTop = i => 62 + i * 40;
  const plotBottom = rowTop(rows.length) - 6;
  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 468" role="img" aria-label="Landed operations per second for one EOA with sequential nonces, fleets of hot wallets, and nonce lanes, all calling the same venue on Sei Testnet. The block-gas ceiling is about 73 per second" style={{
    width: '100%',
    minWidth: 640,
    height: 'auto',
    display: 'block'
  }}>
          <text x={20} y={24} fontSize="13" fontWeight="600" fill={ink}>Landed operations per second, same venue call, Sei Testnet</text>
          <rect x={x0} y={37} width={10} height={10} rx={2} fill={accent} />
          <text x={x0 + 15} y={45} fontSize="9.5" fill={ink} fillOpacity="0.8">chain-side: landed ÷ block-timestamp span</text>
          <rect x={x0 + 240} y={37} width={10} height={10} rx={2} fill={ink} fillOpacity="0.3" />
          <text x={x0 + 255} y={45} fontSize="9.5" fill={ink} fillOpacity="0.8">client-side: landed ÷ wall time, first broadcast to last receipt</text>

          {[0, 20, 40, 60, 80].map(v => <g key={'g' + v}>
              <line x1={x0 + v * scale} y1={58} x2={x0 + v * scale} y2={plotBottom} stroke={ink} strokeOpacity="0.12" strokeWidth="1" />
              <text x={x0 + v * scale} y={plotBottom + 14} fontSize="9.5" textAnchor="middle" fill={ink} fillOpacity="0.6">{v}</text>
            </g>)}
          <text x={x0 + 40 * scale} y={plotBottom + 30} fontSize="10" textAnchor="middle" fill={ink} fillOpacity="0.7">landed operations per second</text>

          <line x1={x0 + ceiling * scale} y1={58} x2={x0 + ceiling * scale} y2={plotBottom} stroke={warn} strokeWidth="1.2" strokeDasharray="4 3" />
          <text x={x0 + ceiling * scale + 5} y={rowTop(0) + 12} fontSize="9.5" fill={ink} fontWeight="600">block-gas ceiling</text>
          <text x={x0 + ceiling * scale + 5} y={rowTop(0) + 25} fontSize="9.5" fill={ink}>≈ 73 per second</text>

          {rows.map((r, i) => {
    const y = rowTop(i);
    return <g key={r.label}>
                <text x={x0 - 12} y={y + 22} fontSize="10.5" textAnchor="end" fill={r.lanes ? accent : ink} fontWeight={r.lanes ? '600' : '400'}>{r.label}</text>
                <rect x={x0} y={y + 6} width={r.chain * scale} height={11} rx={2} fill={accent} fillOpacity="0.9" />
                <text x={x0 + r.chain * scale + 4} y={y + 15} fontSize="9.5" fill={ink}>{r.chain.toFixed(1)}</text>
                <rect x={x0} y={y + 19} width={r.client * scale} height={11} rx={2} fill={ink} fillOpacity="0.3" />
                <text x={x0 + r.client * scale + 4} y={y + 28} fontSize="9.5" fill={ink} fillOpacity="0.7">{r.client.toFixed(1)}</text>
              </g>;
  })}
        </svg>
      </div>
      <div className="mt-2 text-xs text-neutral-500 dark:text-neutral-400">One measurement session on Sei Testnet. Every strategy called the same MockPerpVenue.place at about 330,000 gas per call. Block gas, not the nonce model, sets the ceiling. Lanes came within 12 percent of it from one address whose EVM nonce never moved. The single sequential queue matched the hot-wallet fleet only when every transaction left in one JSON-RPC batch. Short runs read high on the chain-side scale because Sei stamps blocks in whole seconds.</div>
    </div>;
};

export const NonceLaneFailureIsolation = () => {
  const ink = 'currentColor';
  const ok = 'var(--sei-diagram-ok, #1a7a00)';
  const bad = 'var(--sei-diagram-error, #c20a00)';
  const warn = 'var(--sei-diagram-warning, #b45309)';
  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-600 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 = 'var(--sei-diagram-ok, #1a7a00)';
  const warn = 'var(--sei-diagram-warning, #b45309)';
  const bad = 'var(--sei-diagram-error, #c20a00)';
  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-600 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-diagram-gold, var(--sei-gold-100))';
  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-600 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-diagram-gold, var(--sei-gold-100))';
  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-600 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 = 'var(--sei-diagram-ok, #1a7a00)';
  const bad = 'var(--sei-diagram-error, #c20a00)';
  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-600 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 = 'var(--sei-diagram-ok, #1a7a00)';
  const bad = 'var(--sei-diagram-error, #c20a00)';
  const warn = 'var(--sei-diagram-warning, #b45309)';
  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-600 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. 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. With it, one funded externally owned account (EOA) holds many mutually independent operations in flight at the same time. The account keeps its address, balance, and approvals.

The pattern applies whenever one funded address needs to do many things that do not depend on each other, for example:

* Market making and order flow
* Liquidation and keeper bots
* Oracle updates
* Payout and claim batching
* 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 you point it at real funds.
</Info>

<Note>
  This page is about submission concurrency. For 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 problem worse than on Ethereum:

* **Strict nonce admission.** Under Giga, the Autobahn producer mempool admits EVM transactions in per-sender nonce order. It 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. 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 Sei Testnet endpoints.
* **No dependable pending view.** Sei does not expose Ethereum-style pending state. [Finality and block tags](/evm/evm-parity/finality#pending-state) tells you not to rely on a pending nonce that differs from the confirmed nonce. `txpool_content` is also [truncated and does not separate pending from queued transactions](/evm/reference). Even where a node answers a pending-nonce query, you cannot use the value 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. It is therefore not an alias for `"latest"`. The finality guidance still marks the pending view as unreliable. The value also 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. However, 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, and 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

The canonical EntryPoint v0.8 singleton is already deployed at `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108` on both Sei Mainnet (chain ID `1329`) and Sei Testnet (chain ID `1328`). Nothing in this guide asks you to deploy one. The EntryPoint stores a UserOperation nonce as a 192-bit key plus a 64-bit sequence:

```text theme={null}
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. 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={null}
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. As a result, `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={null}
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 it increments the sender's nonce for the transaction itself. When the authority is also the sender, the authorization must therefore be signed over `nonce + 1`. Applying it then increments the account's nonce 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's nonce by one: the increment that the authorization itself performs. In both cases, after the designator is in place, the submission path signs only UserOperations. The funded account's EVM nonce then stops moving.

<Note>
  EIP-7702 alone does not create independent nonces. It preserves the account, and 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 the queue is gone when the process exits. It therefore never enters the canonical ERC-4337 alt-mempool. 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 that competing bundlers can safely pack operations from unrelated senders into one bundle. Here, every operation comes from one account that you control, so this 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). It then works through a fixed sequence.

<NonceLaneBundleLifecycle />

Most of the safety comes from two rules:

* **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 the rename. A written record therefore 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. It then 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. A bundle that has already landed can also be reported as timed out. This triggers an unnecessary fee-bumped replacement.

Two more 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 it estimates. That 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 that 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. The process re-checks that it still holds the lock immediately before it signs and before every journal write. A process that lost a lock race therefore stops instead of signing.

Before it creates any new work, 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, and 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. When isolation matters more than amortized cost, keep it small.

## 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 it keeps 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 all of them can be unresolved at the same time. [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 transaction 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 give a large pool of independent intents, not a large number of simultaneous transactions. The approximate per-block submission width is `RELAYER_COUNT × MAX_OPS_PER_BUNDLE`.
</Note>

On Sei Testnet, with the same venue call in every run, one address with sequential nonces landed 1 operation per second when it sent serially. When it pipelined one request at a time, it landed 7 per second. It reached 51 per second only when it sent 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. For that call shape, the block-gas ceiling was about 73 per second. 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 during submission. 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. 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. 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 for the network that you submit to. Base the size on measurements, not assumptions.
</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 Sei Testnet path: a fresh throwaway key funded from the [Sei faucet](/learn/faucet)

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

    ```bash theme={null}
    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={null}
    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 these properties:

    * Different lanes can land in any order.
    * An execution revert affects only its own lane.
    * An operation that is never submitted blocks nothing.
    * A gap on one shared lane reproduces sequential blocking.
    * One validation failure reverts the whole bundle.
    * Lane `0` is rejected.
    * A 50-lane bundle fits in one outer transaction.
  </Step>

  <Step title="Choose a target network">
    Start with a local Prague fork. It carries Sei Testnet's state, including the deployed EntryPoint, but spends only local funds.

    <Danger>
      Never use Anvil, Hardhat, tutorial, or shared test mnemonics on Sei Testnet or Sei Mainnet. Their addresses and keys are public, so anyone can move funds sent to them. An address derived from a published mnemonic may also carry an existing EIP-7702 delegation, and a fork inherits that code. The fork below therefore needs a fresh mnemonic too. It also runs as chain `1328`, so an authorization signed against it is replayable on Sei Testnet itself whenever the account's nonce matches. 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. Do not fall back to Anvil's default mnemonic. Generate one for the fork to use:

        ```bash theme={null}
        cast wallet new-mnemonic
        ```

        Keep this terminal running:

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

        Anvil funds the first ten accounts of whatever mnemonic you give it. A fresh phrase avoids Anvil's published addresses. Do not use the default mnemonic here. A fork keeps any account code that is already attached to those public addresses, and every mutating command refuses a relayer that has code.

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

        Create the configuration and point it at the fork:

        ```bash theme={null}
        cp .env.example .env
        ```

        ```dotenv title=".env" theme={null}
        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. Set `RELAYER_MNEMONIC` to the phrase that you generated. Starting relayers at index `1` keeps the funded account and relayer identities distinct. The application rejects overlapping identities.

        Anvil prints that key in the clear, and you store it in `.env`. Treat the phrase as throwaway, and keep this `.env` pointed at `127.0.0.1`.

        Deploy the demo contracts from the repository root. Anvil unlocks its own accounts, so pass account `0`'s address from its startup output as the sender:

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

      <Tab title="Sei 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={null}
        cast wallet new            # funded account key
        cast wallet new-mnemonic   # relayer mnemonic
        ```

        Fund the address from the [Sei faucet](/learn/faucet). Then create the configuration:

        ```bash theme={null}
        cp .env.example .env
        ```

        ```dotenv title=".env" theme={null}
        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={null}
        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. The key therefore 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={null}
    LANE_ACCOUNT_IMPL=0x...
    VENUE=0x...
    ```

    <Note>
      Mutating commands refuse to write to a remote Sei Mainnet RPC unless you set `ALLOW_MAINNET=1` explicitly. They also refuse remote writes when `TRADER_PRIVATE_KEY` is Anvil account `0` or `RELAYER_MNEMONIC` is the default Anvil mnemonic. The fresh mnemonic above does not trigger that credential guard. Keep its `.env` pointed at `127.0.0.1`. If you point it at Sei Testnet, chain ID `1328` is still valid, and the application cannot know that the change was accidental. These guards 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={null}
    cd app
    npm run status
    ```

    It prints the chain ID and whether there is code at the EntryPoint address `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108`. It also reports the account's current delegation, balances, its EntryPoint deposit, each relayer's confirmed nonce and gas balance, the lane sequences, and the venue state.

    Against current Sei Testnet or Sei Mainnet state, the EntryPoint line reads `present` because the singleton is deployed on both. On a local fork or against a misconfigured endpoint, the check catches missing fork state or the wrong network. Check the relayer lines carefully. Any `HAS CODE/DELEGATION` marker means that the mutating commands refuse that relayer.
  </Step>

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

    This sends one type-4 transaction with an authorization for `LANE_ACCOUNT_IMPL`, and the account itself signs and pays for it. The account's EVM nonce therefore advances by two: once for the transaction, and once for the authorization.

    The command reads the account's confirmed nonce once. It pins both the transaction nonce and the authorization nonce (`nonce + 1`) to that read. It does not let the client fill them from a pending-nonce lookup. This matters because the EVM does not reject a wrong authorization nonce. It 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, the command tells you before it replaces that delegation.
  </Step>

  <Step title="Fund the relayers and the EntryPoint deposit">
    ```bash theme={null}
    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. When the EntryPoint validates each operation, it draws prefund from this deposit. On a public network, you can instead send SEI to relayer `0` and run `npm run dispense`. That command 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={null}
    npm run submit
    ```

    One `submit` process performs the complete run and exits. It runs the preflight, estimates the delegated call's gas, and reads each lane's sequence once. It then signs 24 operations concurrently with no nonce RPCs, journals them, and bundles them. Finally, it 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={null}
=== preflight ===
chain          Sei Testnet (1328)
rpc            http://127.0.0.1:8545
entryPoint     0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 (… bytes)
trader         0x9A3c…1f74
delegated to   0xC7a1…8e05
…
trader nonce   9  <- watch this, it must not move
journal        0 pending op(s)
relayer        0x2Ef8…b310  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
```

Look for these items:

* **`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 it runs 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 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. When the RPC can answer receipt and nonce queries, run `npm run submit` again. Before it creates new work, the process recovers or replaces the bundle at the same relayer nonce. Do not delete the journal. Do not send the relayer's next nonce by hand.

### Optional: real swaps on Sei Testnet

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

```bash theme={null}
# 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. If the factory has no live pair, it also creates and seeds the WSEI/USDC pair. `swap:submit` alternates SEI to USDC and USDC to SEI swaps through independent lanes. It reports outcomes from EntryPoint events rather than one RPC read per swap. The same knobs apply:

```bash theme={null}
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. To measure maximum throughput, set `REVERT_ORDER_INDEX=-1`.

### Optional: see the baseline you are escaping

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

`baseline` measures the constraint directly. It uses a gas-only relayer key, so nothing of value is at risk. The probe deliberately sends nonce `n + 1` without nonce `n`, and waits to see whether that transaction can be included above a gap. Next, it fills the gap with nonce `n` and checks whether the skipped-ahead transaction lands after 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, and 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`, which means 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. [Why this is hard to replicate](#why-this-is-hard-to-replicate) answers it: many, as long as they arrive in order.

The probe samples both the `latest` and `pending` nonce tags before, during, and after the run. It 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. The disagreement means that either the account has work in flight, or the node reports a mempool-derived pending nonce. In both cases, the gap that the probe is about to create may not be a gap at all. You therefore cannot attribute the verdict to nonce ordering.

Compare the result with a `submit` run. There, 24 operations from one account are mutually independent, and one failure strands nothing.

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

## Tuning

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

<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. They also add startup `getNonce` reads and increase the recovery state that you must understand after a failure. The startup reads are batched with bounded concurrency so that a public RPC does not rate-limit you. `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 that it read at startup. It rejects a bundle whose estimate cannot fit.

    A bundle does not need the sum of its operations' declared call gas. Before each operation, the EntryPoint checks that enough gas remains to honor that operation's `callGasLimit`. Whatever an operation leaves unspent passes to the next one. The outer transaction therefore needs the gas that the bundle consumes, plus about one operation's declared limit in reserve.

    Over-declaring still has a cost. EntryPoint v0.8 charges 10 percent of unused call gas beyond a 40,000-gas threshold. Each operation also reserves prefund from the deposit against its declared limits, not its measured cost. An inflated limit therefore ties up deposit that is refunded only 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 it runs the operation, a floor above the measurement costs block gas. It therefore costs operations per block, but it does not change the gas that the operations use.

    As a reference point, the real-swap path sustained 77 operations per bundle on Sei Testnet with `CALL_GAS_LIMIT=500000`. That value was the default in an earlier revision of the repository. A bundle of 78 no longer fit the 12,500,000 block gas limit in effect during that run. It 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. The widest bundle that fit was therefore 36 operations, and a bundle of 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`. For those shapes, raise the timeout 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). For anything beyond a demo, use a [dedicated provider](/learn/rpc-providers) or your own node.

    One relayer's bundle cycle is five sequential RPC round trips (gas estimate, fee estimate, block number, broadcast, receipt poll) plus inclusion. On a 120 ms endpoint, that takes about 1.5 seconds. Below roughly 120 operations in flight, the pool is the bottleneck, and throughput scales with `RELAYER_COUNT`. Above that, block gas is the bottleneck.

    In the [benchmark session](#benchmarks), the bundle-width sweep used 4 relayers and widths of 1, 4, 8, 16, 32, and 36. Its client-side rates were 2.7, 8.9, 15.3, 24.3, 45.4, and 50.5 operations per second. Its chain-side rates were 3.2, 9.8, 17.1, 28.4, 51.2, and 56.9. At width 4, the relayer-count sweep used 1, 4, 8, 16, and 32 relayers. Its client-side rates were 2.2, 8.9, 15.4, 27.9, and 47.2 operations per second. Its chain-side rates were 2.7, 9.8, 17.1, 32.0, and 51.2.

    <NonceLaneScalingSeries />
  </Accordion>
</AccordionGroup>

### Configuration reference

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

| Variable | Default | Meaning |
| - | - | - |
| `SEI_CHAIN_ID` | `1328` | `1328` (Sei Testnet) or `1329` (Sei Mainnet) |
| `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 Sei Mainnet 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 it) |
| `CALL_GAS_LIMIT` | unset | Optional floor for each operation's declared call gas. When it is unset, the application uses the live estimate plus 25 percent |
| `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 that sends 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={null}
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`. It 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`. However, it 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 [repository README](https://github.com/sei-protocol/sei-nonce-lanes#benchmarks) documents the remaining `BENCH_*` and `REPORT_*` knobs.

### Measured against the alternatives

Every row below comes from one measurement session on Sei Testnet. During the session, the block gas limit was 12,500,000, and the base fee was 50 gwei plus a 1 gwei tip. The network produced about 2.0 blocks per second under load, and warm request latency to the configured endpoint was 120 ms.

Each direct `place` transaction used 328,425 gas, and each lane operation used about 331,000 gas of outer transaction gas. The block therefore admits 36 to 38 operations, regardless of how they are submitted. For this call shape, that is roughly 73 landed operations per second. 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.

<NonceLaneThroughputComparison />

| Submission | Landed ops/s, chain-side | Client-side | Block gas: average / fullest / blocks ≥ 90% |
| - | - | - | - |
| 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 | 10.6% / 31.1% / 0 of 7 |
| Fleet of 4 wallets × 100 tx | 26.7 | 24.8 | not recorded |
| Fleet of 8 wallets × 64 tx | 51.2 | 49.7 | 54.4% / 66.7% / 0 of 25 |
| One EOA, 1,024 tx in one JSON-RPC batch | 51.2 | 47.3 | 68.1% / 97.2% / 24 of 40 |
| Lanes, 32 relayers × 8 per bundle (1,024 ops) | 60.2 | 55.3 | 82.5% / 94.6% / 1 of 34 |
| Lanes, 16 relayers × 16 per bundle (1,024 ops) | 64.0 | 54.6 | 81.1% / 91.4% / 1 of 34 |
| Lanes, 16 relayers × 36 per bundle (1,024 ops) | 64.0 | 55.1 | 88.4% / 98.1% / 28 of 31 |

<NonceLaneBlockGasUtilization />

The numbers show four things:

* Block gas, not the nonce model, set the ceiling. Lanes came closest to that ceiling, and they did so from one address whose EVM nonce never moved. In the 16 × 36 run, 28 of 31 blocks were at least 90 percent full. A single ordered queue matched the fleet only when every transaction left in one JSON-RPC batch. With one request at a time, the round trip bounds it to about 7 per second.
* Small relayer pools are client-bound. Throughput scales with `RELAYER_COUNT` until roughly 120 operations are in flight. After that, block gas is the limit. The relayer and width series are in [Tuning](#tuning).
* A gap costs the whole queue. One transaction was lost in the sequential queue (`pipelined-gap`, nonce 50 of 100). The 49 transactions behind it stayed accepted but unmined until the client resent the lost transaction 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 Sei Testnet endpoints in the same session.

<Note>
  These are measurements of one call shape, one network state, and one client machine, not protocol limits. The cost was about 0.02 SEI per landed operation at the effective 52 gwei. Rerun the benchmarks against the network and endpoint that you will 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`. Treat it like `pending-ops.json`.
</Note>

## Adapting it to your contract

The demo's `MockPerpVenue` is a stand-in that reverts on slippage so that you can observe a failure. To replace it with a real target, encode a different call. `LaneAccount.execute(target, value, data)` forwards any call, and the target sees the funded EOA as `msg.sender`:

```ts theme={null}
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. It also forwards 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. It raises the value to `CALL_GAS_LIMIT` only when you set that floor higher. Do not copy Ethereum-sized static limits in either direction. A limit that is too low runs out of gas, and a limit that is too high reserves block gas that 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`). Do not fall 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
* Load, fault-injection, and live-chain recovery testing

<Warning>
  Before you delegate, verify the implementation source, the deployed address, and the target chain. Inspect any existing delegation. Use a throwaway account for this demo. EIP-7702 changes the code that executes at your address. `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 that contain credentials as sensitive. The journal does not contain private keys, but it contains signed UserOperations and replayable raw transactions until their nonces are consumed. For a teammate, clone the repository 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 canonical v0.8 singleton is deployed at `0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108` on both Sei Mainnet and Sei Testnet. A `MISSING` line against current state usually means the configured RPC is not serving Sei. Check `SEI_RPC_URL`. For a local Anvil, check that it was started with `--fork-url` against a Sei endpoint, not as a bare chain. `submit` fails closed on the same condition with `No EntryPoint at … on this chain`, before it signs anything.

    Do not deploy your own EntryPoint to work around it. A second EntryPoint is at a different address, so it has a different EIP-712 domain. It also shares no lane sequences or deposits with the EntryPoint that every other integration on Sei uses.

    Confirm the deployment against the same endpoint. Export `SEI_RPC_URL` from your `.env`, and then run:

    ```bash theme={null}
    cast call 0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108 \
      'eip712Domain()(bytes1,string,string,uint256,address,bytes32,uint256[])' \
      --rpc-url "$SEI_RPC_URL"
    ```

    It returns `ERC4337`, version `1`, its own address, and the chain ID that you queried. Comparing bytecode with another chain's deployment is the wrong test. EntryPoint v0.8 caches that domain separator in an immutable. The runtime code at the canonical address therefore differs by that one word on every chain where it is deployed. It also differs by the chain ID word.
  </Accordion>

  <Accordion title="Delegation mismatch">
    Run `npm run status`, and compare `delegated to` with `LANE_ACCOUNT_IMPL`. Do not blindly replace an unexpected designator. First confirm the account, chain, and implementation. 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="Refuses a relayer that already has code or a delegation">
    Relayers must be plain EOAs, and every mutating command checks that before it signs. The usual cause is a public mnemonic whose addresses already have code or a delegation on Sei. A fork inherits that account code. Generate a fresh mnemonic with `cast wallet new-mnemonic`, and point `RELAYER_MNEMONIC` at it. Fund the new relayers instead of trying to undelegate the old ones.
  </Accordion>

  <Accordion title="simulation failed: AA…">
    `AA24` means an invalid signature, or the wrong EIP-712 chain or domain. `AA25` means a stale or incorrect lane sequence. Other common validation causes are an insufficient EntryPoint prefund and delegation to the wrong account implementation. Run `npm run status`. Resolve the cause before you widen bundles or retry.
  </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. When you widen bundles, the usual cause is the block gas limit. Simulation cannot find an outer gas amount under that limit that satisfies the check. The relayer never signs above the limit that 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 the process could not parse its contents, so it cannot tell whether a live holder owns the lock. It refuses to remove a lock that it cannot attribute. A torn or truncated file left by a dead process produces the same message as a live holder caught mid-publish. A message that repeats across runs does not distinguish between them either.

    If you remove a lock that a live process holds, two processes can sign for the same account. That produces duplicate operations on the same lane sequences and conflicting relayer nonces.

    Before you delete a lock file manually, confirm that no `submit`, `swap:submit`, or `bench:lanes` process is running against this account. If the file is legible, read the `pid` from it. Check the process table on every host that shares the state directory. Both locks are 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. Do not send the relayer's next nonce manually. When the RPC can answer receipt and nonce queries, run `npm run submit` again. If the application reports partial lane consumption or a state that it cannot reconcile, stop. Then 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 that do not apply to the in-process bundling queue.
  </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>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.