Skip to main content
glnc

Field notes

Monitor an Ethereum wallet and ping Slack when the balance drops, from the terminal

by Arya Rahimi · · 9 min read

Browser explorers are good at showing a page. Shell tools are good at answering a narrower question: did this wallet just drop below $50k? This note wires glnc into a Slack incoming webhook so an on-call engineer learns about a balance change from the same channel that already pages them, without opening Etherscan. The progression is small on purpose. One line, then a script, then a script that does not lie to you when the data is degraded.

When you actually need a balance alert

Three situations show up over and over. A treasury wallet you want to know about the moment it moves. A hot wallet that funds an automated process and quietly drains until it stops working. A multisig signer's gas wallet that needs topping up before the next execution window. None of those want a dashboard. They want a message in the channel the team already watches.

That is the shape of the workflow this post builds. If the bigger question is why a CLI instead of an explorer, the longer answer is in why I built a CLI alternative to Etherscan.

The one-liner

Before the script, the smallest thing that works. This reads a wallet's USD total and posts to Slack if it is under the threshold:

glnc balance vitalik.eth --chain eth --json | jq -e '.data.wallets[0].grandTotalUsd < 50000' >/dev/null && curl -sS -X POST -H 'Content-type: application/json' --data '{"text":"wallet under $50k"}' "$SLACK_WEBHOOK_URL"
# success: no stdout. Slack receives the message.
# threshold not crossed: jq -e exits non-zero, the && chain stops.

That is the whole idea. The rest of this post is making it safe to leave running.

Install glnc

On macOS or Linux with Homebrew, install the prebuilt binary:

brew install aryarahimi1/glnc/glnc
==> Installing glnc from aryarahimi1/glnc
🍺 glnc was successfully installed

Or use the install script after reading it first:

curl -fsSL https://glnc.dev/install.sh -o install.sh && less install.sh && bash install.sh
installed glnc to ~/.local/bin/glnc

There is no Node or Bun requirement when you install the binary. If you want the full reference, the quickstart covers verification and the JSON envelope shape lives in the JSON docs.

Get just the number you need with jq

The human-readable table is the default. For anything programmatic, add --json:

glnc balance vitalik.eth --chain eth --json | jq '.data.wallets[0].grandTotalUsd'
663208.68

The numbers in this post are example output, not a promise about a live wallet. A few other fields you will reach for in a monitoring script:

glnc balance vitalik.eth --chain eth --json | jq '{usd: .data.wallets[0].grandTotalUsd, block: .data.wallets[0].chains[0].blockNumber, partial: .meta.partial}'
{
"usd": 663208.68,
"block": 19874211,
"partial": false
}

grandTotalUsd is the headline number. chains[0].blockNumber is what the RPC actually saw, useful when you suspect drift. meta.partial is the field that decides whether you should trust the rest. More on that two sections down.

Add a threshold and a Slack webhook

First real script. Export SLACK_WEBHOOK_URL and WALLET in the environment, then save this as ~/.local/bin/glnc-alert.sh:

cat ~/.local/bin/glnc-alert.sh
#!/usr/bin/env bash
set -euo pipefail
: "${SLACK_WEBHOOK_URL:?}"
: "${WALLET:?}"
THRESHOLD="${THRESHOLD:-50000}"
payload="$(glnc balance "$WALLET" --chain eth --json --rpc-quorum=majority)"
usd="$(jq -r '.data.wallets[0].grandTotalUsd' <<<"$payload")"
if awk -v a="$usd" -v b="$THRESHOLD" 'BEGIN{exit !(a<b)}'; then
curl -sS -X POST -H 'Content-type: application/json' \
--data "$(jq -nc --arg w "$WALLET" --arg u "$usd" --arg t "$THRESHOLD" \
'{text: "wallet \($w) at $\($u), below threshold $\($t)"}')" \
"$SLACK_WEBHOOK_URL" >/dev/null
fi

Two small things worth pointing at. awk handles the comparison because bash cannot do floating point. jq -nc builds the Slack body so a stray quote in the wallet label cannot corrupt the webhook payload.

Don't alert on degraded data

meta.partial flips to true when glnc could not fully resolve the picture: a price feed missed, a token contract did not respond, an RPC fell over and the sequential fallback only got part of the way. The USD total is still emitted, but it is incomplete. Paging on a low number that is low because half the tokens failed to price is worse than not paging at all.

Add a short-circuit near the top of the script:

cat ~/.local/bin/glnc-alert.sh # near the top
partial="$(jq -r '.meta.partial' <<<"$payload")"
if [[ "$partial" == "true" ]]; then
echo "skipping: partial data" >&2
exit 0
fi

You can choose to send a low-priority Slack note when data is partial ("price feed flaky, no balance alert sent"). For most teams, silence is correct; the next poll tells you the real story.

Stop spamming Slack: state file and hysteresis

The previous script fires every time it runs while the balance is below threshold. Schedule it every minute and Slack mutes the channel by lunchtime. Two fixes: persist the last balance, and only alert on the transition from above to below.

cat ~/.local/bin/glnc-alert.sh # full version
#!/usr/bin/env bash
set -euo pipefail
: "${SLACK_WEBHOOK_URL:?}"
: "${WALLET:?}"
THRESHOLD="${THRESHOLD:-50000}"
DELTA_PCT="${DELTA_PCT:-10}"
STATE="${STATE:-$HOME/.cache/glnc-watch/${WALLET}.state}"
mkdir -p "$(dirname "$STATE")"
payload="$(glnc balance "$WALLET" --chain eth --json --rpc-quorum=majority)"
[[ "$(jq -r '.meta.partial' <<<"$payload")" == "true" ]] && { echo "skipping: partial" >&2; exit 0; }
cur="$(jq -r '.data.wallets[0].grandTotalUsd' <<<"$payload")"
prev="$(cat "$STATE" 2>/dev/null || echo "$cur")"
read -r crossed dropped <<<"$(awk -v c="$cur" -v p="$prev" -v t="$THRESHOLD" -v d="$DELTA_PCT" '
BEGIN {
crossed = (p >= t && c < t) ? 1 : 0
dropped = (p > 0 && (p - c) / p * 100 >= d) ? 1 : 0
print crossed, dropped
}')"
if [[ "$crossed" == "1" || "$dropped" == "1" ]]; then
reason="threshold"
[[ "$dropped" == "1" ]] && reason="delta ${DELTA_PCT}%"
curl -sS -X POST -H 'Content-type: application/json' \
--data "$(jq -nc --arg w "$WALLET" --arg c "$cur" --arg p "$prev" --arg r "$reason" \
'{text: "wallet \($w): $\($p) -> $\($c) (\($r))"}')" \
"$SLACK_WEBHOOK_URL" >/dev/null
fi
printf '%s\n' "$cur" > "$STATE"

Two alert paths now. A strict crossing of THRESHOLD (silences once you are below). A percentage drop since the last poll (catches a $200k wallet bleeding to $150k well before it touches $50k). Both write to the same state file so they cannot fight each other.

Running it: cron versus --watch

Cron is the boring choice and the right one for most teams:

crontab -l | tail -1
*/5 * * * * /usr/local/bin/glnc-alert.sh >> /var/log/glnc-alert.log 2>&1

It survives reboots, it survives the script crashing, it composes with whatever you already use for log rotation. The cost is one full handshake per invocation: RPC setup, price fetch, token discovery.

--watch keeps the process alive and emits NDJSON, one envelope per poll, on the configured interval. Cheaper per tick, useful when you want sub-minute granularity:

glnc balance $WALLET --chain eth --json --watch --interval 60 --rpc-quorum=majority | while IFS= read -r line; do printf '%s\n' "$line" | THRESHOLD=$THRESHOLD SLACK_WEBHOOK_URL=$SLACK_WEBHOOK_URL WALLET=$WALLET STATE=$STATE /usr/local/bin/glnc-alert-stdin.sh; done
# glnc-alert-stdin.sh reads payload from stdin instead of calling glnc itself.
# Run the loop under systemd, tmux, or any supervisor you trust to restart it.

If you do not have that supervisor infrastructure, use cron.

Caveats: what the number actually is

grandTotalUsd is the sum of priced balances glnc could see at the block your RPC returned. Things that move it:

  • Token discovery. glnc enumerates tokens from chain activity and known lists. A freshly airdropped or obscure token may not appear in the next poll's snapshot. The USD total reflects what was discovered, not what exists on chain.
  • Dust filter. Very small balances are suppressed in the default table view to keep it readable; the JSON envelope still contains them, and grandTotalUsd still sums them. If you see a table versus --json mismatch, this is usually why.
  • Price freshness. Prices are cached. A sudden market move is visible to the next poll, not the current one.
  • RPC drift. Two providers may answer at different block heights. Sequential fallback (the default) takes the first one that answers. For alerting, prefer --rpc-quorum=majority: it requires agreement before reporting, trading a little latency for not paging you on a single flaky endpoint. Use --rpc-quorum=all if you would rather skip a poll than risk drift.

The script above already passes --rpc-quorum=majority. That is the version you should run.

Extending the alert

Nothing in the script is Slack-specific past the final curl. Swap the URL and payload shape for a Discord webhook, a PagerDuty Events v2 enqueue, a Telegram bot send, or an internal alert router. The jq -nc body construction stays the same wherever you point it.

If you are coming from cast and want the side-by-side, glnc vs cast covers where each tool fits. For the broader explorer comparison, see glnc vs Etherscan.

What this does not do

A balance watcher is not a mempool watcher. It cannot tell you about a pending transaction before it lands, and it will not catch a drain that happens and recovers between polls. It also does not page on per-token deltas yet: the alert is on the USD aggregate. If you need per-token thresholds, fan out the same script with a jq filter that selects a specific asset and compares its balance instead.

FAQ

How do I send a Slack alert when an Ethereum wallet balance drops? Pipe glnc balance --json through jq, compare .data.wallets[0].grandTotalUsd to a threshold, and POST to a Slack incoming webhook. The script in this post adds a state file and a partial-data guard so the alert is safe to leave running.

Can I monitor an Ethereum wallet without running my own node? Yes. glnc uses public RPC endpoints. For alerting, pass --rpc-quorum=majority so the reported balance reflects agreement between providers.

How do I check an ENS balance from a shell script? Pass the ENS name directly: glnc balance vitalik.eth --chain eth --json. ENS resolution is built in.

What is the best way to cron an Ethereum balance check? A short shell script every five minutes, with stdout and stderr appended to a log file, and the previous balance persisted to disk so the alert only fires on transition. Cron survives reboots and composes with log rotation; a long-lived --watch loop needs a supervisor.

How do I avoid false alerts when an RPC provider lags? Check meta.partial and skip when true. For stricter monitoring, use --rpc-quorum=majority or --rpc-quorum=all.

Can glnc watch multiple wallets in one process? Not in a single --watch process. The common pattern is one cron entry per wallet, each with its own state file.

Next, read the full balance command reference or the JSON envelope reference.