The first activity alert most teams write is "tell me if there are more than N transactions in an hour", or the reverse, "fewer than N", meaning the contract went quiet. It is simple, and for most contracts it is wrong. This part explains why, what to use instead, and where the fixed number is still the right tool.
It follows part 10: there we watched a value that must never be a certain number. Here we watch a value that is always changing, and the question is what "unusual" means.
Contracts have a day and a night
On-chain activity follows people. A lending pool, a DEX pair or a game contract is busy when its users are awake, and quiet when they are not. The same hour repeats from day to day. In a week of data from a large lending pool on BNB Chain, about 120 transactions an hour was normal at 20:00 UTC and about 500 was normal at noon1.
Now pick one number.
- Set it for daytime. "More than 500 per hour" is mostly quiet at noon. At 20:00 the hour would have to quadruple before it fires, so an unusual evening goes unnoticed.
- Set it for the night. "More than 170 per hour" catches a jump at 20:00 and fires on almost every hour of the day, because daytime is normally above it. That is not an alert, it is a log.
- The "went quiet" version ("fewer than 150") fires every night on ordinary quiet, and at noon, when the contract drops from 500 to 200, it sees nothing.
There is no number that is right for both noon and 20:00. Every choice trades night blindness for daytime noise.
The second half of the problem is worse. You set a threshold once. A quarter later the contract has more users, the norm has moved, and the alert fires all day. The team mutes it, and from that moment there is no monitoring. This is the most common way alerts die, and no tuning on day one prevents it.
The idea: compare each hour with the same hour
Stop comparing with a constant. Compare each hour with what that hour usually is.
For 20:00 today, take the value at 20:00 on each of the previous seven days. Two numbers describe them:
- the usual value is their median;
- the usual spread is the median of how far each of those values sits from the median (the MAD), multiplied by 1.4826 so it is on the same scale as a standard deviation.
The rule fires when today's value is more than *k* spreads from the median. The limit becomes median + k × 1.4826 × MAD. At noon the median is high and the band is wide. At 20:00 it is low and narrow. The threshold follows the clock, and it follows the contract: when the pool grows, the next week's medians grow with it.
SmartAlert calls this condition "deviates from the norm" and it is a documented formula, not a trained model. You can recompute it by hand from the same data. We think an alert you can check is an alert you will trust, and one you do not trust gets muted.
Why the median, not the average
The seven-day window will often contain the very thing you want to catch: last Tuesday's incident, an airdrop, a bot that went wild. An average and a standard deviation absorb that day. The band widens, and the next incident looks normal. A rule that learned from the incident stops seeing incidents.
The median and the MAD barely notice one bad day out of seven. One outlier moves the average a lot and the median hardly at all, so a polluted history still gives a usable norm.
The floor under the spread
On a very steady series the MAD can be zero. Then any difference from the median looks infinitely unusual, and a quiet contract would alert on every transaction. So the spread has a floor in absolute units: the larger of 1 and 10% of the median. At the evening hour above, that floor, not the data, sets how wide the band is.
The one knob: sensitivity
*k* is the sensitivity, 3 by default and between 1 and 10 in SmartAlert. A smaller *k* is a narrower band and more alerts. A larger one is fewer alerts, and a bigger jump needed.
You do not have to guess it. Two things help.
- Backtest. Before the rule goes live, look at the metric, the band, and the points where the rule would have fired over past days. Move *k* until the marks are ones you would want to hear about.
- Work backwards from your tolerance. Say "I will wake up at most twice a week", and the tool picks the *k* that gives at most that many alerts on the chosen history. People can state their own patience for noise. They rarely know how many transactions an hour is "normal".
Two more options matter in practice.
- Direction. For a swap count, a spike is often fine and a drop to zero is the incident. Choose "below" only, "above" only, or both.
- Your own limit for one hour. If you know that 03:00 UTC is when a keeper batch runs, you can set a threshold for that hour that replaces the norm for that hour.
Same hour on previous days, or same weekday
A week has a shape too. A DEX can be quiet on Sunday and busy on Tuesday. The baseline can be chosen:
| Baseline | Compares with | Points |
|---|---|---|
| same hour, previous 7 days | the same hour on each of the last 7 days | 7 |
| same hour, previous 28 days | the same hour, last 28 days | 28 |
| same weekday and hour, previous 4 weeks | e.g. Sunday 20:00 on each of the last 4 Sundays | 4 |
Seven days is the default. It adapts fast, but a weekend will be compared with weekdays. The weekday baseline fixes that, at the price of only four points per comparison. Four points is fewer than the default minimum of five, so choose that baseline together with a lower minimum.
When a fixed threshold is the right tool
The norm does not replace fixed numbers. It replaces them where the border comes from statistics. A fixed number is right when the border comes from outside:
- a collateral ratio below 150%;
- a balance below the sum of the next day's payouts;
- a fee above the one in the contract.
Here the value can sit near the dangerous border for months. If you used "unusual for this hour", the dangerous border would become the norm, and the rule would go quiet exactly where it must be loud. So: a limit from the contract, the code or a policy is a fixed threshold. A limit of "not like usual" is the norm.
Where the norm does not work yet
- No history, no norm. The norm needs data from the past: seven days of a series for the 7-day baseline. Until then the rule is saved but not evaluated, and the interface shows the date from which the norm appears. You can set a fallback threshold for that period.
- Launches and fast growth. If a contract is launching, or an airdrop is running, the norm lags and the rule says "above the norm" hour after hour. Put the rule on "Silence until…" for 1, 3 or 7 days or up to a date: it keeps evaluating, logs firings as "not sent: silenced" and turns itself back on.
- Attacks that look normal. An exploit that moves $10M in a few transactions does not change the transaction count of a busy contract. Activity norms catch changes in rate, not a single transaction. For that use a dollar threshold on the outflow, which is a fixed one by nature.
How to catch it
- For rates, use the norm. The transfer-spike, swap-spike and contract-went-quiet recipes are the norm with medium sensitivity: the first two fire above it, the last one below it. For your own rule choose "below" for a contract that must not stop and "above" for one that must not be hammered.
- Backtest before enabling. Open the rule on 30 days of history, then move *k* or give the number of alerts per week you accept.
- For money limits, use fixed numbers in dollars. large-transfer-usd, treasury-below-usd. Also fixed: the balance of a wallet that pays out, via keeper-gas-runway, which already turns spending into days left.
- Review after the first week. If a rule fires on every normal hour, raise *k*. If it never fires, check on the backtest that it could.
Sources
- 1.SmartAlert, Aave V3 Pool on BNB Chain, transactions per hour, one week of October 2026. The numbers in the text are the typical level of each hour, rounded. ↩