Wallet Balance Change Alerts in n8n: Telegram Notifications With Zero Code

Get a Telegram alert the moment a wallet's USDT or USDC balance changes, using n8n's evmquery trigger node. No Schedule Trigger, no IF node, no state store.

evmquery team··8 min read
Share
Wallet balance change alerts in n8n — Telegram notifications with the evmquery trigger node

The usual way to build “notify me when X changes” in n8n is a Schedule Trigger, a node to store the last-seen value, an IF node to diff the current value against it, and a habit of remembering to update the stored value on every run. Miss that last step once and your alerts either never fire or fire on every poll.

The evmquery community node ships a dedicated trigger — evmQueryTrigger — that does the diffing for you. Point it at a wallet and a token, and it only wakes up your workflow when the value actually changed.

TL;DR

The evmquery trigger node polls a contract read on a schedule and only fires when the value differs from the last poll. Wire it straight to Telegram (or Slack, or Discord) and skip the Schedule Trigger, the state store, and the IF node entirely.

Why watch a wallet balance

A few illustrative cases, not case studies — the pattern generalizes to whatever balance you care about:

  • Treasury or ops alerts. Know the moment a payout wallet receives or loses funds, without polling a block explorer.
  • Personal wallet peace of mind. Get pinged the instant a wallet you hold keys for moves, which is a cheap tripwire against a compromised key.
  • DAO multisig changes. Catch a multisig’s stablecoin balance shifting before a proposal executes, so the team isn’t surprised by a transaction they didn’t expect.

None of these need custom infrastructure. They need a poll, a diff, and a message — which is exactly what the trigger node is built for.

Install the community node

Same node as our Execute Query walkthrough, just used in trigger mode instead of action mode.

n8n Cloud / Desktop: Settings → Community Nodes → Install → paste n8n-nodes-evmquery → Install. Both the action node and the trigger node appear in the node picker under “evmquery.”

Self-hosted: npm install n8n-nodes-evmquery inside your .n8n/custom directory, or add it to your package.json and rebuild the container. Restart n8n.

Credentials are shared between the action and trigger nodes: Credentials → New → evmquery API → paste your key. Grab one from the dashboard — the free tier’s 700 units/hour cap comfortably covers hourly polling of a handful of wallets.

Build the trigger workflow

The whole workflow is two nodes: the evmquery trigger and a Telegram node. We’ll use vitalik.eth’s public address (0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045) as the worked example, watching its USDT and USDC balance on Ethereum.

Drop an evmquery Trigger node into a new workflow and configure it:

Node type: evmQueryTrigger
Chain: Ethereum
Contracts:
usdt = 0xdAC17F958D2ee523a2206206994597C13D831ec7
usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Context:
wallet : sol_address = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
Expression:
{
"usdt": formatUnits(usdt.balanceOf(wallet), usdt.decimals()),
"usdc": formatUnits(usdc.balanceOf(wallet), usdc.decimals())
}
emitOn: change
Poll: Every hour

Note the type field on the context entry — sol_address — the same typed-context convention as the REST API and the MCP tools. This isn’t optional decoration; the node needs it to know how to encode the value into the call.

The formatUnits(x.balanceOf(wallet), x.decimals()) pattern avoids hardcoding a decimals literal — both USDT and USDC use 6 decimals on Ethereum mainnet today, but calling .decimals() means the expression still works if that ever changes, or if you copy the pattern to a token that uses 18.

Wire a Telegram node off the trigger’s output. The message text references the fields the trigger node emits directly:

New balance detected for {{ $json.value.usdt }} USDT / {{ $json.value.usdc }} USDC
Previous: {{ $json.previousValue.usdt }} USDT / {{ $json.previousValue.usdc }} USDC
Block: {{ $json.blockNumber }}

That’s the entire workflow. No Schedule Trigger — the trigger node owns its own polling schedule. No IF node — emitOn: change only lets the workflow run when something actually moved. No data-store node — the diff state lives inside the trigger node itself.

How the diff actually works

This is the part that’s different from the Execute Query approach, where you build the diff yourself with a Schedule Trigger, a stored “last state,” and an IF node.

The trigger node’s output shape on a real fire is:

{
"value": { "usdt": 290.27, "usdc": 37.19 },
"previousValue": { "usdt": 305.0, "usdc": 37.19 },
"blockNumber": 21034992,
"type": "object"
}

emitOn has two modes. change (the default) fires only when value differs from the last stored poll — this is what you want for an alert. everyPoll fires on every scheduled poll regardless of whether anything moved, which is closer to a heartbeat than an alert. If your workflow doesn’t fire and you expected it to, check which mode is selected before assuming something’s broken — someone testing with change selected but expecting everyPoll behavior will spend a while chasing a phantom bug.

The one behavior that trips up almost everyone testing this for the first time: the first successful poll after you activate the workflow never fires an event. It silently seeds the node’s stored state so there’s something to diff against on the next poll. Activate the workflow, wait for the first poll, see nothing happen — that’s correct, not broken. The alert fires starting from the second poll, whenever the value has actually changed since the first one.

Manually clicking “Fetch Test Event” in the n8n editor behaves differently from a real scheduled poll: it always returns the current value with previousValue: null, and it does not touch the node’s stored state. It’s the right way to confirm your expression and contracts are configured correctly before you activate the workflow, but it won’t tell you anything about the change-detection behavior itself — for that you need to actually activate the workflow and let two real polls run.

The pollTimes / “every hour” schedule UI you configure the trigger with is n8n core’s standard polling-schedule component, shared by every polling trigger in n8n (Airtable, Google Sheets, etc.) — not something evmquery built. What’s evmquery-specific is the contract read, the typed context, and the value/previousValue/blockNumber diff payload.

Generalizing it: watch anything, not just balances

The trigger node doesn’t know or care that the expression happens to compute a token balance. Swap the expression and it becomes a trigger for anything a CEL/SEL expression can compute:

Aave health factor. Same trigger, same emitOn: change, different expression — fire when a position’s health factor crosses into risky territory:

Expression:
aave_pool.getUserAccountData(wallet).healthFactor

getUserAccountData returns a struct; dot-accessing .healthFactor off the result works the same way in the trigger node as it does in the REST API and MCP surfaces. A position with no active debt returns an enormous number (2^256 / 1e18, roughly 1.16e59) rather than an error — that’s “infinite health,” not a bug, and worth filtering out in a downstream IF if you only care about at-risk positions.

DAO proposal state. The same enum-mapping trick from the Execute Query post’s DAO proposal recipe applies here, except the trigger node’s change mode replaces the manual “store last state, diff, update store” plumbing entirely:

Expression:
Governor.state(proposalId)

Map the returned integer to Pending / Active / Succeeded / etc. in a downstream Code node if you want a readable label in the Telegram message — the trigger node itself only cares whether the raw value changed.

NFT supply or floor proxies. totalSupply() on an NFT contract makes a clean trigger for “a mint just happened”:

Expression:
Collection.totalSupply()

Fire on change, and you’ve got a mint-watcher with no polling logic of your own to write.

In every case the shape is identical: pick a chain, name your contracts, write one expression, set emitOn, set a poll interval. The trigger node doesn’t distinguish between “balance” and “any other view function” — that distinction only exists in your expression.

Watching more than one wallet

Two ways to do it, and which one you pick depends on whether the wallets are on the same chain.

Same chain, multiple context entries: add a second sol_address context entry and reference both wallets in the expression:

Context:
wallet1 : sol_address = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045
wallet2 : sol_address = 0xYourSecondWalletHere
Expression:
{
"wallet1_usdc": formatUnits(usdc.balanceOf(wallet1), usdc.decimals()),
"wallet2_usdc": formatUnits(usdc.balanceOf(wallet2), usdc.decimals())
}

The engine batches both calls into a single Multicall3 round on the wire, so adding a second wallet doesn’t cost you a second round trip.

Different chains, multiple trigger nodes: Multicall3 batching is per-chain, so a wallet on Base and a wallet on Ethereum can’t share one call regardless of how you structure the expression. Drop in a second evmquery trigger node pointed at the other chain. Both can feed the same downstream Telegram node.

Mistakes that trip people up

  • Expecting everyPoll behavior with change selected. If you want a heartbeat message every poll, you need emitOn: everyPoll. With change selected (the default), silence between polls means nothing moved — that’s the node working as intended, not a stuck workflow.
  • Forgetting formatUnits and decimals. A raw balanceOf call returns the smallest unit, not a human number. If a balance reads about a million times too large, you skipped formatUnits(x.balanceOf(wallet), x.decimals()).
  • Expecting the first poll to fire. It won’t — it seeds the stored state silently. Give the workflow two poll cycles before assuming it’s broken.
  • Tight polling intervals across many wallets. Each query costs a handful of units — the worked example above costs 5 units per poll, trivial against the 700 units/hour free-tier cap for hourly polling. But a 10-second interval scanning a large wallet list can add up fast; pace your poll frequency to the number of wallets you’re actually watching, the same caveat the Execute Query post makes about rate limits.

Next steps

  • Need the manual diffing approach instead — Schedule Trigger, IF node, and full control over the state store? See Read Smart Contracts in n8n, which covers the node’s Execute Query action rather than this trigger.
  • The automation landing page has the full expression language reference and more n8n patterns.
  • Ready to wire this into your own stack? Get a free API key — the free tier covers hourly polling of several wallets with room to spare.
Share

Wire up your first trigger workflow

Grab a free API key and you can have Telegram alerts on a wallet balance running in ten minutes.