Compare markets side by side
Intermediate
Build a comparison table across cities in the Gulf and Egypt and save it as CSV.
Setup
This tutorial uses the jokerprice.mjs helper from the quickstart.
// jokerprice.mjs: a small client for the JokerPrice API (Node 18+).
const BASE = "https://jokerprice.com";
const KEY = process.env.JOKERPRICE_API_KEY;
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
export async function api(path, { method = "GET", query = {}, body } = {}) {
const url = new URL(BASE + path);
for (const [name, value] of Object.entries(query)) url.searchParams.set(name, String(value));
for (;;) {
const res = await fetch(url, {
method,
headers: { Authorization: "Bearer " + KEY, ...(body ? { "Content-Type": "application/json" } : {}) },
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (res.status === 429 && data.error.code === "RATE_LIMIT") { await sleep(Number(res.headers.get("Retry-After") || 60) * 1000); continue; }
if (!res.ok) throw new Error(res.status + " " + data.error.code + ": " + data.error.message);
return data;
}
}Pick the markets
One call to /api/v1/markets already covers supply, rates and the 90-night tracker summary for every destination. Fetch each full report only for the cities you want bedroom detail for.
import { api } from "./jokerprice.mjs";
const SLUGS = ["riyadh", "jeddah", "dubai", "doha", "cairo"];
const { markets } = await api("/api/v1/markets");
const known = new Set(markets.map(m => m.slug));
const chosen = SLUGS.filter(slug => known.has(slug));
console.log("Comparing", chosen.join(", "));Fetch the reports
const reports = [];
for (const slug of chosen) reports.push((await api("/api/v1/markets/" + slug)).market);
const twoBed = m => m.bedrooms.find(b => b.bedrooms === 2)?.median ?? null;
const live = m => m.tracker && m.tracker.maturity === "estimating" ? m.tracker : null;
const rows = reports.map(m => ({
market: m.name,
listings: m.listings,
median: Math.round(m.nightly.median ?? 0),
spread: m.nightly.p75 && m.nightly.p25 ? +(m.nightly.p75 / m.nightly.p25).toFixed(2) : null,
twoBedMedian: twoBed(m),
topType: m.types[0]?.type ?? null,
occupancy: live(m) ? +(live(m).occupancy * 100).toFixed(1) : null,
adr: live(m) ? Math.round(live(m).adr) : null,
revenuePerListing: live(m) ? Math.round(live(m).monthlyRevenuePerListing) : null,
}));
console.table(rows);spread is the 75th percentile rate divided by the 25th: a high value means a market with very different price tiers.
Save as CSV
import { writeFileSync } from "node:fs";
const cols = Object.keys(rows[0]);
const cell = v => (v === null || v === undefined ? "" : '"' + String(v).replace(/"/g, '""') + '"');
writeFileSync("markets.csv", [cols.join(","), ...rows.map(r => cols.map(c => cell(r[c])).join(","))].join("\n"));Compare fairly
- Compare occupancy only between markets whose tracker says
estimating. - All rates are requested in SAR from the platforms, so cities in different countries compare directly; they are still asking rates, not achieved rates.
- Property mix matters: a city dominated by chalets will have a different median than one dominated by studios. Use the bedroom medians or
typesto compare like with like.