[object Object]

← back to Homesonspec

feat: HomesOnSpec data-feed API (:9799) — share the development pipeline with RENTV/partners

80fb67760f86c868fca8fa6240e95c14416b0321 · 2026-07-28 09:21:18 -0700 · Steve

Read-only CORS JSON feed over homes + building_permits: /feed/permits (sector/geo/value/
date filters), /feed/homes, /feed/developments (CRE pipeline by city, e.g. LA $17.4B),
/feed/summary. Source for RENTV commercial-real-estate content + usrealestate.

Files touched

Diff

commit 80fb67760f86c868fca8fa6240e95c14416b0321
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Jul 28 09:21:18 2026 -0700

    feat: HomesOnSpec data-feed API (:9799) — share the development pipeline with RENTV/partners
    
    Read-only CORS JSON feed over homes + building_permits: /feed/permits (sector/geo/value/
    date filters), /feed/homes, /feed/developments (CRE pipeline by city, e.g. LA $17.4B),
    /feed/summary. Source for RENTV commercial-real-estate content + usrealestate.
---
 scripts/feed-api.mjs | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 117 insertions(+)

diff --git a/scripts/feed-api.mjs b/scripts/feed-api.mjs
new file mode 100644
index 0000000..1083af5
--- /dev/null
+++ b/scripts/feed-api.mjs
@@ -0,0 +1,117 @@
+// HomesOnSpec DATA FEED — read-only, CORS-enabled JSON API for downstream consumers
+// (RENTV / usrealestate / any partner). Exposes the new-construction development
+// pipeline: building permits (commercial + residential + multifamily) and new-home
+// listings, filterable by geo/sector/value/date. Zero-dep (Node http + psql).
+// Run: node scripts/feed-api.mjs   (PORT env, default 9799)
+import { createServer } from "node:http";
+import { execFile } from "node:child_process";
+
+const DBURL = "postgresql://macstudio3@localhost/homesonspec?host=/tmp";
+const PORT = Number(process.env.PORT || 9799);
+const q = (sql) => new Promise((r) => execFile("psql", [DBURL, "-tAc", sql], { maxBuffer: 128 << 20 }, (e, o) => r(e ? "[]" : (o.trim() || "[]"))));
+const esc = (s) => String(s ?? "").replace(/'/g, "''").replace(/[;\\]/g, "").slice(0, 60);
+const int = (v, d, max) => Math.min(max, Math.max(0, parseInt(v ?? d, 10) || d));
+
+// sector → permit_type predicate
+function sectorPred(sec) {
+  if (sec === "commercial") return "(permit_type ilike '%commercial%')";
+  if (sec === "multifamily") return "(permit_type ilike '%apartment%')";
+  if (sec === "residential") return "(permit_type ilike '%family dwelling%' or permit_type ilike '%residential%' or permit_type ilike '%new construction%')";
+  return "1=1";
+}
+
+async function permits(p) {
+  const w = ["1=1"];
+  if (p.get("state")) w.push(`state='${esc(p.get("state")).toUpperCase()}'`);
+  if (p.get("city")) w.push(`city ilike '${esc(p.get("city"))}%'`);
+  if (p.get("sector")) w.push(sectorPred(esc(p.get("sector"))));
+  if (p.get("min_value")) w.push(`valuation >= ${int(p.get("min_value"), 0, 1e12)}`);
+  if (p.get("since")) w.push(`issued_date >= '${esc(p.get("since"))}'`);
+  if (p.get("q")) w.push(`(address ilike '%${esc(p.get("q"))}%' or applicant ilike '%${esc(p.get("q"))}%' or zip='${esc(p.get("q"))}')`);
+  if (p.get("geo") === "1") w.push("lat is not null");
+  const where = w.join(" and ");
+  const limit = int(p.get("limit"), 100, 1000), offset = int(p.get("offset"), 0, 5e6);
+  const total = Number((await q(`select count(*) from building_permits where ${where}`)).trim() || 0);
+  const rows = await q(`select coalesce(json_agg(t),'[]') from (
+    select source as jurisdiction, permit_number, address, city, state, zip, lat, lon,
+           permit_type, valuation, status, issued_date::text, applicant
+    from building_permits where ${where}
+    order by valuation desc nulls last, issued_date desc nulls last
+    limit ${limit} offset ${offset}) t`);
+  return { total, count: limit, offset, results: JSON.parse(rows) };
+}
+
+async function homes(p) {
+  const w = ["status='PUBLISHED'", "\"isDemo\"=false"];
+  if (p.get("state")) w.push(`state='${esc(p.get("state")).toUpperCase()}'`);
+  if (p.get("city")) w.push(`city ilike '${esc(p.get("city"))}%'`);
+  if (p.get("min_price")) w.push(`price >= ${int(p.get("min_price"), 0, 1e12)}`);
+  if (p.get("max_price")) w.push(`price <= ${int(p.get("max_price"), 0, 1e12)}`);
+  const where = w.join(" and ");
+  const limit = int(p.get("limit"), 100, 1000), offset = int(p.get("offset"), 0, 5e6);
+  const total = Number((await q(`select count(*) from "InventoryHome" where ${where}`)).trim() || 0);
+  const rows = await q(`select coalesce(json_agg(t),'[]') from (
+    select h.street, h.city, h.state, h.zip, h.lat, h.lon, h.price, h.beds, h."bathsTotal" as baths,
+           h.sqft, h."constructionStatus" as status, b.name as builder
+    from "InventoryHome" h join "Builder" b on b.id=h."builderId"
+    where ${where} order by h.price desc nulls last limit ${limit} offset ${offset}) t`);
+  return { total, count: limit, offset, results: JSON.parse(rows) };
+}
+
+async function developments(p) {  // CRE pipeline: top commercial/multifamily by value, grouped by city
+  const st = p.get("state") ? `and state='${esc(p.get("state")).toUpperCase()}'` : "";
+  const rows = await q(`select coalesce(json_agg(t),'[]') from (
+    select city, state, count(*) as projects,
+           count(*) filter (where permit_type ilike '%commercial%') as commercial,
+           count(*) filter (where permit_type ilike '%apartment%') as multifamily,
+           round(sum(valuation)) as total_valuation, round(max(valuation)) as largest
+    from building_permits
+    where (permit_type ilike '%commercial%' or permit_type ilike '%apartment%') and valuation is not null ${st}
+    group by city, state order by total_valuation desc nulls last limit 50) t`);
+  return { pipeline: JSON.parse(rows) };
+}
+
+async function summary() {
+  const rows = await q(`select coalesce(json_agg(t),'[]') from (
+    select state,
+      count(*) as permits,
+      count(*) filter (where permit_type ilike '%commercial%') as commercial,
+      count(*) filter (where permit_type ilike '%apartment%') as multifamily,
+      count(*) filter (where lat is not null) as geocoded
+    from building_permits group by state order by permits desc) t`);
+  const homes = (await q(`select count(*) from "InventoryHome" where status='PUBLISHED'`)).trim();
+  const perm = (await q(`select count(*) from building_permits`)).trim();
+  return { total_permits: Number(perm), total_homes: Number(homes), by_state: JSON.parse(rows) };
+}
+
+const DOCS = {
+  service: "HomesOnSpec Data Feed",
+  description: "Read-only new-construction development pipeline: building permits (commercial/residential/multifamily) + new-home listings. Source for RENTV & partners.",
+  endpoints: {
+    "GET /feed/permits": "Building permits. params: state, city, sector(commercial|multifamily|residential), min_value, since(YYYY-MM-DD), q, geo(1=has coords), limit(<=1000), offset",
+    "GET /feed/homes": "New-home listings. params: state, city, min_price, max_price, limit, offset",
+    "GET /feed/developments": "CRE pipeline — top commercial/multifamily projects by total valuation, grouped by city. params: state",
+    "GET /feed/summary": "Counts by state + sector",
+  },
+  cors: "enabled (Access-Control-Allow-Origin: *)",
+  examples: [
+    "/feed/permits?sector=commercial&state=CA&min_value=1000000&geo=1&limit=50",
+    "/feed/developments?state=CA",
+    "/feed/homes?state=CA&min_price=500000",
+  ],
+};
+
+const server = createServer(async (req, res) => {
+  res.setHeader("Access-Control-Allow-Origin", "*");
+  res.setHeader("content-type", "application/json");
+  const u = new URL(req.url, "http://x");
+  try {
+    if (u.pathname === "/" || u.pathname === "/feed") return res.end(JSON.stringify(DOCS, null, 2));
+    if (u.pathname === "/feed/permits") return res.end(JSON.stringify(await permits(u.searchParams)));
+    if (u.pathname === "/feed/homes") return res.end(JSON.stringify(await homes(u.searchParams)));
+    if (u.pathname === "/feed/developments") return res.end(JSON.stringify(await developments(u.searchParams)));
+    if (u.pathname === "/feed/summary") return res.end(JSON.stringify(await summary()));
+  } catch (e) { res.statusCode = 500; return res.end(JSON.stringify({ error: String(e) })); }
+  res.statusCode = 404; res.end(JSON.stringify({ error: "not found", see: "/" }));
+});
+server.listen(PORT, () => console.log(`feed-api on http://127.0.0.1:${PORT}`));

← 847b94d admin/ingestion: honest error signal — surface per-record ho  ·  back to Homesonspec  ·  admin/ingestion: LEFT JOIN active sources — surface stale/ab 04910e0 →