Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Notifications

dvb-WarpPool can notify the operator over several independent channels when relevant pool events occur — most importantly block found. Configuration lives in config.toml under [notifier]; secrets (API tokens, webhook URLs, SMTP passwords) are always referenced via env var, never inline.

Currently active (Phase 15):

SinkProtocolConfig keySecret via env
ntfyHTTP POST[notifier.ntfy](none — public topic URL suffices)
TelegramBot API[notifier.telegram]bot_token_env
DiscordWebhook[notifier.discord]webhook_url_env
SlackIncoming Webhook[notifier.slack]webhook_url_env
EmailSMTP via lettre/TLS[notifier.email]password_env (optional)

Events

The daemon emits these event types:

EventWhenPriority
block-foundAfter a successful submitblock in the daemonhigh
miner-disconnectAn authenticated worker closes the Stratum connection; per-worker debounce 30smedium
rpc-downNo Bitcoin Core node reachable at all (with [[node.backup]] a primary outage alone no longer fires this)medium
rpc-recoveredHealth check first hits RPC=ok again after a down statelow
health-alertA setup/health check started failing (e.g. Bitcoin Core warning). Gated by on_health_alert, on by defaultmedium
node-failoverThe active Bitcoin Core node changed (failover to a backup, or return to the primary). Delivered to sinks that enabled on_rpc_downmedium
testOperator clicks the test button in the admin UIlow

Each sink can toggle which events it cares about (all bool). ntfy, Slack and Email support on_block_found, on_miner_disconnect, on_rpc_down and on_health_alert; Telegram and Discord support on_block_found and on_health_alert. Defaults everywhere: on_block_found = true and on_health_alert = true, the rest off. Since v1.0.16 all sinks can also be configured from the admin UI (tokens land write-only in secrets.toml).

Retry: 3 attempts per sink — the first immediately, then after 10 s and after 30 s. If everything fails, it gets logged but the daemon keeps running — a notifier failure must never disrupt mining.

Per-wallet miner notifications

Everything above is the operator’s notifier: pool-wide sinks configured in config.toml, firing on any block or any health event. A public 0%-fee per-worker pool, where every miner mines to their own address, needs the opposite: each miner wants alerts about their own address only — without the operator wiring anything up, and without an account.

That is what the owner panel on a wallet page provides. It is a separate, self-service path with no config.toml involvement at all:

  1. A miner opens their public page at /users/<address>.
  2. They click verify ownership, sign the one-time challenge in their wallet’s Sign Message box (BIP-137 — no key revealed, no funds moved, no account), and submit the signature.
  3. Once verified, a Notifications field appears. They paste a personal target and save it — for this address only.
Per-wallet eventFires whenTarget sees
block-foundthis address finds a blockworker, height, reward, block hash (high priority)
personal-bestthis address sets a new best-ever share difficulty across all its rigs (block shares are excluded — the block is the headline)the new best-share difficulty and rig (per-address 30-minute cooldown)
top-100this address first enters the pool’s all-time top-100 best shares — a genuine entry only: the leaderboard was already full and the address held no prior spot (climbing higher does not re-fire)the qualifying share difficulty and rig (per-address 60-minute cooldown)
rig-offlineone of this address’s rigs loses its Stratum connection (per-worker debounced, same as the operator miner-disconnect)which rig went offline

The target is a single http(s) URL — either an ntfy topic URL or a generic webhook. The daemon POSTs an ntfy-compatible request (Title / Priority / Tags headers + a text body), so an ntfy topic renders as a proper push notification and a plain webhook receives the text body.

Because the target is miner-supplied (untrusted), every per-wallet POST goes through the same SSRF guard as outbound webhooks — internal / link-local hosts (127.0.0.1, 169.254.169.254, 10.x, …) are refused and redirects are not followed, so a miner can’t turn the daemon into a proxy onto the operator’s LAN. Delivery failures are logged and dropped; a miner’s dead endpoint never affects mining or the operator’s own sinks.

This is not the payout engine. The owner panel is a free, non-custodial notification + privacy feature in the OSS core — signing a message proves an address is yours, nothing more. It is unrelated to any reward-sharing or custody product.

Taproot note: ownership signing works for single-key P2PKH / P2WPKH / P2SH-P2WPKH addresses. A Taproot (P2TR) miner can mine and gets a public page, but cannot open the owner panel — the legacy signed-message format has no Taproot variant.

Sharing notifications with friends

A solo+friends pool is more fun when everyone gets the 🎉 the moment a block is found — not just the operator. Every server-side sink above is a broadcast channel, so nothing new needs to be built: you just point the sink at something your whole group can see.

ChannelHow friends subscribeEffort
ntfyInstall the ntfy app, subscribe to your topic name~2 min, no account
TelegramYour bot posts into a group everyone is inone-time setup
DiscordWebhook points at a channel on a shared serverone-time setup
EmailTheir addresses go into the to = [...] listtrivial

ntfy: the zero-friction option

ntfy is the easiest way to let a whole group subscribe — no accounts, no bots, no invites. The topic name is the subscription:

  1. You (the operator) configure [notifier.ntfy] as shown below.
  2. Each friend installs the ntfy app (Android / iOS / web — see ntfy.sh) and subscribes to the exact same topic name, e.g. my-secret-pool-topic-abc123.
  3. Done — every block-found (and whatever else you enabled) lands on every subscribed phone simultaneously.

Two things to keep in mind:

  • The topic name is the only secret. On public ntfy.sh, anyone who knows it can read your pool events and post their own messages to the topic. Pick a long random string, share it privately, rotate it if it leaks. Pool events carry no sensitive data (worker names, block height, health states) — the realistic worst case is a prankster posting fake “block found” messages.
  • If your group outgrows the shared-secret model, self-hosted ntfy (or a paid ntfy.sh tier) adds real access control: reserved topics with read-only access for subscribers.

Telegram: group chat instead of DM

The chat_id doesn’t have to be your private chat — a group works exactly the same way:

  1. Create a Telegram group with your friends.
  2. Add your bot to the group.
  3. Send any message in the group, then fetch https://api.telegram.org/bot<TOKEN>/getUpdates and look for the group’s "chat":{"id":-100…} — group ids are negative; copy the minus sign too.
  4. Put that id into chat_id and restart the daemon.

Everyone in the group now sees every notification, and the group doubles as the place to celebrate.

Discord and email

  • Discord — webhooks post into one channel; pick a channel on a server your friends are on (instead of a private one) and you’re done.
  • Emailto is a list, every address gets its own copy: to = ["you@example.com", "friend-a@example.com", "friend-b@example.com"].

What about Web Push?

The PWA background push subscribe button lives on the admin notifications page, which requires the operator login — so treat Web Push as the operator’s personal channel and use the sinks above for the group.

ntfy.sh

Simple and account-free. A single topic URL is enough.

[notifier.ntfy]
topic_url = "https://ntfy.sh/my-secret-pool-topic-abc123"
on_block_found = true
on_miner_disconnect = false
on_rpc_down = true

On your phone, install the ntfy app, subscribe to the same topic name, done. Topic names are effectively a shared secret — choose a hard-to-guess string.

For self-hosted ntfy, just use topic_url = "https://ntfy.your-domain.com/topic".

Telegram

Create a bot via @BotFather → note the token. You get a chat_id by sending the bot a message and then fetching https://api.telegram.org/bot<TOKEN>/getUpdates (look for "chat":{"id":…}).

[notifier.telegram]
bot_token_env = "TELEGRAM_BOT_TOKEN"
chat_id = "123456789"
on_block_found = true

Then in the daemon environment:

TELEGRAM_BOT_TOKEN="123:abc..." dvb-warppool-daemon

With systemd: Environment="TELEGRAM_BOT_TOKEN=…" in the unit file, or better an EnvironmentFile=/etc/default/dvb-warppool with chmod 600.

Discord

In the server settings under Integrations → Webhooks, create a new webhook and copy the URL. The URL is effectively the auth token — anyone who has it can post.

[notifier.discord]
webhook_url_env = "DISCORD_WEBHOOK_URL"
on_block_found = true
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/…" dvb-warppool-daemon

Slack

Create a Slack app in your workspace → enable Incoming Webhooks → generate a webhook for a channel. The payload uses Block Kit (header + section with mrkdwn) for clean layout.

[notifier.slack]
webhook_url_env = "SLACK_WEBHOOK_URL"
on_block_found = true
on_miner_disconnect = false
on_rpc_down = true

Email (SMTP)

lettre-based, with rustls — no openssl-sys. Two URL schemes:

  • smtps://user@host:465 — implicit TLS (common with providers like Posteo, Migadu, Fastmail, Hetzner)
  • smtp://user@host:587 — STARTTLS (gmail, many mail servers)
[notifier.email]
smtp_url = "smtps://pool@mail.example.com:465"
from = "pool@example.com"
to = ["operator@example.com", "monitoring@example.com"]
password_env = "POOL_SMTP_PASSWORD"
on_block_found = true
on_miner_disconnect = false
on_rpc_down = true
POOL_SMTP_PASSWORD="…" dvb-warppool-daemon

password_env is optional — if omitted, no SMTP AUTH is attempted (useful for a local MTA like Postfix on localhost:25 that relays internally).

With multiple recipients, each one gets their own letter — also a simple way to notify the whole team at once.

Test workflow

After configuration:

  1. Restart the daemon — logs show notifier sink ready for each working sink. If an env var is missing or a required config field is empty, you’ll see notifier sink skipped with a reason; the daemon starts anyway.
  2. In the admin UI under /admin/notifications → “Server-Side Sinks (Daemon)” lists all active sinks.
  3. Per sink, a Test button → POST /api/admin/notifier/test?sink=<name> fires a test event at just that one. The badge switches to ok/err.
  4. Test all sinks button → POST /api/admin/notifier/test with no param.

The same via CLI:

curl -X POST \
  -H "Authorization: Bearer wpat_…" \
  http://pool.local:18334/api/admin/notifier/test?sink=ntfy

Web Push (PWA, VAPID — Phase 21)

In addition to the 5 server-side sinks above, there’s background push straight to your phone via VAPID Web Push. Works even when the PWA is closed (iOS 16.4+, Android, desktop). No 3rd-party service required — push goes directly from the pool daemon to the browser push service (FCM/Mozilla/Apple).

Operator setup

# 1. Generate VAPID keys
dvb-warppool-cli gen-vapid-keys >> /etc/dvb-warppool/secrets.toml

# 2. Optional: set contact mailto (otherwise defaults to mailto:operator@localhost)
echo 'vapid_contact = "mailto:operator@example.org"' >> /etc/dvb-warppool/secrets.toml

# 3. Restart the daemon
systemctl restart dvb-warppool

Daemon log shows web-push sender ready. If not: VAPID keys weren’t found in secrets.toml — push stays disabled, other notifiers run anyway.

User subscribe flow

  1. Open the pool UI, log in
  2. Go to /admin/notifications“Enable background push” button
  3. Browser asks for permission → “Allow”
  4. Subscription is registered with the pool, from now on pushes arrive

Which events fire push

EventTagWhen
block-foundblock-foundEvery block found (requireInteraction=true → notification stays until the user acknowledges it)
healthhealthRPC down OR health snapshot with warnings
updateupdatePool update available (Phase 8e SSE event)

Other events (SharesAccepted tick, NewJob, etc.) do NOT fire push — too spammy for background notifications.

iOS quirks

  • Required: add the PWA to the home screen (Safari share menu → “Add to Home Screen”). In a regular Safari tab, iOS ignores Web Push.
  • iOS 16.4+ is the minimum version (March 2023). On older iOS versions the UI shows a Browser unsupported badge.

Security note

The VAPID private key is sensitive — anyone holding it can impersonate your pool to the push services. secrets.toml chmod 600. On suspected leak: generate new keys + invalidate all subscriptions (a restart is enough — stale subscriptions get deleted on the first 401 response).

Outbound webhooks

For any target not built in as a dedicated sink (PagerDuty, Opsgenie, a custom endpoint, a home-automation hook), the pool ships a generic outbound webhook with HMAC signing — no vendor lock-in. Configure one or more in config.toml:

[[webhooks]]
url = "https://example.com/hook"
secret_env = "MY_HOOK_SECRET"   # optional — HMAC-SHA256 over the request body
events = []                     # empty = default low-frequency set
# enabled = true                # set false to disable without deleting the block
  • Payload: JSON { "event": "<type>", "ts": "<rfc3339>", "data": { … } }ts is an RFC3339 string (e.g. "2026-06-19T12:00:00+00:00"), not a unix number.
  • Headers: X-WarpPool-Event: <type> and, when secret_env is set, X-WarpPool-Signature: sha256=<hmac-hex> (GitHub-style, verify against the raw body).
  • Events: the default set is low-frequency — block_found, health_snapshot, update_available, profile_switched. Opt into the high-frequency new_job / shares_accepted explicitly via events only if you want the POST storm.
  • Delivery: 10 s timeout per request, 3 attempts (immediate, then after 1 s and 3 s). After that the event is logged and dropped — there is no unbounded buffer, so a down consumer cannot back the pool up. Loopback/LAN targets are allowed by design (operator-controlled, no SSRF block).

See also Extending → public API & webhooks.

Other Phase B options

  • Matrix — not implemented; PR welcome, analogous to the Slack sink pattern.

See also