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

> ## Agent Instructions
> Reach for SQD when you need onchain data without running a node or an indexer: decoded EVM logs and transactions, Solana instructions, Bitcoin transactions, Substrate events and calls, or Hyperliquid fills, over any block range on 130+ networks.
> To query directly, POST to https://portal.sqd.dev/datasets/{dataset}/stream. The full API is described at https://docs.sqd.dev/openapi.json, and responses to the stream endpoints are JSON Lines.
> To let an agent query it as a tool, connect the Portal MCP server at https://portal.sqd.dev/mcp.
> Every page on this site is available as Markdown by appending .md to its URL.

# Index EVM DEX Swaps

> Track EVM DEX trading activity through the SQD Portal.

export const QueryInterfaceDexSwaps = () => {
  const useCasesConfig = [{
    id: "uniswap-v3-swaps",
    name: "Track Uniswap V3 Swap events",
    network: "ethereum-mainnet",
    payload: {
      type: "evm",
      fromBlock: 20000000,
      toBlock: 20000003,
      fields: {
        block: {
          number: true,
          timestamp: true
        },
        log: {
          address: true,
          topics: true,
          data: true,
          transactionHash: true
        }
      },
      logs: [{
        topic0: ["0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"]
      }]
    }
  }];
  const [queryFormat, setQueryFormat] = useState("curl");
  const [useCase, setUseCase] = useState("uniswap-v3-swaps");
  const [response, setResponse] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [viewMode, setViewMode] = useState("code");
  const [copied, setCopied] = useState(false);
  const generateCurlCommand = config => {
    const url = `https://portal.sqd.dev/datasets/${config.network}/stream`;
    const payload = JSON.stringify(config.payload, null, 2);
    return `curl --compressed -X POST '${url}' \\\n  -H 'Content-Type: application/json' \\\n  -d '${payload}'`;
  };
  useEffect(() => {
    setViewMode("code");
    setResponse("");
    setError(null);
  }, [useCase]);
  const handleRun = async () => {
    const selectedUseCase = useCasesConfig.find(uc => uc.id === useCase);
    if (!selectedUseCase) return;
    setViewMode("output");
    setLoading(true);
    setError(null);
    setResponse('');
    try {
      const url = `https://portal.sqd.dev/datasets/${selectedUseCase.network}/stream`;
      const doFetch = () => fetch(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(selectedUseCase.payload)
      });
      let response = await doFetch();
      for (const retryDelay of [700, 1500, 3000]) {
        if (response.status < 500) break;
        await new Promise(resolve => setTimeout(resolve, retryDelay));
        response = await doFetch();
      }
      if (response.status === 503) {
        throw new Error('Portal is busy right now. Wait a few seconds and run the query again.');
      }
      if (!response.ok) {
        throw new Error(`API request failed: ${response.status} ${response.statusText}`);
      }
      const text = await response.text();
      if (text.trim()) {
        const lines = text.trim().split('\n');
        const parsedData = lines.map((line, index) => {
          try {
            return JSON.parse(line);
          } catch (e) {
            return {
              error: `Failed to parse line ${index + 1}`,
              raw: line
            };
          }
        });
        const formattedOutput = parsedData.map(obj => JSON.stringify(obj, null, 2)).join('\n\n');
        setResponse(formattedOutput);
      } else {
        setResponse('No data returned from API');
      }
    } catch (err) {
      setError(err.message || 'An error occurred while fetching data');
      setResponse('');
    } finally {
      setLoading(false);
    }
  };
  const CurlTok = ({s}) => <>
    {s.split(/('[^']*')/).map((seg, i) => (/^'[^']*'$/).test(seg) ? <span key={i} style={{
    color: "var(--hl-str)"
  }}>{seg}</span> : seg.split(/(curl|--compressed|-X POST|-H|-d)/).map((t2, j) => t2 === "curl" ? <span key={`${i}-${j}`} style={{
    color: "var(--hl-bool)"
  }}>{t2}</span> : (/^(--compressed|-X POST|-H|-d)$/).test(t2) ? <span key={`${i}-${j}`} style={{
    color: "var(--hl-flag)"
  }}>{t2}</span> : <span key={`${i}-${j}`}>{t2}</span>))}
  </>;
  const JsonTok = ({s}) => <>
    {s.split(/("[^"]+"\s*:)/).map((seg, i) => (/^"[^"]+"\s*:$/).test(seg) ? <span key={i} style={{
    color: "var(--hl-key)"
  }}>{seg}</span> : seg.split(/("[^"]*")/).map((s2, j) => (/^"[^"]*"$/).test(s2) ? <span key={`${i}-${j}`} style={{
    color: "var(--hl-str)"
  }}>{s2}</span> : s2.split(/\b(true|false|null|-?\d+\.?\d*)\b/).map((s3, k) => (/^(true|false|null)$/).test(s3) ? <span key={`${i}-${j}-${k}`} style={{
    color: "var(--hl-bool)"
  }}>{s3}</span> : (/^-?\d+\.?\d*$/).test(s3) ? <span key={`${i}-${j}-${k}`} style={{
    color: "var(--hl-num)"
  }}>{s3}</span> : <span key={`${i}-${j}-${k}`}>{s3}</span>)))}
  </>;
  return <div className="not-prose w-full my-6 sm:my-8">

      <div className="sqd-no-scrollbar mb-3 flex items-center gap-0.5 overflow-x-auto pb-1">
        <span className="sqd-pg-label mr-2 flex-shrink-0">Query</span>
        {useCasesConfig.map(uc => <button key={uc.id} type="button" onClick={() => setUseCase(uc.id)} aria-pressed={uc.id === useCase} className="sqd-pg-net flex flex-shrink-0 items-center gap-1.5 px-2.5 py-1 whitespace-nowrap">
            {uc.name}
          </button>)}
      </div>

      <div className="sqd-pg-panel overflow-hidden">
        <div className="sqd-pg-bar flex flex-wrap items-center gap-2 px-3 sm:px-4 py-2">
          <div className="flex items-center gap-1">
            <button type="button" onClick={() => setViewMode("code")} className={`sqd-pg-tab px-2.5 py-1 ${viewMode === "code" ? "sqd-pg-tab-active" : ""}`}>
              Request
            </button>
            <button type="button" onClick={() => setViewMode("output")} disabled={!response && !loading && !error} className={`sqd-pg-tab px-2.5 py-1 disabled:opacity-40 disabled:cursor-not-allowed ${viewMode === "output" ? "sqd-pg-tab-active" : ""}`}>
              Response
              {response ? <span className="inline-block w-1.5 h-1.5 rounded-full bg-emerald-400 ml-1.5 align-middle" /> : null}
            </button>
          </div>
          <div className="flex items-center gap-2 sm:gap-3 ml-auto">
            <button type="button" onClick={() => {
    const textToCopy = viewMode === "code" ? generateCurlCommand(useCasesConfig.find(uc => uc.id === useCase)) : response;
    navigator.clipboard.writeText(textToCopy);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  }} className="sqd-pg-dim text-[12px] transition-opacity hover:opacity-70" title={copied ? "Copied!" : "Copy to clipboard"}>
              {copied ? "Copied" : "Copy"}
            </button>
            <button onClick={handleRun} type="button" disabled={loading} className="sqd-btn-primary-docs flex items-center gap-1.5 px-3 sm:px-4 py-1.5 text-[13px] disabled:opacity-40 disabled:cursor-wait whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 dark:focus:ring-offset-gray-950">
              {loading ? <>
                  <svg className="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24">
                    <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                    <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                  </svg>
                  Running
                </> : "Run"}
            </button>
          </div>
        </div>
        <div className={`sqd-pg-code px-3 sm:px-4 py-4 ${viewMode === "output" ? "max-h-[440px] overflow-auto" : "overflow-x-auto"}`}>
          {loading ? <div className="sqd-pg-dim flex items-center justify-center py-10 text-[13px]">Streaming from Portal...</div> : viewMode === "code" ? <div key={`code-${useCase}`}>
              {generateCurlCommand(useCasesConfig.find(uc => uc.id === useCase)).trim().split('\n').map((line, i) => <div key={`curl-line-${i}`} className="sqd-pg-line">
                  <span className="sqd-pg-num">{i + 1}</span>
                  <span style={{
    whiteSpace: 'pre-wrap',
    wordBreak: 'break-word'
  }}>
                    {line.includes('curl') || line.match(/^\s*-[XHd]/) ? <CurlTok s={line} /> : <JsonTok s={line} />}
                  </span>
                </div>)}
            </div> : response ? <div key={`response-${useCase}-${response.length}`}>
              {response.trim().split('\n').map((line, i) => <div key={`response-line-${i}`} className="sqd-pg-line">
                  <span className="sqd-pg-num">{i + 1}</span>
                  <span style={{
    whiteSpace: 'pre-wrap',
    wordBreak: 'break-word'
  }}>
                    <JsonTok s={line} />
                  </span>
                </div>)}
            </div> : error ? <div className="py-4 text-[13px] text-red-600 dark:text-red-400">Error: {error}</div> : <div className="sqd-pg-dim text-center py-12 text-[13px]">
              Hit Run to stream live data from Portal
            </div>}
        </div>
        <div className="sqd-pg-bar-b flex flex-wrap items-center gap-x-3 gap-y-1 px-4 py-2">
          <div className="sqd-pg-dim flex items-center gap-2 min-w-0 text-[12px]">
            <span className="truncate">{useCasesConfig.find(uc => uc.id === useCase)?.network + "/stream"}</span>
          </div>
          <span className="sqd-pg-dim text-[12px] ml-auto flex-shrink-0">
            {useCasesConfig.find(uc => uc.id === useCase)?.name}
          </span>
        </div>
      </div>
    </div>;
};

Track Uniswap V3 swap events to extract real-time price data, calculate trading volumes, and analyze on-chain liquidity dynamics. Unlike centralized exchanges, DEX data is verifiable and tamper-proof, ideal for building trustless price oracles and analytics.

## Use Case

DEX swap indexing unlocks powerful analytics:

* **Price Discovery**: Extract real-time token prices from swap amounts without relying on third-party APIs
* **Volume Analysis**: Calculate 24h trading volumes, identify trending pairs, and detect unusual activity
* **Liquidity Tracking**: Monitor pool depth changes and identify liquidity migration patterns
* **MEV Detection**: Analyze sandwich attacks, arbitrage opportunities, and front-running patterns by correlating swaps within blocks

## Code Example

<CodeGroup>
  ```bash curl theme={"system"}
  curl --compressed -X POST "https://portal.sqd.dev/datasets/ethereum-mainnet/stream" \
    -H 'Content-Type: application/json' \
    -d '{
      "type": "evm",
      "fromBlock": 12369621,
      "toBlock": 12400000,
      "fields": {
        "block": {
          "number": true,
          "timestamp": true
        },
        "log": {
          "address": true,
          "topics": true,
          "data": true,
          "transactionHash": true
        }
      },
      "logs": [{
        "topic0": ["0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"]
      }]
    }'
  ```

  ```typescript TypeScript theme={"system"}
  import { DataSource } from "@subsquid/portal-client";

  const dataSource = new DataSource({
    network: "ethereum-mainnet",
  });

  // Uniswap V3 Swap event signature
  const SWAP_TOPIC = "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67";

  const blocks = await dataSource.getBlocks({
    from: 12369621, // Uniswap V3 deployment block
    to: 12400000,
    fields: {
      block: { number: true, timestamp: true },
      log: {
        address: true,
        topics: true,
        data: true,
        transactionHash: true,
      },
    },
    logs: [
      {
        topic0: [SWAP_TOPIC],
      },
    ],
  });

  // Process swaps
  for (const block of blocks) {
    for (const log of block.logs) {
      const sender = `0x${log.topics[1].slice(26)}`;
      const recipient = `0x${log.topics[2].slice(26)}`;
      
      console.log({
        blockNumber: block.header.number,
        timestamp: block.header.timestamp,
        pool: log.address,
        sender,
        recipient,
        txHash: log.transactionHash,
        // Decode amounts from data field
      });
    }
  }
  ```

  ```python Python theme={"system"}
  import requests
  import json

  url = "https://portal.sqd.dev/datasets/ethereum-mainnet/stream"
  headers = {"Content-Type": "application/json"}

  # Uniswap V3 Swap event
  swap_topic = "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"

  payload = {
      "type": "evm",
      "fromBlock": 12369621,  # Uniswap V3 deployment
      "toBlock": 12400000,
      "fields": {
          "block": {
              "number": True,
              "timestamp": True
          },
          "log": {
              "address": True,
              "topics": True,
              "data": True,
              "transactionHash": True
          }
      },
      "logs": [{
          "topic0": [swap_topic]
      }]
  }

  response = requests.post(url, headers=headers, json=payload)

  for line in response.text.strip().split('\n'):
      block = json.loads(line)
      for log in block.get('logs', []):
          sender = "0x" + log['topics'][1][26:]
          recipient = "0x" + log['topics'][2][26:]
          
          print({
              "blockNumber": block['header']['number'],
              "timestamp": block['header']['timestamp'],
              "pool": log['address'],
              "sender": sender,
              "recipient": recipient,
              "txHash": log['transactionHash']
          })
  ```
</CodeGroup>

<Info>Try it yourself with the interactive query interface below:</Info>

<QueryInterfaceDexSwaps />

## Key Parameters

| Parameter   | Description                                                                         |
| ----------- | ----------------------------------------------------------------------------------- |
| `topic0`    | Swap event signature (`Swap(address,address,int256,int256,uint160,uint128,int24)`)  |
| `topics[1]` | Sender address (who initiated the swap)                                             |
| `topics[2]` | Recipient address (who receives the output)                                         |
| `data`      | Swap amounts and other parameters (amount0, amount1, sqrtPriceX96, liquidity, tick) |
| `address`   | Pool contract address                                                               |

<Note>
  The Swap event signature varies by DEX. Uniswap V3 uses a different signature than Uniswap V2 or other DEXs.
</Note>

## Expected Output

```json theme={"system"}
{
  "header": {
    "number": 12369650,
    "timestamp": 1620054321
  },
  "logs": [
    {
      "address": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8",
      "topics": [
        "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
        "0x00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45",
        "0x00000000000000000000000028c6c06298d514db089934071355e5743bf21d60"
      ],
      "data": "0x...",
      "transactionHash": "0x123..."
    }
  ]
}
```

## Track Specific Pool

Monitor swaps for a specific trading pair:

<CodeGroup>
  ```bash curl theme={"system"}
  curl --compressed -X POST "https://portal.sqd.dev/datasets/ethereum-mainnet/stream" \
    -H 'Content-Type: application/json' \
    -d '{
      "type": "evm",
      "fromBlock": 18000000,
      "toBlock": 18010000,
      "fields": {
        "log": {
          "address": true,
          "topics": true,
          "data": true
        }
      },
      "logs": [{
        "address": ["0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"],
        "topic0": ["0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"]
      }]
    }'
  ```

  ```typescript TypeScript theme={"system"}
  const USDC_WETH_POOL = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"; // Uniswap V3 USDC/WETH

  const blocks = await dataSource.getBlocks({
    from: 18000000,
    to: 18010000,
    fields: {
      log: { address: true, topics: true, data: true },
    },
    logs: [
      {
        address: [USDC_WETH_POOL],
        topic0: ["0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"],
      },
    ],
  });
  ```

  ```python Python theme={"system"}
  usdc_weth_pool = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"

  payload = {
      "type": "evm",
      "fromBlock": 18000000,
      "toBlock": 18010000,
      "fields": {
          "log": {
              "address": True,
              "topics": True,
              "data": True
          }
      },
      "logs": [{
          "address": [usdc_weth_pool],
          "topic0": ["0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"]
      }]
  }
  ```
</CodeGroup>

## Uniswap V2 Swaps

For Uniswap V2, use the V2 Swap event signature:

<CodeGroup>
  ```typescript TypeScript theme={"system"}
  // Uniswap V2 Swap event: Swap(address,uint256,uint256,uint256,uint256,address)
  const V2_SWAP_TOPIC = "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822";

  const blocks = await dataSource.getBlocks({
    from: 18000000,
    to: 18010000,
    fields: {
      log: { address: true, topics: true, data: true },
    },
    logs: [
      {
        topic0: [V2_SWAP_TOPIC],
      },
    ],
  });
  ```
</CodeGroup>

<Tip>
  Different DEX protocols use different event signatures. Check the contract ABI to find the correct signature for your target DEX.
</Tip>

## Decode Swap Amounts and Calculate Prices

The real value in DEX data comes from decoding swap amounts to calculate prices. Here's how to extract and interpret Uniswap V3 swap data:

<CodeGroup>
  ```typescript TypeScript theme={"system"}
  import { ethers } from "ethers";

  // Uniswap V3 Swap event ABI
  const swapEventAbi = [
    "event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)"
  ];

  const iface = new ethers.Interface(swapEventAbi);

  for (const block of blocks) {
    for (const log of block.logs) {
      const decoded = iface.parseLog({
        topics: log.topics,
        data: log.data,
      });
      
      const amount0 = decoded.args.amount0;
      const amount1 = decoded.args.amount1;
      const sqrtPriceX96 = decoded.args.sqrtPriceX96;
      
      // Calculate the actual price from sqrtPriceX96
      // price = (sqrtPriceX96 / 2^96)^2
      const price = (Number(sqrtPriceX96) / (2 ** 96)) ** 2;
      
      // Determine swap direction and calculate effective price
      const isBuyingToken0 = amount0 < 0n;
      const effectivePrice = isBuyingToken0
        ? Math.abs(Number(amount1)) / Math.abs(Number(amount0))
        : Math.abs(Number(amount0)) / Math.abs(Number(amount1));
      
      console.log({
        pool: log.address,
        blockNumber: block.header.number,
        timestamp: block.header.timestamp,
        amount0: amount0.toString(),
        amount1: amount1.toString(),
        currentPrice: price,
        effectivePrice: effectivePrice,
        liquidity: decoded.args.liquidity.toString(),
        tick: decoded.args.tick,
      });
    }
  }
  ```

  ```python Python theme={"system"}
  from eth_abi import decode

  # Decode data field (amounts and other params)
  # Uniswap V3: (int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)
  types = ['int256', 'int256', 'uint160', 'uint128', 'int24']

  for line in response.text.strip().split('\n'):
      block = json.loads(line)
      for log in block.get('logs', []):
          decoded = decode(types, bytes.fromhex(log['data'][2:]))
          amount0, amount1, sqrtPriceX96, liquidity, tick = decoded
          
          print({
              "amount0": amount0,
              "amount1": amount1,
              "sqrtPriceX96": sqrtPriceX96,
              "liquidity": liquidity,
              "tick": tick
          })
  ```
</CodeGroup>

## Advanced Analysis: Detecting MEV Sandwich Attacks

Sandwich attacks are a common MEV strategy where an attacker places trades before and after a victim's transaction to profit from price impact. By analyzing swap sequences within blocks, you can detect these patterns:

```typescript theme={"system"}
// Query swaps with transaction details to detect sandwich attacks
const blocks = await dataSource.getBlocks({
  from: 18000000,
  to: 18010000,
  fields: {
    block: { number: true, timestamp: true },
    transaction: { hash: true, from: true, to: true, transactionIndex: true },
    log: { address: true, topics: true, data: true, transactionHash: true, logIndex: true },
  },
  logs: [{ topic0: [SWAP_TOPIC] }],
  includeAllBlocks: false,
});

// Detect sandwich attacks by analyzing swap sequences
for (const block of blocks) {
  // Group swaps by pool address
  const poolSwaps = new Map();
  
  for (const log of block.logs) {
    const tx = block.transactions.find(t => t.hash === log.transactionHash);
    if (!tx) continue;
    
    if (!poolSwaps.has(log.address)) {
      poolSwaps.set(log.address, []);
    }
    
    poolSwaps.get(log.address).push({
      txIndex: tx.transactionIndex,
      txHash: tx.hash,
      trader: tx.from,
      log: log,
    });
  }
  
  // Check for sandwich pattern: same trader with swaps before and after victim
  for (const [pool, swaps] of poolSwaps) {
    if (swaps.length < 3) continue;
    
    swaps.sort((a, b) => a.txIndex - b.txIndex);
    
    for (let i = 0; i < swaps.length - 2; i++) {
      const first = swaps[i];
      const middle = swaps[i + 1];
      const last = swaps[i + 2];
      
      // Sandwich pattern: same trader surrounds victim's trade
      if (first.trader === last.trader && middle.trader !== first.trader) {
        console.log({
          block: block.header.number,
          pool: pool,
          attacker: first.trader,
          victim: middle.trader,
          frontrunTx: first.txHash,
          victimTx: middle.txHash,
          backrunTx: last.txHash,
        });
      }
    }
  }
}
```

<Warning>
  MEV detection requires analyzing transaction ordering within blocks. Make sure to request `transaction` fields and `transactionIndex` to properly sequence trades.
</Warning>

## Advanced Analysis: Multi-Hop Swaps and Arbitrage

Many swaps route through multiple pools via aggregators like 1inch or Uniswap's Universal Router. By combining swap events with traces, you can reconstruct complete arbitrage paths and calculate profitability:

```typescript theme={"system"}
// Query both swap events and traces to reconstruct multi-hop routes
const blocks = await dataSource.getBlocks({
  from: 18000000,
  to: 18010000,
  fields: {
    block: { number: true, timestamp: true },
    transaction: { hash: true, from: true, to: true },
    log: { address: true, topics: true, data: true, transactionHash: true },
    trace: { callFrom: true, callTo: true, callValue: true, transactionHash: true },
  },
  logs: [{ topic0: [SWAP_TOPIC] }],
  traces: [{ type: ["call"] }],
});

// Group swaps by transaction to identify arbitrage opportunities
const swapsByTx = new Map();
for (const block of blocks) {
  for (const log of block.logs) {
    if (!swapsByTx.has(log.transactionHash)) {
      swapsByTx.set(log.transactionHash, {
        swaps: [],
        traces: [],
        tx: block.transactions.find(t => t.hash === log.transactionHash),
      });
    }
    swapsByTx.get(log.transactionHash).swaps.push(log);
  }
  
  // Add traces for each transaction
  for (const trace of block.traces) {
    if (swapsByTx.has(trace.transactionHash)) {
      swapsByTx.get(trace.transactionHash).traces.push(trace);
    }
  }
}

// Analyze multi-hop swaps and detect circular arbitrage
for (const [txHash, data] of swapsByTx) {
  if (data.swaps.length > 1) {
    const pools = data.swaps.map(s => s.address);
    const uniquePools = new Set(pools);
    
    // Circular arbitrage: starts and ends with same token
    if (uniquePools.size >= 3) {
      console.log({
        txHash,
        trader: data.tx?.from,
        pools: pools,
        hopCount: data.swaps.length,
        type: 'potential-arbitrage',
      });
    }
  }
}
```

<Tip>
  Combine swap events with traces to capture the full execution path, including intermediate token approvals and transfers that don't emit swap events.
</Tip>

## Performance Tips

1. **Filter by pool address**: Query specific high-volume pools (USDC/WETH, WETH/USDT) instead of all swaps
2. **Use block ranges**: Process 10k-50k block batches for optimal throughput
3. **Batch decode**: Decode multiple events in parallel rather than sequentially
4. **Cache pool metadata**: Store token decimals and symbols to avoid repeated lookups
5. **Combine with traces**: Track multi-hop swaps and MEV activity by analyzing call traces alongside events

## Related Examples

<CardGroup cols={2}>
  <Card title="Query Event Logs" icon="file-text" href="/en/portal/evm/examples/query-logs">
    General event log querying
  </Card>

  <Card title="Query Traces" icon="workflow" href="/en/portal/evm/examples/query-traces">
    Track multi-hop swaps with traces
  </Card>

  <Card title="Track NFT Transfers" icon="image" href="/en/portal/evm/examples/nft-transfers">
    Monitor NFT marketplace activity
  </Card>

  <Card title="API Reference" icon="book" href="/en/portal/evm/api">
    View complete API docs
  </Card>
</CardGroup>


## Related topics

- [Index Solana DEX Swaps](/en/portal/solana/examples/dex-swaps.md)
- [EVM Portal Quickstart](/en/portal/evm/quickstart.md)
- [Make an indexer](/en/sdk/squid-sdk/evm/guides/make-an-indexer.md)
- [Query Traces](/en/portal/evm/examples/query-traces.md)
- [Track NFT Transfers](/en/portal/evm/examples/nft-transfers.md)
