Lightweight Go microservice that indexes token balances, transfers, and holder rankings for the Koinos blockchain. Plugs directly into the node's AMQP message bus — no external dependencies.
┌─────────────────────────────────────────────────────┐
│ Koinos Node │
│ │
│ chain ─── block_store ─── p2p ─── block_producer │
│ │ │ │
│ └──── AMQP (RabbitMQ) ────┐ │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ koinos-token-tracker│ │
│ │ │ │
│ │ ┌── Syncer ◄─ AMQP RPC │
│ │ │ (GetBlocksByHeight) │
│ │ │ │
│ │ ├── Indexer │
│ │ │ (extract addresses, │
│ │ │ parse token events, │
│ │ │ track balances) │
│ │ │ │
│ │ ├── SQLite Store │
│ │ │ (addresses, balances, │
│ │ │ transfers, blocks) │
│ │ │ │
│ │ └── REST API (:8090) │
│ │ (Swagger UI, JSON) │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────┘
| Data | Count | Source |
|---|---|---|
| Addresses | ~384,000 | Block headers, tx headers, event impacted fields |
| KOIN balances | ~9,250 holders | Transfer/mint/burn events (KCS-4 + KCS-1) |
| VHP balances | ~194 holders | Transfer/mint/burn events + REST reconciliation |
| Token transfers | ~55M records | All KOIN/VHP events from genesis |
| Block metadata | ~34.5M blocks | Height, timestamp, producer, tx count |
- Confirmed-only indexing: Only processes blocks up to the Last Irreversible Block (LIB = head - 60). Balances lag ~3 minutes but are guaranteed fork-safe.
- Event-sourced balances: Tracks balance changes from on-chain transfer/mint/burn events rather than querying contract state.
- KCS-4 migration handling: Old KOIN contract (KCS-1) events affect balances (migration did proper burn/mint). Old VHP events are stored for transfer history only — VHP balances are reconciled via REST API because the migration pre-loaded state without events.
- Migration-height reset: At block 24,804,034 (KCS-4 migration), VHP balances are reset to zero to prevent double-counting from the old contract.
# Build
go build -o koinos-token-tracker cmd/koinos-token-tracker/main.go
# Run (assumes node AMQP on localhost:5672)
./koinos-token-tracker \
--basedir /home/koinos/.koinos \
--amqp amqp://guest:guest@localhost:5672/ \
--port 8090docker build -t koinos/koinos-token-tracker .
docker run -v /home/koinos/.koinos:/koinos \
--network host \
koinos/koinos-token-tracker --basedir=/koinostoken_tracker:
image: koinos/koinos-token-tracker:latest
restart: always
profiles: ["token_tracker", "api", "all"]
depends_on:
- amqp
- chain
- block_store
volumes:
- "${BASEDIR}:/koinos"
- "./config:/config:ro"
ports:
- "${TOKEN_TRACKER_PORT:-8090}:8090"
command: --basedir=/koinos --config=/config/config.yml| Flag | Default | Description |
|---|---|---|
--basedir, -d |
~/.koinos |
Base directory for data |
--amqp, -a |
amqp://guest:guest@localhost:5672/ |
AMQP connection URL |
--port, -p |
8080 |
HTTP API listen port |
--log-level |
info |
Log level (debug, info, warn, error) |
--reset |
false |
Delete database and re-sync from genesis |
--reconcile |
false |
After sync, reconcile VHP balances against REST API |
--rest-url |
http://127.0.0.1:3000 |
REST API URL for reconciliation |
--config, -c |
Path to Koinos node config.yml (token addresses, migration height) | |
--version, -v |
Print version and exit |
| Endpoint | Description |
|---|---|
GET / |
Explorer UI |
GET /docs |
Swagger API documentation |
GET /v1/token-tracker/status |
Sync status, holder counts |
GET /v1/token-tracker/stats |
Chain statistics |
GET /v1/token-tracker/addresses?limit=50&offset=0 |
All addresses (paginated) |
GET /v1/token-tracker/address/{address} |
Address balances and first-seen info |
GET /v1/token-tracker/holders/{token}?limit=50 |
Top token holders ranked by balance |
GET /v1/token-tracker/transfers/{address}?limit=50 |
Token transfer history for an address |
GET /v1/token-tracker/blocks?from=N&to=M |
Block metadata |
GET /v1/token-tracker/tokens |
Tracked token contracts |
GET /openapi.json |
OpenAPI 3.0 spec |
- Startup: Reads last indexed height from SQLite
- Historical catch-up: Calls
chain.GetHeadInfovia AMQP to get LIB height, then fetches blocks in batches of 1,000 fromblock_store.GetBlocksByHeight - Live mode: Polls every 10 seconds, syncs new irreversible blocks
- Per block: Extracts addresses from headers + events, parses token transfer/mint/burn events, applies balance deltas, stores transfer records
- Database: SQLite (pure Go via
modernc.org/sqlite, no CGO) - Size: ~28GB with full transfer history from genesis
- Tables:
sync_state,addresses,balances,transfers,blocks,tokens - Single connection:
MaxOpenConns(1)with mutex-protected batch transactions
Token contract addresses and migration parameters default to Koinos mainnet values. For testnet or custom deployments, specify them in the node's config.yml:
token-tracker:
koin-contract: "19GYjDBVXU7keLbYvMLazsGQn3GTWHjHkK"
vhp-contract: "12Y5vW6gk8GceH53YfRkRre2Rrcsgw7Naq"
old-koin-contract: "15DJN4a8SgrbGhhGksSBASiSYjGnMU8dGL"
old-vhp-contract: "1AdzuXSpC6K9qtXdCBgD5NUpDNwHjMgrc9"
kcs4-migration-height: 24804034Pass the config file path with --config:
./koinos-token-tracker --basedir=/koinos --config=/config/config.ymlAny omitted fields default to mainnet values. A testnet deployment only needs to override the relevant addresses.
| Token | Contract | Tracks |
|---|---|---|
| KOIN (KCS-4) | 19GYjDBVXU7keLbYvMLazsGQn3GTWHjHkK |
Balances + Transfers |
| VHP (KCS-4) | 12Y5vW6gk8GceH53YfRkRre2Rrcsgw7Naq |
Balances + Transfers |
| KOIN (KCS-1) | 15DJN4a8SgrbGhhGksSBASiSYjGnMU8dGL |
Balances + Transfers |
| VHP (KCS-1) | 1AdzuXSpC6K9qtXdCBgD5NUpDNwHjMgrc9 |
Transfers only |
MIT