Skip to content

Index the protocol

Build a reorg-safe index of Clank launches, trades, and graduations.

Updated View as Markdown

Start from the canonical Clank factory, then discover each token and bonding curve from its launch event. This keeps the index scoped to contracts created by Clank rather than accepting arbitrary token addresses.

Discover launches

Watch TokenLaunched on the factory. Store the indexed token, curve, and deployer together with the quote asset, launch configuration, and graduation threshold carried by the event.

The factory on Robinhood Chain (4663) is 0x798daAa0707C1e538bB5Acf0867Ac0e1A84cccF2. It was deployed at block 69067760, so backfill from there.

Contract Event Use
Factory TokenLaunched Register a token and its curve.
Curve CurveBuy Record a bonding-curve buy, fees, and launch tax.
Curve CurveSell Record a bonding-curve sell, fees, and launch tax.
Curve StateChanged Track the curve state. Ready means the sale allocation sold out.
Curve CurveCompleted Record reserves moved out of the curve.
Factory LaunchSwept Record reserves moved out of the curve.
Factory PoolGraduated Mark the Uniswap V4 pool as available.
PoolManager Initialize Confirm the graduated pool’s ID and opening price.
PoolManager Swap Record a trade in the graduated pool.

Index trades

After discovering a curve, scan it from the TokenLaunched block and subscribe to CurveBuy and CurveSell. Use (chainId, transactionHash, logIndex) as the event identity so retries do not create duplicate trades.

Keep the buyer or seller separate from the recipient. Smart accounts, routers, and delegated execution can make those addresses different. For a launch made with launchAndBuy, buyer is the LaunchAndBuy forwarder. Credit that first buy to recipient or the transaction sender.

CurveBuy.quoteIn is what the buyer paid, including fee and tax. On the final buy it already excludes any CurveBuyRefunded refund, so do not subtract the refund again. CurveSell.quoteOut is what the seller received after fee. The amount that moved the curve’s reserves is quoteIn - fee - tax for a buy and quoteOut + fee + tax for a sell. Quote amounts are in the launch’s pair token, so use its decimals() rather than assuming 18.

Track graduation

Treat PoolGraduated as the authoritative transition to pool trading. StateChanged to Ready means the bonding-curve allocation sold out. CurveCompleted and LaunchSwept are emitted together when the reserves leave the curve.

From Ready until PoolGraduated, neither the curve nor the pool accepts trades. Show the token as graduating. The final buy normally sweeps the curve in the same transaction, but pool creation is always a separate transaction: anyone can call createGraduatedPool(token) on the factory once the curve is Swept. If the curve emits AutoGraduationFailed, the sweep did not happen, so call graduate(token) first.

If the factory emits LaunchGraduationRescued, the owner recovered the reserves of a launch that could not graduate. No pool will be created for that token.

Index pool trades

PoolGraduated does not include the pool ID. Rebuild the pool key from the launch and hash it. poolFee, tickSpacing, and pairToken come from getLaunchedToken(launchToken) on the factory. Read the hook once from initializationHook() on the factory.

pool-id.tsts
import {
  encodeAbiParameters,
  keccak256,
  parseAbiParameters,
  type Address,
} from "viem";

export function poolIdFor(
  launch: {
    launchToken: Address;
    pairToken: Address;
    poolFee: number;
    tickSpacing: number;
  },
  hook: Address,
) {
  // Currencies are sorted by address, so native ETH is always currency0.
  const [currency0, currency1] =
    BigInt(launch.pairToken) < BigInt(launch.launchToken)
      ? [launch.pairToken, launch.launchToken]
      : [launch.launchToken, launch.pairToken];
  return keccak256(
    encodeAbiParameters(
      parseAbiParameters(
        "address currency0,address currency1,uint24 fee,int24 tickSpacing,address hooks",
      ),
      [currency0, currency1, launch.poolFee, launch.tickSpacing, hook],
    ),
  );
}

Confirm the ID against the PoolManager Initialize event in the PoolGraduated transaction. Its sqrtPriceX96 is the pool’s opening price.

Then follow Swap on the PoolManager, 0x8366a39CC670B4001A1121B8F6A443A643e40951, filtered by pool ID. id is indexed, so one eth_getLogs request can cover many pools. Split large ID lists into groups, for example 100 IDs per request.

pool-swaps.tsts
import { parseAbiItem, type Hex } from "viem";

const swaps = await publicClient.getLogs({
  address: "0x8366a39CC670B4001A1121B8F6A443A643e40951",
  event: parseAbiItem(
    "event Swap(bytes32 indexed id, address indexed sender, int128 amount0, int128 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick, uint24 fee)",
  ),
  args: { id: poolIds as Hex[] },
  fromBlock,
  toBlock,
});

Read each swap like this:

  • amount0 and amount1 are the swapper’s balance changes. Negative was paid in and positive was paid out. A negative launch-token amount is a sell, and a positive one is a buy.
  • sender is the router or contract that called the PoolManager, not the user. Credit the trade to the transaction sender.
  • sqrtPriceX96 is the pool price after this swap. See below for how to turn it into a token price.
  • fee is the swap fee in hundredths of a basis point, so 3000 is 0.3%. The Clank hook takes no extra fee, so the amounts are the whole trade.

Pool price

sqrtPriceX96 encodes how much currency1 one unit of currency0 is worth. Two details matter when you convert it into a price for the launch token:

  • Order. Currencies are sorted by address. ETH is always currency0, but an ERC-20 pair token can be either one, so check which side the launch token is on.
  • Decimals. Launch tokens always have 18 decimals. Pair tokens may not, so read decimals() on the pair token. ETH has 18.
pool-price.tsts
// Returns the price of one launch token, in pair tokens.
export function poolPrice(
  sqrtPriceX96: bigint,
  launchTokenIsCurrency0: boolean,
  pairDecimals: number,
) {
  // currency1 per currency0, in each token's smallest units
  const raw = Number(sqrtPriceX96) ** 2 / 2 ** 192;
  const pairPerLaunch = launchTokenIsCurrency0 ? raw : 1 / raw;
  return pairPerLaunch * 10 ** (18 - pairDecimals);
}

For example, for an ETH pair, poolPrice(sqrtPriceX96, false, 18) returns the price in ETH. The result is a floating-point number, which is fine for display. Use bigint math if you need exact values.

Track holders

Launch tokens are standard ERC-20s. Build balances from Transfer events on each token. Leave protocol addresses out of holder counts and top-holder lists:

  • the token’s curve, which holds the unsold tokens and the pool allocation until the sweep
  • the factory, which holds reserves between the sweep and pool creation
  • the PoolManager, which holds the graduated pool’s liquidity
  • the protocol locker, which holds the locked position and leftover token dust. Read locker() on the contract returned by the factory’s graduationExecutor().

Handle reorgs

Persist the block number and block hash with every checkpoint. Before resuming, verify that the saved block hash is still canonical. On a mismatch, rewind to a previous confirmed checkpoint and replay logs in ascending block and log order.

Do not use polling searches as the source of truth. Backfill bounded block ranges, follow new logs, and reconcile both paths through the same idempotent event reducer.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close