← 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 →