Skip to content

Lizard-King101/StakePulse

Repository files navigation

StakePulse

Multi-chain validator/pool analytics as an API + light dashboard + alerts. MVP chain: Solana (live). Next: Ethereum Beacon, Cosmos Hub, Bitcoin pools.


Features (high-signal)

  • Normalized operator metrics (per chain)

    • Availability: hourly buckets → 24h/7d uptime
    • Yield/APY: daily snapshots (MVP: inflation.validator × (1 - commission))
    • Stake flows: daily net change (MVP: votingPower diffs)
    • States: commission, delinquent/flags, voting power
  • Alerts: rules + signed webhooks (x-sp-signature HMAC-SHA256)

  • Docs: Swagger UI at /docs (raw /v0/openapi.json)

  • Dashboard (starter): Next.js + Mantine (validators list/detail, alerts CRUD, status)


Stack & Services

  • Backend: NestJS (Fastify) + TypeScript (CommonJS)
  • DB: Postgres 16 via Prisma ORM
  • Cache/Jobs: Redis + BullMQ (workers & scheduler)
  • Containers: Docker Compose (postgres, redis, migrate, api, worker)
  • Auth/limits: x-api-key (SHA-256 stored). Tiers w/ Redis sliding window Defaults: Free 30/min, Pro 60/min, Team 240/min.

Core API (v0)

Public health

  • GET /v0/status

Operators

  • GET /v0/chains/:chain/operators?min_availability&max_commission&limit → rows joined with latest state, 24h availability, latest APY, 7d net flow
  • GET /v0/chains/:chain/operators/:id/metrics?window=24h|7d|30d&granularity=1h|6h|1d&from&to{ meta, availability[{t, produced, missed, uptime_pct}], yields[{t, apy}], flows[{t, net}] }

Alerts (QoL)

  • POST /v0/alerts/webhooks { url, secret }
  • GET /v0/alerts/webhooks (includes their rules)
  • POST /v0/alerts/webhooks/:id/ping { payload? } (signed test)
  • DELETE /v0/alerts/webhooks/:id (cascades rules)
  • POST /v0/alerts/rules { chainId, operatorId?, kind, threshold, window, webhookId } Kinds (MVP): availability_below, apr_below
  • GET /v0/alerts/rules?webhookId=...
  • DELETE /v0/alerts/rules/:id
  • (Optional PATCH for enable/threshold/window)

Auth header for protected routes: x-api-key: <your_key>


Ingestion (Solana MVP)

  • operators_poll (5m): getVoteAccountsOperator upsert + OperatorState snapshot
  • availability_bucket (5m): last full hour via getBlockProduction → one 1h bucket/operator
  • apy_snapshot (daily 00:15 UTC): inflationRate.validator × (1 - commission)YieldSnapshot
  • stakeflow_rollup (daily 00:45 UTC): day-over-day diff of latest votingPowerStakeFlowDaily
  • alerts_eval (1m): compute windows (24h uptime, 7d APR) → POST to matching webhooks

Queues: operators_poll, availability_bucket, apy_snapshot, stakeflow_rollup, alerts_eval Repeats: registered at API boot (BullMQ repeatables in Redis)


Data model (Prisma, key tables)

  • Chain(id, name, nativeSymbol)
  • Operator(chainId, operatorId, kind, name, website) PK (chainId, operatorId)
  • OperatorState(chainId, operatorId, ts, commission, delinquent, votingPower) PK (chainId, operatorId, ts)
  • AvailabilityBucket(chainId, operatorId, bucketStart, produced, missed, pct) PK (chainId, operatorId, bucketStart)
  • YieldSnapshot(chainId, operatorId, ts, apy) PK (chainId, operatorId, ts)
  • StakeFlowDaily(chainId, operatorId, day, netChangeBaseUnits) PK (chainId, operatorId, day)
  • Webhook(id, userId?, url, secret, isActive)
  • AlertRule(id, userId?, chainId, operatorId?, kind, threshold, window, webhookId, isActive)
  • ApiKey(id, userId?, keyHash, tier)
  • JobState(jobName, lastSlot?, lastEpoch?, lastHeight?, lastRun?)

Indexes exist on (chainId, operatorId, ts|bucketStart|day) across time-series tables.


Run it (Docker)

# Build & start
docker compose up -d --build

# Quick smoke tests
curl http://localhost:3000/v0/status
curl -H 'x-api-key: sp_dev_key_123' "http://localhost:3000/v0/chains/solana/operators?limit=5"
curl -H 'x-api-key: sp_dev_key_123' "http://localhost:3000/v0/chains/solana/operators/<votePubkey>/metrics?window=7d"

Env (typical)

DATABASE_URL=postgresql://stakepulse:stakepulse@postgres:5432/stakepulse?schema=public
REDIS_URL=redis://redis:6379
SOLANA_RPC_URL=...
SOLANA_RPC_COMMITMENT=confirmed
ENABLE_DOCS=true
DEV_API_KEY=sp_dev_key_123

Swagger: visit /docs (persist auth) or fetch /v0/openapi.json.


Backfill CLI

Script: src/scripts/backfill.tsdist/scripts/backfill.js

# Availability + APY (last 7 days)
docker compose exec api node dist/scripts/backfill.js --chain solana --days 7 --availability --apy

# Stake flows (votingPower diffs)
docker compose exec api node dist/scripts/backfill.js --chain solana --days 7 --flows

Availability backfill uses approximate slot→time mapping (good for 1h buckets).


Operational notes & gotchas

  • CommonJS for dev (avoid ESM path/extension pain)
  • Prisma client is generated inside the API image (prevents init errors)
  • Prisma columns use camelCase; quote identifiers in raw SQL (e.g., "chainId")
  • ioredis: pass URL as first arg; avoid path:; set maxRetriesPerRequest: null for BullMQ
  • Schedulers currently boot with the API (can extract later)

Roadmap (succinct)

  • Polish: status health, hardened backfill, exact slot bounds (later), instruction-scan stake flows (later)
  • Accounts: User + ownership on ApiKey/Webhook, magic-link auth, Stripe billing/webhooks
  • Chains: ETH Beacon (attestation %, proposer success, APR), Cosmos (precommit miss, inflation APR), Bitcoin pools
  • Alerts: more kinds (participation_below, commission_above, net_flow_below), HMAC timestamp + replay guard, DLQ

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published