Browse the notebookWorkflows

Build a Real Estate Listing Price Monitor

By RealtyCrawl team10 min read

Design a bounded collection workflow that detects observed asking-price changes, preserves history, and avoids false delisting alerts.

Editorial illustration accompanying Build a Real Estate Listing Price Monitor

A listing price monitor needs more than a scheduled request and a comparison operator. It needs to know which publication it is watching, whether the newest response is actually fresh, and whether a missing record reflects a source change or a failed collection. Without those distinctions, a monitor can send confident alerts about events it never observed.

This guide builds the workflow around RealtyCrawl’s source observations. Your application owns the scheduler, historical storage, comparison rules, and alert delivery. The examples concern observed asking prices; they do not establish sale prices, completed transactions, or property valuations. Start with one source and a small watch area before extending the workflow.

Decide whether you are watching a listing or a search

A listing watch follows a specific source publication. A search watch samples the results of a location query over time. These are different products: a search can stop returning a listing simply because its rank moved outside the response window, while a direct lookup may provide more specific evidence about that publication.

Check which collection operations are active for your source. The current implementation supports bounded sale searches for verified adapters and canonical Zillow URL resolution where enabled. It does not provide universal fresh detail lookup by any native listing ID. Design the watch around an operation the deployment actually supports.

Store the target definition with the watch. Include platform, selector, transaction, output limit, and the reason the target was chosen. If a user changes the bounding box or source, create a new watch revision. Otherwise a change in the query can be mistaken for a change in the underlying market.

Separate acquisition from saved-resource reads

GET /v1/listings/{listing_id} reads an account’s stored observation. Repeating that endpoint every hour does not reacquire the portal page and therefore cannot establish an hourly price history. The stored resource is retained for 24 hours and may be updated by later collection.

Acquire a new observation through a supported search or resolution operation, then record its source and observation time. Keep collection-start time, completion time, and ingestion time separately if operational timing matters. These timestamps help distinguish a delayed job from stale source evidence.

An idempotent replay is another important case. It can successfully return the original body without performing new acquisition. Record the replay header and avoid appending it as a new price observation. The monitor’s freshness rule should depend on acquired evidence, not only on an HTTP 200 response or the fact that a scheduler ran.

Store history before deriving alerts

Use a collection-run record for each logical acquisition and an observation record for each returned listing. Preserve the original envelope, including source outcomes and warnings. Link observations to a source publication using platform and native ID, while retaining the returned RealtyCrawl ID for supported reads.

Do not rely on the API’s saved-resource retention as your historical database. Store observations in your own system for the period your product needs. Keep immutable observation rows and derive a current view from them so that a bad comparison can be corrected without erasing the evidence.

The following storage sketch belongs to the monitoring application. It deliberately separates acquisition, comparison, and delivery. An alert can then be retried independently from a paid source request, and a comparison rule can be rerun over saved observations without crawling the source again.

Application records for a listing monitor
RecordImportant fields
Watch revisionPlatform, selector, limit, transaction, schedule
Collection runLogical request key, status, request ID, charge
ObservationSource identity, observed time, money fields, raw envelope
ComparisonPrevious observation, current observation, rule version
Alert deliveryComparison ID, destination, delivery state

Compare amounts only after checking their meaning

Before subtracting two prices, confirm that both observations describe the same source publication, transaction type, currency, price kind, and payment period. An amount that changes from one currency to another is not a price reduction. An unknown amount is not a zero-price event.

Use decimal arithmetic and retain both input amounts. The function below returns an application event only when the observations are compatible and the amount changed. It is a pure comparison helper for already validated observations; it does not fetch data or resolve property identity.

A production rule can apply an absolute or percentage threshold after this compatibility check. Keep the raw delta even if you suppress a small alert. Version the threshold so a user can understand why a change generated an alert yesterday but would be ignored under today’s settings.

from decimal import Decimal

def price_change(previous, current):
    old_source, new_source = previous["source"], current["source"]
    for field in ("platform", "native_id"):
        if not old_source.get(field) or old_source[field] != new_source.get(field):
            return None
    if previous.get("transaction") != current.get("transaction"):
        return None
    old, new = previous["price"], current["price"]
    if current.get("transaction") != "sale" or new.get("kind") != "asking":
        return None
    for field in ("currency", "kind", "period"):
        if old.get(field) is None or old[field] != new.get(field):
            return None
    if old.get("amount") is None or new.get("amount") is None:
        return None
    before, after = Decimal(old["amount"]), Decimal(new["amount"])
    if not before.is_finite() or not after.is_finite() or before <= 0 or after < 0:
        return None
    if before == after:
        return None
    return {
        "kind": "observed_asking_price_change",
        "before": str(before), "after": str(after),
        "delta": str(after - before),
        "percent": str((after - before) / before * 100),
        "currency": new["currency"],
        "observed_at": current["observed_at"],
    }

Treat disappearance as an observation gap

If yesterday’s listing is absent today, first inspect the collection run. Did the source complete? Was the query unchanged? Was the response truncated? Did the source return a bounded window with no continuation? A missing row in any of these situations is insufficient evidence for a delisting or sale.

Record “not observed in this run” separately from an explicit source status. Your product can choose to show an observation gap after repeated absences, but the label should describe the evidence. Repeated absence from the same narrow window still does not establish why the source stopped returning a listing.

When an explicit status is supplied, retain both the normalized status and the source’s own text where available. A portal reporting a listing as sold is source-reported evidence; it does not turn the observation into a verified transaction record. Alert wording should preserve that distinction.

Give the scheduler a total request budget

A per-request X-Max-Credits header limits what one call may authorize. It does not cap the cost of a schedule, a set of sources, or repeated attempts. Track accumulated charges in the job controller and stop when the watch reaches its approved budget.

Estimate the request count before enabling a schedule. Ten search watches run once per day mean ten planned source searches per day, before additional reads or recovery attempts. Use the current operation pricing to translate that plan into a credit budget; do not equate one returned listing with one request.

Set maximum attempts, a deadline, and a concurrency policy. Honor Retry-After and coordinate workers that share an account. If a source becomes unavailable, pause affected watches or record a failed run according to your policy. Increasing the retry rate does not create source coverage and can consume the resources needed for healthy watches.

Keep acquisition retries separate from notification retries

Persist a logical request key before the first POST. If the connection drops, recover using the same key, credential, and body so you can discover whether the original work completed. A changed selector or a deliberately new collection needs a new logical request identity.

After a successful comparison, create an alert event with a stable identity derived from the watch revision and the pair of observations. Store delivery state separately. If notification delivery fails, retry that delivery rather than issuing another source request to reconstruct the event.

This separation also prevents duplicate alerts when a worker restarts after saving a comparison but before recording delivery. Your job runner can resume from the saved event. Keep the delivery destination and message template version with the event so operational recovery does not silently change the meaning of the notification.

Write alerts that show the evidence

An alert should state the source, previous amount, new amount, currency, and observation times. Include a source URL when available and a link to the saved evidence in your own application. Use “observed asking price changed” rather than implying that a buyer paid the new amount.

Show freshness in the product, especially after collection gaps. A current screen can display the latest known observation while clearly stating when it was acquired. Do not replace an unavailable source with an old value and preserve a “live” badge.

For aggregate reports, distinguish changes to existing publications from changes in the composition of the search sample. A falling median across the returned window can occur even when no matched listing reduced its price. The alert type should explain whether it compares a publication, a fixed cohort, or a changing source window.

Follow one watch through a failed collection

Imagine a watch observes a listing at 500,000 USD on Monday. Tuesday’s acquisition fails before returning a valid source observation. Wednesday’s new acquisition sees the same publication at 485,000 USD, with the same price kind and currency. The monitor can identify a 15,000 USD decrease between Monday’s and Wednesday’s observations.

It cannot establish that the change occurred on Wednesday or that the price was unchanged on Tuesday. The alert should describe the observation interval, and the history should retain Tuesday’s failed collection as an operational gap. Filling Tuesday’s history with Monday’s price and labeling it as newly observed would create false evidence.

If Wednesday’s request is replayed after a connection interruption, the returned body should reconcile with the original Wednesday run. It should not become Thursday’s observation merely because the recovery happened later. Keep acquisition identity, observed time, and recovery time distinct so a restarted worker cannot rewrite the apparent timeline.

Keep source history separate from your collected series

Some supported operations can expose history reported by a source. That history is useful, but it differs from the series your application collected. A source event may have only a date, while your acquisition has a timestamp. Preserve the event’s time precision rather than inventing an exact hour.

Store source-reported events with their origin and source text. Keep your own observed changes in a separate event category. If the two accounts disagree, show the difference for review instead of silently replacing one with the other. Neither a source label nor a comparison of asking prices independently verifies a completed transaction.

This separation also makes backfills honest. Importing historical events today does not mean your collector observed those changes when they happened. Record both the reported event date and the import or acquisition time, then let the interface explain which chronology a chart is displaying.

Validate a monitor before enabling delivery

Run the collector with delivery disabled and inspect saved events first. Test unchanged prices, a genuine compatible change, null amounts, currency changes, repeated response replay, and a failed source. Include a listing that leaves the bounded result window without an explicit status update.

Confirm that each event points to two distinct acquired observations and that each request’s charge is recorded once. Simulate a worker restart between acquisition and comparison, then between comparison and delivery. The system should resume without duplicating source work or sending the same event twice.

Start with a small set of watches whose outputs someone can inspect manually. Expand only after the evidence, budget, and recovery paths are clear. A dependable monitor is one whose quiet periods and failures are as understandable as its successful alerts.

References for your implementation

Use the source catalog to choose an operation and the API reference to check its inputs, response contract, and current availability.

Topics

#price-monitoring#observations#automation