A backend service to index all blob-related transactions from the Ethereum blockchain starting from EIP-4844 activation.
The Blob Indexer API continuously indexes new blocks and pending blob transactions from the Ethereum blockchain, persists the data in a PostgreSQL database, and provides REST APIs to access the data.
- Continuously index new blocks and pending blob transactions
- Persist historical blob data with correct base fee, tip, and total cost
- Attribute blobs to known rollups based on from_address matching
- Provide simple, fast REST APIs to access blob data
- Support multiple Ethereum networks simultaneously (e.g., mainnet, Sepolia)
- Configuration-based RPC URLs (not stored in database for flexibility)
- In-process rate limiting (100 req/s per IP with burst of 200)
- Chain reorganization detection via block hash tracking
- Structured JSON logging with configurable levels
- Swagger/OpenAPI documentation
- AsyncAPI WebSocket message documentation
- Development dashboard with metrics, indexer status, and database stats
- Language: Go 1.26.1
- HTTP Framework: Chi
- Database: PostgreSQL (with sqlx query builder)
- Ethereum Client: go-ethereum (ethclient)
- Configuration: Viper (YAML + environment variables)
- Logging: Zap (structured JSON)
- Migrations: golang-migrate via
cmd/migrate,make db-migrate, or Helm-managed migration containers - API Docs: Swagger/OpenAPI (swag + http-swagger), plus AsyncAPI for WebSocket messages
- Deployment: Docker, Kubernetes with Helm, Tilt for development
GET /api/v1/networks- List all available networksGET /api/v1/networks/{chainId}- Get status of a specific network
GET /api/v1/blob/latest?network=mainnet&limit=10- Fetch latest blobs from recent blocksGET /api/v1/blob/mempool?network=mainnet&limit=10- Fetch pending (unconfirmed) blob transactionsGET /api/v1/blob/pricing?network=mainnet&blocks=20- Fetch recent blob pricing and utilization metricsGET /api/v1/blob/by-hash/{versionedHash}?network=mainnet- Fetch the blob transaction carrying an EIP-4844 versioned blob hash (0x01-prefixed)GET /api/v1/blob/{txHash}?network=mainnet- Fetch a specific blob by transaction hash
GET /api/v1/block/{number}?network=mainnet- Fetch a single indexed block with its blobs and pricing (same shape as the WebSocketnew_blockevent); 404 if the block is not indexed
GET /api/v1/users?network=mainnet&limit=10&range=24h- Top blob users by blobs submitted;range(1h,24h,7d,30d,all; defaultall) scopes counts, spend, and share percentages to a recent window and is echoed back asmeta.rangeGET /api/v1/users/unattributed?network=mainnet&limit=10- Top unattributed blob senders by blobs submitted (samerangesupport)
GET /api/v1/stats?network=mainnet- Historical blob cost trends, base fee history
GET /api/v1/status?network=mainnet- Indexer status
GET /api/v1/ws?network=mainnet- Subscribe to live blob, stats, and user updates
WebSocket clients receive raw JSON text frames with a top-level type field
and a type-specific data payload. users_update events also carry a
top-level range field naming the aggregation window they cover (currently
always all), so clients viewing a different REST range can ignore them.
The committed AsyncAPI contract is in docs/asyncapi.yaml, and the API serves
it at GET /asyncapi.yaml.
Clients may send an optional subscription filter after connecting:
{"subscribe":["new_block","mempool_update","stats_update","users_update"]}If no subscription filter is sent, all event types are delivered. The server
also sends an application-level {"type":"ping"} heartbeat every 30 seconds.
These endpoints are available only when server.dev_mode is enabled, and can be protected with server.dev_api_key. Set server.dev_port to serve them from a dedicated listener instead of the main API listener:
GET /api/v1/dev/metrics- System-wide metrics (memory, goroutines, uptime)GET /api/v1/dev/indexers- Per-network indexer statusGET /api/v1/dev/database- Database statisticsGET /api/v1/dev/logs- Log entriesGET /api/v1/dev/queries- Database query statsGET /api/v1/dev/dashboard- HTML development dashboard
Legacy /api/* paths redirect to /api/v1/*. API endpoints accept an optional network query parameter (name or chain ID) to specify which network to query. If not provided, the first enabled network is used.
The API is documented using Swagger/OpenAPI. When the server is running, you can access the Swagger UI at:
http://localhost:8080/swagger/index.html
To generate the Swagger documentation:
make swaggerGenerated Swagger files are intentionally ignored by Git. Build, test, Docker, and CI targets generate them when they need the docs package.
The WebSocket message contract is documented separately with AsyncAPI:
http://localhost:8080/asyncapi.yaml
- Go 1.26.1 or later
- Docker
- Kubernetes cluster (local or remote)
- Helm
- Tilt
The application can be configured using either a YAML configuration file or environment variables.
Create a config.yaml file:
database:
url: "postgres://postgres:postgres@localhost:5432/blobindexer?sslmode=disable"
server:
port: 8080
dev_port: 8081 # optional; 0 keeps dev endpoints on the main API listener
dev_mode: true
cors:
enabled: true
allowed_origins:
- "http://localhost:3000"
- "http://localhost:3001"
- "http://127.0.0.1:3000"
- "http://127.0.0.1:3001"
allowed_origin_patterns: []
allow_all_origins: false
allowed_methods: ["GET", "OPTIONS"]
allowed_headers: ["Accept", "Content-Type", "Authorization"]
exposed_headers: ["Content-Length", "ETag"]
allow_credentials: false
max_age_seconds: 86400
logging:
level: "info"
format: "json"
indexer:
version: "v1.0.0"
batch_size: 100
polling_interval: 15s
mempool_polling_interval: 30s
networks:
- name: "mainnet"
chain_id: 1
rpc_url: "https://mainnet.infura.io/v3/your-key"
start_block: "LATEST-1000"
enabled: true
- name: "sepolia"
chain_id: 11155111
rpc_url: "https://sepolia.infura.io/v3/your-key"
start_block: "LATEST-100"
enabled: trueSet the CONFIG_PATH environment variable to point to your config file:
export CONFIG_PATH=config.yamlAlternatively, you can use environment variables:
DB_URL- PostgreSQL connection stringPORT- API server port (default: 8080)DEV_PORT- Optional dedicated dev endpoint port (default: 0, disabled)DEV_MODE- Enable development mode (default: false)LOG_LEVEL- Logging level (default: info)LOG_FORMAT- Logging format (default: json)INDEXER_VERSION- Version of the indexerDEV_API_KEY- Optional API key for development endpointsCONFIG_PATH- Path to YAML configuration file (optional)CORS_ENABLED- Enable browser CORS responses (default: true)CORS_ALLOWED_ORIGINS- Comma-separated list of exact browser originsCORS_ALLOWED_ORIGIN_PATTERNS- Comma-separated wildcard origin patterns for preview deploysCORS_ALLOW_ALL_ORIGINS- Allow any request origin while still echoing the origin (default: false)CORS_ALLOWED_METHODS- Comma-separated allowed methods (default: GET,OPTIONS)CORS_ALLOWED_HEADERS- Comma-separated allowed request headersCORS_EXPOSED_HEADERS- Comma-separated response headers exposed to browsersCORS_ALLOW_CREDENTIALS- SendAccess-Control-Allow-Credentials: true(default: false)CORS_MAX_AGE_SECONDS- Preflight cache duration in seconds (default: 86400)
For backward compatibility, you can configure a single network using:
RPC_URL- Ethereum node endpointETH_RPC_URL- Ethereum node endpoint (takes precedence overRPC_URL)START_BLOCK- Starting block for indexing (e.g., "LATEST-1000")
For multiple networks, use the following pattern:
NETWORK_<NAME>_RPC_URL- RPC URL for the networkNETWORK_<NAME>_START_BLOCK- Starting block for the networkNETWORK_<NAME>_ENABLED- Whether the network is enabled (true/false)
Example:
NETWORK_SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/your-key
NETWORK_SEPOLIA_START_BLOCK=LATEST-100
NETWORK_SEPOLIA_ENABLED=true
- Install Tilt: https://docs.tilt.dev/install.html
- Create a
.envfile with your environment variables (see.env.example):ETH_RPC_URL=https://mainnet.infura.io/v3/your-api-key START_BLOCK=LATEST-1000 - Start Tilt:
tilt up - Access the API at http://localhost:8080/api/v1
make build # Build both binaries
make build-api # Build API server only
make build-indexer # Build indexer only
make build-migrate # Build migration runner only
make run-api # Build and run API server
make run-indexer # Build and run indexer
make test # Run all tests
make clean # Remove built binary
make deps # Download and tidy Go module dependencies
make docker-build # Build both Docker images
make docker-run # Run Docker container
make tilt-up # Start Tilt development environment
make seed-data # Seed test data (runs cmd/testdata)
make swagger # Generate local Swagger/OpenAPI artifacts
make db-migrate # Run database migrations
make db-rollback # Rollback one database migration
make helm-dep-update # Update Helm chart dependencies
make helm-install-dev # Install Helm chart with dev values
make helm-install-prod # Install Helm chart with prod values
make helm-upgrade # Upgrade Helm release
make helm-uninstall # Uninstall Helm release# Build the Docker images
make docker-build
# Run the API container
docker run -p 8080:8080 \
-e DB_URL="postgres://postgres:postgres@postgres:5432/blobindexer?sslmode=disable" \
-e LOG_LEVEL="info" \
blob-indexer-api# Install the chart. PostgreSQL is external to this chart; provide a DB URL.
helm install blob-indexer ./charts/blob-indexer \
--set appConfig.database.url="postgres://user:pass@postgres.example.com:5432/blobindexer?sslmode=require" \
--set appConfig.networks[0].name="mainnet" \
--set appConfig.networks[0].chain_id=1 \
--set appConfig.networks[0].rpc_url="https://mainnet.infura.io/v3/your-api-key" \
--set appConfig.networks[0].start_block="LATEST-1000" \
--set appConfig.networks[0].enabled=trueFor GitOps installs with an externally managed database Secret, disable Secret creation and point the chart at the existing key. The API, indexer, and migration Job all use the same Secret by default:
migrations:
enabled: true
databaseSecret:
create: false
name: prod-blob-indexer-blob-indexer-db
key: DB_URLFor multi-replica deployments, configure edge rate limiting on Ingress so limits are enforced across all pods. Set ingress.annotations in Helm values (for example with NGINX: nginx.ingress.kubernetes.io/limit-rps and nginx.ingress.kubernetes.io/limit-burst-multiplier).
To expose dev endpoints through a separate Kubernetes Service, set appConfig.server.dev_port and enable devService. The chart will create <release>-dev targeting only /api/v1/dev/* on the dedicated listener.
This project uses release-please to automate releases. The Go app and Helm chart are versioned independently.
How it works:
- PR titles must use Conventional Commits format (e.g.,
feat: add new endpoint,fix: correct query logic,deps: bump module) — this is enforced by CI - When PRs are merged to
main, release-please automatically maintains a release PR with a generated changelog - Merging the release PR creates a GitHub Release
- App releases publish Docker images to Harbor at
registry.ahkc.win/public/blob-indexer-api-apiandregistry.ahkc.win/public/blob-indexer-api-indexer; chart releases package and push the Helm chart tooci://registry.ahkc.win/public/charts
blob-indexer-api/
├── cmd/
│ ├── api/ # API server binary
│ ├── indexer/ # Indexer binary
│ ├── migrate/ # Database migration runner
│ └── testdata/ # Test data seeding utility
├── internal/
│ ├── api/ # API handlers, router, middleware, rate limiting
│ ├── attribution/ # User attribution (maps addresses to rollup names)
│ ├── config/ # Configuration management (Viper-based)
│ ├── db/ # Database access layer
│ │ ├── migrations/ # SQL migration files
│ │ └── models/ # Database models
│ ├── ethereum/ # Ethereum client wrapper
│ ├── indexer/ # Core indexing logic
│ └── logger/ # Centralized structured logging (Zap)
├── charts/ # Helm chart for Kubernetes deployment
├── docs/ # Local generated Swagger/OpenAPI artifacts
├── .env.example # Environment variable template
├── config.yaml # Default YAML configuration
├── Dockerfile.api # API Docker image
├── Dockerfile.indexer # Indexer Docker image
├── Makefile # Build, test, and deployment tasks
├── Tiltfile # Tilt configuration for local development
├── tilt-config.yaml # Tilt settings
├── go.mod # Go module definition
└── README.md # Project documentation
MIT