Skip to content

Repository files navigation

monprobe

monprobe

A lightweight, private, on-node Telegram "pocket glance" for Monad validator nodes. Send /status from your phone and get a one-screen answer about your validator — no dashboards, no third-party service seeing your node.

This is an on-demand glance, not a remote alerting service: check your node from your phone, and if something looks wrong, go to your laptop and dig in. It does include a minimal watchdog, but it runs on the node — if the server dies, the bot dies with it.

  • On-node, private: reads only your own node. No data leaves the machine except the Telegram messages to you.
  • Lightweight: one Go binary, one systemd unit, zero listening ports.
  • Glance-first: a compact /status card, with drill-down buttons only when you want more.

Installation

Prerequisites: a binary-mode Monad node (systemd services, no Docker) with monad-status at /usr/local/bin/monad-status and the monad-ledger-tail service running. Go 1.25+ if building from source.

One-liner (CI-built release)

Downloads the prebuilt static binary from the latest GitHub release (sha256 verified), checks the node preconditions, asks for the Telegram bot token / chat id, validator name and secp public key, then installs and enables the service. No repo clone or Go toolchain needed.

curl -fsSL https://raw.githubusercontent.com/EmberStake/monprobe/main/scripts/install-remote.sh -o monprobe-install.sh
sudo bash monprobe-install.sh

Interactive prompts only work when the script is run as a local file (as above). Unattended runs (e.g. curl ... | bash or Ansible) are supported via environment variables:

sudo env MONPROBE_TG_TOKEN="123:ABC" MONPROBE_TG_CHAT_ID=123456789 \
  MONPROBE_VALIDATOR_NAME="my-validator" \
  MONPROBE_SECP_KEY="03..." \
  bash monprobe-install.sh

Additional env overrides: MONPROBE_REPO (source repo, default EmberStake/monprobe), MONPROBE_VERSION (release tag, default latest), MONPROBE_NO_PROMPT=1 (never prompt).

Manual build & install

git clone https://github.com/EmberStake/monprobe.git
cd monprobe
CGO_ENABLED=0 go build -o monprobe ./cmd/monprobe
sudo ./scripts/install.sh
sudo nano /etc/monprobe/config.yaml   # token, allowed chat/user ids, secp key
sudo systemctl enable --now monprobe
journalctl -u monprobe -f

The installer creates the monprobe user, grants journal read access, validates and installs the sudoers rule, installs the binary, unit and config, and fails closed if any security precondition is violated. Once the service is running, send /status in Telegram — if the card shows the Identity section (hostname, network, public keys), the sudo monad-status path works inside the systemd sandbox.

Uninstall

sudo systemctl disable --now monprobe
sudo rm -f /etc/systemd/system/monprobe.service
sudo systemctl daemon-reload
sudo rm -f /etc/sudoers.d/monprobe /usr/local/bin/monprobe
sudo rm -rf /etc/monprobe
sudo userdel monprobe

Configuration

Copy config.example.yaml and fill in:

  • telegram.token — from @BotFather
  • telegram.allowed_chat_ids — chat IDs allowed to talk to the bot. For a private chat (the normal case) this is the only thing you need — your user ID equals your chat ID. For group chats, leave this for the group and use allowed_user_ids instead.
  • telegram.allowed_user_ids — optional user allowlist (needed only for group chats). Note that this allowlist is not scoped to a chat: a listed user is answered wherever they talk to the bot, including a group someone else added it to. allowed_chat_ids alone never authorises a group, because in a group the sender's user ID never equals the chat ID.
  • validator.secp_public_key — your validator's secp public key (used to attribute own blocks in the ledger stream and to discover your validator ID)
  • validator.id — optional on-chain validator ID (skips ID discovery)
  • metrics.url — your node's metrics endpoint (default 127.0.0.1:9143)
  • rpc.url — your node's own JSON-RPC endpoint (default 127.0.0.1:8080); used exclusively for staking precompile reads
  • staking.* — enable/disable the 🏦 card and set the refresh interval
  • watchdog.* — thresholds and intervals

Commands

Command Shows
/status the glance card: sync status, height and lag vs external, epoch/round, peers, 24h own blocks, throughput, raptorcast p99, txpool, services, identity
/start same as /status
/help short command reference
button 🔄 Refresh re-scrape and redraw the card in place
button 📡 Live recent own block events from the ledger tail
button 🏦 Staking stake, active-set standing, reliability (24h/7d/30d), payouts, registration

Staking card

The 🏦 Staking drill-down answers "is my stake working?" — ordered so the answer comes first and the paperwork last. Every number is read from your own node's RPC:

  • Header — validator name (from config), on-chain ID, and active-set membership (getConsensusValidatorSet) at the current epoch.
  • 💎 Stake — total stake (getValidator execution view), staker count (paginated getDelegators), self-stake (getDelegator of the auth address), consensus stake, and commission.
  • 📈 Reliability — uptime over 24h/7d/30d computed from your own leader slots in the ledger stream (a slot either proposed one of your blocks or timed out), plus the last own commit and last timeout. monprobe does not replay journald on start, so these windows only cover this process's lifetime; when a window is longer than what has actually been observed the card says so instead of implying 30 days of evidence. Own-block events are retained for 31 days and older ones are dropped, so memory reaches a plateau rather than growing.
  • 💵 Payouts — unclaimed commission rewards for the auth address and wallet balance (eth_getBalance).
  • 🪪 Registration — stake flags (Ok / StakeTooLow / Withdrawn / DoubleSign) and the auth address.

If validator.id is not configured, monprobe discovers the ID once by scanning the staking precompile for your secp public key (bounded scan, cached in memory). Set validator.id explicitly to skip the scan.

Watchdog

Sends a message only when something is actually wrong — once per condition, with a recovery message when it clears, plus a cooldown to prevent noise. A condition that flaps inside the cooldown stays silent in both directions: the suppressed alert produces no 🔴, and its clearing produces no orphan 🟢 either. Conditions covered:

  • block height frozen longer than the stall threshold
  • metrics endpoint unreachable
  • core service down (monad-bft, monad-execution, monad-rpc)
  • burst of own timeout events (your validator missing its leader slots; threshold/window configurable, recovered automatically when the burst ends)

Alerts are prefixed with the validator name from the config, so one Telegram chat can receive from several nodes without confusion.

Data sources (all self-hosted)

Source Provides
Node metrics 127.0.0.1:9143 height, commits, peers, proposals, statesync, consensus events, raptorcast p99 latency, txpool
journalctl -u monad-ledger-tail your own proposed / finalized / timeout blocks (filtered by your secp key) → 24h own-block summary, 24h/7d/30d uptime, last commit/timeout, missed-block watchdog
monad-status (via the fixed sudo rule) service states, epoch/round, statesync, block lag vs external RPC
Local RPC 127.0.0.1:8080 (staking precompile 0x1000) validator ID, auth address, stake flags, self-stake, total stake, staker count, consensus stake, commission, unclaimed rewards, wallet balance, active-set membership — read from the staking precompile through your own node's RPC, never an external endpoint

Security model

monprobe is designed to open zero holes in your validator:

  • No inbound ports. The bot uses Telegram long polling (outbound-only). A port scan of your validator looks exactly the same before and after installing monprobe. The binary is built with CGO_ENABLED=0 (static).
  • No shell, no injection surface. Telegram messages are matched against a fixed dispatch table. User input never becomes part of a command, path or URL. All external processes run via fixed, hard-coded argument vectors. The bot accepts no arguments and is strictly read-only — it can report, never act.
  • Least privilege. Runs as a dedicated monprobe user with no membership in the monad group — it cannot read your validator keys even if compromised.
  • One narrow sudo rule. Exactly one command is permitted: monprobe ALL=(monad) NOPASSWD: /usr/local/bin/monad-status "" (the trailing "" permits the command only with no arguments) The installer verifies that monad-status is a root-owned ELF binary that neither group/other nor the monad user can modify (including the containing directory), and refuses to install otherwise (a monad-writable monad-status would be an escalation path). monad-status prints public identifiers only.
  • Allowlist. Only chat IDs (and optionally user IDs) listed in the config get any response. Unknown senders get silence. Per-chat rate limiting included.
  • Sandboxed systemd unit: read-only filesystem view, private /tmp, no device access, no namespace/kernel access, minimal capability set. NoNewPrivileges is deliberately absent because the single fixed sudo command requires setuid — this is documented in the unit file so it is not "fixed" into silently disabling the monad-status data source.
  • Config file holds the bot token at 0600, owned by the monprobe user. The bot token is additionally redacted from all error/log output, so DNS blips can't leak it into the journal.

Development

go test ./...          # fixtures live in ./testdata, all synthetic
gofmt -l . && go vet ./...
CGO_ENABLED=0 go build -o monprobe ./cmd/monprobe

Every push and pull request runs gofmt, vet, the test suite and a static build (.github/workflows/test.yml). The parser tests read from testdata/ and fail — rather than skip — when a fixture is missing, so a checkout that cannot see them is a loud error and not a quietly shrunken suite. Those fixtures are trimmed captures from a live testnet node with every identifier replaced: placeholder secp keys, RFC 5737 peer addresses, generic hostnames.

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages