DEV Community

UDI Crypto
UDI Crypto

Posted on

Wrapping nine crypto miners behind one dashboard: what I learned building a mining manager in Electron

If you've ever run more than one mining rig, you know what it looks like: a terminal per miner, a folder of .bat and .sh files with slightly different flags, a pool dashboard in one tab and a spreadsheet in another. Every miner has its own argument syntax, its own log format and its own idea of what "hashrate" means. I got tired of that, so I built a desktop app to manage it. This post covers how it's put together and what surprised me along the way.

The problem

My setup was a mix of CPU and GPU machines on Windows and Linux. CPU work went to XMRig for Monero. The GPUs ran whatever fit the card and the coin: lolMiner, SRBMiner-MULTI, T-Rex, TeamRedMiner, BzMiner, GMiner, Rigel or NBMiner.

The pain points came down to four things:

  • Configuration drift. The same pool and wallet lived in a dozen scripts, each with that miner's flag syntax (-o/-u here, --pool/--user there, a JSON config somewhere else).
  • No single view. Checking "is everything hashing?" meant opening several terminals or web pages.
  • Silent failures. A miner crashing at 3 AM meant hours of lost work until I noticed.
  • Two operating systems. Different binaries, paths, and process behavior.

Architecture overview

The app is Electron. The main process (main.js) owns everything that touches the system: spawning miners, reading their output, polling their APIs and storing state. The renderer is just the dashboard. They communicate over IPC, and the renderer never spawns a process directly. That split keeps the UI responsive and the privileged code in one place.

Design decision 1: one config model, many CLI dialects

Rather than storing per-miner command lines, the app stores an intent:

{
  "miner": "xmrig",
  "algorithm": "rx/0",
  "pool": "pool.example.com:3333",
  "wallet": "YOUR_WALLET",
  "worker": "rig-01",
  "extraArgs": []
}
Enter fullscreen mode Exit fullscreen mode

Each miner gets a small adapter that translates that intent into its own arguments, and adds the flags needed to turn on its local stats API. Adding a miner means writing an adapter. The rest of the app doesn't change.

I kept an extraArgs escape hatch on purpose. Miner users love tuning flags, and trying to model every option for nine miners is a losing game. The adapter handles the common fields and passes the rest through untouched.

Design decision 2: a normalized hashrate model

This was the most interesting part. Every miner reports stats differently.

XMRig has a proper HTTP API once you enable it (--http-host, --http-port, optionally an access token). Its summary endpoint returns hashrate as an array of averages over different windows:

"hashrate": {
  "total": [1234.5, 1230.1, 1228.9],
  "highest": 1290.2
}
Enter fullscreen mode Exit fullscreen mode

Those are 10-second, 60-second and 15-minute averages in H/s. Early entries can be null right after startup, so the parser has to tolerate that.

lolMiner exposes its own JSON API when started with --apiport, structured around algorithms and per-GPU performance, with the unit reported alongside the number rather than always being H/s. The other GPU miners each have their own endpoint shape and field names, and some report per-device values you have to sum yourself.

So each adapter also includes a stats parser that turns whatever the miner returns into one internal shape: total hashrate in base units (H/s), per-device hashrate where available, accepted and rejected shares, and uptime. The dashboard only ever sees that normalized shape. Unit conversion happens in one place, which made a class of "why does this rig show 1000x too much?" bugs go away.

When a miner has no usable API, or the API isn't up yet, the fallback is parsing stdout. It's fragile, so I treat it as a last resort and keep those regexes isolated in the adapter.

Design decision 3: process supervision

Miners crash, especially GPU miners running aggressive overclocks. The main process supervises each miner it starts:

  • It listens for the child process exiting and records the exit code and the last lines of output.
  • It restarts with a backoff, so a miner that dies instantly (bad config, wrong algorithm for the card) doesn't get relaunched in a tight loop.
  • After repeated fast failures it stops and marks the miner as errored in the UI instead of retrying forever.
  • It treats "process alive but hashrate zero for too long" as unhealthy too, because a hung miner is worse than a crashed one.

On shutdown the app tries a graceful stop first and only force-kills if the miner doesn't exit. Orphaned miners still holding a GPU after the manager closes are a miserable experience.

Design decision 4: Windows vs Linux

Most of the platform differences end up in a small layer:

  • Binary names and paths. xmrig.exe vs xmrig, plus each miner's own release layout. The adapter resolves the right binary for process.platform.
  • Executable bits. On Linux, extracted binaries sometimes lose +x, so the app checks and fixes permissions before launching.
  • Killing process trees. Some miners spawn helpers. Signals behave differently on the two platforms, so stopping a miner cleanly needs platform-specific handling.
  • Privileges. XMRig performs better with huge pages / MSR tweaks, which need elevated rights. I chose to surface that as guidance rather than silently escalating.

Design decision 5: remote rig status

One dashboard is only useful if it shows every machine. Each rig runs the app and keeps its own normalized stats. The dashboard collects per-machine status (miner state, hashrate, shares, uptime) so you can see every rig side by side and spot the one that's down. Because the data is already normalized locally, the remote view doesn't need to know which miner a rig runs. Earnings tracking sits on top of the same data, so it's consistent across miners.

Lessons learned

Antivirus false positives are a real product problem. Miner binaries are flagged by many antivirus engines, because malware frequently bundles the same miners. Legitimate users see scary warnings, files get quarantined mid-run, and "the miner disappeared" becomes a support question. I can't fix the detection, but I can be transparent: explain why it happens, tell users where binaries come from, and detect a missing binary and say so clearly instead of failing with a cryptic spawn error.

Never trust a stats API to be stable or ready. APIs come up a few seconds after the process starts, fields are sometimes missing or null, and versions change things. Every parser is defensive, and a failed poll means "unknown," not "zero."

Model the intent, not the command line. Once the config was miner-agnostic, switching a rig from one miner to another became a dropdown change instead of rewriting a script.

Supervision needs a give-up condition. Infinite restart loops hide real problems. Surfacing "this keeps crashing, here's the last output" turned out to be more useful than auto-recovery alone.

What's next

Keeping the adapters current as miners ship new versions is ongoing work. I'd especially like feedback on which stats people actually look at and which they ignore.


Disclosure: I'm the developer of UDI Mining Manager, the app described here. It's a paid app with a 30-day free trial. You can try the live demo in your browser, or see the product page. If you've built something similar, I'd love to hear how you handled the stats-API mess.

Top comments (0)