The on-chain data pipeline for Tokenzyme, a permissionless token launchpad for EVM chains. It follows the launchpad contract's event log, materializes it into Postgres, and pushes live updates to connected clients over WebSocket.
Built on Subsquid. Deployed against Sonic, but nothing here is chain-specific.
New to the project? The system-wide overview lives in tokenzyme-core / ARCHITECTURE.md.
- What it does
- Data model
- How it shares a database with the API
- Getting started
- Regenerating code
- Running with Docker
- Contributing
- Security
- Donations
- License
The launchpad contract is the only source of truth. This service turns its events into queryable rows, because reading a bonding curve's trade history straight off the chain is far too slow to render a UI.
EvmBatchProcessor subscribes to six topics on the launchpad address and runs one
handler per event:
| Event | Handler | Effect |
|---|---|---|
TokenLaunched |
handleTokenLaunched |
Creates the Token row and its SocialMedia row, plus the creator's Account. |
MarketDataCreated |
handleMarketDataCreated |
Seeds supply, reserves and opening price on the token. |
TokensBought |
handleTokensBought |
Records a BUY trade with its average price and fee. |
TokensSold |
handleTokensSold |
Records a SELL trade. |
MarketDataUpdated |
handleMarketDataUpdated |
Updates reserves and current price after a trade. |
TokenMigrated |
handleTokenMigrated |
Stamps dexPoolAddress and migratedAt, and records the DexLiquidity seeded into the pool. |
Handlers do not write to the database themselves. They accumulate into an in-memory
Models batch, and src/index.ts flushes the whole batch in one transaction per
block range. That keeps writes ordered and lets Subsquid roll back cleanly on a reorg.
Two things affect throughput:
- On
mainnetandtestnetthe processor uses the Subsquid archive gateway, which serves historical logs far faster than any RPC endpoint. Backfilling from genesis over RPC alone takes hours; over the gateway it takes minutes. FINALITY_CONFIRMATIONsets how many blocks to wait before treating data as final. Below that depth Subsquid handles reorgs by rolling back and reapplying.
After each batch commits, src/ws.ts broadcasts on a Socket.IO server on port
3002:
| Room | Payload |
|---|---|
tokens:<tokenAddress>:trades |
Each new trade — type, amounts, fee, average price. |
tokens:<tokenAddress>:updates |
New reserves, price, and DEX pool address once migrated. |
Clients subscribe per token, so a token page gets a live chart and trade feed without polling. This is a broadcast-only channel: it never reads from clients and requires no authentication.
Entities are declared in schema.graphql and the TypeORM models are generated from
it into src/model/generated.
Account ──< Token ──< Trade
│
├──── SocialMedia (1:1, the token's links)
└──── DexLiquidity (1:1, written at migration)
| Entity | Notes |
|---|---|
Account |
An address that created or traded a token. Just an ID. |
Token |
Metadata, supply, live reserves, current and final price, migration state. |
Trade |
One buy or sell against the bonding curve. |
SocialMedia |
Optional links supplied at launch. |
DexLiquidity |
Liquidity seeded into the DEX pool at migration. |
Amounts use BigDecimal — token amounts are already scaled down from wei by
@subsquid/big-decimal, so consumers never handle raw 18-decimal integers.
This is the part that surprises people, so it is worth stating plainly.
The indexer and tokenzyme-core
share one Postgres database, and one schema: public.
This service creates no tables. Core's Prisma migrations define the whole schema,
including the on-chain tables this indexer fills — accounts, tokens, trades,
social_media, dex_liquidities. The indexer only writes rows into them.
That means you must apply core's migrations before starting the indexer
(yarn prisma:migrate:deploy in tokenzyme-core), or it has nowhere to write.
Both services must resolve to the same schema. Subsquid ignores any ?schema=
parameter in DB_URL and always uses public, so core's DB_URL must not carry one
either — otherwise core reads from one schema while this service writes to another,
and every token query silently returns nothing.
Requirements: Node.js 24+, Corepack, and a Postgres 17 server.
# Start Postgres if you do not have one
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres --name postgres postgres:17.5
# Enable Corepack to use the Yarn version configured in the project
corepack enable
# Install all dependencies
yarn
# Copy the environment template and fill it in
cp .env.template .envEvery variable is documented inline in .env.template. The ones
that matter most:
| Variable | Why it matters |
|---|---|
NETWORK |
mainnet, testnet, testnet-legacy or fork. Selects the archive gateway. |
RPC_URL |
Must match NETWORK. |
LAUNCHPAD_ADDRESS |
The proxy address of your own deployment — this project publishes none. See tokenzyme-contracts. |
FROM_BLOCK |
The block the proxy was deployed in. Starting from 0 wastes hours on empty history. |
RPC_RATE_LIMIT |
Keep under your provider's limit; public endpoints throttle hard. |
Then run it:
yarn start:devThere is no migration step here. The tables must already exist, created by
yarn prisma:migrate:deploy in
tokenzyme-core.
To index against a local chain, run a mainnet fork from
tokenzyme-contracts
(yarn test:fork:startNode), deploy the contracts to it, and set NETWORK="fork"
with the resulting launchpad address.
yarn lint # ESLint
yarn format # Prettier
yarn build # compile to ./libTwo directories are generated and committed. Both need regenerating when the contracts change.
# Rebuild TypeORM entities from schema.graphql into src/model/generated
yarn generate:models
# Rebuild typed event decoders from abi/launchpad.json into src/generated/abi
yarn generate:abiImportant
abi/launchpad.json is a copy of the launchpad ABI. It does not update itself.
After changing an event in
tokenzyme-contracts, copy the
new ABI over it and run yarn generate:abi, or the processor will silently stop
matching the event whose topic hash changed.
yarn generate:models runs scripts/fixEntities.mjs afterwards to patch the
generated entities — do not bypass it by calling squid-typeorm-codegen directly.
Postgres is defined in core's compose file, and both stacks join a shared external network:
docker network create tokenzyme
docker compose -f ../tokenzyme-core/compose.yaml up -d # brings up Postgres + the API
docker compose up -d # this serviceTo build and push an image, set DOCKER_IMAGE to your own fully-qualified tag — it
defaults to a bare local tokenzyme-indexer:latest, which is fine locally but is not
pushable anywhere. Log in to your registry first; the scripts do not do it for you.
export DOCKER_IMAGE=registry.example.com/you/tokenzyme-indexer:latest
docker login registry.example.com
yarn image:build
yarn image:publishSee CONTRIBUTING.md. Contributions are welcome — please open an issue before starting on anything substantial.
Do not open a public issue for a vulnerability. Follow the disclosure process in SECURITY.md.
| Repository | Role |
|---|---|
| tokenzyme-contracts | The on-chain protocol. Emits the events this service consumes. |
| tokenzyme-core | GraphQL API. Reads this service's tables. |
| tokenzyme-indexer | This repository. |
| tokenzyme-app | React web front end. |
| tokenzyme-mobile | Flutter mobile app. |
If this project helped you in any way, consider making a donation. It is not expected and nothing here is gated behind it.
0x31c8EDeD9D516a7BD854457710B9629F13BBA4FD
HsgFMPqjzUwbsd7dyFp2vjfLLLN6eG2Et2H9qmCBEBng
Warning
Send only on a network shown above, and check the address against this page on GitHub rather than a copy elsewhere. Transfers cannot be reversed.
Licensed under the Apache License 2.0. See NOTICE for attribution requirements.
The license covers the source code. It does not grant rights to the "Tokenzyme" name, logo, or other brand assets. You are free to fork and run this software, but please do so under your own name and branding.