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.
The problem it solves
Section titled “The problem it solves”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.
How the sweep works
Section titled “How the sweep works”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/--stepmake 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).
Always --dry-run first
Section titled “Always --dry-run first”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:
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.
Reading the real result
Section titled “Reading the real result”Drop --dry-run and roost issues one request per sampled date:
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: falsemeans the sweep ran for that date and found nothing; a missingperNight/total(bothnull) goes with it. Every priced entry keeps the standard{amount, currency}money shape.cheapest— the single lowest-perNightentry among the available dates, ornullif none were available. It’s a convenience pointer intodates, not a separate lookup.requestsIssued— how many upstream requests the sweep actually made. Compare it to the planned count from--dry-runto see whether the sweep completed or stopped early.truncated—truewhen 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.
What happens if a block occurs mid-sweep
Section titled “What happens if a block occurs mid-sweep”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).
Using --wait for long sweeps
Section titled “Using --wait for long sweeps”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:
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.
Recipes
Section titled “Recipes”A month at 3-day steps, staying inside the default request budget:
roost dates "austin" --window 30 --step 3 --max-requests 10 --nights 2 --jsonwindow=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:
roost dates "austin" --check-in 2026-09-04 --window 56 --step 7 --max-requests 8 --nights 2 --jsonroost 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.