Watches Kaspa L1 via gRPC, indexes all IGRA transactions, and serves instant L2→L1 lookups via HTTP API.
docker build -t igra-simple-indexer .
# First run — provide a recent block hash as starting point:
docker run -d --name indexer --restart unless-stopped \
-v indexer-data:/data -p 3000:3000 \
igra-simple-indexer --start-hash <RECENT_BLOCK_HASH>
# Subsequent runs resume automatically from saved cursor:
docker run -d --name indexer --restart unless-stopped \
-v indexer-data:/data -p 3000:3000 \
igra-simple-indexerThe indexer needs a Kaspa block hash to begin from. Get the current tip from logs:
# Run briefly to see the sink hash, then stop and restart with it:
docker run --rm igra-simple-indexer 2>&1 | head -5
# Look for: sink=<HASH>Every IGRA L2 transaction originates as a Kaspa L1 transaction. This indexer maps L2 tx hashes back to their L1 origin, so block explorers can link each L2 transaction to its L1 proof.
GET /api/v1/l1tx/{l2_tx_hash}
The l2_tx_hash is the standard EVM transaction hash (with or without 0x prefix, case-insensitive).
Found:
{
"l2_tx_hash": "0x8a3c...f1b2",
"l1_tx_id": "97b1a4c3...e8f0",
"daa_score": 380089434,
"status": "found"
}Not found:
{
"l2_tx_hash": "0x8a3c...f1b2",
"status": "not_found"
}The l1_tx_id is a Kaspa transaction ID. Link to it on a Kaspa explorer:
https://explorer.kaspa.org/txs/{l1_tx_id}
GET /api/v1/l1tx/entry/{withdrawal_index}
Found:
{
"withdrawal_index": 42,
"l1_tx_id": "97b1f2a1...c3d4",
"recipient": "0x742d35cc6634c0532925a3b844bc9e7595f2bd38",
"amount_sompi": 100000000,
"daa_score": 380100000,
"l2_block_number": 12345,
"status": "found"
}GET /api/v1/health
{
"send_txs_indexed": 1842,
"entry_txs_indexed": 15,
"last_daa_score": 381085692,
"last_chain_hash": "26f073...",
"status": "ok"
}- Base URL: The indexer runs on port 3000 by default. We will provide the production URL.
- Latency: Lookups are instant (SQLite). Indexing lag is ~1 second behind L1 tip.
- Not-found responses return HTTP 200 with
"status": "not_found"— the tx may not have been indexed yet (if very recent) or may not be an IGRA transaction. - CORS is enabled, so browser-side calls work.
const res = await fetch(`${INDEXER_URL}/api/v1/l1tx/${l2TxHash}`);
const data = await res.json();
if (data.status === "found") {
// Show link: "View L1 proof on Kaspa"
const kaspaUrl = `https://explorer.kaspa.org/txs/${data.l1_tx_id}`;
}| Flag | Default | Description |
|---|---|---|
--kaspa-rpc-url |
grpc://igra-mainnet.jobberwocky.co:16310 |
Kaspa gRPC endpoint (requires V2 API, kaspad v1.1.0+) |
--evm-rpc-url |
https://rpc.igralabs.com:8545 |
IGRA L2 EVM JSON-RPC |
--tx-id-prefix |
97b1 |
Required Kaspa TX ID prefix |
--db-path |
indexer.db |
SQLite database path |
--listen |
0.0.0.0:3000 |
HTTP API listen address |
--start-daa |
366020000 |
DAA score to start from (slow, walks chain) |
--start-hash |
— | Block hash to start from (fast, skips walk) |
Logs: docker logs -f indexer
Log levels: Set RUST_LOG env var. Default: simple_indexer=info. Use simple_indexer=debug for per-block output.
docker run -d --name indexer --restart unless-stopped \
-e RUST_LOG=simple_indexer=debug \
-v indexer-data:/data -p 3000:3000 \
igra-simple-indexer --start-hash <HASH>Storage: ~12 MB/day at 1 TPS. SQLite DB lives in the Docker volume at /data/indexer.db.
Monitoring: Poll /api/v1/health — check that last_daa_score advances. If it stalls, the gRPC connection may be down (auto-reconnects with 5s backoff).
Backup: docker cp indexer:/data/indexer.db ./backup.db (stop container first for clean copy, or copy both .db and .db-wal).
Kaspa node requirement: The gRPC endpoint must support GetVirtualChainFromBlockV2 (kaspad v1.1.0+). The default igra-mainnet.jobberwocky.co:16310 supports this. Standard Kaspa nodes on port 16110 may not.
Pruning: Kaspa prunes old blocks (~24h). The indexer can only start from blocks within the pruning window. Historical data requires an archival node.