Skip to content

Smart Contract Alert Conditions: Condition vs Change Triggers for evmquery Monitors

Write a smart contract alert condition that fires when you want it. Condition and change triggers for evmquery monitors, with six real expressions.

evmquery team5 min read
Try it: Gnosis Safe InspectorOwners, threshold, nonce and version on Ethereum, Base, BNB Smart Chain, Polygon. Free, no signup.

Every evmquery monitor has one expression and one trigger, and the pair decides exactly when you get notified. Pick the wrong trigger and you either get nothing or get paged every check. This guide explains the two trigger kinds, condition and change, and then writes six real smart contract alert conditions you can copy.

TL;DR

Use condition when the expression returns a boolean and you want to hear when it turns true. Use change when you want to hear that a value moved, optionally by a minimum amount or in one direction. Both fire once per event, not on every check.

Condition: fire when it becomes true

A condition trigger needs an expression that returns a bool. The monitor fires on the edge: the check where the result goes from false (or unknown, on the first check) to true. While it stays true, the monitor stays quiet. It fires again only after it has gone false and then true again.

usdc.balanceOf(wallet) < parseUnits("1000", 6)

If the wallet is already under 1,000 USDC when you create the monitor, the first check fires. After that you hear nothing until the balance has recovered above the line and fallen below it again. To hear about recovery too, add a second monitor with the opposite comparison.

Change: fire when the value moves

A change trigger accepts expressions that return an integer, boolean, address, bytes or string. The first check records the value as a baseline and does not fire. From then on, any difference from the baseline fires, and the baseline resets to the new value. Each fire compares against the last fire, not against the original value.

For integers you can narrow what counts:

  • direction: any, up or down.
  • threshold: the minimum movement, either bps (relative to the baseline, where 10000 is 100%) or absolute (raw units of the value). The amount is a positive integer string.

Other types have no direction or threshold: any difference fires.

{
"kind": "change",
"direction": "down",
"threshold": { "unit": "bps", "amount": "500" }
}

That fires when the value falls to 95% of the baseline or lower. Because the baseline resets on each fire, a slow slide produces one alert per 5% step, not one alert forever.

Which one to use:

You want to know Trigger Expression returns
A number crossed a fixed line condition bool
A flag turned on condition or change bool
Something moved, whatever the size change, any int, address, bytes, string
Something moved by at least N percent change with bps int
Something dropped by at least N units change, down, absolute int

Six expressions

All of these use the contract alias syntax from evmquery queries. Aliases such as usdc are declared in the monitor’s schema.contracts, and variables such as wallet in schema.context.

1. Balance below a threshold

usdc.balanceOf(wallet) < parseUnits("1000", 6)

Trigger condition, with usdc at 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 on evm_ethereum and wallet declared as sol_address. USDC has 6 decimals, so parseUnits("1000", 6) is 1,000 USDC. For a hot wallet’s native balance, the low gas balance guide uses w.balance() on an address.

2. Total supply moves 5%

usdc.totalSupply()

Trigger change, direction any, threshold bps 500. The expression returns an integer, so thresholds apply. 500 bps is 5%, relative to the baseline.

3. An NFT changes owner

nft.ownerOf(solInt(1))

Trigger change, any. nft is the collection address, for example 0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D (BAYC), and token ID 1 is a literal. The expression returns an address, so any new owner fires.

4. A pause flag flips

usdc.paused()

USDC exposes paused(). Use condition to be told when it becomes true. Use change if you want both directions, since a bool under change fires whenever it flips, so you hear about the unpause too.

5. A Safe threshold changes

safe.getThreshold()

Trigger change, any, with safe at a Safe multisig address, for example 0x0da0c3e52c977ed3cbc641ff02dd271c3ed55afe on Ethereum. A Safe that goes from 3-of-5 to 1-of-5 fires with the old and new value. To watch the owner count instead, use safe.getOwners().size(). The Safe owners and threshold guide shows what else the contract exposes.

6. Aave health factor below 1.1

aave_pool.getUserAccountData(wallet).healthFactor < parseUnits("1.1", 18)

Trigger condition, with aave_pool at 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2 (Aave V3 on Ethereum) and wallet as sol_address. The health factor is an 18-decimal value, so 1.1 is parseUnits("1.1", 18). Wallets with no debt return a huge number, which is never below 1.1, so they never fire. The health factor guide explains the full struct.

Checking an expression before you save it

When you create a monitor, evmquery evaluates the expression once and returns the current value, its type and the block it was read at. If you pick condition but the expression returns an integer, the definition is rejected. The same test runs when you resume a paused monitor. A constant expression such as 1 < 2 is also rejected, because it reads no onchain state and could never change.

The result type is stored at creation. If a contract upgrade later changes what the expression returns, the monitor reports a deterministic error instead of guessing.

Limits to design around

  • An expression can use at most 10 distinct contract methods and make at most 25 contract calls per check.
  • The interval must be a multiple of 60 seconds, between your plan’s minimum and 24 hours. The Free plan checks every 15 minutes at best.
  • A monitor reads one chain: Ethereum (evm_ethereum), Base (evm_base), BNB Chain (evm_bnb_mainnet) or Polygon (evm_polygon).
  • Monitors compare state at each check, so pick an interval shorter than the window you care about.

Editing a trigger

Changing the expression, schema, context or trigger resets the monitor’s state. A condition that is still true fires again, and a change monitor takes a new baseline. Renaming a monitor or changing only its channels leaves the history alone. Edits go through the web app or PATCH /api/v1/monitors/:id, and the MCP guide covers creating monitors from prompts. See the developers page for the REST routes, and the webhook receiver guide for what arrives when a trigger fires.

Next steps

Share

Try an expression as a monitor

Sign up free, pick a template and swap in your own address. The Free plan includes 3 active monitors with 15-minute checks.