Skip to content

Latest commit

 

History

History
818 lines (654 loc) · 25.6 KB

File metadata and controls

818 lines (654 loc) · 25.6 KB

SHIELDSTACK

A CLI tool for ZEC/BTC portfolio tracking with goal-based scenario analysis.


🚀 BUILD INSTRUCTIONS

Follow these steps in order. Each step should be completed and tested before moving to the next.

Step 1: Project Setup

Create the project structure and config files.

Create:

  • package.json (copy from spec below)
  • tsconfig.json (copy from spec below)
  • .gitignore (node_modules, dist, .DS_Store)
  • Folder structure: src/, src/commands/, src/services/, src/lib/, src/ui/

Test: npm install completes without errors


Step 2: Storage Service

Create src/services/storage.ts

Functions needed:

  • getConfigDir() - returns path to ~/.shieldstack, creates if missing
  • loadHoldings() - read holdings.json, return null if missing
  • saveHoldings(data) - atomic write (tmp then rename), convert Decimals to numbers
  • loadConfig() - read config.json, create with defaults if missing
  • saveConfig(data) - atomic write
  • loadCache() - read cache/prices.json, return null if missing/invalid
  • saveCache(data) - atomic write

Key patterns:

  • Use path.join(homedir(), '.shieldstack') for cross-platform paths
  • Atomic writes: write to .tmp, then fs.rename
  • Type guards to validate JSON shape
  • Never store Decimal objects - convert to number first

Test: Manually call functions, verify files created in ~/.shieldstack/


Step 3: Price Service

Create src/services/prices.ts

Functions needed:

  • fetchCurrentPrices() - hit CoinGecko simple/price endpoint
  • fetch30dAverage() - hit market_chart endpoint, average the daily prices
  • getPrices() - main function: check cache, fetch if stale, handle offline

Key patterns:

  • Cache current prices for 5 minutes, historical for 24 hours
  • Include User-Agent header from package.json version
  • On 429 or network error: use cache, warn on stderr
  • Validate response shape before using
  • Store fetched_at as epoch ms

Test: Run with network on/off, verify caching works


Step 4: Calculations Library

Create src/lib/calculations.ts

Functions needed:

  • calculateTotal(holdings, prices) - total portfolio in BTC
  • calculateProgress(total, goal) - progress % and gap
  • calculateAllocation(holdings, prices, total) - % in ZEC/BTC/USD
  • calculateDeviation(currentRatio, avg30d) - % above/below average
  • calculateScenarios(holdings, prices, avg30d) - mean reversion, +25%, -25%
  • calculateBreakeven(holdings, prices, goal) - required ratio to hit goal

Key patterns:

  • Use Decimal for ALL math
  • Guard every division: check for zero, return { display: "N/A", hint: "..." }
  • Return both raw Decimal and formatted string for each calculation

Test: Unit test with edge cases: zero holdings, goal met, tiny numbers


Step 5: Display Utilities

Create src/ui/display.ts

Functions needed:

  • formatBtc(decimal) - 8 decimal places
  • formatUsd(decimal) - 2 decimal places
  • formatPercent(decimal) - 1 decimal place
  • formatRatio(decimal) - 6 decimal places
  • progressBar(percent, width) - ASCII bar with █ and ░
  • renderDashboard(data) - full dashboard output string

Key patterns:

  • Use chalk for colors (green/red/yellow/cyan)
  • Respect NO_COLOR env var (chalk does this automatically)
  • Output to stdout only (warnings go to stderr elsewhere)

Test: Call with sample data, verify output looks correct


Step 6: View Command

Create src/commands/view.ts

Logic:

  1. Load holdings → if missing, print message + exit 1
  2. Load config → create defaults if missing
  3. Get prices (handles caching/offline)
  4. Run all calculations
  5. Render dashboard
  6. Print to stdout (warnings already went to stderr)
  7. Exit 0

Test: Run npm run dev with and without holdings file


Step 7: Update Command

Create src/commands/update.ts

Logic:

  1. Load existing holdings (or use zeros)
  2. Use inquirer to prompt for ZEC, BTC, USD (show current, Enter to keep)
  3. Validate inputs (positive numbers)
  4. Confirm before saving
  5. Save holdings
  6. Print success message

Test: Run update, verify file changes


Step 8: Goal Command

Create src/commands/goal.ts

Logic:

  • With argument: set new goal, save config
  • Without argument: display current goal

Test: Run both variations


Step 9: Entry Point

Create src/index.ts

Must include:

  • Shebang: #!/usr/bin/env node (first line)
  • Commander setup with default action → view
  • Wire update and goal subcommands

Test:

  • npm run dev runs view
  • npm run dev update runs update
  • npm run dev goal 0.2 sets goal

Step 10: Build & Test

npm run build
node dist/index.js

Verify:

  • Shebang present in dist/index.js
  • All commands work
  • Offline mode works
  • NO_COLOR=1 works
  • Edge cases handled

Step 11: Documentation

Create README.md with:

  • One paragraph description
  • Installation: npm install -g shieldstack
  • Usage examples
  • Screenshot (after you have working output)
  • Privacy & security notes
  • Disclaimers
  • Attribution

Create LICENSE (MIT)


Step 12: Publish

npm publish --dry-run  # verify contents
npm publish

Test after publish:

npm install -g shieldstack
shieldstack

REFERENCE SPECIFICATIONS

Everything below is reference material for the build steps above.


Project Overview

Shieldstack helps cryptocurrency holders (specifically those weighing ZEC vs BTC allocation) track their portfolio, visualize progress toward a BTC goal, analyze the ZEC/BTC ratio against historical averages, and run "what if" scenarios.

Built by and for the Zcash community. All data stored locally - no telemetry, no cloud sync. Only external call is to CoinGecko for price data.

Tech Stack

  • Language: TypeScript
  • Runtime: Node.js (18+)
  • CLI Framework: commander
  • Prompts: inquirer
  • Display: chalk
  • Math: decimal.js (precise decimal arithmetic)
  • HTTP: native fetch
  • Storage: Local JSON files in ~/.shieldstack/

File Structure

shieldstack/
├── src/
│   ├── index.ts              # Entry point - MUST start with #!/usr/bin/env node
│   ├── commands/
│   │   ├── view.ts           # Main dashboard (default command)
│   │   ├── update.ts         # Update holdings interactively
│   │   └── goal.ts           # Set/view goal
│   ├── services/
│   │   ├── prices.ts         # CoinGecko API integration + caching
│   │   └── storage.ts        # Read/write JSON files
│   ├── lib/
│   │   └── calculations.ts   # Portfolio math, scenarios, breakeven
│   └── ui/
│       └── display.ts        # Formatting, colors, progress bars, tables
├── package.json
├── tsconfig.json
├── README.md
├── LICENSE                   # MIT license file
├── DECISIONS.md              # Technical decisions documentation
└── .gitignore

Important: src/index.ts must begin with shebang for CLI to work:

#!/usr/bin/env node

TypeScript preserves this in compiled output.

Commander Default Command

Commander doesn't automatically run a "default command" with no args. Wire it explicitly:

import { Command } from 'commander';
import { view } from './commands/view.js';

const program = new Command();

program
  .name('shieldstack')
  .description('ZEC/BTC portfolio tracker')
  .version('0.1.0')
  .action(view);  // Default action when no subcommand

program.command('update').action(update);
program.command('goal').action(goal);

program.parse();

Test that shieldstack with no args runs the view command, not help.

Data Files

Location: ~/.shieldstack/

Path Handling

Use path.join() for cross-platform compatibility:

import { homedir } from 'os';
import { join } from 'path';

const CONFIG_DIR = join(homedir(), '.shieldstack');
const HOLDINGS_FILE = join(CONFIG_DIR, 'holdings.json');

This works on Windows (C:\Users\name\.shieldstack) and Unix (/home/name/.shieldstack).

Storage Principles

Atomic writes: Always write to .tmp file first, then rename. Prevents corruption if process interrupted.

await fs.writeFile('holdings.json.tmp', data);
await fs.rename('holdings.json.tmp', 'holdings.json');

Decimal → primitive before storage: Never store Decimal objects in JSON. Convert to number or string first.

// WRONG - outputs "{}"
JSON.stringify({ value: new Decimal(0.1) })

// CORRECT - convert to number first
JSON.stringify({ value: new Decimal(0.1).toNumber() })

Treat Decimal as calculation-only, not a data type. Storage layer should only accept primitives.

Simple validation: Use type guard functions to validate JSON shape before using. No external deps (zod etc) for MVP - just manual checks:

function isValidHoldings(data: unknown): data is HoldingsFile {
  if (typeof data !== 'object' || data === null) return false;
  const d = data as Record<string, unknown>;
  if (typeof d.version !== 'number') return false;
  if (typeof d.holdings !== 'object') return false;
  // ... check each field is number, not NaN, not Infinity
  return true;
}

Cache is expendable: If cache is missing or invalid, fetch fresh. Never crash due to cache issues.

holdings.json

{
  "version": 1,
  "holdings": {
    "zec": 45.2,
    "btc": 0.042,
    "usd": 150
  },
  "updated_at": "2025-01-15T08:30:00Z"
}

config.json

{
  "version": 1,
  "goal": {
    "amount": 0.1,
    "currency": "btc"
  }
}

cache/prices.json

{
  "version": 1,
  "prices": {
    "zec_usd": 18.75,
    "btc_usd": 99000,
    "zec_btc": 0.000189
  },
  "historical": {
    "zec_btc_30d_avg": 0.000220
  },
  "fetched_at": 1736931900000
}

Note: fetched_at stored as epoch ms (not ISO string) to avoid timezone/parsing issues. If Date.now() - fetched_at < 0, treat cache as stale and refetch.

API Integration

CoinGecko Endpoints

Current prices:

GET https://api.coingecko.com/api/v3/simple/price?ids=zcash,bitcoin&vs_currencies=usd,btc

Historical data (for 30d average):

GET https://api.coingecko.com/api/v3/coins/zcash/market_chart?vs_currency=btc&days=30&interval=daily

Returns daily price data as [timestamp, price] pairs. Average the prices to get 30d average.

Note: The OHLC endpoint returns 4-hour candles for 30 days (~180 candles), which requires downsampling. Using market_chart with interval=daily is simpler.

Caching Strategy

  • Cache current prices for 5 minutes
  • Check fetched_at timestamp before API call
  • If Date.now() - fetched_at < 0 (clock skew), treat as stale and refetch
  • If Date.now() - fetched_at > 86400000 (> 1 day), treat as suspicious/stale
  • If offline or API fails, use cached data with warning showing cache age
  • If rate limited (429), use cache and show: "Rate-limited by CoinGecko; using cached data"
  • Historical data (30d avg): use market_chart endpoint with interval=daily, average the prices. Cache for 24 hours.

API Request Headers

Always include User-Agent header, pulling version from package.json:

import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __dirname = dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));

const response = await fetch(url, {
  headers: {
    'User-Agent': `shieldstack/${pkg.version}`
  }
});

Defensive API Parsing

  • Validate response shape before using
  • If OHLC response is empty, malformed, or missing expected fields → set historical avg to null
  • If historical avg is null → show "N/A (insufficient data)" for ratio deviation section
  • Never crash due to unexpected API response shape

Core Calculations

IMPORTANT: Use decimal.js for all financial math to avoid float precision issues. Guards must check for division by zero, NaN, and Infinity. Return descriptive strings instead of nonsense numbers.

Using decimal.js

import Decimal from 'decimal.js';

// All calculations use Decimal, not native floats
const zecValueBtc = new Decimal(holdings.zec).mul(prices.zec_btc);
const totalBtc = new Decimal(holdings.btc)
  .plus(zecValueBtc)
  .plus(new Decimal(holdings.usd).div(prices.btc_usd));

// For display, convert to string with fixed precision
const displayBtc = totalBtc.toFixed(8);  // "0.04200000"

Guards (apply to all calculations)

// Before any calculation, check for valid inputs
if (totalBtc.lte(0)) return { value: null, display: "N/A", hint: "Add holdings first" };
if (holdings.zec === 0) return { value: null, display: "N/A", hint: "No ZEC holdings" };
if (avg30d === 0 || isNaN(avg30d)) return { value: null, display: "N/A", hint: "Historical data unavailable" };

Display formatting

// Use Decimal's toFixed() for consistent precision
// This prevents scientific notation (e.g., 1e-8) and float artifacts
const displayRatio = ratio.toFixed(6);      // "0.000190"
const displayBtc = btc.toFixed(8);          // "0.04200000"
const displayUsd = usd.toFixed(2);          // "150.00"
const displayPercent = percent.toFixed(1);  // "62.1"

// Test with tiny numbers to ensure no scientific notation sneaks in
// 0.00000001 should display as "0.00000001", not "1e-8"

Total portfolio in BTC

const totalBtc = new Decimal(holdings.btc)
  .plus(new Decimal(holdings.zec).mul(prices.zec_btc))
  .plus(new Decimal(holdings.usd).div(prices.btc_usd));

Progress to goal

const progress = totalBtc.div(goal.amount).mul(100);
const gap = new Decimal(goal.amount).minus(totalBtc);

Allocation percentages

const zecValueBtc = new Decimal(holdings.zec).mul(prices.zec_btc);
const btcValue = new Decimal(holdings.btc);
const usdValueBtc = new Decimal(holdings.usd).div(prices.btc_usd);

const zecPercent = zecValueBtc.div(totalBtc).mul(100);
const btcPercent = btcValue.div(totalBtc).mul(100);
const usdPercent = usdValueBtc.div(totalBtc).mul(100);

Note: This shows portfolio allocation, not on-chain shielding status.

Rounding tolerance: Don't force allocation to sum to exactly 100%. If rounding gives 99.9% or 100.1%, that's acceptable. Display integers or 1 decimal max. Users accept small visual imperfections more than "why doesn't this add up?"

Ratio deviation from 30d average

const currentRatio = new Decimal(prices.zec_btc);
const avg = new Decimal(avg30d);
const deviation = currentRatio.minus(avg).div(avg).mul(100);
// Negative = ZEC is cheap relative to BTC historically

Scenario projection

// If ZEC/BTC returns to 30d average
const projectedZecInBtc = new Decimal(holdings.zec).mul(avg30dRatio);
const projectedTotal = new Decimal(holdings.btc)
  .plus(projectedZecInBtc)
  .plus(new Decimal(holdings.usd).div(btcPrice));

Breakeven ratio

// What ZEC/BTC ratio needed for total to equal goal?
const neededFromZec = new Decimal(goal.amount)
  .minus(holdings.btc)
  .minus(new Decimal(holdings.usd).div(btcPrice));

// Guard edge cases
if (holdings.zec === 0) return { display: "N/A", hint: "No ZEC to calculate" };
if (neededFromZec.lte(0)) return { display: "Goal already met!", hint: "Your BTC + USD already exceeds goal" };

const breakevenRatio = neededFromZec.div(holdings.zec);

CLI Commands

shieldstack (default: view)

Display main dashboard with:

  • One-line signal summary at top
  • Current holdings and values
  • Total in BTC terms
  • Progress bar to goal
  • Allocation breakdown (% in ZEC/BTC/USD)
  • ZEC/BTC ratio vs 30d average
  • Scenario projections (if ratio reverts to avg, +25%, -25%)
  • Breakeven calculation

If holdings file missing: Do NOT prompt interactively. Print friendly message: "No holdings found. Run shieldstack update to get started." Then exit with code 1.

shieldstack update

Interactive prompts to update ZEC, BTC, USD amounts. Show current value, allow Enter to keep unchanged. Confirmation step before saving. This command is explicitly interactive - user expects prompts here.

shieldstack goal <amount>

Set new BTC goal amount. No argument = display current goal.

Dashboard Output Format

🛡️ SHIELDSTACK                                    3m ago · CoinGecko

You need 0.038 BTC to reach goal. ZEC is 12% below 30d avg.

HOLDINGS
  ZEC     45.20        0.0086 BTC       $847.15
  BTC      0.042       0.0420 BTC     $4,158.00
  USD    150.00        0.0015 BTC       $150.00
  ───────────────────────────────────────────────
  TOTAL                0.0621 BTC     $5,155.15

GOAL PROGRESS
  Current:  0.0621 BTC
  Target:   0.1000 BTC
  Progress: ████████████░░░░░░░░ 62%
  Gap:      0.0379 BTC (~$3,762)

ALLOCATION
   ZEC:  14%
   BTC:  68%
   USD:  18%

ZEC/BTC RATIO
  Current:   0.000190
  30d avg:   0.000216
  Status:    12% below average

SCENARIOS (hypothetical · BTC/USD held constant, ZEC/BTC varies)
  If ZEC/BTC returns to 30d avg:
    Your ZEC → 0.0098 BTC | Total: 0.0633 BTC (63%)
  
  If ZEC/BTC +25% from current:
    Your ZEC → 0.0108 BTC | Total: 0.0643 BTC (64%)

BREAKEVEN
  ZEC/BTC needs to reach 0.00084 for goal (4.4x from current)

─────────────────────────────────────────────────────
Rates aggregated by CoinGecko · may differ from your exchange

UI Guidelines

  • Shield emoji (🛡️) in header
  • Box-drawing or simple separators for structure
  • Progress bar for goal tracking (ASCII: █ and ░)
  • Color coding:
    • Green: positive/gains/on track
    • Red: negative/losses/below target
    • Yellow: warnings/cache age
    • Cyan: informational/labels
  • Respect NO_COLOR environment variable
  • Keep output compact - avoid long lines that wrap on narrow terminals
  • Keep numbers short with appropriate precision
  • NO editorializing or advice (e.g., avoid "accumulation territory", "time to buy")
  • Just show the data, let users decide what it means

stdout vs stderr separation

// Dashboard output → stdout (for piping)
console.log(dashboardOutput);

// Warnings and errors → stderr
console.error('⚠ Using cached prices from 3m ago');
console.error('Error: Holdings file corrupted');

This matters for users piping output (shieldstack | tee snapshot.txt). Still exit 0 on stale cache warnings.

Exit Codes

Standard CLI exit codes for scripts and automation:

Situation Exit Code
Dashboard displayed successfully 0
Success with stale cache (warning shown) 0
Holdings file missing 1
Config file corrupted / invalid 1
API failed AND no cache available 1
Invalid command or input 1
process.exit(0);  // success
process.exit(1);  // failure

Error Handling

  • No holdings file on shieldstack (view): Print friendly message: "No holdings found. Run shieldstack update to get started." Exit with code 1. Do NOT prompt interactively.
  • No holdings file on shieldstack update: Treat as zero values, let user set them interactively. This is expected behavior for the update command.
  • No config file: Create with default goal (0.1 BTC)
  • API failure: Use cached prices, show warning: "Offline. Using cached prices from Xm ago."
  • Invalid input: Validate numbers are positive, show helpful error
  • Corrupted JSON: Show clear error message with path, suggest deleting file to reset
  • Missing cache: Fetch fresh data, don't crash
  • Rate limited (429): Use cache, warn user

NPM Package Setup

  • Package name: shieldstack
  • Binary name: shieldstack
  • Entry in package.json bin field
  • Include shebang in compiled output: #!/usr/bin/env node
  • Target: ES2022, Node 18+
  • License: MIT

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Critical: module: NodeNext and moduleResolution: NodeNext are required for ESM compatibility with chalk v5 and inquirer v9.

package.json outline

{
  "name": "shieldstack",
  "version": "0.1.0",
  "description": "CLI tool for ZEC/BTC portfolio tracking with goal-based scenario analysis",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "shieldstack": "./dist/index.js"
  },
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ],
  "engines": {
    "node": ">=18"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js",
    "prepublishOnly": "npm run build"
  },
  "keywords": ["zcash", "bitcoin", "crypto", "portfolio", "cli"],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "commander": "^11.0.0",
    "inquirer": "^9.0.0",
    "chalk": "^5.0.0",
    "decimal.js": "^10.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "@types/node": "^20.0.0",
    "@types/inquirer": "^9.0.0",
    "tsx": "^4.0.0"
  }
}

Notes:

  • "type": "module" for ESM. Chalk v5 and Inquirer v9 are ESM-only.
  • "files" explicitly lists what gets published (no junk).
  • "engines" enforces Node 18+.
  • "prepublishOnly" ensures build runs before publish.
  • tsx instead of ts-node for better ESM compatibility in dev.
  • No postinstall scripts - crypto community distrusts them.

## Pre-Publish Checklist

Before running `npm publish`:

1. **Test CoinGecko endpoints** - actually hit both endpoints, verify response parsing
2. **Verify shebang** - after `npm run build`, check `dist/index.js` starts with `#!/usr/bin/env node`
3. **Test full flow:**
   - No files → `shieldstack` → message + exit 1
   - `shieldstack update` → set values
   - `shieldstack` → dashboard displays
   - Offline mode (airplane mode) → cache warning shown (on stderr)
4. **Test edge cases:**
   - Zero holdings
   - Goal already met (total ≥ goal)
   - Very tiny numbers (no scientific notation: `1e-8` should show as `0.00000001`)
5. **Test NO_COLOR:** `NO_COLOR=1 shieldstack` renders clean ASCII without color codes
6. **Test global install:** `npm install -g .` → `shieldstack` works
7. **Dry run:** `npm publish --dry-run` to see exactly what's packaged
8. **README:** Has install command, screenshot, disclaimers

## Edge Cases to Handle (MVP)

- API returns error → use cache with warning
- Holdings file missing → prompt setup
- Config file missing → create with defaults
- Cache missing → fetch fresh
- Zero holdings → graceful display, prompt to add
- Very small BTC amounts → format correctly, don't round to zero
- Offline → show cache age prominently

## Edge Cases (Future Polish)

- Negative values (debt representation)
- Large numbers (column alignment)
- Terminal width detection
- Rate limit backoff (429 retry logic)
- Partial API data (one coin fails)
- Platform path differences (Windows)
- Concurrency: two terminals running `update` simultaneously (last write wins for MVP - acceptable for personal tool)
- Schema validation with zod/valibot (replace manual guards)

## Future Features (v0.2+)

- `--compact` flag for narrow terminals
- Share mode (`--share` anonymized output for posting)
- Encryption (optional passphrase for holdings file)
- 7/30/90 day averages (expand from 30d only)
- `--json`, `--compact`, `--refresh`, `--offline` flags
- Non-interactive update flags (`--zec 10 --btc 0.05`)
- Backup file on write (.bak)
- Custom tickers (add any coin)
- Goal in USD/ZEC (not just BTC)
- Decision journal (log trades, review outcomes)
- CSV export
- Historical portfolio tracking over time
- Price alerts
- Web UI

## README Sections to Include

- What it does (one paragraph)
- Screenshot/demo
- Installation (`npm install -g shieldstack`)
- Quick start
- Commands reference
- Technical Decisions section
- Privacy & Security note:
  - "No telemetry. Only external call is to CoinGecko for price data (they see your IP, not your holdings)."
  - "Holdings stored in plaintext at `~/.shieldstack/`. No private keys stored, but amounts are visible to anyone with file access. Exclude from cloud backup if concerned."
  - "Future versions may add optional encryption."
- Disclaimer (not financial advice)
- Data attribution: "Price data provided by [CoinGecko](https://www.coingecko.com/). See their [API terms](https://www.coingecko.com/en/api_terms)."
- Rate limit note: "Uses CoinGecko public API. Rate limits vary (5-30 calls/min). For stable limits, register for a free [demo account](https://www.coingecko.com/en/api)."
- Contributing / feedback links
- License

## Technical Decisions (for README)

**Decimal precision:** Using decimal.js for all financial calculations to avoid JavaScript float precision issues. This prevents embarrassing display artifacts like `0.00019999997` and ensures credible output.

**Single API source:** Using CoinGecko only for MVP. Offline caching handles short outages. If reliability becomes an issue, potential fallbacks include CoinMarketCap, CoinPaprika, or direct exchange APIs.

**Encryption:** Holdings are stored in plaintext. No private keys are stored—only amounts. Optional encryption via passphrase may be added in future for users who want it.

**Historical averages:** MVP uses 30-day average only. Expanding to 7/30/90 day comparisons planned for v0.2.

## Disclaimer Text

This tool is for informational purposes only and does not constitute financial advice. Cryptocurrency investments carry risk. Always do your own research. Prices sourced from CoinGecko—actual exchange rates may vary.