Skip to content
LitVMSwapDocs
Open app
Developers / Events and indexing
Developers · On chain

Events and indexing

Running a quest, building analytics or just curious who swapped what? Everything happens on chain, so the events are the source of truth. Here's which ones to watch and a ready-made way to count a wallet's swaps.

The one event you probably want

Every swap that goes through the LitVMSwap app, the widget or the Swap API lands on the swap entrypoint, which emits this:

Solidity
event SwapExecuted(
    address indexed user,      // the wallet that sent the swap
    address indexed receiver,  // who got the output (usually the same wallet)
    address tokenIn,           // 0x000…000 means native zkLTC
    address tokenOut,          // 0x000…000 means native zkLTC
    uint256 amountIn,          // base units of tokenIn
    uint256 amountOut,         // base units of tokenOut, what the receiver got
    uint256 pointsEarned       // legacy field, currently always 0
);
DetailValue
Emitted bySwap entrypoint 0xF664B56933f3cF0d7d69982b5A8eC9101b80059D
Topic 00x48622207111e60c470744a6f806ceaa4ba7cf81a041ea2acd4ebd9dbde26efec
Filter by walletuser is indexed, so filter on topic 1 and let the RPC do the work
Native zkLTC shows up as the zero address

In this event native zkLTC is 0x0000000000000000000000000000000000000000, not the 0xEeee… placeholder the APIs use. Handle both if you mix API data and chain data.

You'll also see an AGGFlowSwap event from another contract in the same transaction. That one is internal plumbing and its user is the entrypoint itself, not the trader, so stick with SwapExecuted.

Scanning logs in chunks

LitVM makes about 4 blocks a second, so a day is roughly 345,000 blocks. The public RPC is happy with ranges of a few thousand blocks and gets slow on very large ones. This helper walks a range in chunks:

TypeScript
import { createPublicClient, http, parseAbiItem, type Address } from 'viem'
import { litvm } from './litvm'

export const client = createPublicClient({ chain: litvm, transport: http() })

export const ENTRYPOINT = '0xF664B56933f3cF0d7d69982b5A8eC9101b80059D'
export const swapExecuted = parseAbiItem(
  'event SwapExecuted(address indexed user, address indexed receiver, address tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut, uint256 pointsEarned)',
)

export async function getSwaps(user: Address, fromBlock: bigint, toBlock: bigint, step = 5_000n) {
  const logs = []
  for (let start = fromBlock; start <= toBlock; start += step) {
    const end = start + step - 1n < toBlock ? start + step - 1n : toBlock
    logs.push(
      ...(await client.getLogs({
        address: ENTRYPOINT,
        event: swapExecuted,
        args: { user },
        fromBlock: start,
        toBlock: end,
      })),
    )
  }
  return logs
}

For a long range (weeks), run it once, save what you found and the last block you scanned, then only scan new blocks next time. Your RPC bill and your users will thank you.

From a date to a block

Campaigns are usually "from Monday to Friday", not "from block X". Binary search over block timestamps does it in about 25 calls:

TypeScript
export async function blockAt(timestamp: bigint) {
  let lo = 1n
  let hi = await client.getBlockNumber()
  while (lo < hi) {
    const mid = (lo + hi) / 2n
    const { timestamp: t } = await client.getBlock({ blockNumber: mid })
    if (t < timestamp) lo = mid + 1n
    else hi = mid
  }
  return lo // first block at or after `timestamp`
}

const start = await blockAt(BigInt(Date.parse('2026-11-02T00:00:00Z') / 1000))

Recipe: did this wallet swap my token?

The classic quest check. Count swaps that bought or sold a token, and add up the volume.

TypeScript
import { formatUnits, type Address } from 'viem'
import { blockAt, getSwaps } from './indexing'

export async function verifySwaps(opts: {
  user: Address
  token: Address
  decimals: number
  from: Date
  to: Date
  minSwaps?: number
}) {
  const fromBlock = await blockAt(BigInt(Math.floor(opts.from.getTime() / 1000)))
  const toBlock = await blockAt(BigInt(Math.floor(opts.to.getTime() / 1000)))
  const token = opts.token.toLowerCase()

  let swaps = 0
  let volume = 0n
  for (const log of await getSwaps(opts.user, fromBlock, toBlock)) {
    const { tokenIn, tokenOut, amountIn, amountOut } = log.args
    if (tokenIn?.toLowerCase() === token) {
      swaps++
      volume += amountIn ?? 0n
    } else if (tokenOut?.toLowerCase() === token) {
      swaps++
      volume += amountOut ?? 0n
    }
  }

  return {
    swaps,
    volume: formatUnits(volume, opts.decimals),
    eligible: swaps >= (opts.minSwaps ?? 1),
  }
}

Want "swapped token X against zkLTC" specifically? Also check that the other side is the zero address (native) or wzkLTC.

Pool level events

Not everything goes through the entrypoint. Bots and other contracts can call the routers or pools directly, and those swaps only show up on the pools. These are the standard V2 and V3 events:

Solidity
// V2 pair
event Swap(address indexed sender, uint amount0In, uint amount1In, uint amount0Out, uint amount1Out, address indexed to);
event Sync(uint112 reserve0, uint112 reserve1);
event Mint(address indexed sender, uint amount0, uint amount1);
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);

// V2 factory
event PairCreated(address indexed token0, address indexed token1, address pair, uint);

// V3 pool
event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick);

// V3 factory
event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool);

// V3 position manager
event IncreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
event DecreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1);
event Collect(uint256 indexed tokenId, address recipient, uint256 amount0, uint256 amount1);
sender is usually a router

On pool events, sender is whoever called the pool, which is normally a router or the entrypoint, not a person. To find the human, look at the transaction's from or at SwapExecuted.

Watching live

For a live feed, watchContractEvent polls new blocks for you:

TypeScript
const unwatch = client.watchContractEvent({
  address: ENTRYPOINT,
  abi: [swapExecuted],
  eventName: 'SwapExecuted',
  onLogs: (logs) => {
    for (const l of logs) console.log(l.args.user, 'swapped', l.args.amountIn, '→', l.args.amountOut)
  },
})