5 Real Estate Data Automations You Can Build This Weekend
Real estate investing involves a lot of repetitive data lookups -- checking listings, verifying contractors, pulling violation records. Most investors do this manually, tabbing between Redfin, city databases, and license lookup portals.
Here are 5 automations you can build in an afternoon using publicly available marketplace APIs. Each one includes working code you can copy and run.
What You'll Need
- Node.js 18+ (or Python 3.8+)
- A free RapidAPI account for API keys
- About 2 hours
The APIs used in this article all have free tiers with 50 requests/month, which is plenty for testing and small-scale use.
1. Property Deal Screener
The problem: You want to filter Redfin listings by your investment criteria -- price per square foot, price drops, minimum beds/baths -- without refreshing Redfin all day.
The automation: A script that pulls listings from a Redfin search URL and filters them against your deal criteria.
// deal-screener.js
const BACKEND = "https://rapidapi-backend-production.up.railway.app";
async function screenDeals(redfinUrl, criteria) {
const res = await fetch(
`${BACKEND}/redfin/search?url=${encodeURIComponent(redfinUrl)}&limit=50`
);
const { results } = await res.json();
return results.filter((listing) => {
const pricePerSqft = listing.price / (listing.sqft || 1);
return (
pricePerSqft <= (criteria.maxPricePerSqft || Infinity) &&
(listing.beds || 0) >= (criteria.minBeds || 0) &&
(listing.baths || 0) >= (criteria.minBaths || 0) &&
listing.price <= (criteria.maxPrice || Infinity)
);
});
}
// Example: Find deals in Austin under $200/sqft
const deals = await screenDeals(
"https://www.redfin.com/city/30818/TX/Austin/filter/max-price=500000",
{
maxPricePerSqft: 200,
minBeds: 3,
minBaths: 2,
maxPrice: 500000,
}
);
console.log(`Found ${deals.length} deals:`);
deals.forEach((d) => {
console.log(` $${d.price.toLocaleString()} | ${d.beds}bd/${d.baths}ba | ${d.sqft} sqft | $${Math.round(d.price / d.sqft)}/sqft`);
console.log(` ${d.address}`);
});
Run this on a cron (daily or hourly) and you have a deal pipeline without manual Redfin browsing.
2. Violation Check Before Purchase
The problem: Before buying a property in NYC, you need to check for open building violations. The city has three separate databases (DOB Violations, DOB Safety Violations, DOB ECB Violations), and navigating them manually is painful.
The automation: A single API call that queries all three databases and gives you a risk summary.
// violation-check.js
const BACKEND = "https://rapidapi-backend-production.up.railway.app";
async function checkViolations(address) {
// Parse house number and street from address
const match = address.match(/^(\d+[\w-]*)\s+(.+)/);
if (!match) throw new Error("Address format: '123 Main Street'");
const [, houseNumber, street] = match;
const summaryRes = await fetch(
`${BACKEND}/nyc-violations/summary?house_number=${encodeURIComponent(houseNumber)}&street=${encodeURIComponent(street)}&borough=manhattan`
);
const summary = await summaryRes.json();
return {
address,
totalViolations: summary.totalViolations,
openViolations: summary.openVsClosed.open,
closedViolations: summary.openVsClosed.closed,
unpaidPenalties: summary.ecbPenalties.totalBalanceDue,
riskLevel:
summary.openVsClosed.open > 10 ? "HIGH" :
summary.openVsClosed.open > 3 ? "MEDIUM" : "LOW",
violationsByYear: summary.byYear,
};
}
// Example: Check a building before making an offer
const report = await checkViolations("240 East 46th Street");
console.log(`Risk Level: ${report.riskLevel}`);
console.log(`Open violations: ${report.openViolations}`);
console.log(`Unpaid ECB penalties: $${report.unpaidPenalties.toLocaleString()}`);
console.log(`Violations by year:`, report.violationsByYear);
A building with 15 open violations and $50K in unpaid ECB penalties is a very different purchase than one with a clean record. This automation takes what used to be an hour of manual lookups and turns it into a 2-second check.
3. Contractor License Verification
The problem: Your contractor says they are licensed. Are they? Checking each state's licensing board website is a different UX every time -- California CSLB, Texas TDLR, Florida DBPR, NYC DOB all have different interfaces.
The automation: A unified verification script that checks across CA, TX, FL, and NY.
// verify-contractor.js
const BACKEND = "https://rapidapi-backend-production.up.railway.app";
async function verifyContractor(state, licenseNumber) {
const res = await fetch(
`${BACKEND}/contractor-license/verify?state=${state}&licenseNumber=${encodeURIComponent(licenseNumber)}`
);
const data = await res.json();
if (!data.success || data.count === 0) {
return { verified: false, message: "License not found" };
}
const license = data.results[0];
return {
verified: true,
status: license.status,
businessName: license.businessName || license.name,
expirationDate: license.expirationDate,
classifications: license.classifications || license.licenseType,
isExpired: license.status?.toLowerCase().includes("expired"),
isActive: license.status?.toLowerCase().includes("active"),
};
}
// Example: Verify a California contractor
const result = await verifyContractor("CA", "1098967");
if (result.verified && result.isActive) {
console.log(`License ACTIVE: ${result.businessName}`);
console.log(`Classifications: ${result.classifications}`);
console.log(`Expires: ${result.expirationDate}`);
} else {
console.log(`WARNING: ${result.message || result.status}`);
}
Python version (for those who prefer it):
import requests
BACKEND = "https://rapidapi-backend-production.up.railway.app"
def verify_contractor(state, license_number):
resp = requests.get(f"{BACKEND}/contractor-license/verify", params={
"state": state,
"licenseNumber": license_number
})
data = resp.json()
if not data.get("success") or data.get("count", 0) == 0:
return {"verified": False, "message": "License not found"}
lic = data["results"][0]
return {
"verified": True,
"status": lic.get("status"),
"business": lic.get("businessName") or lic.get("name"),
"expires": lic.get("expirationDate"),
}
result = verify_contractor("CA", "1098967")
print(result)
4. Price Drop Alert System
The problem: You are watching a few neighborhoods for price reductions. Redfin sends generic email alerts, but you want custom thresholds -- only notify me when a listing drops more than 5%.
The automation: A script that compares current listings against previously saved data and alerts you via Slack webhook when significant drops occur.
// price-alerts.js
import fs from "fs";
const BACKEND = "https://rapidapi-backend-production.up.railway.app";
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
const DATA_FILE = "./price-history.json";
// Load previous snapshot
function loadHistory() {
try { return JSON.parse(fs.readFileSync(DATA_FILE, "utf-8")); }
catch { return {}; }
}
function saveHistory(data) {
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
}
async function checkPriceDrops(redfinUrl, dropThreshold = 0.05) {
const res = await fetch(
`${BACKEND}/redfin/search?url=${encodeURIComponent(redfinUrl)}&limit=50`
);
const { results } = await res.json();
const history = loadHistory();
const alerts = [];
for (const listing of results) {
const key = listing.address || listing.url;
if (!key) continue;
const prev = history[key];
if (prev && listing.price < prev.price) {
const dropPct = (prev.price - listing.price) / prev.price;
if (dropPct >= dropThreshold) {
alerts.push({
address: key,
previousPrice: prev.price,
currentPrice: listing.price,
dropPercent: (dropPct * 100).toFixed(1),
beds: listing.beds,
baths: listing.baths,
sqft: listing.sqft,
});
}
}
// Update history
history[key] = { price: listing.price, lastSeen: new Date().toISOString() };
}
saveHistory(history);
return alerts;
}
async function sendSlackAlert(alerts) {
if (!alerts.length || !SLACK_WEBHOOK) return;
const lines = alerts.map((a) =>
`*${a.address}*\n$${a.previousPrice.toLocaleString()} -> $${a.currentPrice.toLocaleString()} (${a.dropPercent}% drop) | ${a.beds}bd/${a.baths}ba`
);
await fetch(SLACK_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `Price Drop Alerts (${alerts.length} found):\n\n${lines.join("\n\n")}`,
}),
});
}
// Run daily via cron
const alerts = await checkPriceDrops(
"https://www.redfin.com/city/30818/TX/Austin/filter/max-price=600000",
0.05 // alert on 5%+ drops
);
console.log(`${alerts.length} price drops found`);
await sendSlackAlert(alerts);
Set this up as a cron job (crontab -e, run every morning at 8am) and you will get Slack notifications only when real price drops happen.
5. Comp Analysis Automation
The problem: When evaluating a property, you need comparable sales ("comps") in the area. You want to pull active listings near a target address, check them for violations, and score them.
The automation: A script that combines Redfin data with violation checks for a complete comp analysis.
// comp-analysis.js
const BACKEND = "https://rapidapi-backend-production.up.railway.app";
async function analyzeComps(redfinSearchUrl) {
// Step 1: Pull listings
const listingsRes = await fetch(
`${BACKEND}/redfin/search?url=${encodeURIComponent(redfinSearchUrl)}&limit=20`
);
const { results: listings } = await listingsRes.json();
// Step 2: Calculate market stats
const prices = listings.map((l) => l.price).filter(Boolean);
const pricesPerSqft = listings
.filter((l) => l.price && l.sqft)
.map((l) => l.price / l.sqft);
const stats = {
count: listings.length,
medianPrice: median(prices),
avgPrice: Math.round(avg(prices)),
medianPricePerSqft: Math.round(median(pricesPerSqft)),
priceRange: { min: Math.min(...prices), max: Math.max(...prices) },
};
// Step 3: Score each listing
const scored = listings.map((l) => {
const ppsf = l.sqft ? l.price / l.sqft : null;
const belowMedian = ppsf && ppsf < stats.medianPricePerSqft;
return {
address: l.address,
price: l.price,
pricePerSqft: ppsf ? Math.round(ppsf) : null,
beds: l.beds,
baths: l.baths,
sqft: l.sqft,
yearBuilt: l.yearBuilt,
dealScore: belowMedian ? "BELOW MARKET" : "AT MARKET",
};
});
return { stats, listings: scored };
}
function median(arr) {
const s = [...arr].sort((a, b) => a - b);
const mid = Math.floor(s.length / 2);
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
}
function avg(arr) {
return arr.reduce((a, b) => a + b, 0) / arr.length;
}
// Example: Analyze comps in Brooklyn
const analysis = await analyzeComps(
"https://www.redfin.com/neighborhood/655/NY/New-York/Brooklyn-Heights/filter/max-price=1500000"
);
console.log("Market Stats:", analysis.stats);
console.log("\nBelow-market listings:");
analysis.listings
.filter((l) => l.dealScore === "BELOW MARKET")
.forEach((l) => {
console.log(` ${l.address} | $${l.price?.toLocaleString()} | $${l.pricePerSqft}/sqft`);
});
Putting It Together
The real power comes from chaining these automations. Here is a workflow an investor might use:
- Morning cron runs the deal screener and price drop alerts
- When a deal surfaces, automatically run violation checks on the property
- Before hiring a contractor, run license verification
- When evaluating an offer, run comp analysis on the neighborhood
Each piece is a standalone script under 50 lines. String them together and you have a lightweight real estate intelligence system that runs on a $5/month server.
All the APIs used here are available with free tiers at RapidAPI. The NYC Violations API queries the city's open data directly (Socrata), so it is fast and does not count against any scraping quotas.
Top comments (0)