Notifio can send the first enquiry on a rental listing for you, as you, from your own account on the portal. Everyone's first question about that feature is how it decides what to write. The more interesting question is how it decides not to write anything at all, which is what it does most of the time.
The guard module is 365 lines and every rule in it is a plain function over the page's URL, title, text and HTML. No model is involved anywhere in the decision. That is deliberate: a refusal has to be predictable, reproducible and explainable after the fact, and "the model thought it looked like a paywall" is none of those.
Two of the rules were much harder to get right than they look.
"€1450 per month" and "€9.99 per month" are the same sentence
A listing site that wants money to let you contact a landlord is a hard stop. So there is a paywall detector, and part of it looks for a subscription price on the page.
The naive version of that regex matches a currency symbol, an amount and a period. It also matches the rent. A rental listing says "€ 1450 per month" in bigger type than anything else on the page, so a detector written that way refuses every listing it is shown, which is a feature that silently does nothing.
The amount is the only thing separating the two cases:
/**
* The price of a subscription to the site itself: "€9,99 per month", "9.99 p/m",
* "€ 15 per maand", "15 € pro Monat".
*
* Capped at two digits on purpose. A rent is three or four figures and is very
* often written exactly like this, so matching any amount makes the guard refuse
* ordinary listings. Nothing in the European rental market is under €100 a month,
* and no listing site charges three figures a month for access, so the boundary
* is not close to either case.
*/
const SUBSCRIPTION_PER_MONTH = [
/[€£$]\s?\d{1,2}([.,]\d{1,2})?\s*(\/|per\s|p\/)\s*(month|maand|monat|mes|mois|mese|mnd|m\b)/i,
// ...
];
\d{1,2} is the whole guard. It works because there is a two order of magnitude gap between what a portal charges for a membership and what a room costs, and no real case sits in the gap. That is a much better basis for a threshold than a number someone picked because it felt about right, and it is worth writing the reasoning into the comment, because the next person to see \d{1,2} will assume it is a typo.
"iDEAL" is a payment method, Idealista is a listing site
The other hard stop is a page that wants a card. Detecting that by looking for payment-method words is the obvious approach and it is wrong in a specifically Dutch way: iDEAL is the bank transfer method every Dutch site accepts, and Idealista is one of the biggest rental portals in southern Europe. A keyword match on "ideal" takes out an entire market.
So the detector does not read words at all. It reads the page for the things a payment page actually loads:
/**
* Payment-provider and card-field markers. Deliberately narrow: a bare "ideal"
* keyword would match Idealista (a real listing site), so we key off PSP script
* hosts and card input attributes instead of payment-method words.
*/
const PAYMENT_HTML_MARKERS = [
/js\.stripe\.com|checkout\.stripe\.com|hooks\.stripe\.com/i,
/\.mollie\.com|mollie\.nl/i,
/(checkoutshopper|live\.adyen|test\.adyen)\./i,
/autocomplete=["']?cc-(number|exp|csc)/i,
/name=["']?(cardnumber|card_number|cc-number|creditcard)/i,
// ...
];
A script host is a fact about the page. A word is a fact about the language. When a guard has to work across six languages, prefer the facts that are not in any of them.
The domain lock, which is not a heuristic
The rule that matters most is not fuzzy at all. Aggregator sites routinely hand you off to a different company's site to make the enquiry, where you would have to register, agree to their terms, and quite often pay. Notifio never follows that hop.
export function registrableDomain(url: string): string | null {
try {
return getDomain(url, { allowPrivateDomains: true }) ?? null;
} catch {
return null;
}
}
tldts with allowPrivateDomains: true is the strict reading: it treats a.someplatform.io and b.someplatform.io as different sites rather than as one. For a guard, strict is the direction you want the edge cases to fall, even though it means a small curated allowlist for the handful of portals that legitimately move between two domains they own. That allowlist ships as data and is not user editable, because "let me add a domain" is exactly the request an attacker would make.
There is a constant next to it that says the same thing in one line:
/** Off-domain replies are refused by design. Not configurable, ever. */
export const ALLOW_OFF_DOMAIN = false;
Is this even a listing?
The last guard is the one protecting you from your own search. The app finds new items by sweeping a results page, and a sweep can pick up a blog post or a category page. Sending a rental enquiry to a blog post, signed with the user's real name, is the single worst thing this feature could do.
So a page has to pass a structural check before anything is typed into it. Price signals score two, an address or a contact affordance score one each, and the page needs a price plus at least one of the others:
const score = (price ? 2 : 0) + (address ? 1 : 0) + (contact ? 1 : 0);
Price is weighted double because it is the signal that nothing except a listing has. Plenty of pages on a rental site have an address in the footer and a "Contact" link in the header. Very few have a rent.
Every refusal has a name
None of this is allowed to fail quietly. Each guard maps to its own status on the reply record: skipped_offsite, skipped_paywall, skipped_signup, skipped_payment, skipped_captcha, skipped_login, skipped_not_listing. The user can see which rule stopped a given listing and decide whether to go and do it by hand.
That granularity is also what keeps the refusals from being permanent. I wrote earlier about the 8 statuses that mean never touch this listing again, and every status in the list above is deliberately excluded from that set. A skip means nothing was sent, so the listing stays eligible the moment the reason goes away.
The feature itself is described on notifio.app/pricing, the setup walkthrough is at notifio.app/help, and the portals it runs against are listed at notifio.app/alerts.
Top comments (0)