PRISM-INSIGHT
These are 13 specialized AI agents that analyze Korean and US stocks in real-time, and this is open source and free to use.
AI translation
Track crypto transactions, holdings, and realised and unrealised profit and loss.
Translation pending
FinanceJava · TypeScriptGitHub @kevinasramoskaOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A full-stack crypto portfolio tracker for recording crypto BUY and SELL transactions, viewing current holdings, tracking market value, and calculating realised and unrealised profit/loss.
The app is transaction-driven. The backend replays active transaction revisions to rebuild stored holdings for open positions, preserves transaction revisions as an audit log, and records portfolio snapshots for performance history.
Project status and next work: repository audit and roadmap, updated 2026-10-25. It records confirmed calculation/frontend issues, dependency advisory triage, and the recommended portfolio-demo milestones. The working backlog distinguishes implemented features from remaining work.
crypto-portfolio-app-hazel.vercel.app · API health: /actuator/health
| Part | Hosting |
|---|---|
| Next.js frontend | Vercel |
| Spring Boot API (Docker) | Render |
| PostgreSQL | Neon (TLS verify-full) |
Runs on free tiers: the API sleeps after ~15 minutes idle, so the first request can take about a minute while it wakes. Register a throwaway account and use fictional data only. Setup and verification steps: free-tier deployment guide.
| Layer | Technology |
|---|---|
| Backend | Java 25 LTS, Spring Boot 4.1.2 |
| API | Spring MVC REST controllers |
| Security | Spring Security, JWT, BCrypt |
| Database | PostgreSQL, Spring Data JPA, Hibernate, Flyway |
| Market data | CoinGecko-compatible API via RestTemplate |
| Backend tests | JUnit Jupiter 6, Spring Boot Test, MockMvc, H2, PostgreSQL Testcontainers |
| Frontend | Next.js 16.4.3, React 19.2.8, TypeScript |
| Styling | Tailwind CSS |
| Frontend tests | Vitest, Testing Library, Playwright config |
| Local services | Docker Compose, PostgreSQL, pgAdmin |
401 (not ordinary permission-denied 403).BUY and SELL transactions..
├── README.md
├── .env.example
├── .github/workflows/ci.yml
├── backend/
│ ├── Dockerfile
│ ├── docker-compose.yml
│ ├── pom.xml
│ └── src/
│ ├── main/java/com/kevinas/crypto_portfolio_backend/
│ ├── main/resources/application.yaml
│ ├── main/resources/db/migration/
│ └── test/
├── docs/
│ ├── AGENTS.md
│ ├── REPO_OVERVIEW.md
│ └── tasklist.MD
└── frontend/
├── .env.example
├── package.json
└── src/
├── app/
├── components/
├── hooks/
└── lib/
Frontend:
cd frontend
cp .env.example .env.local
Optional backend shell env from the root example:
cp .env.example .env.local
set -a
source .env.local
set +a
Spring Boot also has local defaults in backend/src/main/resources/application.yaml, so the backend can run without sourcing a .env file when using the default Docker database.
cd backend
docker compose up -d
| Service | URL/Port | Default Login |
|---|---|---|
| PostgreSQL | localhost:5432 | postgres / postgres, database cryptodb |
| pgAdmin | http://localhost:5050 | [email protected] / admin123 |
cd backend
./mvnw spring-boot:run
Backend default URL: http://localhost:8080
cd frontend
npm install
npm run dev
Frontend default URL: http://localhost:3000
| Area | Command |
|---|---|
| Start database | cd backend && docker compose up -d |
| Stop database | cd backend && docker compose down |
| Run backend | cd backend && ./mvnw spring-boot:run |
| Backend tests | cd backend && ./mvnw test |
| Backend build | cd backend && ./mvnw package |
| Run frontend | cd frontend && npm run dev |
| Frontend tests | cd frontend && npm test |
| Frontend lint | cd frontend && npm run lint |
| Frontend build | cd frontend && npm run build |
| Playwright browsers | cd frontend && npx playwright install chromium |
| E2E test script | cd frontend && npm run test:e2e |
| Variable | Default | Purpose |
|---|---|---|
DB_URL | jdbc:postgresql://localhost:5432/cryptodb | JDBC database URL |
DB_USERNAME | postgres | Database username |
DB_PASSWORD | postgres | Database password |
PORT | — | Hosting-provider HTTP port; takes precedence over SERVER_PORT |
SERVER_PORT | 8080 | Local backend HTTP port fallback |
JWT_SECRET | local development default | JWT HMAC signing secret; required in the prod profile, must be at least 32 bytes, and is validated at startup |
JWT_EXPIRATION_SECONDS | 3600 | JWT lifetime |
APP_CORS_ALLOWED_ORIGINS | http://localhost:3000 | Comma-separated frontend origins allowed to call the API; required in the prod profile |
AUTH_RATE_LIMIT_MAX_REQUESTS | 10 | Maximum login or registration attempts from one client address per auth window |
AUTH_RATE_LIMIT_WINDOW_SECONDS | 60 | Auth rate-limit window length in seconds |
MARKET_RATE_LIMIT_MAX_REQUESTS | 60 | Maximum market-data requests from one client address per market window |
MARKET_RATE_LIMIT_WINDOW_SECONDS | 60 | Market-data rate-limit window length in seconds |
CRYPTO_API_BASE_URL | https://api.coingecko.com/api/v3 | CoinGecko-compatible market API base URL |
SPRING_JPA_SHOW_SQL | true | SQL logging toggle |
PORTFOLIO_SNAPSHOT_FIXED_DELAY_MS | 3600000 | Scheduled snapshot interval |
PORTFOLIO_SNAPSHOT_INITIAL_DELAY_MS |
Rate limits apply to POST /api/auth/login, POST /api/auth/register, and GET /api/market/**. Exceeded requests receive 429 Too Many Requests with a Retry-After header. The limiter is in-memory and per application instance; use a shared store or edge rate limiting before scaling the API horizontally.
| Variable | Example | Purpose |
|---|---|---|
NEXT_PUBLIC_API_BASE_URL | http://localhost:8080/api | Browser-visible backend API base URL |
The repository includes a basic deployment path using a Vercel frontend, Dockerized API, and managed PostgreSQL database.
Build and run the backend image from the backend directory:
docker build -t crypto-portfolio-api .
docker run --rm -p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
--mount type=bind,src=/absolute/path/to/provider-ca.crt,dst=/certs/provider-ca.crt,readonly \
-e DB_URL='jdbc:postgresql://<host>/<database>?sslmode=verify-full&sslrootcert=/certs/provider-ca.crt' \
-e DB_USERNAME='<database-user>' \
-e DB_PASSWORD='<database-password>' \
-e JWT_SECRET='<at-least-32-byte-secret>' \
-e APP_CORS_ALLOWED_ORIGINS='https://<your-vercel-app>.vercel.app' \
crypto-portfolio-api
The image runs as a non-root user and defaults to prod. Production requires a unique signing key, explicit database credentials, verified PostgreSQL TLS (install the provider's trusted certificate), and exact HTTPS CORS origins. The hosting platform should provide PORT. The public application check is GET /api/health. Readiness and liveness probes are public at /actuator/health/readiness and /actuator/health/liveness; /actuator/metrics requires the ADMIN role, not just a registered account. Follow the release and restore runbook before deployment.
Deploy frontend/ as a Next.js project and set this production environment variable:
NEXT_PUBLIC_API_BASE_URL=https://<your-api-domain>/api
Set the backend's APP_CORS_ALLOWED_ORIGINS to the exact production frontend origin. Never commit real database credentials or JWT secrets.
Protected endpoints require:
Authorization: Bearer <accessToken>
| Method | Endpoint | Auth | Request | Response |
|---|---|---|---|---|
GET | /api/health | No | None | { "status": "ok" } |
GET | /actuator/health/readiness | No | None | Actuator readiness status |
GET | /actuator/health/liveness | No | None | Actuator liveness status |
GET | /actuator/metrics | Yes | None | Available Micrometer metric names |
POST | /api/auth/register | No | { "email": string, "password": string } | { "accessToken": string } |
POST | /api/auth/login | No | { "email": string, "password": string } | { "accessToken": string } |
GET | /api/market/prices?symbols=BTC,ETH | No | Query symbols list | { "BTC": 60000.00 } |
GET | /api/market/supported-coins | No | None | [{ "symbol": "BTC", "name": "Bitcoin", "coinGeckoId": "bitcoin" }] |
GET | /api/portfolio/holdings | Yes | None | HoldingResponse[] |
GET | /api/portfolio/summary | Yes | None | PortfolioSummaryResponse |
GET | /api/portfolio/performance/history?range=30d | Yes | Query range: 7d, 30d, |
{
"symbol": "BTC",
"name": "Bitcoin",
"type": "BUY",
"quantity": 0.5,
"priceUsd": 45000
}
type must be BUY or SELL. quantity must be greater than 0; priceUsd must be 0 or greater. A SELL that exceeds the stored holding quantity returns a conflict response.
Edits preserve the transaction's historical ledger position by voiding the current audit row and returning a replacement row with a new ID. Edits and deletes replay the active ledger atomically; a correction that would make any later SELL historically invalid returns 409 Conflict without changing transactions, holdings, or snapshots.
{
"id": 1,
"symbol": "BTC",
"name": "Bitcoin",
"quantity": 0.5,
"averageBuyPriceUsd": 45000.00,
"currentPriceUsd": 60000.00,
"investedValueUsd": 22500.00,
"currentValueUsd": 30000.00,
"profitLossUsd": 7500.00,
"marketPriceAvailable": true
}
{
"totalInvestedUsd": 22500.00,
"totalCurrentValueUsd": 30000.00,
"totalUnrealisedProfitLossUsd": 7500.00,
"totalRealisedProfitLossUsd": 0.00,
"totalProfitLossUsd": 7500.00,
"hasUnsupportedMarketData": false,
"holdings": [
{
"symbol": "BTC",
"name": "Bitcoin",
"quantity": 0.5,
"averageBuyPriceUsd": 45000.00,
"currentPriceUsd": 60000.00,
"investedValueUsd": 22500.00,
"currentValueUsd": 30000.00,
"unrealisedProfitLossUsd": 7500.00,
"marketPriceAvailable": true
}
]
}
{
"content": [
{
"id": 1,
"symbol": "BTC",
"name": "Bitcoin",
"type": "BUY",
"quantity": 0.5,
"priceUsd": 45000.00,
"totalValueUsd": 22500.00,
"realisedProfitUsd": 0.00,
"createdAt": "2026-06-19T20:00:00Z"
}
],
"pageNumber": 0,
"pageSize": 20,
"totalElements": 1,
"totalPages": 1,
"hasNext": false,
"hasPrevious": false
}
Supported market symbols are served by GET /api/market/supported-coins.
Current backend-supported symbols:
BTC, ETH, SOL, LINK, DOGE, ADA, XRP, DOT, MATIC, AVAX, UNI, ATOM, XLM, LTC, TRX, BCH, ALGO, ICP, NEAR, VET, BNB
Unsupported symbols can still be recorded manually, but market-value fields are flagged with marketPriceAvailable=false and portfolio totals set hasUnsupportedMarketData=true. /api/market/quotes adds fetchedAt and FRESH/STALE/UNAVAILABLE status; the legacy prices map remains compatible. Fresh quotes last 60 seconds, fallback expires after five minutes, and network waits are bounded (2s connect, 3s read). Stale totals are labelled. Snapshots with missing/stale data and legacy snapshots have valuationComplete=false and are excluded from chart lines.
Trade quantities support eight decimal places; unit prices support twelve. Cost basis and realised profit retain intermediate precision and round to cents only at USD response/summary boundaries. Displayed rounded row amounts or realised/unrealised components can differ by a cent from correctly aggregated totals; totals use underlying exact amounts, not sums of rounded displays. V5–V7 preserve audit revisions and rebuild legacy cost basis using retained prices; input discarded by old cent-rounding cannot be recovered automatically. Page size is limited to 100; CSV/legacy full-history output is limited to 10,000 records. Exports neutralize formula-leading text. Ledger replay remains linear in active transaction count.
| Problem | Check |
|---|---|
| Backend cannot connect to DB | Confirm docker compose up -d is running in backend, port 5432 is free, and DB_URL, DB_USERNAME, DB_PASSWORD match Docker defaults. |
| Flyway or schema startup errors | Confirm the database is the expected local cryptodb. V2__transaction_constraints.sql is intentionally a no-op and should remain versioned. |
Port 8080 already in use | Stop the other process or run backend with SERVER_PORT=8081; then update NEXT_PUBLIC_API_BASE_URL. |
Port 3000 already in use | Run npm run dev -- --port 3001, then set APP_CORS_ALLOWED_ORIGINS=http://localhost:3001 for the backend. |
| Registration/login fails with JWT key errors | Use a JWT_SECRET at least 32 bytes long. Very short local secrets fail JJWT HMAC validation. |
| Prices are missing or zero | Check CoinGecko availability, rate limits, and whether the symbol exists in /api/market/supported-coins. Unsupported symbols are intentionally marked unavailable. |
| Frontend cannot reach backend | Confirm NEXT_PUBLIC_API_BASE_URL=http://localhost:8080/api, backend is running, and browser requests are not blocked by CORS. |
| E2E tests cannot launch browser | Run cd frontend && npx playwright install chromium. |
/api/market/supported-coins.V2__transaction_constraints.sql is intentionally a no-op placeholder to keep existing Flyway version history stable./Users/kevinasramoska/Desktop/crypto-portfolio-app before committing.300000 |
| Scheduled snapshot startup delay |
90d{ "range": "30d", "history": PerformancePoint[] } |
POST | /api/transactions | Yes | TransactionRequest | TransactionResponse |
PUT | /api/transactions/{id} | Yes | TransactionRequest | Replacement TransactionResponse |
DELETE | /api/transactions/{id} | Yes | None | 204 No Content |
GET | /api/transactions | Yes | None | TransactionResponse[] |
GET | /api/transactions/paginated?page=0&size=20 | Yes | Query page, size | PaginatedTransactionsResponse |
GET | /api/transactions/summary | Yes | None | { "totalBuyVolumeUsd": number, "totalSellVolumeUsd": number, "totalRealisedProfitUsd": number } |