Google Trends
Google Trends is reached through an unofficial HTTP adapter with no third-party
wrappers. An official API exists but is in closed alpha with application-gated
access, so this project has none; OfficialTrendsAdapter only reserves the seam
for a future integration.
How a request is performed
The adapter uses a three-step chain:
GET /_/TrendsUi/data/batchexecutewarms up the session and obtains theNIDcookie. The expected response here is HTTP 405: the method is not allowed, but the cookie is already set, so that response counts as success.GET /trends/api/explorereturns the descriptions of the available widgets together with their requests and tokens.GET /trends/api/widgetdata/*loads the time series, the geography and the related queries in turn. A configured pause is held between widgets.
explore starts its body with )]}' and a newline, while widgetdata starts
with )]}', and a newline. The parser strips the common marker and discards
everything before the first JSON object; no fixed-length slice is used.
A failure of one widget yields a partial result with a warning. A failure of the
warm-up or of explore fails the whole request, and so does a request whose
every widget was refused: nothing came back either way. A widget explore never
listed is not requested and is not a failure — its absence is Google's answer.
A request that failed in any of those three ways counts towards the circuit breaker, which rejects new calls immediately once a configured number of them land in a row, so as not to deepen the block or waste time on a network that is bound to fail. A request whose widgets all failed is also kept out of the cache, so the next run asks again rather than reading the outage back for the rest of the TTL. Whether a widget answered is what decides this, not whether the answer had rows in it: a keyword Google has no interest data for comes back just as empty, and that result is cached like any other.
The envelope reports on the whole reply, not on the timeline alone: geography and
related queries are answers too, so a keyword with regions but no timeline is
complete. A reply holding none of the three is empty, and it names the widget
that failed when one did -- "no trend data" reads as Google's verdict on the
keyword, which is the wrong thing to say about a request that never landed.
Normalization and comparison
The 0-100 values are normalized only within a single request: 100 means the
maximum in that particular response. Results of separate requests cannot be
compared directly. The gkai trends compare command and the analyze_trends
MCP tool send up to five keywords in one request.
A comparison splits some widgets one per keyword and suffixes their ids:
explore for two keywords returns TIMESERIES and GEO_MAP under their plain
names but related queries only as RELATED_QUERIES_0 and RELATED_QUERIES_1.
Those are not merged into the single list the result carries -- each is
normalised inside its own widget, so one list of values across keywords would
mean nothing -- and the reply says so in a warning rather than reporting no
related queries at all. tests/fixtures/trends/explore_compare.json is the live
capture this is read from.
A comparison is therefore reported as partial every time, and `gkai trends
compare` exits 1 with it: related queries really are absent from the payload,
and partial is what this project calls usable data that is missing. That is
the expected outcome of a healthy comparison, not a failure — the envelope on
stdout is valid, and completeness_reason names the split rather than claiming
a widget failed. A single-keyword gkai trends is unaffected: there the widget
comes back under its plain name and the answer is complete.
normalization_scope is the first 16 characters of the SHA-256 of canonical
JSON containing the keywords, country, timeframe and language. Identical
parameters produce an identical scope, different parameters a different one.
Comparing numeric values is safe only within the same scope.
Settings
The unofficial provider is enabled by default. It can be turned off with a kill switch:
export GKAI_TRENDS_ENABLED=false
The pause, cache TTL, circuit-breaker threshold and timezone are set by
trends_pacing_seconds, trends_cache_ttl_seconds,
trends_circuit_breaker_failures and trends_timezone_minutes.
Edit on GitHub · 2026-09-04