Keyword research for Kickside — Google Ads search demand and keyword ideas through a WebAssembly client, exposed as agent tools, a pullable demand source, and a deterministic demand score.
Run
wippy run butschster/keyword-researchbutschster/keyword-research
Search demand as a first-class capability inside Kickside: keyword metrics and keyword ideas from the Google Ads Keyword Planner, reached natively by the host rather than through an external service or a scraped page.
What it is for
Anything that decides what to write about, what to bid on, or what to build next eventually needs the same fact: how many people actually search for this, and what else they search for around it. Without a number, that question is settled by whoever argues best.
It answers it three ways, for three kinds of caller:
| You have | You want | Use | |---|---|---| | an agent weighing topics or products | numbers it can compare mid-conversation | the agent tools | | a scoring pipeline, a content machine, a dashboard | the same numbers on a schedule, no model in between | the demand source | | your own Lua | normalised rows and an authenticated client | the libraries |
What it exposes
Agent tools — KeywordMetrics for demand on keywords you name,
KeywordIdeas for expanding a seed or a page into what people actually
type. Both return markdown tables, because an agent comparing numbers reads
a table, not JSON. They ship together as the Google Keyword Research
trait.
A pullable demand source (butschster.keyword_research:demand) — the
deterministic path. A Data Sync polls a configured keyword set on a
schedule and the numbers reach a scoring factor without a model in between.
Demand is a full snapshot, keyed per keyword per geo per language per day,
so history accumulates and drift is visible instead of a frozen figure
quietly going stale.
Libraries — keywords (normalised rows), client (token exchange with
a cached access token), config (settings), signer (the assertion, and
the WASM result → (value, err) boundary).
Why a WebAssembly component
Google service accounts authenticate with an RS256-signed assertion, and the
Wippy Lua runtime cannot produce one — crypto verifies JWTs but has no
private-key signature. The component covers exactly that one operation and
nothing else: no host capability beyond the defaults, and the private key
travels a single call path. Networking stays in Lua's own http_client.
Configuration
All settings live in the Settings UI under butschster.keyword_research,
encrypted where they are secret. Nothing is read from the environment:
service_account_json (the whole key file, so a mismatched
client_email/private_key pair is impossible), developer_token,
customer_id, login_customer_id (only behind a manager account),
subject (the user the service account impersonates), and api_version —
Google retires a version about a year after release, so it is configuration
rather than a constant.
Units, and data that is absent
Bids arrive in micros and are converted once, at the boundary. A keyword
Google has no data for keeps a nil demand figure rather than a zero:
collapsing the two understates demand silently, and both tools say which
they mean instead of printing a confident 0.
Mounting
One dependency (kickside/contract) and one binding —
butschster.keyword_research.security:user_security_scope. The signing
component travels with the module and its integrity hash is checked at load.