Skip to content

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.

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.

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

roost 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:

roost/client.py
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.

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

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:

roost/backend.py
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:

roost/backend.py
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 exc

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

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.