Hedera Stats is a PostgreSQL-based analytics platform that provides quantitative statistical measurements for the Hedera network. The platform calculates and stores metrics using SQL functions and procedures, leveraging open-source methodologies, Hedera mirror node data, third-party data sources, and Hgraph's GraphQL API. These statistics include network performance metrics, NFT analytics, account activities, and economic indicators, enabling transparent and consistent analysis of the Hedera ecosystem.
All metrics are pre-computed and stored in the ecosystem.metric table, with automated updates via pg_cron and visualization through Grafana dashboards.
- PostgreSQL database (v14+) with the following extensions:
- pg_http - For external API calls (HBAR price data, etc.)
- pg_cron - For automated metric updates (requires superuser privileges)
- timestamp9 - For Hedera's nanosecond precision timestamps
- Hedera Mirror Node or access to Hgraph's GraphQL API
- Prometheus (
promtool) foravg_time_to_consensus(download) - DeFiLlama API for decentralized finance metrics (view docs)
- Grafana (optional) for visualization dashboards
Clone this repository:
git clone https://github.com/hgraph-io/hedera-stats.git
cd hedera-statsInstall Prometheus CLI (promtool):
curl -L -O https://github.com/prometheus/prometheus/releases/download/v3.1.0/prometheus-3.1.0.linux-amd64.tar.gz
tar -xvf prometheus-3.1.0.linux-amd64.tar.gz
cp prometheus-3.1.0.linux-amd64/promtool /usr/binThe schema and every metric live in ordered migrations under src/migrations/,
applied once each and tracked in ecosystem.schema_migrations. They are pure
schema SQL — the timestamp9 and http extensions and the logical replication
subscription are external prerequisites, installed by a superuser beforehand.
Migrations run as a one-shot Docker container (no long-running service): it
applies pending migrations to a network's database over the host Postgres socket,
then exits. Objects are owned by the per-network ecosystem_owner role.
docker compose build # build the runner image once
docker compose run --rm mainnet # → 5433 / hedera_mainnet
docker compose run --rm testnet # → 5432 / hedera_testnetConnection is credential-free: the host socket dir is bind-mounted and the
container runs as the host postgres UID (peer auth). Set POSTGRES_UID in .env
to id -u postgres (26 on RHEL/Fedora, 999 on Debian) — it must match or peer
auth fails. On SELinux-enforcing hosts the compose file already disables label
confinement so the container can reach the host socket. Re-running is safe —
already-applied migrations are skipped. After adding a migration, pass --build
so the new file is baked into the image (docker compose run --rm --build mainnet).
To run outside Docker, invoke the runner directly with libpq vars:
PGHOST=/var/run/postgresql PGPORT=5433 PGDATABASE=hedera_mainnet PGUSER=postgres \
PGOPTIONS='-c role=hedera_mainnet_ecosystem_owner' bash src/migrations/migrate.sh
pg_cronscheduling (src/jobs/pg_cron_metrics.sql) is not applied by the migration runner. It drives metric loading and belongs on the publisher.
Configure environment variables (create .env file):
# Database connection
DATABASE_URL="postgresql://user:password@localhost:5432/hedera_stats"
POSTGRES_CONNECTION_STRING="postgresql://user:password@localhost:5432/hedera_stats"
# API Keys
HGRAPH_API_KEY="your_api_key"
# Prometheus (for avg_time_to_consensus)
PROMETHEUS_ENDPOINT="https://your-prometheus-endpoint"Schedule automated updates:
-
pg_cron for metric updates:
# Edit src/jobs/pg_cron_metrics.sql # Replace <database_name> and <database_user> with your database name and database user psql -d your_database -f src/jobs/pg_cron_metrics.sql
-
Time-to-consensus updates (if using Prometheus):
crontab -e # Add these entries: # Hourly collection 1 * * * * cd /path/to/hedera-stats/src/time-to-consensus && bash ./run.sh >> ./.raw/cron.log 2>&1 # Daily collection 2 0 * * * cd /path/to/hedera-stats/src/time-to-consensus && bash ./run_day.sh >> ./.raw/cron_day.log 2>&1
hedera-stats/
├── src/
│ ├── metric_descriptions.sql # PUBLISHER reference (run on the publisher); NOT a migration
│ ├── migrations/ # SOLE apply path: NNN-name.sql, applied once each in order
│ │ ├── migrate.sh # Runner: applies unapplied migrations, tracks in schema_migrations
│ │ ├── 001-init.sql # Schema, tables, metric_total type, row helpers
│ │ └── 0NN-*.sql # One migration per function / description / seed / load procedure
│ ├── grafana/ # Grafana dashboard JSON exports
│ │ └── Hgraph_Hedera-Stats-Grafana_V2.json
│ ├── public-migrations/ # Separate set for PUBLIC-schema objects (last-24h matviews)
│ ├── jobs/
│ │ └── pg_cron_metrics.sql # PUBLISHER-only cron job definitions; NOT a migration
│ ├── metrics/ # NOT the apply path (see "Where the SQL lives" below)
│ │ ├── hbar-defi/
│ │ │ └── hbar_total_supply.sql # PUBLISHER reference (hbar_total_supply constant)
│ │ └── legacy/ # Promotion queue: publisher objects with no migration yet
│ └── time-to-consensus/ # Prometheus ETL pipeline
├── Dockerfile # Slim one-shot migration runner (psql + bash)
├── docker-compose.yml # Per-network one-shot services (mainnet, testnet)
├── CLAUDE.md # AI assistant guidance
├── WORKFLOW.md # Development workflow
├── CHANGELOG.md # Version history
├── LICENSE
└── README.md
src/migrations/ is the only copy of every live ecosystem object, and
src/public-migrations/ the only copy of the public-schema ones. There is no
parallel "readable source" tree: src/metrics/ and src/jobs/ once held a
per-metric mirror of the migrations, but nothing kept the two in sync, so the
duplicates were removed. To read a metric's definition, open its migration —
or \sf ecosystem.<metric_name> against a deployed database.
Three things under those paths are deliberately not migrations:
| Path | What it is |
|---|---|
src/metric_descriptions.sql |
Publisher reference — ecosystem.metric_description rows are publisher-owned and replicated to subscribers |
src/metrics/hbar-defi/hbar_total_supply.sql |
Publisher reference — the hbar_total_supply constant, same reason |
src/jobs/pg_cron_metrics.sql |
Publisher-only cron schedule; cron drives metric loading and does not belong on a subscriber |
src/metrics/legacy/ is a promotion queue: objects that exist on the
hand-built publisher (several tracked in Hasura) but have no migration yet.
Promote one by writing a migration for it, then delete the legacy file.
- ecosystem.metric - Central table storing all calculated metrics
- Columns:
name,period,timestamp_range(int8range),total(bigint) - Unique constraint:
(name, period, timestamp_range)
- Columns:
- ecosystem.metric_total - Standard return type for metric functions:
(int8range, total) - ecosystem.metric_description - Metadata with name, description, and methodology
All metric functions follow this standard signature:
CREATE OR REPLACE FUNCTION ecosystem.<metric_name>(
period TEXT, -- 'minute','hour','day','week','month','quarter','year'
start_timestamp BIGINT DEFAULT 0,
end_timestamp BIGINT DEFAULT CURRENT_TIMESTAMP::timestamp9::BIGINT
) RETURNS SETOF ecosystem.metric_total- External Data Sources → PostgreSQL via pg_http or mirror node tables
- Metric Functions → Calculate metrics using SQL/PL/pgSQL
- Job Procedures → Orchestrate metric loading via stored procedures
- pg_cron Jobs → Automate execution on schedule
- ecosystem.metric table → Store pre-computed results
- Grafana Dashboards / GraphQL API → Query and visualize metrics
| Category | Count | Examples |
|---|---|---|
| Activity & Engagement | 11 | active_accounts, new_accounts, total_accounts, *_ecdsa_*, *_ed25519_* |
| EVM/Smart Contracts | 5 | active_smart_contracts, new_smart_contracts, *_ecdsa_accounts_real_evm |
| HBAR & DeFi | 4 | avg_usd_conversion, hbar_market_cap, hbar_total_released, hbar_total_supply |
| Network Performance | 4 | network_fee, avg_network_fee, network_tps, avg_time_to_consensus |
| Transactions | 14+ | new_transactions, total_hcs_transactions, new_crypto_transactions |
| NFTs | 2 | nft_collection_sales_volume, nft_collection_sales_volume_total |
View all metrics & documentation →
| Period | Description | Typical Use Case |
|---|---|---|
minute |
Per-minute data | High-frequency price tracking (72h retention) |
hour |
Hourly aggregates | Real-time monitoring |
day |
Daily aggregates | Standard reporting |
week |
Weekly aggregates | Trend analysis |
month |
Monthly aggregates | Monthly reports |
quarter |
Quarterly aggregates | Quarterly reports |
year |
Yearly aggregates | Annual comparisons |
| Job | Schedule | Time (UTC) |
|---|---|---|
| Minute | * * * * * |
Every minute |
| Hour | 1 * * * * |
:01 past each hour |
| Day | 2 0 * * * |
00:02 daily |
| Week | 2 0 * * 0 |
00:02 Sundays |
| Month | 4 0 1 * * |
00:04 on 1st |
| Quarter | 8 0 1 1,4,7,10 * |
00:08 on Jan/Apr/Jul/Oct 1st |
| Year | 14 0 1 1 * |
00:14 on Jan 1st |
-- Query metrics from the ecosystem.metric table
SELECT *
FROM ecosystem.metric
WHERE name = 'active_accounts'
AND period = 'day'
ORDER BY timestamp_range DESC
LIMIT 20;-- Call a metric function with time range
SELECT * FROM ecosystem.active_accounts(
'day',
(current_timestamp - interval '7 days')::timestamp9::bigint,
current_timestamp::timestamp9::bigint
);
-- Check job execution status
SELECT * FROM cron.job_run_details ORDER BY start_time DESC LIMIT 10;-- Run a specific period's loader
CALL ecosystem.load_metrics_hour();
CALL ecosystem.load_metrics_day();
-- Backfill/initialize metrics
CALL ecosystem.load_metrics_init();Use Grafana to visualize metrics:
- Import
Hgraph_Hedera-Stats-Grafana_V2.jsonfromsrc/grafana/ - Configure PostgreSQL data source pointing to your database
- Dashboards query
ecosystem.metrictable directly
Query available metrics dynamically via GraphQL API (test in our developer playground):
query AvailableMetrics {
ecosystem_metric(distinct_on: name) {
name
description {
description
methodology
}
}
}- Verify you're querying the correct API endpoint:
- Staging environment (
hgraph.dev) may have incomplete data - Production endpoint (
hgraph.io) requires an API key
- Staging environment (
-- Check scheduled cron jobs
SELECT * FROM cron.job;
-- Check recent job execution history
SELECT * FROM cron.job_run_details ORDER BY start_time DESC LIMIT 10;-- Verify the metric function exists
\df ecosystem.<metric_name>
-- Check if metric is in the load procedure's array
-- (review src/migrations/*-load_metrics_<period>.sql, or the deployed
-- definition: \sf ecosystem.load_metrics_<period>)
-- Test the function directly
SELECT * FROM ecosystem.<metric_name>(
'day',
(current_timestamp - interval '30 days')::timestamp9::bigint,
current_timestamp::timestamp9::bigint
);- Use broader granularity (day/month) for extensive time periods
- Limit result size with
LIMITand filter byperiod - Query pre-computed data from
ecosystem.metricinstead of calling functions directly
- Full Hedera Stats Documentation →
- Hedera Mirror Node Docs
- Hedera Transaction Result Codes
- Hedera Transaction Types
- DeFiLlama API Documentation
- PostgreSQL Documentation
We welcome contributions!
- Fork this repository
- Create your feature branch (
git checkout -b feature/new-metric) - Commit changes (
git commit -am 'Add new metric') - Push to the branch (
git push origin feature/new-metric) - Submit a Pull Request detailing your changes
See WORKFLOW.md for detailed development guidelines.