Real-time monitoring for WhatsMiner ASIC fleets. One binary. No Docker. No dependencies. Boots in 500ms.
Built because MicroBT's stock UI looks like it was designed in 1998 and tells you nothing useful when your hashboards are on fire.
- Scans your network for WhatsMiner devices on port 4028
- Polls telemetry in real-time — hashrate, temperatures, power, fan speeds, chip data, PSU diagnostics
- Monitors per-slot hashboard health down to individual chip temperatures
- Detects errors with 150+ WhatsMiner error codes translated to human-readable meanings AND solutions
- Exports everything to Prometheus at
/metrics— plug Grafana and go - Handles both WhatsMiner firmware response formats transparently (some wrap in
Msgenvelope, some don't — we normalize both)
dotnet build
dotnet runOpen http://localhost:1442. That's it.
Prometheus metrics at http://localhost:1442/metrics.
Configuration lives in app.xml — ports, HTTPS, CORS, everything. Change it from the UI via CONFIG_SET or edit the file directly.
┌──────────────────────────────────────────────────────┐
│ μBiT::EYE │
│ │
│ Kestrel ──→ SignalR Hub ──→ CommandDispatch │
│ HTTP/WS (LiteHub) Will → Handler │
│ │ │ │ │
│ │ WiredStream MinerService │
│ │ (OPENFIRE) (TCP to ASICs) │
│ │ │
│ /metrics ── PrometheusCollector │
│ Per-miner, per-slot, per-PSU telemetry │
└──────────────────────────────────────────────────────┘
Stack:
- C# / .NET 10 — single binary, single process
- SignalR over WebSocket with MessagePack binary protocol
- Command pattern —
Willstring → handler →WiredAnswerwith microsecond benchmarks - Vanilla JS frontend, zero frameworks
- Native Prometheus exporter — no sidecar, no agent
Every command goes over WebSocket. Self-documenting — send LIST_ENDPOINTS_OPTIMIZED to get the full schema at runtime.
| Command | Description |
|---|---|
MINER_SCAN |
Sweep IP range, discover WhatsMiner devices |
MINER_POLL |
Query all registered miners, return telemetry snapshots |
MINER_GET |
Full detail for single miner — devices, PSU, errors |
MINER_ADD |
Manually register a miner by IP |
MINER_REMOVE |
Remove miner from registry |
MINER_LIST |
List registered miners (no TCP, instant) |
MINER_CLEAR |
Wipe miner registry |
| Command | Description |
|---|---|
SERVER_STATUS |
Uptime, memory, connections, ports, command stats |
SERVER_RESTART |
Restart Kestrel — reloads config, client auto-reconnects |
SERVER_CONNECTIONS |
List active WebSocket connections |
CONFIG_GET |
Dump running configuration |
CONFIG_SET |
Change config values, writes to app.xml, reports if restart needed |
CONFIG_RELOAD |
Re-read app.xml from disk without restart |
| Command | Description |
|---|---|
PING |
Connectivity test (~8µs round trip) |
NTP_TIME |
Server time for clock sync detection |
ECHO |
Echo payload for serialization testing |
| Command | Description |
|---|---|
LIST_ENDPOINTS_OPTIMIZED |
All commands with request/response schemas |
GET_DOC_API |
Schema for a specific command |
LIST_MODULES |
List all modules and their commands |
GET_MODULE_SCHEMA |
Full schema for all commands in a module |
GET http://localhost:1442/metrics
liteioctl_uptime_seconds 3421.5
liteioctl_memory_bytes 104857600
liteioctl_connections 3
liteioctl_commands_total 1847
liteioctl_command_avg_us{will="PING"} 8.6
liteioctl_command_avg_us{will="MINER_POLL"} 4521.3
miner_online{id="56",ip="192.168.15.56"} 1
miner_hashrate_ths{id="56",ip="192.168.15.56"} 62.10
miner_temp_board{id="56",ip="192.168.15.56"} 74.0
miner_temp_chip_max{id="56",ip="192.168.15.56"} 89.16
miner_temp_env{id="56",ip="192.168.15.56"} 42.5
miner_power_watts{id="56",ip="192.168.15.56"} 1792
miner_efficiency_jpth{id="56",ip="192.168.15.56"} 28.86
miner_fan_in{id="56",ip="192.168.15.56"} 6334
miner_fan_out{id="56",ip="192.168.15.56"} 6510
miner_accepted{id="56",ip="192.168.15.56"} 6301
miner_rejected{id="56",ip="192.168.15.56"} 7
miner_uptime_seconds{id="56",ip="192.168.15.56"} 1700936
miner_slot_hashrate_ths{id="56",ip="192.168.15.56",slot="0"} 22.53
miner_slot_hashrate_ths{id="56",ip="192.168.15.56",slot="1"} 21.31
miner_slot_hashrate_ths{id="56",ip="192.168.15.56",slot="2"} 18.26
miner_slot_temp{id="56",ip="192.168.15.56",slot="0"} 74.0
miner_slot_chip_temp_max{id="56",ip="192.168.15.56",slot="0"} 89.23
miner_slot_chips{id="56",ip="192.168.15.56",slot="0"} 78
miner_slot_freq{id="56",ip="192.168.15.56",slot="0"} 476
miner_psu_temp{id="56",ip="192.168.15.56",model="P222B"} 58.0
miner_psu_voltage_in{id="56",ip="192.168.15.56"} 212.5
miner_psu_fan{id="56",ip="192.168.15.56"} 9824
miner_error_active{id="74",ip="192.168.15.74",code="352",meaning="Hashboard 2 over-temperature protection triggered",category="Temp Sensor",severity="temp"} 1
miner_error_active{id="74",ip="192.168.15.74",code="600",meaning="Ambient temperature too high",category="Environment",severity="temp"} 1
150+ WhatsMiner error codes with human-readable meanings, solutions, categories, and severity levels. Covers fans, PSU, temperature sensors, EEPROM, hashboards, chips, firmware, pools, and security.
Every error returned by the API includes:
{
"Code": 352,
"Meaning": "Hashboard 2 over-temperature protection triggered",
"Solution": "Check ambient temperature",
"Category": "Temp Sensor",
"Severity": "temp",
"Timestamp": "2026-04-06 02:41:36"
}WhatsMiner has two different API response formats depending on firmware version:
Format A (M30S++, M50, etc.) — data comes direct:
{"STATUS":[...],"SUMMARY":[...],"id":1}Format B (M30S_V10, etc.) — data wrapped in Msg envelope:
{"STATUS":"S","When":...,"Msg":{"STATUS":[...],"SUMMARY":[...]}}μBiT::EYE detects and normalizes both formats transparently. The frontend never sees the difference.
All settings in app.xml. Changeable at runtime via CONFIG_SET (writes to disk) or by editing the file and calling CONFIG_RELOAD.
| Setting | Default | Description |
|---|---|---|
HTTPPorts/Port |
1442 | HTTP listen port |
HTTPSPorts/Port |
1443 | HTTPS listen port |
HTTPSCertificate/Enabled |
false | Enable TLS |
HTTPSCertificate/FilePath |
wired_dev.pfx | Certificate path |
WebRoot |
html | Static file directory |
SignalR/MaxMessageSizeMB |
10 | Max WebSocket message size |
SignalR/KeepAliveSeconds |
15 | Ping interval |
CorsOrigins/Origin |
(all) | Allowed origins |
SpaRewrites/Path |
/app, /blog | SPA fallback routes |
Domain |
localhost | Server domain |
Changes to ports, HTTPS, or SignalR settings require SERVER_RESTART. Logging and domain changes apply immediately.
No ASICs at home? Run the Python mock server:
python mock_miners.pyServes 3 fake miners with real telemetry data on localhost:
| Port | Miner | Notes |
|---|---|---|
| 4028 | M30S++ (ID 56) | Clean, no errors |
| 4029 | M30S_V10 (ID 44) | Different firmware envelope format |
| 4030 | M30S++ (ID 74) | Active error codes (352, 600) |
Hashrate and temperatures jitter ±5% per poll for realistic chart testing.
// Register mock miners from browser console
BrutalNetwork.send('MINER_ADD', { IP: '127.0.0.1', Port: 4028, Label: 'Mock 56' })
BrutalNetwork.send('MINER_ADD', { IP: '127.0.0.1', Port: 4029, Label: 'Mock 44' })
BrutalNetwork.send('MINER_ADD', { IP: '127.0.0.1', Port: 4030, Label: 'Mock 74' })
// Poll
BrutalNetwork.send('MINER_POLL').then(r => console.log(r.Obj))- .NET 10 SDK
- That's it
No Docker. No Node. No npm. No webpack. No node_modules black hole. Single binary, ~33MB RAM, runs on a Raspberry Pi.
- Read-only — only queries miners via standard WhatsMiner API on port 4028. Same commands the stock UI uses. Never writes to firmware.
- No authentication data stored — miner registry is in-memory only, lost on restart.
- Config on disk —
app.xmlis the only persistent file. No database. No state files.
GPL-2.0-or-later
Author: D. Leatti (Forbannet) — kernelriot.com — github.com/layer07
Donations: bc1qjj5vqw9t6pl4lhydsspll075skfuxgqkj7u97m — ko-fi.com/soloween
