A professional investment command center for THORChain bond providers and liquidity providers — monitor bonded RUNE exposure, rewards, risk metrics, LP provenance, and provider-safe alerts with institutional-grade precision.
Canonical project: This repository (
Reedtrullz/Heimdall, local path/Users/reidar/Projectos/Heimdall) is the canonical THORChain dashboard. Do not revive or implement new work in the olderTHORNode Watcher/ BondTrack checkout; keep it archive-only for historical QA/audit artifacts.
- Total bonded RUNE tracking with USD valuation
- Real-time RUNE price (USD)
- Weighted APY calculations with benchmark comparison
- Provider exposure scoring and ranked next actions
- Net earnings transparency (gross vs net after fees)
- Active/Standby/Ready status monitoring
- Bond amount and rank tracking
- Slash points and jail status
- Churn-out risk assessment
- Node operator fee impact analysis
- Background browser push alerts for provider exposure transitions
- P&L dashboard with initial bond tracking
- Fee impact breakdown (leakage analysis)
- Auto-compound projections
- RUNE price chart with multiple timeframes
- Yield benchmarking (User vs Network vs Top Nodes)
- Slash monitor with severity levels
- Network security metrics (TVL, bond-to-pool ratio)
- Unbond window tracker
- Churn-out risk indicators
- Provider exposure score with actionable insights
- BOND/UNBOND memo composer
- Transaction history from Midgard using
type=filter - Wallet connection (Ledger address review, Vultisig, Keplr, XDEFI)
- Watchlist management
- USD-based LP portfolio hero (
Total LP Value,Net P/L,Positions,Last Activity) - Investor-facing LP cards and table rows with real asset symbols (
ATOM,DOGE,BCH) - Honest
current-onlyfallback when historical entry pricing cannot be proven - Pricing-confidence banner instead of fake
0.00%LP performance metrics - Truthful missing-address state on
/dashboard/lp
- Framework: Next.js 16.2.7 (App Router, Turbopack)
- Language: TypeScript
- Styling: Tailwind CSS v4
- Data Fetching: SWR
- Charts: Recharts (with ResponsiveContainer fixes for clean rendering)
- Testing: Vitest source tests + Playwright E2E specs across desktop and focused mobile-critical projects
- Icons: lucide-react
- Deployment: Ansible → VPS (GHCR, Docker, Caddy reverse proxy)
- Security: Ansible Vault for sensitive variables
# Use the project Node version
nvm use
# Install dependencies
npm ci
# Start development server
npm run dev
# Run unit tests
npm test
# Run production E2E tests; release verification should use CI=true to force a fresh server/build
CI=true npm run e2e
# Reuse an already-running local server only when explicitly requested
npm run e2e:reuse
# Run E2E tests with the default config (fresh production server unless PLAYWRIGHT_REUSE_SERVER=true)
npm run e2e
# Run E2E tests with UI
npm run e2e:ui
# Build for production
npm run buildOpen http://localhost:3000 with your browser.
Copy .env.example to .env.local when you need local overrides. Do not put
secrets in NEXT_PUBLIC_* variables; they are bundled into client-side code.
| Variable | Description | Default |
|---|---|---|
NEXT_PUBLIC_THORNODE_API |
THORNode API endpoint | https://gateway.liquify.com/chain/thorchain_api |
NEXT_PUBLIC_MIDGARD_API |
Midgard API endpoint | https://gateway.liquify.com/chain/thorchain_midgard |
NEXT_PUBLIC_MIDGARD_FALLBACK |
Secondary Midgard fallback | https://midgard.thorchain.network |
NEXT_PUBLIC_THORCHAIN_RPC |
THORChain RPC | https://gateway.liquify.com/chain/thorchain_rpc |
NEXT_PUBLIC_TRACK_API |
THORChain tracker URL | https://track.thorchain.org/ |
NEXT_PUBLIC_COINGECKO_API |
CoinGecko API base URL | https://api.coingecko.com/api/v3 |
NEXT_PUBLIC_THORCHAIN_NETWORK |
THORChain network label baked into the client bundle | mainnet |
NEXT_PUBLIC_APP_URL |
Canonical app URL used by CORS/server diagnostics | https://bond.thorchain.no |
NEXT_PUBLIC_USE_MOCK_DATA |
Local/test-only mock data toggle | false |
COINAPI_KEY |
Optional server-side CoinAPI key; never expose as NEXT_PUBLIC_* |
unset |
TRUST_PROXY_HEADERS |
Server-side rate limiting: trust a deployment proxy's overwritten X-Real-IP header |
false locally; true in production deploy |
TRUST_X_FORWARDED_FOR |
Server-side rate limiting: also trust X-Forwarded-For; enable only when the full proxy chain sanitizes it |
false |
TRUST_CLOUDFLARE_HEADERS |
Server-side rate limiting: trust CF-Connecting-IP when directly behind Cloudflare |
false |
TRUST_VERCEL_PROXY_HEADERS |
Server-side rate limiting: trust Vercel forwarded IP headers outside the Vercel runtime | false |
VERSION |
Runtime app version; Ansible sets this to the immutable deployed image tag | sha-<short> |
HEIMDALL_DATA_DIR |
Durable server-side data directory for background notification subscriptions | .heimdall-data locally; /data in production |
WEB_PUSH_VAPID_PUBLIC_KEY |
Public VAPID key returned by /api/notifications/status for browser push subscriptions |
unset |
WEB_PUSH_VAPID_PRIVATE_KEY |
Server-only VAPID private key used to send browser push notifications | unset |
WEB_PUSH_CONTACT |
VAPID subject/contact for Web Push delivery | unset |
NOTIFICATION_POLL_INTERVAL_MS |
Background provider-alert monitor interval; values below 30s are clamped | 60000 |
HEIMDALL_NOTIFICATION_RUNNER_TOKEN |
Bearer token for the optional internal notification monitor trigger route | unset |
HEIMDALL_NOTIFICATION_MAX_SUBSCRIPTIONS |
Runtime-wide cap for stored browser push subscriptions | 1000 |
HEIMDALL_NOTIFICATION_MAX_SUBSCRIPTIONS_PER_ADDRESS |
Per-address cap for stored browser push subscriptions | 10 |
NEXT_PUBLIC_* variables are build-time public configuration: Docker/CI passes
them as --build-arg so browser JavaScript is baked deterministically. Runtime
Ansible or Compose entries with the same names are for server-side rendering and
diagnostics only; they do not rewrite an already-built client bundle.
For a throwaway local mock-data build, set NEXT_PUBLIC_USE_MOCK_DATA=true
before npm run build and use thor1qypqxpq9qcrsszg2pvxq6rs0zqg3yyc5e949nr
as the demo provider address.
Proxy trust variables only affect server-side best-effort rate-limit identity.
Leave forwarding headers untrusted unless the deployment proxy overwrites or
sanitizes them.
Background notifications are server-triggered browser push alerts. They require
VAPID keys and a durable HEIMDALL_DATA_DIR; without those, the notification
settings page stays read-only and the local open-tab alert fallback still works.
Subscription caps are enforced server-side because these routes are public.
Before changing API clients, live-data charts, RUNE/APY math, or LP valuation copy, read docs/thorchain-data-conventions.md. It is shared with tcwiki and defines endpoint routing, 1e8 RUNE units, APY decimal-vs-percent boundaries, and current-only LP valuation provenance.
Heimdall uses a push-based deployment model from your local machine to the VPS via Ansible.
Developer Push → GitHub → CI workflow (test, build, e2e, publish)
↓ (publish job runs after the others pass)
GHCR (ghcr.io/reedtrullz/heimdall:sha-<short>)
↓
Local Machine (ansible-playbook)
↓
VPS (198.23.137.16)
↓
Docker Container (port 3001)
↓
Caddy Reverse Proxy (bond.thorchain.no)
# 1. Ensure Ansible is installed (via Homebrew)
brew install ansible
# 2. Run deployment playbook
cd /Users/reidar/Projectos/Heimdall
IMAGE_TAG=sha-<exact-short-sha> ansible-playbook \
-i inventory/hosts.yml ansible-playbook.yml \
--vault-password-file ~/.vault_pass.txtBy default the playbook deploys
ghcr.io/reedtrullz/heimdall:sha-<local short sha> and sets runtime VERSION
to the same immutable tag. Override with IMAGE_TAG=sha-<exact-short-sha> when
deploying a specific published image; the playbook rejects mutable deploy tags
such as latest.
compose.production.yml is only a manual/diagnostic path and also refuses to
default to a mutable image: run it through the validation wrapper with
IMAGE_SHA=<exact-short-sha> so the image and runtime VERSION both resolve to
sha-$IMAGE_SHA:
IMAGE_SHA=<exact-short-sha> scripts/compose-production.sh config
IMAGE_SHA=<exact-short-sha> scripts/compose-production.sh up -d- Liveness Check: Docker/Compose healthchecks use
/api/healthfor local process liveness - Readiness Gate: Promotion and rollback wait on
/api/readyso THORNode and Midgard are reachable through runtime config - Durable Notifications: Background browser push subscriptions persist in the mounted
/datadirectory - Rollback: Automatically reverts to the previous image ID/digest/reference on readiness check failure
- Vault: Sensitive vars (e.g. CoinAPI key) stored in
group_vars/vps/vault.yml(encrypted) See DEPLOYMENT.md for full details. The Inebotten Discord bot is a separate project — see Reedtrullz/inebotten-discord (itsdeploy/directory contains its own playbook).
src/
├── app/ # Next.js App Router pages
│ ├── api/ # Server-side API proxy routes (bypass CORS)
│ ├── dashboard/ # Dashboard pages (portfolio, nodes, rewards, risk, transactions, lp)
│ ├── layout.tsx # Root layout with ThemeProvider
│ └── page.tsx # Landing page
├── components/
│ ├── dashboard/ # 18+ domain components (charts, tables, monitors)
│ ├── layout/ # sidebar, dashboard-shell, theme-toggle
│ ├── wallet/ # wallet-connect, transaction-review
│ ├── alerts/ # alert-toast
│ ├── shared/ # address-input, status-badge, export-button
│ └── ui/ # shadcn-style primitives
└── lib/
├── api/ # THORNode + Midgard API clients
├── hooks/ # SWR data hooks
├── transactions/ # BOND/UNBOND signing
├── types/ # TypeScript interfaces
└── utils/ # formatters, calculations, health scoring
External Midgard/THORNode APIs block browser requests due to CORS. The app uses server-side proxy routes:
/api/midgard/*→ proxies togateway.liquify.com/chain/thorchain_midgard(falls back tomidgard.thorchain.network)/api/thorchain/*→ proxies togateway.liquify.com/chain/thorchain_api/thorchain
All API calls from frontend go through these proxies, bypassing browser CORS restrictions.
The legacy
gateway.liquify.com/chain/thorchain_mainnetpath returns HTTP 500 and is not a valid endpoint. Usethorchain_apiandthorchain_midgard.
The
/api/thorchain/*proxy normalises a leadingthorchain/segment in the request path before applying its allowlist (seesrc/app/api/thorchain/[...path]/route.ts). The frontend client adds that prefix, andTHORNODE_API_URLalready ends in/thorchain— don't remove the normalisation.
- Ledger Hardware Wallet — THORChain address and balance review over WebHID after the device is unlocked, the THORChain app is open, and the address is confirmed on device; BOND/UNBOND broadcast remains disabled until Ledger THORChain
MsgDepositsigning is hardware-verified - Vultisig Extension — THORChain extension provider with BOND/UNBOND broadcast support
- Keplr Wallet — Cosmos-based wallet with THORChain support
- XDEFI Wallet — Cross-chain desktop wallet
We use Playwright for end-to-end testing. Tests are in the e2e/ directory.
# Run all E2E tests
npm run e2e
# Run with UI mode (debugging)
npm run e2e:ui
# Run specific test file
npx playwright test e2e/portfolio.spec.ts
# View test report
npx playwright show-report- Prefer semantic locators scoped to the named region, landmark, card, table, tab panel, or dialog under test
- Use
{ exact: true }for page/title headings and compact repeated labels when partial matches could pass against duplicate UI - Mock API endpoints in
test.beforeEach()for predictable tests - Check page headings with
getByRole('heading', { name: ... }) - Avoid fragile XPath locators; use semantic text/role locators
- Do not use broad
.first(),.nth(), or.last()to silence duplicate matches; narrow the surface first - Handle missing elements gracefully (e.g., charts with
minWidth={0} minHeight={0})
The master branch uses a single GitHub Actions workflow at
.github/workflows/ci.yml:
- test — Vitest unit tests + coverage
- build — Next.js production build
- e2e — Playwright E2E tests
- docker-build — non-pushing Docker build verification for PR, staging, and other non-
masterrefs - publish — runs only on
pushtomaster, after the three above pass. Builds the canonicalDockerfilewith Buildx and publishes the GHCRsha-<short>tag.
The workflow keeps GitHub and Docker JavaScript actions on Node 24-capable
majors (actions/checkout@v6, actions/setup-node@v6,
actions/upload-artifact@v7, docker/setup-buildx-action@v4,
docker/build-push-action@v7, docker/login-action@v4, and
docker/metadata-action@v6) so CI does not regress to Node.js 20 deprecation
warnings.
Deploy verification should compare the exact immutable SHA tag in GHCR,
Ansible's selected IMAGE_TAG, docker ps --format '{{.Image}}', and the
version returned by both /api/health and /api/ready. Do not treat this
documentation as a production deployment claim.
There is no separate publish workflow and no cross-workflow workflow_run
trigger. See CLAUDE.md and AGENTS.md for the rationale.
The deployed site at https://bond.thorchain.no is the source of truth for user-facing QA. Production fixes are considered deployed only after the exact image SHA is re-tested there, including matching version values from /api/health and /api/ready.
After deployment, verify:
- Dashboard routes keep rendering when optional THORName or upstream pool-history calls degrade.
- LP valuation clearly labels
current-onlyor degraded pricing when historical data is unavailable. - Notification prompts stay non-blocking; background browser push stays configured after restart/update, and notification settings immediately affect live open-tab alerts.
- Portfolio/node quick actions preserve the intended BOND or UNBOND transaction mode.
- Transaction preview shows wallet/network-confirmed fee copy, memo copy feedback, and clear UNBOND memo semantics.
- Rewards controls, tax export, changelog search, filters, and entry buttons all produce visible results.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests:
npm testandnpm run e2e - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Submit a pull request
MIT