All projects

Realtor → Google Sheets Daily Extraction + Client Dashboard

Apps Script scraper + branded, password-less client dashboard — one deploy, per-client URL.

The goal

Scott's agency (Realtorships) serves dozens of real-estate clients wanting fresh realtor.com listings daily. The goal was a zero-ops daily pipeline that scrapes realtor.com per client's ZIPs, writes to a per-client Google Sheet, and serves each client a branded, password-less dashboard with filters, sort and CSV export.

The solution

A two-layer system: (1) Apps Script Code.gs runs daily on a trigger, iterates the parent Users sheet, fires 8 reverse-engineered realtor.com GraphQL queries, paginates 100+ results, and writes to each client's own spreadsheet. (2) Standalone Apps Script web app under webapp/ serves one URL template ?ss=<id> with a branded Realtorships dashboard — sidebar filters, sortable sticky-header table, pagination, CSV export, ACL via parent-sheet hyperlink whitelist, with caching at both layers.

Highlights

  • Zero-ops daily pipeline — no server, no cron host
  • 8 realtor.com GraphQL queries reverse-engineered from the browser
  • Per-client bookmarkable URL without auth, whitelisted by parent sheet
  • Stable deployment URL across versions — clients' bookmarks never break

The challenge

realtor.com has no public API — only browser-only GraphQL with rotating queries and real rate limits.

Captured 8 GraphQL calls + a detail drill-down from the browser, normalized them into Apps Script UrlFetchApp requests with exact headers/doc_ids/operations, paginated with nextCursor. probe_inline_query.py helps re-capture when realtor.com rotates queries.

Dozens of clients each need their own editable live sheet — building a DB would be overkill.

One parent Users sheet with per-client spreadsheet hyperlinks in column C. Scraper iterates Users, writes to each client's sheet separately. Clients share/edit their sheet natively.

doGet extracts ?ss=<id>, looks it up in the Users sheet, and serves only if the ID appears as a column-C hyperlink. Random IDs get 'Dashboard not found'. Onboarding = add row; offboarding = remove or redeploy.

Naive Apps Script rendering gave 8–10s loads on large sheets.

Server returns already-normalized data; client-side JS runs filter/sort/pagination locally. 60s client + 5min server cache. Embedded base64 logo avoids an extra round-trip.

Clients bookmark the dashboard URL — redeploying the web app would break their links.

Used Apps Script's Manage deployments → New version flow — URL stays stable across deploys and all clients see the new version immediately.

What was delivered

  • Code.gs scraping service with parent-sheet iteration
  • Web app package (webapp/Code.gs, Index.html, Styles.html, Scripts.html, LogoData.html, appsscript.json)
  • webapp/README.md with 6-step deployment guide and troubleshooting
  • Captured GraphQL request/response pairs (graphql{1..8}_fetch.{js,json})
  • Python probes for refreshing GraphQL schemas
  • Branded Realtorships dashboard (navy + orange) with embedded base64 logo
  • AVAILABLE_FIELDS_FOR_REALTOR.md field reference

Results

8 + detail

GraphQL queries

100+/batch

Batch size

7

Dashboard filters

Daily

Schedule

What it taught me

  • For no-auth client portals, ?ss=<whitelisted-id> against a parent sheet beats building real auth
  • Client-side filter/sort/pagination on Apps Script-served data keeps the UI snappy without a backend
  • Capturing the browser's real GraphQL calls is the sustainable way to integrate undocumented APIs