Skip to content

Instantly share code, notes, and snippets.

@Paxa
Last active August 30, 2026 14:37
Show Gist options
  • Select an option

  • Save Paxa/38b6905a5ac55192b3abe7101e6f7a14 to your computer and use it in GitHub Desktop.

Select an option

Save Paxa/38b6905a5ac55192b3abe7101e6f7a14 to your computer and use it in GitHub Desktop.
find apartaments without agents on home.ss.ge
#!/usr/bin/env bun
// Usage: bun ss-scan.ts [pages] [priceTo] [daysBack]
// bun ss-scan.ts 40 600 10
const PAGES = Number(Bun.argv[2] ?? 8);
const PRICE_TO = Number(Bun.argv[3] ?? 550);
const DAYS_BACK = Number(Bun.argv[4] ?? 5);
const CUTOFF = new Date(Date.now() - DAYS_BACK * 86400_000);
const UA = { "User-Agent": "Mozilla/5.0" };
async function get(url: string): Promise<string> {
for (let i = 0; i < 4; i++) {
try {
const r = await fetch(url, { headers: UA, signal: AbortSignal.timeout(30_000) });
if (r.ok) return await r.text();
} catch {}
await Bun.sleep(1000 * (i + 1));
}
throw new Error(`fetch failed: ${url}`);
}
function nextData(html: string): any {
const m = html.match(/<script id="__NEXT_DATA__"[^>]*>(.+?)<\/script>/s);
return m ? JSON.parse(m[1]) : null;
}
async function pool<T, R>(items: T[], n: number, fn: (x: T) => Promise<R>): Promise<R[]> {
const out: R[] = new Array(items.length);
let i = 0;
await Promise.all(Array.from({ length: n }, async () => {
while (true) {
const k = i++;
if (k >= items.length) return;
out[k] = await fn(items[k]);
}
}));
return out;
}
const listUrl = (p: number) =>
`https://home.ss.ge/en/real-estate/l/Flat/For-Rent?cityIdList=96&areaFrom=40&currencyId=2&priceType=1&priceTo=${PRICE_TO}&rooms=2%2C3%2C4&order=1&page=${p}`;
const fetchList = async (p: number) =>
nextData(await get(listUrl(p))).props.pageProps.applicationList.realStateItemModel as any[];
const fetchDetail = async (id: number): Promise<{ infoType: number | null; phone: string | null }> => {
const d = nextData(await get(`https://home.ss.ge/en/real-estate/en-${id}`));
const pp = d?.props?.pageProps ?? {};
const raw = pp.applicationData?.applicationPhones?.find((p: any) => p.isMain)?.phoneNumber
?? pp.applicationData?.applicationPhones?.[0]?.phoneNumber
?? null;
return { infoType: pp.initialAgentInfoData?.infoType ?? null, phone: formatPhone(raw) };
};
// Georgian mobile numbers are 9 digits (5XXXXXXXX). International: +995 XXX XX XX XX.
function formatPhone(raw: string | null): string | null {
if (!raw) return null;
let d = raw.replace(/\D/g, "");
if (d.startsWith("995")) d = d.slice(3);
if (d.length !== 9) return `+${raw}`;
return `+995 ${d.slice(0, 3)} ${d.slice(3, 5)} ${d.slice(5, 7)} ${d.slice(7)}`;
}
const pages = Array.from({ length: PAGES }, (_, i) => i + 1);
const raw = (await pool(pages, 4, fetchList)).flat();
console.log(`total list items: ${raw.length}`);
const recent = raw.filter(a => {
if (!a.title?.includes("room Flat For Rent")) return false;
const cd = a.createDate ? new Date(a.createDate) : null;
return cd && cd >= CUTOFF;
});
console.log(`filtered: ${recent.length}`);
const details = await pool(recent, 6, a => fetchDetail(a.applicationId));
const priv = recent
.map((a, i) => ({ ...a, _phone: details[i].phone, _type: details[i].infoType }))
.filter(a => a._type === 0)
.sort((a, b) => +new Date(b.createDate) - +new Date(a.createDate));
console.log(`private: ${priv.length}`);
const fmt = (a: any) => {
const addr = [a.address?.subdistrictTitle, a.address?.streetTitle, a.address?.streetNumber].filter(Boolean).join(", ") || "?";
const rooms = a.title?.split(" ")[0] ?? "?";
const cd = new Date(a.createDate).toLocaleDateString("en-GB", { day: "2-digit", month: "short" });
return `| ${cd} | https://home.ss.ge/en/real-estate/en-${a.applicationId} | ${addr} | ${rooms} | ${a.totalArea} | $${a.price?.priceUsd} | ${a._phone ?? "?"} |`;
};
const out = [
`# Non-agent Batumi flat rentals (40+ m², 2-4 rooms, ≤$${PRICE_TO}, last ${DAYS_BACK} days)`,
``,
`Scanned pages 1-${PAGES} (${raw.length} raw). ${recent.length} in window; ${priv.length} owner-posted.`,
``,
`| Created | URL | Address | Rooms | m² | Price (USD) | Phone |`,
`|---|---|---|---|---|---|---|`,
...priv.map(fmt),
].join("\n");
await Bun.write("ss-non-agent-listings.md", out);
console.log("wrote ss-non-agent-listings.md");

Non-agent Batumi flat rentals (40+ m², 2-4 rooms, ≤$550, last 5 days)

Scanned pages 1-8 (128 raw). 74 in window; 6 owner-posted.

Created URL Address Rooms m² Price (USD) Phone
30 Aug https://home.ss.ge/en/real-estate/en-36575034 Old Batumi District, Chavchavadze st., 76 2 45 $550 +995 558 62 11 72
30 Aug https://home.ss.ge/en/real-estate/en-36574933 Bagrationi District, 14 2 45 $400 +995 558 68 68 40
30 Aug https://home.ss.ge/en/real-estate/en-36573912 Airport District, 3 3 50 $500 +995 551 00 84 85
29 Aug https://home.ss.ge/en/real-estate/en-36573496 Tamar Settlement, 40 2 53 $383 +995 595 38 08 98
29 Aug https://home.ss.ge/en/real-estate/en-36571675 Tamar Settlement, J. Shartava st., 7 2 50 $550 +995 593 12 68 06
29 Aug https://home.ss.ge/en/real-estate/en-36570020 Airport District, GR. Lortkipanidze st., 24 2 40 $400 +995 593 29 80 20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment