Onchain events
The logs a launch, a trade and a fee collection emit — and the one structural fact about them that makes indexing NASDANK different from indexing a single-contract protocol.
TokenLaunched has several shapes
Every launcher emits an event named TokenLaunched, but the launchers are separate deployments with different feature sets, so the signatures differ between generations. There is no single ABI that decodes them all.
0
TokenLaunched(address,address,bytes32,uint256,address,address,string,string,string,
uint256,uint160,int24,int24,uint16,uint24,uint16,uint256,uint256)
1
TokenLaunched(address,address,address,uint256,address,string,string,string,
uint24,uint8,bool,uint256,uint160,int24,int24,uint256,uint256,uint256)
2
TokenLaunched(address,address,address,address,address,string,string,string,
uint16,uint256,uint256,uint256,uint256,uint256,uint32,uint64,uint256,uint256)
3
TokenLaunched(address,address,bytes32,address,uint256,address,address,string,string,string,
uint256,uint160,int24,int24,uint16,uint24,uint16,uint256,uint256)Across every generation, the first three indexed parameters carry the same three things. That is the invariant to build on: you can identify the token, the creator and the pool or pool id from the topics alone, without knowing which launcher emitted the log.
Decoding strategy
- Group logs by emitting addressThe
addressfield of the log tells you which launcher, and therefore which ABI to apply. This is more reliable than trying signatures until one parses. - Read the topics for identityToken, creator and pool come out of the indexed parameters regardless of generation.
- Apply the launcher's own ABI for the restFee, tick range, supply, treasury and the feature-specific fields live in the data and need the right ABI.
ABI decoding is positional. An ABI that omits one field — a projectTreasury, say — does not fail; it shifts everything after it and produces plausible garbage. If a decoded record looks almost right, suspect the ABI before suspecting the chain.
NASDANK no longer launches on V2. The V2 shapes below are kept because a handful of coins were launched on it before the tier was retired, and an indexer covering full history still has to decode them.
Trading
Trading emits the venue's own events, not ours. There is no NASDANK trade event, because trading does not go through NASDANK.
| Venue | Event | Emitted by |
|---|---|---|
| V2 | Swap(address,uint256,uint256,uint256,uint256,address) | The pair |
| V3 | Swap(address,address,int256,int256,uint160,uint128,int24) | The pool |
| V4 | Swap(bytes32,address,int128,int128,uint160,uint128,int24,uint24) | The PoolManager, keyed by pool id |
On V4 there is no per-pool contract. Every swap on every V4 pool on the chain comes out of the singleton PoolManager, distinguished by the id topic. Indexing "your" pool means filtering on that pool id — you cannot subscribe to a pool address.
Fee collection
FeesCollected is emitted by the locker or hook when fees are pulled and split. It carries both legs and both recipients, which is what makes conservation checkable: for a given collection, the creator amount plus the treasury amount must equal the collected amount, on every leg.
Reward-paying launches additionally distinguish two payout events, and the distinction is user-visible:
- DividendClaimed — the holder called claim themselves.
- DividendPushed — a keeper paid the holder without them doing anything.
An interface that flattens these into one says "you claimed this" about money that simply arrived, which is a different statement. Keep them apart.
Vesting
VestingCreated is emitted per schedule.
You cannot filter VestingCreated by recipient at the RPC. The logs must be pulled from the contract's deploy block and filtered client-side. Getting the fromBlock right is the whole job — start too late and a recipient's schedule is invisible, which reads to them as "I have nothing to claim".
Log-fetching limits on Robinhood Chain
And it times out on dense ranges well before that. Crucially, density is not predictable from range width — a busy thousand-block window can exceed the cap while a quiet million-block window does not. No fixed window size is safe.
The correct pattern is: attempt the range, and on failure split it in half and recurse. A catch => [] here is not a fallback — it turns "the RPC refused" into "there are no results", and a holder-set query that returns empty is indistinguishable from a coin with no holders. That exact bug once suppressed payouts for about fourteen hours.
async function getLogsSafe(client, filter, from, to) {
try {
return await client.getLogs({ ...filter, fromBlock: from, toBlock: to });
} catch (err) {
0
if (to - from <= 1n) throw err;
const mid = from + (to - from) / 2n;
const [a, b] = await Promise.all([
getLogsSafe(client, filter, from, mid),
getLogsSafe(client, filter, mid + 1n, to),
]);
return [...a, ...b];
}
}If you are running your own Ponder indexer
Filtering a V4 pool id at the indexer level creates one RPC cache fragment per topic value, and those fragments are never merged. Measured against a real deployment, the "efficient" filtered version cost roughly thirteen times the unfiltered firehose it was meant to replace. Index broadly and filter in your handler.
Changing an environment variable changes the build id, which changes the schema stamp, which can make a running instance refuse to reuse its existing tables. Plan for that before editing env vars on a live indexer.
Indexer API covers the served surface if you would rather not run one.