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):
| Sink | Protocol | Config key | Secret via env |
|---|---|---|---|
| ntfy | HTTP POST | [notifier.ntfy] | (none — public topic URL suffices) |
| Telegram | Bot API | [notifier.telegram] | bot_token_env |
| Discord | Webhook | [notifier.discord] | webhook_url_env |
| Slack | Incoming Webhook | [notifier.slack] | webhook_url_env |
| SMTP via lettre/TLS | [notifier.email] | password_env (optional) |
Events
The daemon emits these event types:
| Event | When | Priority |
|---|---|---|
block-found | After a successful submitblock in the daemon | high |
miner-disconnect | An authenticated worker closes the Stratum connection; per-worker debounce 30s | medium |
rpc-down | No Bitcoin Core node reachable at all (with [[node.backup]] a primary outage alone no longer fires this) | medium |
rpc-recovered | Health check first hits RPC=ok again after a down state | low |
health-alert | A setup/health check started failing (e.g. Bitcoin Core warning). Gated by on_health_alert, on by default | medium |
node-failover | The active Bitcoin Core node changed (failover to a backup, or return to the primary). Delivered to sinks that enabled on_rpc_down | medium |
test | Operator clicks the test button in the admin UI | low |
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:
- A miner opens their public page at
/users/<address>. - 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.
- Once verified, a Notifications field appears. They paste a personal target and save it — for this address only.
| Per-wallet event | Fires when | Target sees |
|---|---|---|
| block-found | this address finds a block | worker, height, reward, block hash (high priority) |
| personal-best | this 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-100 | this 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-offline | one 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.
| Channel | How friends subscribe | Effort |
|---|---|---|
| ntfy | Install the ntfy app, subscribe to your topic name | ~2 min, no account |
| Telegram | Your bot posts into a group everyone is in | one-time setup |
| Discord | Webhook points at a channel on a shared server | one-time setup |
Their addresses go into the to = [...] list | trivial |
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:
- You (the operator) configure
[notifier.ntfy]as shown below. - 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. - 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:
- Create a Telegram group with your friends.
- Add your bot to the group.
- Send any message in the group, then fetch
https://api.telegram.org/bot<TOKEN>/getUpdatesand look for the group’s"chat":{"id":-100…}— group ids are negative; copy the minus sign too. - Put that id into
chat_idand 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.
- Email —
tois 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:
- Restart the daemon — logs show
notifier sink readyfor each working sink. If an env var is missing or a required config field is empty, you’ll seenotifier sink skippedwith a reason; the daemon starts anyway. - In the admin UI under
/admin/notifications→ “Server-Side Sinks (Daemon)” lists all active sinks. - Per sink, a Test button → POST
/api/admin/notifier/test?sink=<name>fires atestevent at just that one. The badge switches to ok/err. - Test all sinks button → POST
/api/admin/notifier/testwith 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
- Open the pool UI, log in
- Go to
/admin/notifications→ “Enable background push” button - Browser asks for permission → “Allow”
- Subscription is registered with the pool, from now on pushes arrive
Which events fire push
| Event | Tag | When |
|---|---|---|
block-found | block-found | Every block found (requireInteraction=true → notification stays until the user acknowledges it) |
health | health | RPC down OR health snapshot with warnings |
update | update | Pool 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 unsupportedbadge.
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": { … } }—tsis an RFC3339 string (e.g."2026-06-19T12:00:00+00:00"), not a unix number. - Headers:
X-WarpPool-Event: <type>and, whensecret_envis 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-frequencynew_job/shares_acceptedexplicitly viaeventsonly 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
- Setup Health Checks — RPC-down/recovered are fired by the same health loop
- Observability — notifier counter
warppool_notifier_events_sent_total - Configuration Reference — all config.toml fields