The legitimacy boundary
roost reads publicly visible, logged-out Google Hotels results, at personal, single-user
scale — a handful of queries for your own trip planning, at human-like pacing. It never books,
never transacts, never logs in, and never sends a credential.
What it reads
Section titled “What it reads”The google backend fetches one path:
https://www.google.com/travel/hotels/<city-slug>with a ts= search descriptor (dates, guests, currency — see how it works
for the descriptor itself). That path is not disallowed by Google’s robots.txt.
What it refuses to touch
Section titled “What it refuses to touch”Google’s robots.txt disallows a specific set of paths, and roost stays off all of them:
/travel/search/travel/entity/travel/clk/travel/lodging/clk/travel/story/hotels/rpc/hotelfinder/rpcroost also refuses any path containing /stories (/travel/hotels/<city>/stories included),
even though that isn’t one of the literal prefixes above.
This isn’t a convention roost’s authors try to remember — it’s enforced in code, at the one
choke point every google-backend request passes through: backend.http_get. Before any request
leaves the process, it’s checked against an allowlist:
ALLOWED_PATH_PREFIXES = ("/travel/hotels/",)
DISALLOWED_PATH_PREFIXES = ( "/travel/search", "/travel/entity", "/travel/clk", "/travel/lodging/clk", "/travel/story", "/hotels/rpc", "/hotelfinder/rpc",)
def path_allowed(path: str) -> bool: path = path.split("?")[0] if any(path.startswith(p) for p in DISALLOWED_PATH_PREFIXES): return False if "/stories" in path: return False return any(path.startswith(p) for p in ALLOWED_PATH_PREFIXES)A request that fails this check never reaches the network — http_get raises before calling the
HTTP client, with exit code 6 (permission denied). If you ever see that error, it’s a bug in
roost, not something to work around.
No credentials, ever, on the default path
Section titled “No credentials, ever, on the default path”--backend google sends no login, no cookies, no API key, no session token. There is nothing to
leak because there is nothing to send. The only secret anywhere in roost is the optional SerpApi
key used by --backend serpapi, and it’s never sent to Google — it goes to SerpApi’s own API,
read from the OS keyring or ROOST_SERPAPI_KEY, never as a command-line argument. See
configuration for the full resolution order.
No evasion — the hard rule
Section titled “No evasion — the hard rule”roost reduces the volume and pace of its requests. It does not disguise them. Specifically,
roost never does any of the following:
- Solve a CAPTCHA
- Rotate proxies or IPs
- Use residential proxies
- Replay a browser session
The one place this rule gets teeth in code is the HTTP fingerprint. roost presents itself as a
pinned browser build:
IMPERSONATE = "chrome_133"A pre-build spike found that the underlying impersonation library can silently fall back to a random fingerprint when it doesn’t recognize the requested one — meaning the fingerprint you think you’re presenting isn’t the one actually going out on the wire. That’s the exact kind of disguise this boundary forbids, so roost doesn’t allow it to happen quietly. If the pinned fingerprint isn’t available, the request hard-fails — config error, “upgrade roost’s HTTP client” — rather than falling back to a random identity:
raise AppError(ExitCode.CONFIG, "FINGERPRINT_UNAVAILABLE", f"the pinned browser fingerprint is unavailable: {exc}", "upgrade roost's HTTP client; roost will not fall back to a random " "fingerprint, because presenting one we did not choose is exactly " "the disguise the legitimacy boundary forbids") from excIf Google blocks roost — a 429, a 403, a 503, or a challenge page served as HTTP 200 — the
correct response is to stop, which is exactly what the politeness
circuit breaker does. Retrying through a block by rotating identity is off the table by design.
The risk you accept
Section titled “The risk you accept”Google’s Terms of Service discourage automated access to its properties. roost doesn’t get you
out from under that — it minimizes what it takes (read-only, one allowlisted path, no
credentials, throttled) but the tool can still draw a rate limit, a CAPTCHA challenge, or simply
stop working if Google reshapes the page. Expect breakage; that’s what exit code 21
(SCHEMA_DRIFT) exists to name plainly instead of crashing.
Prices roost reports are Google Hotels metasearch lead-in rates — not a booking guarantee.
Every response’s scope.note says so by default:
"note": "lead-in rates from Google Hotels metasearch; not a booking guarantee — confirm on the property's own site"Confirm on the property’s own site (or the OTA named in sources) before relying on a number
roost reports.
roost is built for personal, single-user use: a handful of searches while you plan a trip, not
a scraping operation. The throttle defaults (a minimum interval between requests, a small burst
allowance, and a circuit breaker that opens on a block) exist to keep it there — see
politeness for the numbers.