MLS listings for a real estate site
MLS listings over the RESO Web API: search, detail, replication and a home-value estimate, no vendor SDK
For when the answer to “It shows real estate listings from an MLS” is yes.
Puts live MLS listings on a real estate site straight from the RESO Web API (Trestle, Bridge, MLS Grid, Spark) with the IDX display rules enforced in code, so no page can show an opted-out address or a listing the seller kept off the…
Skip the rebuild
You do not write this one. It arrives in your project as ordinary source you can read, change and keep, with its audit record and its caveats beside it.
- Download the Playground. It is free and runs on your own machine.
- Start a new app and tick “It shows real estate listings from an MLS”.
- This block is written in, along with anything else you ticked.
The mistake it removes
The naive version pastes visitor input into $filter (OData injection), follows a server-supplied nextLink to another host with the bearer token attached, requests private agent fields that then leak into a page, or bursts past the MLS's rate limit and gets the feed suspended.
What is already handled
Each of these was checked by running the code, not by reading it.
- 60 node assertions (counted here: test/mls-kit.test.mjs), all passing, against an injected fetch, clock and sleep
- OData filter values are escaped or refused: single quotes doubled, control characters and over-long strings refused, and an empty string refused as a number
- the bearer token travels only in the Authorization header and never follows a nextLink to another host, including a host that only shares the base URL as a string prefix
- a 401 refreshes the OAuth token and retries, a 429 waits the server's Retry-After, a 5xx gives up after three attempts, and a 4xx fails at once
- concurrent requests are paced to the market's RPS setting instead of firing together
- the IDX display rules hold: an opted-out address and its map pin are withheld, the listing office is always carried, and replication never stores an opted-out listing and deletes closed or MlgCanView=false records
- without credentials, development serves labelled sample rows and production serves none
- the estimate asks the MLS nothing until VALUATION_USE_SOLD_DATA=true, and returns no figure from fewer than three comparable sales
- the JSON store round-trips across two processes, and the CLI exits 2 naming the variables to set when a market is unconfigured
What you still have to do
A copied file cannot own your secrets, your host or your legal obligations. This is the part the block deliberately does not claim.
- Render the MLS's own disclaimer text, its logo if the rules require one, the listing office name with its phone or email on every card, and the fetchedAt time as 'last updated'. The block carries each of these values on every result; it cannot put them on your page.
- Run one process per market, or move the pacer into a shared store, if you scale past one server. The request pacer and the 10-minute cache live in memory per process, so two instances each spend the full RPS allowance against the MLS.
- Set RESO_<M>_CLIENT_SECRET or RESO_<M>_ACCESS_TOKEN in your host's environment or secret manager and keep every file of this block out of browser bundles. The block reads them from env and never stores or rotates them.
- Read your MLS rule book's IDX section and have your broker of record sign the IDX data licence before launch. Then put an 'automated estimate, not an appraisal' statement beside any figure from valuation.js. MLS-SETUP.md lists what the code already enforces and what the page still owes.
- SERVER ONLY. An MLS client secret or access token is a licensed-data credential. Never import these files into browser code.
- No request in the tests reaches a real MLS. Run `node reso-query.mjs <market> --fields Property` against your approved feed first: a field in SELECT that your feed lacks makes every search fail with a 400, so delete it from SELECT.
- IDX display is a broker permission. Credentials alone do not make displaying listings legal; the broker of record's signed data licence does.
- parseCriteria and valuation.js accept 5-digit US ZIP codes only. Change the regex for another country's postal codes.
- Replicate mode keeps the whole feed in one JSON file per market, which suits a single-office or single-county feed. For a large regional MLS, pass setReplicaStore() a database-backed store.
- The block does not schedule anything. Call replicate(market) from your own timer or cron at least as often as your MLS rules require (commonly every 12 hours).
- Saved-search email alerts and the listing page templates from the source project were left out: they were wired to that site's mailer, languages and copy. Build them on search(market, criteria, { since }).
What lands in your project
node
- node/reso.js
- node/listings.js
- node/valuation.js
- node/json-store.js
- node/reso-query.mjs
- node/MLS-SETUP.md
Install into one server directory; the files import each other as ./reso.js and ./json-store.js. listings.js: search(market, criteria), getListing(market, key), stats(market, {city}), replicate(market), parseCriteria(query). valuation.js: estimate({market, zip, sqft}). reso-query.mjs is a CLI for --fields, --count and raw queries. MLS-SETUP.md is the credential and compliance checklist.
What you supply
- RESO_<M>_BASE_URL — the OData service root your MLS vendor gives you; <M> is a market code you choose
- RESO_<M>_TOKEN_URL / CLIENT_ID / CLIENT_SECRET / SCOPE — OAuth2 client credentials (Trestle, Spark)
- RESO_<M>_ACCESS_TOKEN — a static token instead (MLS Grid, Bridge)
- RESO_<M>_MODE — query (live search, default) or replicate (local copy; call replicate(market) on a schedule)
- RESO_<M>_SYSTEM_FILTER / EXTRA_SELECT / RPS / FIXER_MODE — optional per-feed tuning, see the header of node/listings.js
- AGENT_MLS_IDS_<M> — your member ids, so your own listings come back with isAgentListing: true
- VALUATION_USE_SOLD_DATA — true only after the MLS confirms closed-sale data may power an automated estimate
- MLS_DATA_DIR — where replicate mode keeps its JSON copy (default ./data)
Licensed MIT. It is a starting point, not a finished product.
Get it
Download the Playground See the other blocks
Nothing here is locked. The files are yours, in your folder, under a permissive license.
Questions
- Is MLS listings for a real estate site audited?
- No. No full audit has been run against this block yet, which is not the same as a pass. What is verified about it is listed on this page, and what is not is listed beside it.
- What do I still have to do myself?
- Render the MLS's own disclaimer text, its logo if the rules require one, the listing office name with its phone or email on every card, and the fetchedAt time as 'last updated'. The block carries each of these values on every result; it cannot put them on your page.
- How do I get this code?
- Download the Playground, start a new app, and tick “It shows real estate listings from an MLS”. The block is written into your project as ordinary source you can read and edit.
All pre-built code blocks · Learn to build from zero · The coding guide