This post walks through a small but production-shaped app: a service that lets users subscribe to property-search criteria and notifies them when new listings match. We’ll use the PropertyFinder API, but the pattern generalises.
The shape of the problem
Users save search criteria. Every few minutes, our service polls the API for each search. New listings — ones we haven’t seen before — trigger notifications. Old listings (price-change updates excluded) don’t.
Architecture in one sentence
A cron job per search, each poll dedupes against a seen-listings set, each new listing fires an outbox event. Keep state simple: one Redis key per search holding the set of seen listing IDs.
Polling loop
async function poll(search) {
const results = await fetchPropertyFinder(search.criteria);
const seen = await redis.sMembers(`alerts:${search.id}:seen`);
const seenSet = new Set(seen);
const fresh = results.listings.filter(l => !seenSet.has(l.id));
if (fresh.length === 0) return;
await redis.sAdd(`alerts:${search.id}:seen`, fresh.map(l => l.id));
await publishAlertEvents(search.userId, fresh);
}
Three notes:
- Use the listing ID, not a URL hash — IDs are stable.
- Cap the
seenset. Evict IDs for listings not returned in the last N polls so the set doesn’t grow unbounded. publishAlertEventsputs one event per new listing on your outbox. Don’t send notifications synchronously from the poll loop.
Fetching PropertyFinder
Using the API from the library:
async function fetchPropertyFinder(criteria) {
const params = new URLSearchParams({
city: criteria.city,
min_price: criteria.minPrice,
max_price: criteria.maxPrice,
bedrooms: criteria.bedrooms,
per_page: '50',
});
const res = await fetch(`https://propertyfinder.p.rapidapi.com/search?${params}`, {
headers: {
'X-RapidAPI-Key': process.env.RAPIDAPI_KEY,
'X-RapidAPI-Host': 'propertyfinder.p.rapidapi.com',
},
});
if (!res.ok) throw new Error(`PF returned ${res.status}`);
return res.json();
}
Respect the rate limit on your plan — batch polls with a leaky bucket if you have many users.
Cold-start: the first poll
The first time a user saves a search, the poll returns dozens of listings that are “new” to us but probably not new in the world. Two options:
- Mark them all as seen, don’t notify. Users see alerts only for listings that appear AFTER they subscribed. Clean but a little boring for the first notification.
- Show them as “initial matches,” notify once with a summary. Warmer onboarding.
We default to option 2 with a capped summary notification (“23 listings currently match — here are the 5 most recent”).
Deduplication across sources (optional)
If you also want to alert on Bayut, you can query Bayut in parallel. Dedupe by a canonical key (address + bedrooms + approximate area) — different platforms use different IDs, but the same listing shares canonical attributes.
Or — cheaper — use the UAE aggregator which pre-dedupes across sources.
Testing without burning rate limits
Before turning on the real poll, run your whole pipeline against a free sample. The schema matches the live API, so your dedup logic, event publisher, and notification UI all work end-to-end without making a single API call.
What not to do
- Don’t poll every search on a uniform 60-second schedule. Space them out (round-robin) to stay inside rate limits.
- Don’t notify on every field change — only “new listing appeared” and “price dropped >X%” are interesting.
- Don’t store full listing JSON indefinitely. IDs + timestamps + minimal metadata (price, URL) are enough.
Next steps
View the PropertyFinder API to subscribe, or read about the UAE aggregator API if you want cross-platform coverage from the start.