MarketDeck docs
Architecture
Why MarketDeck is CLI-first, what lives where, and the narrow scope reserved for the daemon.
TL;DR#
MarketDeck is CLI-first, permanently. All business logic — market snapshots, paper trading, wallet signing, AI agent runs, backtests, trailing stops — lives in the marketdeck repo and is invokable as marketdeck <subcommand>. Every other surface (the GNOME Shell extension, the self-hosted web UI, future integrations) is a presentation layer that consumes the CLI.
The daemon (lib/daemon/) exists but is not a business-logic server. It is a transport plus event broadcaster — it exposes live mids, userdata tickers, and trailing-stop tick events to clients that need push-style updates, so a plugin doesn't have to poll the CLI in a hot loop. Everything else is a subprocess call.
This shape is intentional. If you are tempted to move logic into a plugin or to grow the daemon into a full RPC API, re-read "Why not a daemon RPC" below first.
What lives where#
CLI repo (this one)#
Canonical domain code:
bin/marketdeck.js— entry point, router, plugin discovery.core/— domain modules: snapshot, paper-trading, backtester, analysis-diff, http client, notifier, icon and meta caches, platform facade.db/— SQLite schema + migrations + per-table stores.exchanges/— Hyperliquid signing,/infoand/exchangewrappers. Adding a new exchange means a new subdir here, not a new plugin.ai/providers/— one adapter per vendor CLI.wallets/— keyring I/O, order lifecycle for real trading.snapshot.js,ai-runtime.js,default-prompt.js— top-level modules reused by CLI commands and (via symlink) by the GNOME extension.lib/cli/— router, arg parsing, plugin loader, command implementations.lib/daemon/— transport plus event broadcaster.lib/plugin-sdk/— stable re-exports the plugin API freezes on.
marketdeck-gnome (separate repo)#
GNOME Shell extension. Consumes the CLI via:
- Symlinks of the shared CLI modules into the extension dir on
marketdeck gnome install. The extension imports those modules directly via GJS so there is no cross-process call on the read path for panel updates. - Subprocess spawning of
marketdeck agent runetc. for long-running operations. - Daemon connection for low-latency live mids and userdata streams when the CLI has spawned a daemon.
marketdeck-webui (separate repo)#
Self-hosted web UI. Pure presentation: an HTTP server that answers requests by shelling out to marketdeck <subcommand> --format json and forwarding the result. No business logic. No direct DB reads. No direct exchange calls.
Why CLI-first, permanently#
- One implementation, N surfaces. The CLI is the contract. The GNOME extension and the web UI each take a few thousand lines of presentation code; the business logic — about twenty thousand lines in
core/+db/+wallets/+exchanges/— is written once. - Auditability. Every trade action, every AI run, every DB write can be reproduced by pasting a shell command. There is no "dashboard made this decision" gap between intent and effect.
- Unix composability.
marketdeck snapshot BTC | jq …,cron */5 * * * * marketdeck agent run …, `watch marketdeck paper list` — all free. - Small blast radius on crash. A panel freeze in the GNOME extension doesn't affect trading. A crashed AI provider doesn't lock the UI. Process isolation is free when every surface shells out.
- Plugins stay honest. Because plugins can't reach into private state, they cannot accidentally take on domain responsibilities.
Why not a daemon RPC#
The alternative — a long-running daemon exposing every CLI operation over a socket — gets proposed because subprocess startup costs roughly 80 ms. That's true, but:
- Startup cost is a non-issue on the read path. The GNOME extension already skips subprocess spawn for reads by importing core modules directly through symlinks. The web UI batches reads per HTTP request, where network latency dwarfs 80 ms. Agent runs take seconds to minutes — the 80 ms vanishes.
- An RPC surface is a permanent maintenance burden. Every new command becomes a method registration plus a request/response schema plus version negotiation plus error-envelope translation. The CLI already has all of that via argv + `--format json`.
- RPC hides failures. A crashed subprocess returns a non-zero exit code the caller sees instantly. A crashed daemon method either silently hangs or returns a 500, and callers end up implementing reconnect/retry logic for a problem they didn't have before.
- Two ways to do everything is worse than one slow way. If half the commands are RPC'd and half are subprocess'd, every plugin author has to learn which is which. CLI-first removes that question.
The daemon is the right answer for one thing only: push-style realtime feeds (live mid prices, userdata deltas, trailing-stop ticks). Polling the CLI at 1 Hz for a ticker is wasteful; polling at 100 Hz would be both wasteful and incorrect because the CLI has no session caching across invocations. The daemon earns its keep as an event broadcaster — and nothing else.
Daemon scope#
Built-in methods are administrative only: ping, subscribe, unsubscribe, channels, methods. Everything else is a feed — a named channel that produces push events when something changes.
Belongs in the daemon: live Hyperliquid mid prices, the userdata stream, the trailing-stop tick loop, future L2 / trade prints / funding feeds.
Does not belong in the daemon: agent runs, paper opens and closes, wallet operations, backtests. Subprocess calls handle those.
File-ownership guide#
| Change kind | Where it goes |
|---|---|
| New exchange integration | exchanges/<name>/ |
| New AI provider CLI adapter | ai/providers/<id>.js |
| New CLI subcommand | lib/cli/commands/<name>.js + register |
| New DB table or column | Append a migration to db/schema.js |
| New domain logic (strategy, fee model, analysis) | core/ |
| New real-time feed (push-driven) | lib/daemon/feeds/<name>.js |
| New plugin capability (surface only) | In the plugin repo, consuming existing CLI |
| New presentation (UI, notification shape) | In the relevant plugin repo |