Bitaxe.de
Guides & knowledge

Monitoring a Bitaxe: catch outages, heat and wrong payouts without false alarms

12 October 2026 · 21 min read

Cover image “Bitaxe monitoring”: a Bitaxe Gamma 601 on its stand, next to an ntfy notification “gamma-living-room: problem, Fan is not spinning”

A Bitaxe runs for weeks without anyone watching. Most of the time that goes well. When it does not, you notice late: from the loud fan, from a hashrate that has been too low for days, or not at all, because the miner quietly switched to the fallback pool. This article shows what you can monitor, which thresholds work without false alarms and which tool suits how many devices. It is based on the source code of AxeOS 2.15.3 and NerdOS 1.1.0.1, the vardiff logic of ckpool and our own calculations.

Short version: For one or two miners the pool alert is enough: @bitaxepool_bot reports via Telegram when a worker stops delivering shares and needs no access to your home network. From three devices on, or for miners running unattended in the basement, a small script on your home network pays off. It polls /api/system/info every minute and sees what the pool cannot see: temperature, fan, power supply, fallback pool and whether the coinbase pays your address at all. Monitoring hardly saves your block chance, though: two days of downtime a month cost a Bitaxe Gamma about one euro a year in expected value.

What an outage costs and what monitoring is really good for

The first question is a sober one: how much does it cost when a miner does nothing for a day? In solo mining the chance of a block depends only on the time spent hashing. At the difficulty of 12 October 2026 (about 132.7 trillion, block 971,119), a Bitaxe Gamma with 1.2 TH/s has a chance of 1 in 5.5 million per day and about 1 in 15,000 per year. With a block subsidy of 3.125 BTC, not counting transaction fees, the expected value is 20,747 sats a year, or a little over €15 at a price of €73,860.

Two days of downtime a month, 24 a year, cost 6.6 % of that: 1,364 sats or about one euro. A miner that hangs while still drawing 17 W uses 0.41 kWh a day, which is 12 cents at €0.30 per kWh. If you monitor to rescue your returns, you are monitoring the wrong thing. The full block-chance maths is in finding a block with a Bitaxe.

Other things get expensive. All of them show up in values AxeOS reports anyway:

Event Consequence How to spot it Urgency
Fan fails, heatsink clogged with dust Chip and voltage regulator heat up; AxeOS stops and then runs throttled for good fanrpm, vrTemp, overheat_mode immediately
Miner on the fallback pool or with a foreign address A block pays the address entered there, and out of the box that is not yours isUsingFallbackStratum, stratumUser, coinbaseValueUserSatoshis immediately
Power supply or cable too weak Restarts, crashes, warm plugs in the long run resetReason, voltage same day
Creeping loss of performance Hashrate stays below “Expected” hashRate_10m, expectedHashrate, frequency no rush
Complete outage no shares, no block chance pool alert, no answer from the API no rush

Three viewpoints: device, pool and network

Every kind of monitoring looks at the miner from one of three directions, and each sees something different. The device knows every measurement but can no longer report anything once it hangs. The pool only sees whether shares arrive. A ping shows whether the device is on the network, not whether it is hashing.

Diagram: miners on the home network are polled every minute by a watch script through the local API, which alerts the phone on problems; in parallel the miners send shares to the pool, whose Telegram bot reports a silent worker; AxeOS answers ordinary requests from the internet with 401, but that is no password protection.
The script on the home network reads the device values, the pool only sees the shares. No path leads from the internet to the miner, and it should stay that way.
Source Sees Does not see Needs access to the home network
AxeOS or NerdOS API Temperatures, fan, voltage, hashrate, shares, pool settings, coinbase anything, once the device stops answering yes
Pool (API, Telegram bot) whether shares arrive, hashrate from shares, best share temperature, causes; a switch to the fallback looks like an outage no
Ping, router whether the device is on the network whether it is hashing yes

The most robust setup combines two directions: the pool view as an independent check that still reports when everything at home goes down, and the device view for the causes.

The AxeOS API: access, endpoints, fields

No password, but only from private networks

AxeOS has no login. Instead, the firmware checks where every API request comes from. Without an Origin header, as with curl, scripts or Home Assistant, the sender must come from the private address ranges 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16. If a browser sends an Origin header, it must point to a private address, a name ending in .local or a name without a dot. AxeOS answers everything else with “401 Unauthorized”. The API is only open while the miner runs its own setup Wi-Fi. That is what is_network_allowed() in http_server.c says.

Three things follow from this. First, every computer on the same private network may do everything: read, but also change pool and address (PATCH /api/system) or write firmware (POST /api/system/OTA). Second, ordinary requests from the internet fail, for example from a rented server or a cloud uptime service, even with port forwarding. That is still no protection against attackers: the check is mainly meant to keep foreign websites in the browser out, and a deliberately crafted request gets through as soon as the miner can be reached from the internet. Third, with VPN access it matters which address the request arrives with at the miner. A Tailscale subnet router rewrites the sender address to its own LAN address by default (SNAT), and then it works. If you switch SNAT off with --snat-subnet-routes=false, requests arrive from an address in 100.64.0.0/10 and AxeOS refuses them. The same rule applies to other VPNs: what counts is the address the request arrives with. A WireGuard tunnel with addresses from 10.x therefore works without rewriting.

The Nerd firmware also checks for private address ranges. Reading works there without further protection, changing settings requires a one-time code if two-factor protection (OTP) is switched on. AxeOS has no comparable lock.

The endpoints that matter for monitoring

Endpoint Method What for
/api/system/info GET the whole state in one JSON, the basis of any monitoring
/api/ws/live WebSocket the same data live: complete once on connect, then only changes
/api/system/statistics?columns=… GET the history the device stores itself (chart in the dashboard)
/api/system/asic GET chip data and the allowed clock and voltage steps
/api/ws WebSocket log output as on the Logs page
/api/system/restart, /pause, /resume POST restart, pause and resume mining
/api/system PATCH change settings

To start, a single call with curl and jq is enough:

Terminal
curl -s http://192.168.1.50/api/system/info | jq '{hashRate_10m, expectedHashrate, temp, vrTemp, fanrpm, sharesAccepted, isUsingFallbackStratum, coinbaseValueUserSatoshis, resetReason}'

The fields that really tell you something

In AxeOS 2.15.3 the answer from /api/system/info has more than a hundred fields. For monitoring, these count:

Field Meaning Unit Worth an alert
hashRate_10m rolling average over 10 minutes (also hashRate_1m, hashRate_1h) GH/s stays below 85 to 90 % of expected
expectedHashrate clock times cores times chips GH/s drops when the clock drops
errorPercentage share of faulty hashes reported by the chip % keeps rising; below 0.1 % is very good
sharesAccepted, sharesRejected, sharesRejectedReasons counters since the start, rejection reasons as a list count counter stands still, rejections above 1 %
poolDifficulty difficulty the pool currently assigns number decides how long “no share” is normal
temp, temp2, vrTemp chip, second sensor, voltage regulator °C chip above 70, regulator above 95
fanrpm, fanspeed, autofanspeed speed, set value, automatic on or off rpm, %, 0/1 speed 0 with a set value above 0; automatic suddenly off
voltage, nominalVoltage input voltage, nominal voltage mV, V below 94.9 % of nominal
power_fault, hardware_fault only appear on a fault, for example “Fan Control Failed” text any message
overheat_mode overheat protection is running 0/1 1
uptimeSeconds, resetReason uptime since the start, reason for the last restart s, text uptime jumps back
isUsingFallbackStratum, stratumUser fallback active, configured user 0/1, text fallback active; user differs
coinbaseValueTotalSatoshis, coinbaseValueUserSatoshis, coinbaseOutputs value of the current job's coinbase and the part that goes to your address sats own share 0 on a solo pool
wifiRSSI Wi-Fi signal strength dBm stays below −75
miningPaused mining paused true/false for exceptions in your own logic

The Nerd firmware delivers largely the same names under the same path, with differences: hostip instead of ipv4, lastResetReason instead of resetReason, asicTemps as a list per chip, no expectedHashrate (calculated as clock times smallCoreCount times asicCount divided by 1000) and no coinbase fields. It reports the fallback in stratum.usingFallback, the temperature at which it shuts down in overheat_temp and per fan channel in fans[].overheatTemp. On top of that it has its own coinbase check with an alert, more on that below.

Six checks that find real problems

1. Reachability and restarts

If the API does not answer three times in a row, the miner is gone. A restart shows up as uptimeSeconds being lower than at the last poll. resetReason delivers the reason along with it, and that is often the diagnosis already:

Text in resetReason Typical cause What to do
Reset due to power-on event power was gone: socket, power cut, loose contact once is no problem; if it repeats, check plug and power strip
Software reset via esp_restart restart from AxeOS or the API, after a firmware update or changed settings normal if it was you
Brownout reset (software or hardware) input voltage briefly collapsed check power supply and cable, see power supply for the Bitaxe
Reset due to task watchdog; Reset (software or hardware) due to interrupt watchdog firmware hung undo the overclock, update the firmware
Software reset due to exception/panic; Reset due to CPU lock up (double exception) crash update the firmware, save the log if it repeats
Reset due to power glitch detected voltage spike or dip check the power supply

2. Shares: the most sensitive outage indicator

If sharesAccepted stops rising, the miner is not hashing or its work is not arriving. That is the most reliable outage indicator, because on ckpool-based solo pools like pool.bitaxe.de shares arrive every few seconds. How fast exactly is in the next section. Judge the share of rejected shares over an hour. If it is above one percent, the reasons in sharesRejectedReasons reveal the cause; what they mean is in the section on rejected shares in Bitaxe connected but not mining.

From the outside you check the same through the pool API: https://pool.bitaxe.de/api/v1/address/ plus your address delivers lastshare for each worker and hashrate averages from 1 minute to 7 days. Only lastshare is useful for outages: ckpool's hashrate fields do not drop to zero after an outage, they stay where they were until shares come in again.

3. Hashrate against expected

expectedHashrate is clock times cores times chips: for the BM1370 with 2,040 cores at 525 MHz that is 1,071 GH/s. A healthy miner reaches at least 95 % of that in the hourly average; why is explained in the hashrate error explained. For an alert, use the 10-minute average hashRate_10m with a more generous threshold of 85 % that has to be undercut for a quarter of an hour, and skip the first 15 minutes after a start, while the averages fill up.

The hashrate the pool shows is less suited for this. The pool estimates it from the shares, and that is noisy: with the AxeOS default a Gamma delivers about 1,000 shares an hour, so the pool's hourly average fluctuates by about ±3 %. At difficulty 32,768 it is only about 30 shares an hour and ±18 %.

4. Temperature, fan, power supply

AxeOS warns in the dashboard from 70 °C at the chip. Above 75 °C at the chip or above 105 °C at the voltage regulator the overheat protection kicks in. Up to 65 °C at the chip and up to 85 °C at the regulator are good for continuous operation. An alert at 70 °C chip and 95 °C regulator therefore fires before the protection does. The Nerd firmware, by contrast, already switches off the core voltage at its “Shutdown Temperature”, 70 °C at the chip by default; devices with two fan channels also shut down at 80 °C at the voltage regulator. The NerdOS 1.1.0.1 API has no message of its own for this; you only notice it because no more shares arrive. The alert limit therefore belongs 5 °C below the shutdown temperature. If the fan does not spin (fanrpm 0, fanspeed above 0), it is broken or the cable is loose. That alert cannot wait. hardware_fault reports for example “Fan Control Failed”, power_fault a fault of the voltage regulator. The input voltage is suspicious as soon as it drops below 94.9 % of nominal: below 4.75 V on the Gamma, below 11.4 V on 12 V devices. From there AxeOS also shows “Danger: Low Voltage”.

5. Payout: fallback, user, coinbase

If isUsingFallbackStratum is 1, the miner works for the fallback pool. That is harmless as long as your address is entered there. Out of the box the fallback, like the main pool, holds a preset address. If you only enter your own address for the main pool, a block found on the fallback pays that preset address. Also check whether stratumUser still matches the value you entered.

The strongest single check is the coinbase. AxeOS 2.15.3 decodes the coinbase of every job (option “Decode Coinbase Tx” in the advanced pool settings, on by default) and reports in coinbaseValueUserSatoshis how many satoshis go to outputs whose address stands at the start of stratumUser. On a solo pool without fees that is the whole coinbase. If it says 0, the pool builds blocks that do not pay you: a typo, the wrong pool, a tampered setting. The dashboard shows the same value as a share and warns with “You don't have a share in the mining reward” at 0 % and “Your share of the mining reward is only …” below 95 %. But only someone who opens the dashboard sees the warning. On most shared pools the coinbase pays the pool; 0 is normal there and the check should be switched off. With SOLO Groups on pool.bitaxe.de the coinbase splits a block among the members, so a share below 100 % is intended there. What is behind the warning is also answered in the FAQ of Bitaxe connected but not mining.

6. Settings that change by themselves

This check is missing from almost every guide. When the overheat protection kicks in, AxeOS 2.15.3 stops mining, sets the fan to manual 100 % and waits at least 30 seconds until the regulator is below 95 °C and the chip, where measurable, below 45 °C. Then it restarts with the clock 100 MHz lower and the voltage 100 mV lower and saves both. Automatic fan control stays off. None of this is reset by itself.

Sequence in four steps: trigger above 75 °C at the chip or 105 °C at the regulator, mining stops and the fan goes to 100 %, cooling below 95 °C, restart with 100 MHz and 100 mV less; below, what stays and which API fields show it.
After the overheat protection the miner keeps running slower and louder until you reset clock, voltage and automatic fan control.

A Gamma at 625 MHz, the way we ship it, then runs at 525 MHz and expects 1.07 instead of 1.28 TH/s, 16 % less. With the original AxeOS values of 525 MHz it drops to 425 MHz and 0.87 TH/s, 19 % less. Monitoring that remembers the baseline of frequency and autofanspeed sees this at once. First fix the cause, meaning dust, fan or location, then reset clock, voltage and automatic fan control in the settings. How to make the fan quiet again afterwards is in making a Bitaxe quieter.

When is “no share” an outage?

Most false alarms come from a wrong threshold for “no share for …”. The right threshold depends on the share rate, and you can calculate that for your miner. A share of difficulty D takes D × 232 hashes on average. The average gap between two shares is therefore D × 4,294,967,296 divided by the hashrate in H/s. The dashboard shows D in the card with the pool data under “Difficulty”, the API in the field poolDifficulty.

On ckpool pools like pool.bitaxe.de the vardiff assigns the difficulty. It aims at 0.3 shares per second and leaves the difficulty alone as long as the rate stays between 0.15 and 0.4. The miner's suggestion sets the lower bound: after logging in, AxeOS suggests 1000 (a pool setting, in the API stratumSuggestedDifficulty). A Gamma with 1.2 TH/s manages 279 shares of difficulty 1 per second, so 0.28 shares per second at difficulty 1000, one every 3.6 seconds. Bigger miners push the difficulty up until they land at about one share every 2.4 seconds.

Shares arrive at random. The probability that a healthy miner finds none for t seconds is e−r·t at r shares per second. Over a year a Gamma produces about 8.8 million gaps between two shares. Even one minute without a share happens less than once a year on average. As a rule of thumb, 16 average gaps without a share plus two minutes of buffer for restarts are enough; that keeps every miner below one false alarm a year.

The difficulty only gets high if you set it up that way. Port 4334 of pool.bitaxe.de is a high-difficulty port for large ASICs and rented hashrate and first gives every miner a floor of 32,768. A Bitaxe with the AxeOS default still ends up at 1000 there, because ckpool accepts the miner's suggestion on every port. That is what the function suggest_diff() in ckpool says, and that is how our pool works too. The 32,768 only stays if you set the suggestion in AxeOS to 0. Then a Gamma delivers only one share every 117 seconds, and an alert after 15 minutes would wrongly report it about 125 times a year, twice a week. The same happens if you suggest a high difficulty yourself.

Chart: expected false alarms per year drop below one after about one minute without a share at difficulty 1000, but only after about 24 minutes at difficulty 32,768; at 15 minutes there are about 125 a year.
Expected false alarms per year by threshold for a Bitaxe Gamma at 1.2 TH/s. At difficulty 1000 one minute is enough, at 32,768 it takes 24 minutes to stay below one false alarm a year.
Device TH/s AxeOS default: one share every Threshold Difficulty 32,768: one share every Threshold
Older Bitaxe, e.g. Ultra 0.5 8.6 s 130 s 282 s 55 min
Bitaxe Gamma 1.2 3.6 s 57 s 117 s 24 min
Bitaxe GT 2.15 2.4 s 39 s 65 s 14 min
Nerdaxe Gaia 2.6 2.4 s 39 s 54 s 12 min
NerdQaxe++ 4.8 2.4 s 39 s 29 s 7 min
NerdOctaxe 12 2.4 s 39 s 12 s 3 min

“Threshold” means: from this duration without a share, a healthy miner causes a false alarm less than once a year on average. In practice you add a little, because restarts, firmware updates and Wi-Fi changes take one to two minutes. With the AxeOS default we recommend 10 minutes. At difficulty 32,768, 30 minutes are enough for a Gamma; older models with about 0.5 TH/s need an hour, with 30 minutes they would collect about 190 false alarms a year. Other pools assign the difficulty differently; there you read poolDifficulty and use the same formula. The watch script below does that by itself.

Tools compared

Decision tree: one or two miners lead to the pool alert via Telegram, three or more unattended ones to the watch script, an existing Home Assistant to the REST sensor, histories over months to InfluxDB and Grafana; on the road only the pool alert or a VPN with SNAT.
Which solution fits which situation.
Solution Alert Device values without access to the home network Effort
AxeOS Swarm no yes, as an overview no none
Pool alert @bitaxepool_bot Telegram no yes one minute
NerdOS alerts Discord partly the device sends them itself low
Watch script (below) ntfy yes no, runs on the home network half an hour
Home Assistant every channel HA has yes no medium
InfluxDB and Grafana Grafana alerts yes, with history no high

AxeOS Swarm: an overview, not a guard

The Swarm page in AxeOS searches the device's network for other miners, but only within the /24 range, meaning the 254 addresses around its own. You add devices in another subnet by hand by address. Swarm recognises AxeOS, NerdOS and Bitforge, refreshes every 30 seconds by default and stores the list in the browser, not on the miner. So it only runs while the page is open and never alerts. It is ideal as an overview and unsuitable as a guard. How the other AxeOS pages work is in AxeOS explained.

Pool alert via Telegram

The pool.bitaxe.de bot only needs your address: send /watch with your address to @bitaxepool_bot, done. It reports when a worker stops delivering shares, after 30 minutes by default, adjustable with /alerts offline 15, 30, 60 or off. On top come messages when the worker delivers again, on a new best share, a rank promotion and a found block, plus the weekly summary on Mondays. With the AxeOS default, 15 minutes work as well. If you have set a high difficulty in AxeOS, keep the 30 minutes, and for older models use 60. Because the bot sits on the pool side, it reports even when power and internet are gone at home. It also notices a switch to the fallback pool, because shares then stop arriving at our pool. It does not see temperature and fan. More on the pool in the Bitaxe Pool.

Nerd firmware: alerts and InfluxDB built in

NerdOS 1.1.0.1 brings two things AxeOS lacks. In the settings you can store a Discord webhook the device reports to itself: a restart by the watchdog when no share came for an hour, a found block, a new best difficulty and a failed coinbase check. That check verifies whether your address is in the coinbase and whether the pool fee exceeds a limit you set. NerdOS can also write directly to an InfluxDB (URL, port, token, organisation, bucket, prefix). Then you no longer need a polling service of your own.

Home Assistant

If you run Home Assistant anyway, poll the API with the built-in REST sensor and build alerts as automations. One request per minute is enough; the fields are the same as above.

configuration.yaml
rest:
  - resource: http://192.168.1.50/api/system/info
    scan_interval: 60
    sensor:
      - name: "Bitaxe hashrate 10 min"
        value_template: "{{ value_json.hashRate_10m | round(0) }}"
        unit_of_measurement: "GH/s"
      - name: "Bitaxe voltage regulator"
        value_template: "{{ value_json.vrTemp }}"
        unit_of_measurement: "°C"
    binary_sensor:
      - name: "Bitaxe on fallback pool"
        value_template: "{{ value_json.isUsingFallbackStratum == 1 }}"

InfluxDB and Grafana

If you want to analyse histories over months, for example temperature against room temperature or hashrate per firmware version, write a record into a time-series database every 30 to 60 seconds and build alerts in Grafana. Nerd devices write by themselves, for AxeOS you need a poller, for example Telegraf with an HTTP input and JSON parsing. For three devices that is a lot of effort. From ten devices on, or for measurement series, it pays off.

A watch script for several miners

The following script runs the six checks for any number of AxeOS and NerdOS devices. It needs Python 3 and nothing else, runs on a Raspberry Pi, a NAS or any computer on the same network and reports to your phone via ntfy. It keeps its state in a file, reports a problem once and tells you when it is fixed. As long as a miner does not answer, old problems stay open instead of counting as fixed. It calculates the threshold for missing shares from the pool difficulty, sets the temperature limits for NerdOS devices 5 °C below their shutdown temperatures and, when started by hand, shows why a device does not answer. It is tested against simulated answers for every case it reports.

bitaxe-watch.py
#!/usr/bin/env python3
"""bitaxe-watch.py: checks Bitaxe and Nerd miners through the local API and reports problems via ntfy.

Runs once per call; start it every minute with cron or a systemd timer. Python standard library only.
The computer must sit in the same private network as the miners (AxeOS only answers private addresses).
"""
import base64
import json
import pathlib
import sys
import time
import urllib.request

MINERS = ["192.168.1.50", "192.168.1.51"]   # IP addresses or names like bitaxe.local
NTFY = "https://ntfy.sh/put-a-long-random-topic-here"
STATE = pathlib.Path.home() / ".bitaxe-watch.json"
# Check the coinbase on solo pools only: on shared pools it usually pays the pool
SOLO_POOL = True

LIMITS = {
    "offline_rounds": 3,       # this many polls in a row without an answer
    "no_share_s": 600,         # lower bound; with a high pool difficulty the script waits longer
    "hashrate_share": 0.85,    # 10-minute average below this share of expected ...
    "hashrate_rounds": 15,     # ... this many polls in a row
    "chip_c": 70,              # NerdOS: 5 °C below its shutdown temperature, see below
    "regulator_c": 95,
    "wifi_dbm": -75,
    "wifi_rounds": 10,
    "voltage_share": 0.949,    # like the "Danger: Low Voltage" warning in AxeOS 2.15
}


def read_info(host):
    with urllib.request.urlopen(f"http://{host}/api/system/info", timeout=5) as response:
        return json.load(response)


def notify(title, text, priority="default"):
    # Title encoded as RFC 2047 so non-ASCII characters arrive intact (recommended in the ntfy docs)
    encoded = "=?UTF-8?B?" + base64.b64encode(title.encode("utf-8")).decode("ascii") + "?="
    request = urllib.request.Request(NTFY, data=text.encode("utf-8"),
                                     headers={"Title": encoded, "Priority": priority})
    try:
        urllib.request.urlopen(request, timeout=10).close()
    except OSError as error:
        print(f"Notification not delivered: {error}")


def expected(info):
    """Expected hashrate in GH/s: AxeOS reports it, for NerdOS it is clock x cores x chips."""
    if info.get("expectedHashrate"):
        return info["expectedHashrate"]
    if info.get("frequency") and info.get("smallCoreCount"):
        return info["frequency"] * info["smallCoreCount"] * info.get("asicCount", 1) / 1000
    return None


def check(host, info, memo, now):
    """Returns a list of (key, priority, text) for the problems that exist right now."""
    problems = []
    uptime = info.get("uptimeSeconds", 0)

    # 1. Restart: uptime lower than at the last poll
    if uptime < memo.get("uptime", 0):
        reason = info.get("resetReason") or info.get("lastResetReason") or "unknown"
        serious = any(w in reason for w in ("Brownout", "watchdog", "panic", "glitch", "lock up"))
        problems.append((f"restart-{now}", "high" if serious else "default", f"Restarted: {reason}"))
        memo["shares_ts"] = now
    memo["uptime"] = uptime

    # 2. No new accepted shares. The limit grows with the pool difficulty: a share takes difficulty x 2^32
    #    hashes on average, and 16 average gaps without a share happen less than once a year.
    accepted = info.get("sharesAccepted", 0)
    if accepted != memo.get("shares") or "shares_ts" not in memo:
        memo["shares"], memo["shares_ts"] = accepted, now
    rate = (expected(info) or info.get("hashRate_10m") or 0) * 1e9
    gap = (info.get("poolDifficulty") or 0) * 2**32 / rate if rate else 0
    limit = min(max(LIMITS["no_share_s"], 16 * gap + 120), 4 * 3600)
    silence = now - memo["shares_ts"]
    if silence > limit and not info.get("miningPaused"):
        problems.append(("shares", "high", f"No accepted share for {silence // 60} minutes"))

    # 3. Hashrate below expected (only judged 15 minutes after a start)
    target, actual = expected(info), info.get("hashRate_10m")
    if target and actual is not None and uptime > 900 and actual < LIMITS["hashrate_share"] * target:
        memo["low"] = memo.get("low", 0) + 1
    else:
        memo["low"] = 0
    if memo["low"] >= LIMITS["hashrate_rounds"]:
        problems.append(("hashrate", "default", f"Hashrate {actual:.0f} GH/s, expected {target:.0f} GH/s"))

    # 4. Temperature, fan, hardware and power faults. NerdOS shuts down per fan channel at
    #    "overheatTemp" (chip 70 °C by default, regulator 80 °C on two channels): warn 5 °C earlier.
    channels = info.get("fans") or []
    cutoff_chip = (channels[0].get("overheatTemp") if channels else None) or info.get("overheat_temp")
    cutoff_regulator = channels[1].get("overheatTemp") if len(channels) > 1 else None
    chip = max([info.get("temp") or 0] + (info.get("asicTemps") or []))
    if chip > min(LIMITS["chip_c"], (cutoff_chip or 999) - 5):
        problems.append(("chip", "high", f"Chip {chip:.0f} °C"))
    regulator = info.get("vrTemp") or 0
    if regulator > min(LIMITS["regulator_c"], (cutoff_regulator or 999) - 5):
        problems.append(("regulator", "high", f"Voltage regulator {regulator:.0f} °C"))
    if info.get("overheat_mode"):
        problems.append(("overheated", "urgent", "Overheat protection active, mining stopped"))
    if info.get("fanspeed", 0) > 0 and info.get("fanrpm", 1) == 0:
        problems.append(("fan", "urgent", "Fan is not spinning"))
    for field in ("hardware_fault", "power_fault"):
        if info.get(field):
            problems.append((field, "urgent", f"{field}: {info[field]}"))
    nominal = info.get("nominalVoltage")
    if nominal and info.get("voltage"):
        nominal_mv = nominal * 1000 if nominal < 100 else nominal
        if info["voltage"] < LIMITS["voltage_share"] * nominal_mv:
            problems.append(("voltage", "high", f"Input voltage {info['voltage'] / 1000:.2f} V"))

    # 5. Payout: fallback pool, changed address, coinbase without your own output
    stratum = info.get("stratum") if isinstance(info.get("stratum"), dict) else {}  # NerdOS
    if info.get("isUsingFallbackStratum") or stratum.get("usingFallback"):
        problems.append(("fallback", "high", "Miner is working on the fallback pool"))
    user = info.get("stratumUser")
    memo.setdefault("user", user)
    if user != memo["user"]:
        problems.append((f"user-{user}", "urgent", f"Stratum user changed to {user}"))
    total, own = info.get("coinbaseValueTotalSatoshis"), info.get("coinbaseValueUserSatoshis")
    if SOLO_POOL and total and own == 0:
        problems.append(("coinbase", "urgent", "The coinbase does not pay your address"))

    # 6. Settings away from the baseline (e.g. after the overheat protection)
    memo.setdefault("clock", info.get("frequency"))
    memo.setdefault("autofan", info.get("autofanspeed"))
    if info.get("frequency") and memo["clock"] and info["frequency"] < memo["clock"] - 50:
        problems.append(("clock", "default", f"Clock {info['frequency']:.0f} MHz instead of {memo['clock']:.0f} MHz"))
    if info.get("autofanspeed") != memo["autofan"]:
        problems.append(("autofan", "default", "Fan control changed"))

    # Wi-Fi: only report a signal that stays weak
    rssi = info.get("wifiRSSI")
    memo["weak"] = memo.get("weak", 0) + 1 if rssi is not None and rssi < LIMITS["wifi_dbm"] else 0
    if memo["weak"] >= LIMITS["wifi_rounds"]:
        problems.append(("wifi", "low", f"Weak Wi-Fi: {rssi} dBm"))
    return problems


def main():
    try:
        state = json.loads(STATE.read_text())
    except (OSError, ValueError):
        state = {}
    now = int(time.time())
    for host in MINERS:
        memo = state.setdefault(host, {})
        info = {}
        try:
            info = read_info(host)
        except (OSError, ValueError) as error:
            if sys.stdout.isatty():
                print(f"{host}: {error}")   # started by hand: show why, e.g. HTTP Error 401
            memo["missing"] = memo.get("missing", 0) + 1
            problems = [("offline", "high", "Miner does not respond")] if memo["missing"] >= LIMITS["offline_rounds"] else []
        else:
            memo["missing"] = 0
            problems = check(host, info, memo, now)
        reachable = memo["missing"] == 0
        name = info.get("hostname") or host
        pending = memo.get("open", {})
        current = {key: text for key, _, text in problems if not key.startswith("restart")}
        if not reachable:
            # Without an answer it is unclear whether old problems are fixed: keep them open
            current = {**{key: text for key, text in pending.items() if key != "offline"}, **current}
        for key, priority, text in problems:
            if key not in pending:
                notify(f"{name}: problem", text, priority)
        for key in pending.keys() - current.keys():
            notify(f"{name}: back to normal", f"Fixed: {pending[key]}", "low")
        memo["open"] = current
    STATE.write_text(json.dumps(state, indent=1))


if __name__ == "__main__":
    main()
  1. Save the script as bitaxe-watch.py and fill MINERS with the addresses of your devices. Ideally give them fixed addresses in the router or use their names ending in .local.
  2. Enter a long, random topic for NTFY and subscribe to it in the ntfy app. Topics on ntfy.sh are not protected; anyone who guesses the name can read along.
  3. On a shared pool set SOLO_POOL = False. no_share_s is only the lower bound; with a high pool difficulty the script waits longer by itself.
  4. Start it every minute with cron: * * * * * /usr/bin/python3 /home/pi/bitaxe-watch.py
  5. Test it: unplug a miner. After three minutes “Miner does not respond” arrives, after plugging it back in “Restarted: Reset due to power-on event” and the all-clear.

On its first run the script remembers user, clock and automatic fan control of every device. If you change these values on purpose, delete ~/.bitaxe-watch.json afterwards so it takes over the new values.

Typical false alarms and real problems

  • Briefly offline at night: the router restarts or changes the Wi-Fi channel, or the miner got a new address. Use a fixed address in the router or the name ending in .local.
  • A restart with “Brownout” every few hours: the voltage collapses. On 5 V devices a long, thin cable or a tired power supply is enough to cost the necessary tenths of a volt.
  • Hashrate 16 % lower, fan loud: the overheat protection kicked in and throttled. Fix the cause, then reset clock, voltage and automatic fan control.
  • Pool reports offline, dashboard looks normal: the miner is working on the fallback pool (isUsingFallbackStratum 1), or user and worker name do not match.
  • The script reports “Miner does not respond”, but the dashboard works: start the script by hand and it shows the reason. If it says “HTTP Error 401”, the request does not come from a private network, typically through a VPN without SNAT.
  • Swarm does not find a device: it is in another subnet. Add it by hand by address.
  • More rejected shares right after a new block: shares that were on their way for the old block arrive too late. Short peaks are normal. If the share stays high, the reasons in sharesRejectedReasons help.
  • Hashrate permanently just below expected at normal temperature: clock and voltage do not match; the steps are in the overclocking guide. If it stays that way on factory settings, the miner troubleshooter helps.

Monitoring without a new attack surface

Monitoring must not make the miner more vulnerable than it is. Four rules are enough:

  • No port forwarding. The AxeOS sender check is not a password. If the miner can be reached from the internet, anyone who knows how can change its settings. On the road the pool alert is enough; fetch device values through a VPN with SNAT.
  • Whoever is on the home network may do everything. That includes guests and smart home devices with questionable firmware. A separate guest network keeps them out. The coinbase check makes a changed address visible at once.
  • Third-party apps get the same rights as you. An app that monitors your miners can also change their settings. Local, open-source tools are therefore the better choice.
  • Encrypt the connection to the pool. With Stratum V2 nobody on the way can swap the jobs; the setup is in setting up Stratum V2 on a Bitaxe.

Frequently asked questions

Does the AxeOS API have a password?

No. AxeOS only answers API requests from the private address ranges 10.x, 172.16 to 172.31 and 192.168.x and from pages under names ending in .local. Anyone on the same network can read and change settings. The check does not replace a password, which is why the miner never belongs on the internet through port forwarding.

Can I monitor my Bitaxe on the road?

Yes, through the pool: the pool.bitaxe.de Telegram bot reports when shares stop arriving. You only get device values such as temperature from outside through a VPN whose router rewrites the sender address (SNAT), never through port forwarding.

How do I find out why my Bitaxe restarted?

From the field resetReason in /api/system/info. “Brownout” points to the power supply, “watchdog” and “panic” to the firmware or too high a clock, “power-on event” to a power cut.

Why does the pool still show hashrate although my miner is off?

Because ckpool leaves the hashrate fields where they were after an outage until shares come in again. What counts for outages is the time of the last share, in the pool.bitaxe.de API the field lastshare.

How long without a share is normal?

On a ckpool pool like pool.bitaxe.de a Bitaxe Gamma with the AxeOS default delivers a share about every 3.6 seconds, bigger miners about every 2.4 seconds. Three minutes without a share are already a real outage there, even for older models with 0.5 TH/s. If you set a high difficulty in AxeOS, you get shares less often: at 32,768 a Gamma has almost two minutes between two shares, and an alert then needs 25 to 30 minutes.

Why is my Bitaxe slower and louder after overheating?

After the overheat protection, AxeOS 2.15.3 restarts with the clock 100 MHz lower and the voltage 100 mV lower and leaves the fan at manual 100 %. Both stay saved until you reset them in the settings.

Can a Bitaxe send alerts by itself?

AxeOS cannot, it only shows warnings in the dashboard. The Nerd firmware NerdOS can: in version 1.1.0.1 it reports to a Discord webhook. The pool.bitaxe.de bot reports via Telegram, and a script or Home Assistant on your home network does the rest.

What does coinbaseValueUserSatoshis mean?

The part of the current job's coinbase that goes to your address. On a solo pool without fees that is the whole block reward. If it says 0, a block you find would not pay you.

Does the high-difficulty port change the alert threshold?

Not for a Bitaxe with the AxeOS default. Port 4334 of pool.bitaxe.de first gives every miner difficulty 32,768, but after logging in AxeOS suggests 1000, and ckpool accepts the suggestion. Only if you set the suggestion in AxeOS to 0 does the 32,768 stay, and then the threshold has to rise to 25 to 30 minutes.

Is there an app to monitor a Bitaxe?

You do not need an app of its own. The AxeOS dashboard runs in the browser, the Swarm page shows several miners at a glance, the pool.bitaxe.de bot reports via Telegram, and the watch script sends messages through the ntfy app. Third-party apps that may query your miners can also change their settings.

How do I add a Bitaxe to Home Assistant?

With the built-in REST sensor. It polls http://<address of the miner>/api/system/info every minute and reads fields such as hashRate_10m, vrTemp or isUsingFallbackStratum; you build alerts as automations. Home Assistant has to be on the same private network as the miner.

At what temperature should I start to worry?

Up to 65 °C at the chip and up to 85 °C at the voltage regulator are fine for continuous operation. From 70 °C the dashboard warns, above 75 °C at the chip or 105 °C at the regulator AxeOS stops mining and then runs throttled. NerdOS already shuts down at 70 °C at the chip by default.

Sources

Matching products