Developer docs menu

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
// 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.

JavaScript (Node 18+)
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

JavaScript (Node 18+)
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

JavaScript (Node 18+)
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 types to compare like with like.