Skip to content

Finding the cheapest dates

roost search answers “what’s available on these exact dates.” roost dates answers a different question: “which dates should I pick?” It sweeps a range of check-in dates for the same stay length and reports availability and price for each one, so you can spot the cheap night without running search in a loop yourself.

An agent trying to find a cheap weekend by hand would run roost search once per candidate date, watch the throttle, and stitch the results together. roost dates does that sweep in one invocation, with the request budget and the politeness throttle built in.

Each sampled check-in date costs one upstream request — roost dates calls search once per date, holding the stay length (--nights or --check-out) fixed. There is no bulk endpoint; the sweep is a bounded loop that reuses the same throttle every other command respects.

That’s also why it’s the only roost command that can burn more than one request per invocation, and why it’s the only one hard-capped by --max-requests.

The three flags that decide the request count

Section titled “The three flags that decide the request count”

Three flags interact to determine exactly how many dates get sampled:

  • --window (default 30) — how many days forward to sweep, starting from --check-in (default: today).
  • --step (default 1) — the gap in days between sampled check-in dates.
  • --max-requests (default 8) — a hard cap on upstream requests, regardless of how large --window / --step make the candidate list.

roost first builds the full list of offsets 0, step, 2*step, ... up to --window, then takes only the first --max-requests of them. If the candidate list is longer than the cap, the result comes back truncated: true — the sweep still ran, it just didn’t cover every date in the window.

All three must be at least 1, or the command exits with code 2 (usage error).

Before spending any requests, run the sweep with --dry-run. It computes the same offset math and prints the plan — no upstream calls, exit 0, requestsIssued isn’t even part of the picture yet:

Terminal window
roost dates "lisbon" --window 30 --nights 2 --dry-run --json
{
"schemaVersion": "1",
"query": {
"location": "lisbon",
"checkIn": "2026-08-21",
"checkOut": "2026-08-23",
"nights": 2,
"guests": { "adults": 2, "children": 0 },
"currency": "USD"
},
"scope": {
"backend": "google",
"brandsFilter": [],
"partial": false,
"note": "dry run: no upstream requests were made"
},
"plan": {
"window": 30,
"step": 1,
"maxRequests": 8,
"requestsPlanned": 8,
"sampledDates": [
"2026-08-21", "2026-08-22", "2026-08-23", "2026-08-24",
"2026-08-25", "2026-08-26", "2026-08-27", "2026-08-28"
]
}
}

With --dry-run, the envelope carries a plan key instead of dates. Read sampledDates before you commit to the real sweep — with the defaults above, a 30-day window at a 1-day step only actually samples the first 8 days, because --max-requests caps it. That’s the signal to either raise --max-requests or widen --step so the sampled dates spread across the whole window instead of clustering at the front.

Drop --dry-run and roost issues one request per sampled date:

Terminal window
roost dates "lisbon" --window 9 --step 3 --max-requests 3 --nights 2 --json
{
"schemaVersion": "1",
"query": { "location": "lisbon", "checkIn": "2026-08-21", "checkOut": "2026-08-23", "nights": 2,
"guests": { "adults": 2, "children": 0 }, "currency": "USD" },
"scope": { "backend": "google", "brandsFilter": [], "partial": false, "note": "" },
"dates": [
{ "date": "2026-08-21", "available": true, "perNight": { "amount": 142.0, "currency": "USD" },
"total": { "amount": 284.0, "currency": "USD" }, "currency": "USD", "property": "Hotel A" },
{ "date": "2026-08-24", "available": true, "perNight": { "amount": 118.0, "currency": "USD" },
"total": { "amount": 236.0, "currency": "USD" }, "currency": "USD", "property": "Hotel B" },
{ "date": "2026-08-27", "available": false, "perNight": null, "total": null, "currency": "USD" }
],
"cheapest": { "date": "2026-08-24", "available": true,
"perNight": { "amount": 118.0, "currency": "USD" },
"total": { "amount": 236.0, "currency": "USD" },
"currency": "USD", "property": "Hotel B" },
"requestsIssued": 3,
"truncated": false
}

Four fields matter beyond the standard envelope:

  • dates — one entry per sampled check-in date, in order. available: false means the sweep ran for that date and found nothing; a missing perNight/total (both null) goes with it. Every priced entry keeps the standard {amount, currency} money shape.
  • cheapest — the single lowest-perNight entry among the available dates, or null if none were available. It’s a convenience pointer into dates, not a separate lookup.
  • requestsIssued — how many upstream requests the sweep actually made. Compare it to the planned count from --dry-run to see whether the sweep completed or stopped early.
  • truncatedtrue when the candidate list (from --window / --step) was longer than --max-requests, so part of the window was never sampled at all. This is independent of whether a block cut the sweep short.

The sweep shares the same throttle and circuit breaker as every other command (see politeness). If a block or rate limit hits partway through — and at least one date has already been sampled — roost dates does not fail the whole command. It stops sampling and returns what it already collected, with a marker entry appended:

{ "date": "2026-08-30", "available": null, "perNight": null, "total": null,
"currency": "USD", "error": "BLOCKED" }

available: null (not false) distinguishes “we don’t know, we got stopped” from “we asked and there was nothing.” The command still exits 0 in this case — a partial sweep is a real, usable answer, not a failure. If the very first request in the sweep is blocked, there’s nothing partial to return, so the command exits with the normal block/rate-limit exit code instead (20 for a block, 7 for plain rate limiting — see exit codes).

By default roost fails fast on a throttle hit — it never sleeps silently. A dates sweep issues several requests back to back, so it’s the command most likely to run into the DEFAULT_MIN_INTERVAL spacing (6 seconds between requests) partway through. Add --wait to let it sleep through spacing and short cooldowns instead of stopping:

Terminal window
roost dates "lisbon" --window 30 --step 2 --max-requests 8 --wait --max-wait 60 --json

--max-wait (default 30 seconds) caps how long any single wait can run. If a cooldown would outlast --max-wait, roost still fails fast rather than blocking indefinitely — raise --max-wait for a sweep you’re willing to let run longer, rather than turning throttling off with --no-throttle.

A month at 3-day steps, staying inside the default request budget:

Terminal window
roost dates "austin" --window 30 --step 3 --max-requests 10 --nights 2 --json

window=30, step=3 produces 10 candidate dates — exactly the cap, so nothing gets truncated.

Weekend-only check-ins, sampling every 7 days starting from a Friday:

Terminal window
roost dates "austin" --check-in 2026-09-04 --window 56 --step 7 --max-requests 8 --nights 2 --json

roost dates doesn’t know about weekdays — it samples evenly from --check-in. Pick a --check-in that already falls on the weekday you want (a Friday here) and a --step of 7 so every sampled date lands on the same weekday.

Always confirm the plan with --dry-run before running either of these for real — sampledDates will tell you immediately if the step/window/cap combination doesn’t cover the dates you actually care about.