Percent and Absolute Thresholds (and Why Yours May Never Fire)
Numeric predicates parse your extracted string with parseFloat. "$1,299" parses to NaN, and NaN never triggers. Here is how to extract a number that works.
The predicate that looks right and never fires
field_increases_by_percent, field_increases_by_absolute, field_decreases_by_percent, and
field_decreases_by_absolute all do the same thing before they do anything else: they take the
field's before and after values - both are strings if your extraction method returns strings, which
CSS, XPath, and regex all do - and run them through a number parser. If that parse fails on either
side, the predicate returns false, silently, with no error anywhere in the run. The monitor looks
correctly configured. The field extracts fine. The value visibly changes every time you check it by
hand. It just never alerts.
This is the single most common reason a numeric monitor goes quiet, and it's almost always caused by one thing: the extracted string still has a currency symbol or a thousands separator in it.
How the parser actually works
The conversion is parseFloat under a thin wrapper. If the value is already a number, it's used as
is. If it's a string, it goes straight into parseFloat with no pre-processing - no stripping of
$, £, €, ,, or anything else:
toNumber("1299.00") → 1299
toNumber("$1,299.00") → NaN // parseFloat stops at "$"
toNumber("1,299.00") → 1 // parseFloat stops at the first comma, keeps only "1"
toNumber("in stock") → NaN
toNumber(null) → NaN
That second line is worth sitting with: "1,299.00" doesn't fail to parse, it partially parses -
parseFloat reads digits until it hits a character it doesn't understand and returns what it got so
far. A comma isn't part of a JavaScript number literal, so parsing stops after the leading 1. You
don't get an error. You get 1, silently wrong, and every downstream percent or absolute comparison
runs against that 1 instead of 1299.
There is no currency or thousands-separator normalization anywhere in this path today. Whatever
string your extraction config hands the predicate is what parseFloat sees.
The zero-baseline rule on percent predicates
field_increases_by_percent and field_decreases_by_percent compute the move as a percentage of
the previous value:
increase: ((after - before) / Math.abs(before)) * 100
decrease: ((before - after) / Math.abs(before)) * 100
If before parses to exactly 0, both predicates return false immediately, before doing the
division. This isn't a bug being worked around - dividing by zero produces Infinity or NaN
either way, and neither of those is a meaningful "percent increase," so a previous value of 0 is
treated as "no percent predicate can fire from here" rather than as an error. A field that goes from
0 to any nonzero number needs field_increases_by_absolute (or field_changes) to catch that
move - a percent predicate on it will never trigger, at any threshold.
The Math.abs(before) in both formulas matters if your baseline can be negative - a temperature or a
balance, say. It keeps the percentage's sign meaningful (a move from -10 to -5 is a real increase)
instead of flipping sign depending on which side of zero the previous value happened to sit.
The absolute predicates have no such rule - field_increases_by_absolute and
field_decreases_by_absolute just compare the raw difference to threshold, so they work the same
way whether before is 0, negative, or anything else. Their only failure mode is the same NaN
problem: if either side doesn't parse, the comparison never happens.
Fixing it at the extraction layer
The parser isn't going to change out from under you, so the fix is upstream: make the extraction config return a bare number, not a formatted string. Where that fix lives depends on the method:
Regex - a capture group returns exactly the characters it spans, nothing stripped out of the middle of it. That's enough to drop a currency symbol, because you can just keep it outside the parentheses:
{ "price": "/\\$([\\d]+\\.\\d{2})/" }
matched against $149.00 returns the clean string "149.00", which parses fine. But it can't do the
same trick for a thousands separator that sits inside the number, like $1,299.00 - the comma is
between two digits, not at an edge you can exclude. Capturing [\d,]+\.\d{2} to make the pattern
match at all just puts the comma back into the returned string, still unparseable. Regex extraction
has no post-match transform step, so once the comma is inside the span you want, it comes out in the
value. For a page whose numbers cross into four figures with a separator, the fix isn't a cleverer
pattern - it's targeting a field that never had the separator to begin with, the same way you'd handle
XPath or JSONPath below. See regex extraction for the capture-group mechanics.
XPath - the same display string usually has a clean number sitting one step away in an attribute.
A price component that renders $1,299 often carries the raw value in data-price,
content, or a similar attribute meant for structured data:
{ "price": "//span[@class='price']/@data-price" }
reading the attribute instead of /text() skips the formatting entirely, because the attribute was
never formatted for display in the first place.
JSONPath - if the source is an API response or an embedded JSON blob, the numeric field is
usually already a bare number, not a formatted string, since JSON has no native currency type. Point
the path at that field directly ($.price, $.amount_cents) rather than a display_price or
formatted_price sibling if one exists.
AI extraction - tell the prompt explicitly to return a bare number with no symbol or separator: "Return the price as a plain number with no currency symbol or comma, e.g. 1299.00, not $1,299.00." The model will follow an explicit numeric-format instruction; it won't infer one from "extract the price" on its own.
Whichever method you use, the fastest way to confirm the fix worked is to preview the monitor before
saving it - see testing a selector before you save - and check that
the previewed value has no $, no ,, and parses as a plain number by eye.
Increase, decrease, or "any move at all"
The four numeric predicates are directional on purpose - an increase predicate never fires on a
decrease and vice versa, regardless of size. If you want to alert on a price moving in either
direction, there's no single "any numeric change" predicate; combine the two with a composite OR:
{
"type": "composite",
"operator": "OR",
"conditions": [
{ "type": "field_increases_by_percent", "field": "price", "threshold": 5 },
{ "type": "field_decreases_by_percent", "field": "price", "threshold": 5 }
]
}
If you don't actually care about magnitude - any price edit at all, no matter how small - skip the
numeric predicates entirely and use field_changes on the same field instead. It has no parsing step
at all, so a value going from "$1,299.00" to "$1,199.00" fires it correctly even though neither
number would parse - it only compares the raw strings for equality, not their numeric value. Reach
for a percent or absolute threshold specifically when small moves are noise and only a move past a
line is worth an alert; reach for field_changes when the number itself, not its size, is what
you're tracking.
What to read next
- How diff predicates decide what alerts you - the full set of predicate types and the changed-field gate that applies to all of them, numeric or not.
- How to use regex extraction - capture-group mechanics for isolating a number out of mixed text.
- Tuning out false alerts - the layer rule for deciding whether a noisy monitor needs a fix at extraction or at the predicate.
Ready to try it? Point Verid at your URL and get a signed alert on every change. 200 free runs a month, no credit card.