Snekwork is a lightweight ClickHouse-backed block explorer for Basilisk. It combines a block-level USD price indexer, a raw on-chain data lake, an API, and the explorer UI. See REMOVED.md for what this fork deliberately does not do.
- Explorer: blocks, extrinsics, events, assets, holders, accounts, identities, tags, proxies, multisigs, and portfolio history.
- Activity: transfers, swaps, cross-chain activity, liquidity, and governance votes.
- Charts: block-level USD prices and OHLCV candles, rendered on the explorer's own asset pages.
- API: Fastify endpoints for explorer data, prices, and indexer status.
The containerized stack requires Docker with Compose. Local development additionally requires Node.js 22+.
git clone https://github.com/galacticcouncil/snekwork.git
cd snekwork
docker compose up --build -dLocal services:
| Service | URL | Purpose |
|---|---|---|
| Explorer | http://localhost:5174 | Live chain explorer |
| API | http://localhost:3000 | Explorer and price API |
| ClickHouse HTTP | http://localhost:18123 | Local database endpoint |
The live pipelines start immediately. Historical ingestion continues in the background, so a fresh installation fills older explorer and price history over time.
One more step is needed before anything shows a USD value: see Valuation bring-up.
Useful status commands:
docker compose ps
docker logs -f snekwork-ingestion-supervisor
docker exec -it snekwork-clickhouse clickhouse-client \
--database=price_data --password "${CLICKHOUSE_PASSWORD:-dev}"Nothing has a USD value until the KSM/USD reference table is populated. This is a
one-time step, and it is separate from docker compose up.
Snekwork does not derive USD from on-chain stablecoin pools. It anchors on an external
KSM/USD reference persisted in price_data.ksm_usd_reference, and prices BSX from it
through the BSX/KSM XYK pool's reserve ratio. With that table empty, the indexer logs
[Reference] price_data.ksm_usd_reference is empty: no KSM anchor, so no asset gets a USD price.
and every price, portfolio total and USD-denominated chart reads as null — the pipeline is healthy, it just has no anchor. Fill it once:
# In Compose (the indexer image carries the script):
docker compose run --rm --no-deps indexer src/scripts/backfill-ksm-reference.ts
# Outside Compose:
npm run backfill:ksm-referenceThe backfill takes CoinGecko's rolling 365 days and Binance daily klines for everything
older, so one run covers the chain's whole history. Re-run it with --refresh to rewrite
stored days, or --from=/--to= to fill a specific span. After that the live indexer
keeps the head current on its own: an indexer run with no --to-block polls CoinGecko
for the current day and settles recent days as they close, so the backfill does not need
to be re-run daily.
Snekwork publishes USD prices for exactly BSX (0) and KSM (1). Every other registered asset is deliberately unpriced — it keeps its symbol, name, decimals and balances everywhere in the explorer, but carries no price, no USD value and no candles. The BSX/USDT pool is deliberately not a pricing input. See REMOVED.md for why this replaced the upstream stablecoin-basket/Omnipool price graph.
These are read by the price indexer (src/config.ts) and passed through to the indexer
service — and therefore to the supervisor-created main-live worker, which is the same
service image — in docker-compose.yml. Leaving one unset in .env
means "use the code default".
| Variable | Default | Purpose |
|---|---|---|
KSM_REFERENCE_POLL_MS |
300000 |
Live CoinGecko poll cadence, wall-clock. Five minutes is what the keyless free tier tolerates; shorter intervals were observed being throttled. |
KSM_REFERENCE_LIVE_WINDOW_HOURS |
48 |
How recent a block must be to be valued at the intraday poll. Older blocks read settled daily closes only, which is what makes a replay reproducible. |
KSM_REFERENCE_COIN_ID |
kusama |
CoinGecko coin id for the anchor. |
KSM_REFERENCE_SYMBOL |
KSMUSDT |
Binance symbol for the pre-CoinGecko history. |
COINGECKO_API_URL |
https://api.coingecko.com/api/v3 |
Point at a keyed endpoint to poll faster than the free tier allows. |
BINANCE_API_URL |
https://api.binance.com |
Daily-klines source used only by the backfill. |
Basilisk RPC
│
├─ raw-live + supervised backfill ── raw chain and derived tables
└─ live + historical price indexers ─ prices and OHLCV
│
ClickHouse
│
Fastify API (:3000)
│
Explorer UI (:5174)
src/contains the price and raw-data indexers, ingestion utilities, and maintenance scripts.clickhouse/schema/is the single declarative schema (tables + materialized views), applied once to an empty database by theschema-bootstrapservice — see Database model. There are no migrations.api/serves indexed data through cached read models; Compose snapshot services refresh bounded current-state datasets.explorer-ui/is the block explorer.ops/contains the ingestion supervisor image.
Historical raw ranges are finalized only after block counts and parent links validate. The supervisor promotes completed raw ranges into the price index and maintains the live pipelines. Writes and checkpoints are designed for replay and crash recovery.
Docker Compose provides working defaults. Override them in an untracked .env file when needed.
| Variable | Default | Purpose |
|---|---|---|
RPC_URL |
https://rpc.basilisk.cloud |
Price indexer RPC |
RAW_LIVE_RPC_URL |
https://rpc.basilisk.cloud |
Live raw-indexer RPC |
RAW_RPC_URL |
https://rpc.basilisk.cloud |
Historical raw-worker RPC |
IDENTITY_RPC_URL |
https://rpc.basilisk.cloud |
Identity snapshot RPC |
SUBSQUARE_BASE_URL |
https://basilisk.subsquare.io |
Source of referendum titles |
CLICKHOUSE_HOST |
http://localhost:18123 outside Compose |
ClickHouse HTTP endpoint |
CLICKHOUSE_PASSWORD |
empty outside Compose; dev in Compose |
ClickHouse password |
CLICKHOUSE_VOLUME_NAME |
see docker-compose.yml |
Docker volume containing ClickHouse data |
RAW_WORKERS |
6 |
Concurrent raw historical workers |
RANGE_SIZE |
1000 |
Blocks per raw historical range |
MAIN_WORKERS |
3 |
Concurrent historical price workers |
MAIN_MAX_RANGES |
3 |
Raw ranges consumed per price batch |
The KSM/USD reference variables (KSM_REFERENCE_*, COINGECKO_API_URL,
BINANCE_API_URL) are listed under Valuation bring-up.
Every RPC endpoint must serve Basilisk. Both indexers read state_getRuntimeVersion
at startup and abort unless specName is basilisk, because the generated codecs in
src/types would otherwise decode another chain into plausible-looking wrong rows.
Ingestion is RPC-only and permanently so: SQD publishes no Basilisk archive, so there is no gateway URL or API key.
See docker-compose.yml for service-specific tuning variables. Keep credentials in .env, never in tracked files.
For changes that are not simple environment values—such as ports, networks,
volumes, commands, or build settings—create a gitignored
docker-compose.override.yml beside docker-compose.yml. Docker Compose loads
and merges it automatically:
services:
clickhouse:
ports: !override
- "127.0.0.1:28123:8123"
ingestion-supervisor:
environment:
RAW_WORKERS: ${RAW_WORKERS:-2}Compose normally appends list values such as ports; !override replaces the
tracked list instead. Inspect the fully merged configuration before starting it:
docker compose config
docker compose up --build -dThe ingestion supervisor starts historical indexer and raw-indexer workers
through Compose from inside its container. If the override changes either worker
service, mount the file into the supervisor so those dynamically created workers
inherit it:
services:
ingestion-supervisor:
volumes:
- ./docker-compose.override.yml:/etc/snekwork/docker-compose.override.yml:roKeep credentials in .env; do not put them in the override file.
The query views support point-in-time prices, continuous block ranges, timestamp lookup, and OHLCV at 5-minute, 15-minute, 30-minute, 1-hour, 4-hour, 1-day, 1-week, and 1-month intervals.
SELECT *
FROM price_data.price_at_block(asset_id=5, block_height=7000000);
SELECT *
FROM price_data.ohlc_1h_query(
asset_id=5,
start_time='2026-01-01 00:00:00',
end_time='2026-01-31 23:59:59'
);See the ClickHouse query guide for the complete SQL reference.
Install each workspace, then run the repository-wide checks:
npm ci
npm --prefix api ci
npm --prefix explorer-ui ci
npm run check:allBrowser tests are separate because they require the relevant services:
npm --prefix explorer-ui run test:e2eCommon indexer commands:
npm start -- --help
npm run start:raw -- --help
npm run detect-gaps
npm run snapshot:balances -- --dry-runsrc/types/ is generated from Basilisk runtime metadata by
typegen.json, which reads a spec-version index that is not tracked
(it is ~19 MB). Regenerate both after a runtime upgrade introduces a shape this
indexer decodes:
npx squid-substrate-metadata-explorer --rpc wss://rpc.basilisk.cloud --out typegen/basiliskVersions.jsonl
npm run typegenBoth inputs are build artifacts and untracked. The type bundle comes from
src/basiliskTypesBundle.ts — the bundle shipped by
@subsquid/substrate-runtime plus the orml-tokens alias it is missing — and supplies
the definitions the pre-V14 metadata of specs 16 and 19 (blocks 1–395,663) cannot
describe on its own. The same module is what the processors pass to
.setTypesBundle(), so generation and ingestion cannot drift apart.
Generated version modules are named for the Basilisk spec version that introduced the
shape (v16, v25, … v128), and the decode call sites select among them with the
metadata-driven .is(block) probes rather than block-height comparisons. The era
boundaries those probes correspond to are named in
src/chainEras.ts, for logging and comments only — never for
branching.
npm run typegen also re-applies the one local edit to the generated
src/types/support.ts (src/scripts/patch-typegen-support.ts), so regeneration
cannot silently drop it. Everything under src/types/ is otherwise generated: edit
typegen.json and regenerate rather than editing it by hand.
The blockchain is the source of truth; every table is a reproducible projection of it, so the database is disposable and rebuildable — there are no migrations.
- Schema is declarative.
clickhouse/schema/*.sqldefines every table and materialized view (MV). Theschema-bootstrapservice applies it — in numeric order, idempotently (CREATE ... IF NOT EXISTS) — to an empty database before ingestion starts. Because the MVs exist first, every MV-backed read model populates itself as raw data is indexed, in any order, with no backfill. - Derived data comes from three places. Most read models are MVs (automatic). The
few an MV cannot express — per-trade netting (
account_trade_volume) and the stateful LP-history reconstructions — are recomputed continuously and idempotently by thederivationsservice. A small set of current-state snapshots (account-directory values) are refreshed on API timers. - To change a model, edit the declaration and rebuild the projection — drop the table/MV and let it refill from raw, or reset the derived layer and let it rebuild. Never write an in-place migration; there is no version ledger.
Fresh-install order (enforced by Compose depends_on):
schema-bootstrap → ingestion (raw) → derivations → api. Applying the schema to a
non-empty database is a safe no-op, so redeploying never risks existing data.
- Keep ClickHouse data and checkpoints together; do not wipe tables to resolve an ingestion problem.
- Let
ingestion-supervisorown its dynamically created historical workers. Do not manually start or stop those containers. - Use bounded, explicit block ranges and distinct pipeline IDs for manual backfills.
- Change a model by editing
clickhouse/schema/and rebuilding that projection from raw; never patch derived data in place, and never wipe raw to fix a derived model. - Back up the ClickHouse volume before production schema or checkpoint maintenance.
ISC