dexterm

docs › Configuration and files on disk

Configuration and files on disk

Environment variables, the files dexterm writes, the optional free RPC key, and how to delete everything.

dexterm has no config server, no account and no telemetry. Everything it remembers is in a handful of local files you can read and delete.

Files on disk#

pathwhatnotes
~/.local/share/dexterm/dexterm.dbSQLite: the data cache, your watchlist and alerts, the feed snapshot, the request and credit meters, the PAPER wallet (balance, positions, trades), your KOL lists and mutes, and the history recorder's tables--db PATH moves it, --db :memory: skips it. $XDG_DATA_HOME/dexterm/dexterm.db when set; %APPDATA%\dexterm\dexterm.db on Windows.
~/.dexterm/state.jsonhow many times dexterm has booted (the first launch gets the full boot and the welcome card)DEXTERM_HOME moves the directory
~/.dexterm/helius.keyyour optional free Helius key, mode 600read in code, URL built in memory, never printed. Optional.
~/.dexterm/popup.jsonchart popup hotkeys, docking, rain, fps capoptional; see popup chart
~/.dexterm/config.json · config.journal.jsonlthe customization config (v4: theme overrides, keymap, rules, installed packs, widget consent, the price display mode) and its journal of applied changes with their inversesmachine-written, mode 600; ctrl+z in the app undoes the last change. Holds no secrets by construction: the schema has no key field, and an older config that carried one gets it moved out to <name>.key. See workspaces, rules, packs
~/.dexterm/workspaces/<name>.json · themes/<name>.jsonyour workspaces and themes, one file eachdexterm workspace, dexterm theme
~/.dexterm/packs/<name>@<version>/ · allowed_signers · reviewed-packs.jsoninstalled packs (exactly the files their lock names), the publisher keys you pinned, pack hashes you marked revieweddexterm pack
~/.dexterm/voice-helper/the Apple Speech helper (recognize.swift, its compiled binary and a source hash), built once by swiftc on the first voice useoptional; see voice. No audio or transcript is ever written here
~/.local/bin/dextermthe launcher install.sh writesuser-level
packages/popup/shell/wry/target/release/dexterm-shell-wrythe native chart window, if builtDEXTERM_POPUP_SHELL points elsewhere

Delete the first three and dexterm forgets everything.

History recorder#

In live mode dexterm keeps a local history of what it saw, for the rug model and backtests on the roadmap: a row per coin when its price, liquidity, holders or volume changed materially (plus an hourly anchor), which feeds listed it, and a code-only outcome label per anchor at 1 h, 2 h, 6 h, 24 h and 72 h (no model is involved). It lives in the same --db file; nothing is uploaded.

  • Requests: recording reuses data the screens already fetched. Outcome checks re-ask DexScreener about coins whose window is due, at the lowest priority inside the app's own limiter: at most 6 calls a minute and 1,500 a day, skipped while DexScreener is rate-limiting.
  • When it runs: the first maintenance pass (outcome check, labels, retention) runs 5 seconds after launch and then once a minute, and quitting labels whatever is due before the database closes. Until 2026-10-12 the first pass waited a full minute and nothing ran at quit, so two days of short runs on the development machine left 1,676 anchors and 0 outcomes; dexterm recorder label labels an older database's backlog in one go.
  • Size: measured at about 100 MB per 8-hour live day with the current rules. Raw JSON is dropped after 7 days, rows are thinned to one per coin per minute after 24 hours, and columns expire after 90 days; outcome labels are kept. dexterm recorder stats shows the size, dexterm recorder prune runs retention now.
  • Off: --no-recorder or DEXTERM_RECORDER=0. It never runs with --fixtures, --replay or --db :memory:.
  • Export: dexterm recorder export writes one record per anchor with its later outcomes (see the CLI reference).

The optional free RPC key#

Keyless, the holders panel shows the holder count and the top-10 share from Jupiter; the ranked top-20 list with owner wallets needs a free RPC key (a 2-minute signup with no card at Helius, Alchemy, QuickNode or Ankr). Configure one of:

$ mkdir -p ~/.dexterm && printf %s "<your-key>" > ~/.dexterm/helius.key && chmod 600 ~/.dexterm/helius.key
$ export DEXTERM_RPC_URL="https://<provider>/<path>"
$ export DEXTERM_RPC_URLS="https://rpc-a/...,https://rpc-b/..."

Resolution order: DEXTERM_RPC_URL, else ~/.dexterm/helius.key, else the public RPC pool. All configured endpoints rotate by health score; a 429 pauses one endpoint (honouring Retry-After) and fails over to the next. Every URL in an error, a --verbose line or a --record file passes a redactor; a recording that still carries a key refuses to save. dexterm doctor reports the provider name only, never the key.

never paste a key into a command line argument, a shell history, a chat or a repository. The file above is the only place dexterm reads it from.

Environment variables#

variableeffect
DEXTERM_HOMEthe directory for state.json, helius.key, popup.json, config.json, workspaces, themes and packs (default ~/.dexterm)
DEXTERM_DB_PATHset by the app for its analyst MCP child so it shares the same cache file; you normally use --db
DEXTERM_RPC_URL · DEXTERM_RPC_URLSread-only RPC endpoints (see above)
DEXTERM_CALM=1effects off (same as --calm)
DEXTERM_INTENSITYoff · normal · fever
DEXTERM_OFFLINE=1refuse live data; --fixtures or --replay only (tests and CI)
DEXTERM_ANALYST_MODELthe Claude model for analyst turns (default claude-sonnet-5-5)
DEXTERM_POPUP_SHELLpath to a prebuilt native chart window
DEXTERM_POPUP_HOTKEY · DEXTERM_POPUP_HOTKEY_CLOSEoverride the popup chords without a config file
DEXTERM_RECORDER=0history recorder off (same as --no-recorder; off, false and no work too)
DEXTERM_TRACEwrite first-frame and first-rows milestones to a file (boot measurements)
DEXTERM_VOICE_STT · DEXTERM_VOICE_TTSpick the voice engines: auto (default) · apple · whisper · none, and auto · say · none
DEXTERM_VOICE_RECORD=1keep the whisper path's temporary WAV instead of deleting it after transcription
DEXTERM_WHISPER_MODEL · DEXTERM_VOICE_HELPER_DIR · DEXTERM_SWIFTCthe whisper model file (default ~/.dexterm/models/ggml-base.en.bin), the compiled Apple Speech helper's cache dir (default ~/.dexterm/voice-helper), the Swift compiler
DEXTERM_SCREEN_STATEset by the app for its MCP child (the current screen context); not for you
NO_COLOR · FORCE_COLOR · COLORTERM · TERMcolour depth detection; NO_COLOR also implies calm
LANG / LC_ALLa non-UTF-8 locale switches the rain to hex glyphs (--ascii-rain forces it)

Variables the app reads at c289d90: DEXTERM_ANALYST_MODEL, DEXTERM_CALM, DEXTERM_DB_PATH, DEXTERM_FFMPEG_MIC, DEXTERM_HOME, DEXTERM_IMAGES, DEXTERM_INTENSITY, DEXTERM_OFFLINE, DEXTERM_POPUP_HOTKEY, DEXTERM_POPUP_HOTKEY_CLOSE, DEXTERM_POPUP_PROXY, DEXTERM_POPUP_SHELL, DEXTERM_RECORDER, DEXTERM_RPC_URL, DEXTERM_RPC_URLS, DEXTERM_SCREEN_STATE, DEXTERM_SOUND, DEXTERM_SWIFTC, DEXTERM_TRACE, DEXTERM_VOICE_HELPER_DIR, DEXTERM_VOICE_RECORD, DEXTERM_VOICE_STT, DEXTERM_VOICE_TTS, DEXTERM_WHISPER_BIN, DEXTERM_WHISPER_MODEL.

Themes and look#

--theme cyberspace|midnight|ember|paper or t at runtime. --intensity off|normal|fever, --fps N, --no-boot, --ascii-rain, --tz ZONE for the header clock (default America/New_York; local for the machine zone). Details in the CLI reference. User themes join the t cycle: see themes and keymaps.

Price display mode#

Memecoin traders think in market cap, so every token price can be shown as market cap instead of the USD price per token. It is a display mode only: the same data underneath, no extra requests, and nothing that trades (signer, caps, tickets, plans, the confirm gate) reads it.

  • $ flips price ⇄ market cap on every screen and both charts, and saves it to config.json as "display": { "priceMode": "mcap" } (or "price", the default) through the journal, so ctrl+z undoes it. dexterm display [price|mcap] shows or sets it from the command line; --price-mode price|mcap overrides it for one run; a workspace may carry "priceMode" and switches to it while active.
  • Market cap = price × supply, where the supply is what the data sources already report (the row's market cap ÷ its price at the latest tick). The screener, watchlist and new pairs lead with MCAP (the PRICE column moves to MCAP's old place, every sort key works as before, mcap still sorts by market cap); the watchlist's entry becomes ENTRY MC; spotlights and the token header lead with the market cap and keep the price beside it.
  • Charts: the terminal chart and the popup chart label the Y axis in K / M / B (300K, 1.2M). Historical candles are scaled by the current supply, and the chart title says mcap (current supply) so nobody reads it as the exact market cap at that time (a burn or a mint since then is not reflected).
  • Order plans already trigger on market cap: the O screen and the plans popup show each level with both numbers (entry mc ≤ $300K ($0.0₃3)), the price taken from the plan's own supply snapshot. Position rows read AVG MC / MCAP; profit and loss stay in USD (or SOL on the paper screen).
  • A token without a known supply falls back to its price and says so: ᵖ after the number in a table (the panel caption counts them), supply unknown · showing price on the spotlight, the header and the chart title. Nothing is estimated.
  • config.json moved to v4 for this: an older file is migrated on load (display.priceMode = price added, a backup kept as config.json.bak-v3), nothing else changes.

Workspaces#

A workspace is one JSON file, ~/.dexterm/workspaces/<name>.json: the screen to land on, the theme and intensity, the screener preset / filter / sort, the token-screen panel toggles, optional per-screen pane trees for the layout solver (weights, fixed widths, priorities, what collapses at which width), and the HUD popups to open together. The shipped default reproduces the five screens exactly; morning-hud is "show me everything": the narrative board, the feeds, order plans and the chart of the selected coin as windows beside the terminal.

  • In the app: ctrl+w then w list · n / N next / previous · s save the current layout under a typed name · m the morning HUD · o open the active workspace's popups · d default · 1–9 pick a slot. --workspace <name> starts in one. The footer shows the active workspace.
  • On the command line: dexterm workspace list | show <name> | remove <name> | init (init writes default as an editable file with the shipped trees).
  • A switch is a UI action and nothing more. It can navigate, re-theme and open the same read-only windows the keys open; it cannot open a ticket, confirm, arm or sign, and a workspace file has no field for any of that (the schema refuses unknown keys by name). Jarvis proposes workspace switches through the same proposal card as everything else.

Themes and keymaps#

~/.dexterm/themes/<name>.json is { "extends": "cyberspace", "tokens": { "accent": "#ffb02e", … } }: a built-in base plus hex overrides. At load every value must be a hex colour, ramps exactly four, unknown tokens are refused, and two semantic checks protect you from a shared theme: a contrast floor (text on background, muted on panel, the up / down / warn colours) and an up / down guard (up must read green/cyan and down red/pink by hue, both must carry real colour — an HSL saturation floor — and they must sit a minimum perceptual distance apart, so neither an amber pair nor a near-grey pair can make a loss look like a gain). A theme that fails loads as paper with a toast naming the token. config.theme.overrides is the same for one-off tokens; dexterm theme list | show | init <name> | check <name>.

config.keymap re-binds the app's actions: "global": { "screen.watchlist": ["3", "W"] } and "screens": { "screener": { "preset.next": ["]"] } }, chords as "ctrl+w s". Refused by name, whatever the file says: assigning ctrl+c, esc, enter, y / Y or ! to anything; changing the keys of quit, back, the alert confirm / dismiss, arm, panic or the gate; two actions on one key within one scope; a chord prefix bound on its own; more than four keys per action. Any error keeps the defaults (a half-applied keymap is worse than none) and names the path. dexterm keymap [screen] prints the effective keys with overrides marked ·; the ? overlay marks them the same way.

Rules engine#

A rule is one line: when <cond> [for <n> ticks | <n>s | <n>m] then <action>+ [cooldown <dur>] [max <n>/h] [scope screener | watchlist | newpairs | positions | mint:<addr>] [window <dur>] [name "<s>"]. Conditions use the filter grammar's fields and tags with and / or / not and parentheses, plus two derived signals over a per-coin history the engine keeps itself: <field>.velocity (current ÷ the mean of the earlier samples in the window) and <field>.delta (current − oldest in the window); both are false until two earlier samples exist, so a rule never fires on a coin's first paint. Actions: alert(level, "text"), ticket(buy | sell, size ≤ 50, sol | usd | pct), ask(skill), popup(chart | token | bubbles | narrative | plans | feeds), tag("label"), sound(name), watch.

vol5m.velocity > 3 and cluster < 20 and liq > 25K for 2 ticks then alert(up, "surge") ask(token-dossier) popup(chart) cooldown 15m max 6/h name "surge-clean"
  • The text is parsed, never evaluated as code. Every rule has a cooldown (default 10 m) and a max per hour (default 12, hard cap 120), so a bad rule is annoying, not dangerous.
  • Rules can alert, annotate your own watchlist (tag, watch), open the read-only windows and ring the bell, or propose. ticket and ask do not act: each fire becomes a proposal card on the rules screen (status proposal, your keypress required, armed by nobody); acting on it opens the PAPER ticket in its editing phase, where you size and confirm, or asks the analyst. Nothing a rule does reaches an order plan, a mandate, the signer or the gate, and a rule argument named like money (arm, auto, confirm, execute, sign, send) is refused at parse time, in text and in JSON.
  • ctrl+r opens the RULES screen: the list with enabled / last fired / source, i types a new rule with a live "would match N of M rows right now" dry-run, tab the proposals pane, space toggles, d removes. dexterm rules list | add "<rule>" | remove | enable | disable <id> | check "<rule>" | dry-run "<rule>" | fields. Rules live in config.json, every change through the journal.

Packs#

A pack is a directory you can share: pack.json (name, version, publisher, what it contains), then workspaces/*.json, themes/*.json, rules/*.rules, keymap.json and widgets/<name>/ (see widgets), plus a generated pack.lock (sha256 per file and a pack hash) and an optional pack.lock.sig from ssh-keygen -Y sign (no new dependency; the key everyone already has).

$ dexterm pack verify ./trenches                 # the install card: integrity, signature, trust, contents, refusals
$ dexterm pack add ./trenches --accept-unsigned  # install into ~/.dexterm/packs/trenches@1.0.0
$ dexterm pack export ./mine --name mine --workspace hud --theme amber --rules --keymap
$ dexterm pack sign ./mine --key ~/.ssh/id_ed25519 ; dexterm pack trust alice "ssh-ed25519 AAAA…"
  • Trust tiers decide defaults, never powers. reviewed (the pack hash is on the list dexterm ships or in your reviewed-packs.json) > signed (a publisher key you pinned with dexterm pack trust) > unsigned (needs --accept-unsigned: the risk card). Rules from a reviewed or signed pack load enabled; from an unsigned pack they load disabled and you enable each one on the rules screen (that makes your own copy). A pack keymap is shown and merged only by dexterm pack keymap <name> --merge, through the same reserved-key checks as your own. Widgets install disabled.
  • A pack never runs code. It is plain files; there is no entry script, no dependency, no network. Refused outright, not skipped: paths with .. or an absolute root, symlinks anywhere, hidden files, node_modules, more than 200 files or 2 MB, a __proto__ key in any JSON, a capability or enabled field on a widget, a rule that tries to arm or oversize, a keymap that rebinds a reserved key. The hash is pinned at install and re-checked on every load: a pack edited after install is skipped and reported, and a directory copied into packs/ without a record is not an install.
  • Export refuses secrets. Anything in your exported files that looks like a base58 private key, a Helius token, a 32-byte hex string or a high-entropy blob stops the export before a file is written.
  • Not built yet: pack add from a git URL or a zip (install is from a directory you already have), and a shipped reviewed list (empty until a first-party pack is reviewed). The example pack is packs/example-widgets in the repo.

Widgets#

A pack widget is an html page the popup host serves in a loopback HUD window, the same window technology as the popup chart. It is the only way third-party UI gets into dexterm, and it is small on purpose: a widget can receive and render; it cannot fetch, import, run anything in the terminal process, or send anything but close / focus / drag.

  • widgets/<name>/widget.json declares pages and their topics, a subset of rows · token · narrative · positions · alerts · theme: the screener's public columns, the open coin, the narrative board, your positions (symbol, size, PnL), the alert log, the theme. Nothing in a view model names a key, a signer, a cap, a mandate or the gate; pair / source ids and provenance internals are not sent.
  • The window is two documents: the host's own wrapper page (it owns the connection to the terminal, the title strip and the close button) and, inside it, the pack page in a sandboxed frame. The pack page runs under a hash-only CSP: the html is served exactly as installed (nothing is substituted into it, there is no nonce), and every inline <script> and <style> block runs because its sha256 is in the header — only the text you read before enabling the widget. It has no network of its own at all (no fetch, no socket, no remote scripts, styles, images or fonts), no cookies or storage (an opaque origin), no inline handlers, no eval, no runtime script injection where the engine supports Trusted Types, and no way to navigate anywhere: a document may always navigate itself, which a sandbox alone cannot stop, so the wrapper's frame-src names the one document the frame may show and the engine refuses any other before a request exists — the wrapper then closes the widget and logs why. Verified in headless Chromium and WebKit. One kind of connection no content-security directive covers — a <link rel=preconnect> built at run time makes WebKit open a bare TCP connection to a host and port the page chose — so the SDK, which runs first in the frame, seals the DOM: the insertion methods, every HTML-parsing entry point (the static Document.parseHTMLUnsafe included), adoptNode / importNode, the element-taking setters and WebRTC are locked on the prototypes before any pack script runs, a click on a link that targets another window (and any form submit) is cancelled before the engine can open a connection for it, a MutationObserver closes the widget if a loader element ever appears anyway, and none of it depends on Trusted Types, which the system WebKit of macOS 14 and 15 lacks (verified inside the real native shell on that WebKit, and in headless Chromium and WebKit with Trusted Types stripped, with a raw TCP sink). Widgets open only in the native popup shell (macOS 14 or newer), whose windows are routed through a loopback sink proxy of the host's as a further backstop for named hosts. A page that carries a loader (<script src>, <link>, a frame, <meta http-equiv>, @import, or a __NONCE__ placeholder) is refused at install, again at open and again when the host issues the window's ticket. A frozen dexterm object is injected ahead of the pack markup: dexterm.on(topic, fn), dexterm.theme (also CSS variables), dexterm.close(), dexterm.log(); the frame can say only ready / log / close back, and the terminal's socket accepts nothing but the generic events from a widget.
  • Consent is yours and re-checked: a widget installs disabled; dexterm pack widget <pack> <name> --enable after you read the page; at every open the app re-hashes the installed pack and refuses a page edited since. A workspace can list a widget popup ({ "kind": "widget", "widget": "<pack>/<name>" }) but cannot enable it.
  • Beta. One example widget (packs/example-widgets, the MOVERS tiles), a popup window only. The in-process plugin sandbox from the design (terminal columns and panels, host-mediated fetch, per-plugin storage) is not built. API and limits: packages/popup/WIDGETS.md in the repo.

The analyst's launch#

For the record, the analyst is spawned as claude -p with --output-format stream-json, --tools '' (no built-in tools), --strict-mcp-config pointing at the dexterm server only, --allowedTools mcp__dexterm__*, --permission-mode dontAsk, --setting-sources '' (none of your Claude Code settings or other plugins), the dexterm plugin directory for the skills, the analyst system prompt appended, a JSON schema for structured skills, and --disallowedTools for the action tools in scan mode. It cannot read files, run commands or reach any other MCP server from inside dexterm.