Advanced filtering and paging
Intermediate
Know which filters the API applies and which you apply, page efficiently and cache to save requests.
Filters the API applies
| Endpoint | Server-side filters |
|---|---|
/api/v1/metrics | city or a rectangle (north, south, east, west), past_days, forward_days |
/api/v1/trends | city, metric, source (all, gathern, airbnb), days |
/api/v1/listings | city, limit, cursor |
/api/v1/estimate | location and radiusKm, bedrooms, typeId |
Everything else, such as listing source, district, rating or price band, you filter in your own code after fetching.
Page through a city efficiently
Use limit=500, the maximum, to minimise requests. Stop when nextCursor is null.
import { api } from "./jokerprice.mjs"; // helper from the quickstart
export async function allListings(city) {
const out = [];
let cursor = null;
do {
const page = await api("/api/v1/listings", { query: { city, limit: 500, ...(cursor ? { cursor } : {}) } });
out.push(...page.listings);
cursor = page.nextCursor;
} while (cursor);
return out;
}Cache for the day
Listings change slowly. Cache each city once a day and run as many filters as you like against the file.
import { existsSync, readFileSync, writeFileSync } from "node:fs";
export async function cachedListings(city) {
const file = city + "-" + new Date().toISOString().slice(0, 10) + ".json";
if (existsSync(file)) return JSON.parse(readFileSync(file, "utf8"));
const listings = await allListings(city);
writeFileSync(file, JSON.stringify(listings));
return listings;
}A composable filter
const where = {
source: s => l => l.source === s,
district: d => l => (l.district || "").toLowerCase().includes(d.toLowerCase()),
type: id => l => String(l.typeId) === String(id),
bedrooms: (min, max = min) => l => l.bedrooms >= min && l.bedrooms <= max,
price: (min, max) => l => l.nightly >= min && l.nightly <= max,
rating: min => l => (l.rating ?? 0) >= min,
reviews: min => l => (l.reviews ?? 0) >= min,
seenWithin: days => l => Date.now() - Date.parse(l.updatedAt) <= days * 86400000,
};
const all = (...tests) => l => tests.every(t => t(l));
const listings = await cachedListings("riyadh");
const picks = listings.filter(all(where.type(6), where.bedrooms(2, 3), where.price(300, 700), where.rating(9), where.reviews(10), where.seenWithin(14)));
console.log(picks.length, "matching listings");Scope metrics the same way
Metrics cannot filter by bedrooms or type through the API, but a rectangle around the listings you kept narrows performance to the same area.
const lat = picks.map(l => l.latitude), lng = picks.map(l => l.longitude);
const box = { north: Math.max(...lat), south: Math.min(...lat), east: Math.max(...lng), west: Math.min(...lng) };
const { metrics } = await api("/api/v1/metrics", { query: { ...box, past_days: 60 } });
console.log(metrics.maturity, metrics.past.occupancy);The rectangle must be non-empty (south below north, west below east) and at most 5 degrees across.
Compare sources in trends
for source in all gathern airbnb; do
curl -s -H "Authorization: Bearer $JOKERPRICE_API_KEY" \
"https://jokerprice.com/api/v1/trends?city=riyadh&metric=asking&source=$source&days=30" | jq -r '.trends.points[-1] | "\(.day) \(.value)"'
done