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.
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.
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:
amount0andamount1are 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.senderis the router or contract that called the PoolManager, not the user. Credit the trade to the transaction sender.sqrtPriceX96is the pool price after this swap. See below for how to turn it into a token price.feeis the swap fee in hundredths of a basis point, so3000is 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.
// 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’sgraduationExecutor().
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.