Skip to content

Repository files navigation

Chainfolio API

A Spring Boot REST API that tracks ETH and ERC-20 token balances for any Ethereum wallet. On-chain data comes from Alchemy, USD pricing from CoinGecko; balances are captured as point-in-time snapshots in PostgreSQL so reads never have to hit either.

Features

  • Wallet lookup — reads the latest cached snapshot for a wallet (no blockchain call)
  • On-demand refresh — forces a fresh sync: live ETH balance + automatic ERC-20 token discovery via Alchemy's enhanced APIs (no need to know in advance which tokens a wallet holds)
  • History — every past snapshot for a wallet, newest first
  • Spam filtering — tokens with a bogus supply/decimals (common with scam contracts) or implausibly long symbols are filtered out before they ever reach the database
  • Health check — independent liveness of Alchemy and the database
  • Interactive API docs — Swagger UI, generated from the actual code
  • One-command startup — the whole stack (API + Postgres + pgAdmin) runs via Docker Compose

Tech stack

Language / runtime Java 21, Spring Boot 4.1
Persistence PostgreSQL, Spring Data JPA, Flyway migrations
Blockchain Web3j + Alchemy (JSON-RPC and enhanced APIs)
Pricing CoinGecko (/coins/list, /coins/markets)
API docs springdoc-openapi (Swagger UI)
Testing JUnit 5, Mockito, AssertJ, Testcontainers
Containerization Docker, Docker Compose

Architecture

flowchart TD
    Client(["Client"]) -->|HTTP| Controllers

    subgraph Controllers["Controllers"]
        PC["PortfolioController"]
        WC["WalletController"]
        TC["TokenController"]
        HC["HealthController"]
    end

    PC --> PS["PortfolioService<br/>(orchestration)"]
    WC --> WS["WalletService"]
    TC --> TS
    HC --> HS["HealthService"]

    PS --> TS["TokenService<br/>(normalize + spam filter + pricing + token details)"]
    PS --> SS["SnapshotService<br/>(persistence only)"]
    PS -->|"validate + ETH balance"| BC
    HS -->|"latest block"| BC

    TS --> BC["BlockchainClient"]
    TS --> PSV["PricingService"]

    SS --> Repos[("Repositories")]
    WS --> Repos
    PS --> Repos

    BC -->|"JSON-RPC +<br/>enhanced APIs"| Web3j["Web3j"]
    Web3j --> Alchemy[("Alchemy")]
    PSV --> CGC["CoinGeckoClient"]
    PSV --> CGM["CoinGeckoIdMappingService<br/>(lazy, memoized)"]
    CGM --> CGC
    CGC -->|"REST"| CoinGecko[("CoinGecko")]

    Repos --> Postgres[("PostgreSQL")]
Loading

Each layer has one job: controllers only handle HTTP, PortfolioService only orchestrates, TokenService only normalizes chain data into clean DTOs (symbol/balance stay Alchemy-sourced; CoinGecko only adds name/price/icon), SnapshotService only persists, BlockchainClient is the only class that talks to Web3j/Alchemy, and CoinGeckoClient is the only class that talks to CoinGecko. CoinGeckoIdMappingService lazily loads and memoizes the contractAddress -> coinGeckoId mapping (~18k entries) on first use rather than blocking startup; a failed load isn't cached, so the next lookup retries.

Data model

wallet (id, address, created_at)
  └── portfolio_snapshot (id, wallet_id, eth_balance, total_usd, created_at)
        └── token_balance (id, snapshot_id, symbol, contract, balance, price_usd, value_usd)

portfolio_snapshot and token_balance cascade-delete with their parent.

Token name and icon URL come from CoinGecko but are not persisted — they're static token metadata, not snapshot data, so persisting them would duplicate the same value across every snapshot row for a token. Practical effect: they're present in the response from POST /refresh (built before persistence) but always null on cached reads (GET /portfolio/{wallet}, /history), which read straight from the database.

Installation

Option 1 — Docker Compose (recommended)

Brings up the API, PostgreSQL, and pgAdmin together with one command.

cp .env.example .env   # then fill in ALCHEMY_API_KEY and COINGECKO_API_KEY (both free)
docker compose up

Option 2 — Local development

Run Postgres/pgAdmin in Docker but the API directly via Gradle (faster iteration, hot reload via DevTools):

cp .env.example .env
docker compose up postgres pgadmin -d
./gradlew bootRun

Running tests

./gradlew test

Requires Docker — repository and integration tests spin up a real, throwaway PostgreSQL via Testcontainers. No .env or Alchemy/CoinGecko credentials needed; the blockchain and pricing clients are mocked in tests.

Note on bootRun + DevTools: adding a new required env var to .env only takes effect on a fresh ./gradlew bootRun launch — DevTools' hot-restart reloads code within the same process, but can't inject new environment variables into an already-running one. If you add a key and a live-reloaded app silently stops pricing/syncing, fully restart bootRun rather than waiting for hot-reload.

Environment variables

Variable Purpose
POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD Database credentials
ALCHEMY_API_KEY Alchemy API key (required)
ALCHEMY_NETWORK Alchemy network, e.g. eth-mainnet
COINGECKO_API_KEY CoinGecko Demo API key (required)
PGADMIN_DEFAULT_EMAIL / PGADMIN_DEFAULT_PASSWORD pgAdmin login
CORS_ALLOWED_ORIGINS Origin(s) allowed to call the API from a browser; defaults to http://localhost:3000

API overview

Method Path Description
GET /api/v1/portfolio/{wallet} Latest cached portfolio
GET /api/v1/portfolio/{wallet}/history All past snapshots, newest first
POST /api/v1/portfolio/{wallet}/refresh Force a new blockchain sync
GET /api/v1/wallets 20 most recently seen wallets
GET /api/v1/tokens/{contract} Full CoinGecko market data for one token (details page) — fetched fresh, not cached
GET /health Alchemy + database status

Full interactive documentation, generated from the code, is at /swagger-ui.html; the raw OpenAPI spec is at /v3/api-docs.

Roadmap

Built so far:

  • Project scaffold, Postgres, Docker Compose for local dev
  • wallet / portfolio_snapshot / token_balance schema with Flyway migrations
  • Blockchain layer — ETH balance, automatic ERC-20 discovery, transaction history, address validation
  • Portfolio orchestration, token normalization, and snapshot persistence as separate services
  • REST API — lookup, history, refresh, recent wallets, token details, health
  • Centralized exception handling with typed error codes
  • Full test suite — controller, service, repository, and integration tests with Testcontainers
  • Full containerization via Docker Compose
  • API documentation (this README, Swagger)
  • USD pricing — priceUsd / valueUsd / name / iconUrl (via CoinGecko, batched) and totalUsd

Screenshots

Interactive Swagger UI, generated from the live API:

Swagger UI overview

A live GET /api/v1/portfolio/{wallet} call executed from Swagger UI against a real wallet:

Live API response in Swagger UI

About

REST API for tracking ETH and ERC-20 wallet balances, built with Spring Boot and Web3j/Alchemy, backed by PostgreSQL — fully containerized with Docker Compose.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages