What it is
Periscope is a command-line tool for SEO research across websites and client projects. It unifies data from Google Search Console, Google Analytics 4, Bing Webmaster Tools, and the Google Ads API (Keyword Planner) into a single local snapshot, so editorial and landing-page decisions can be made from one source instead of toggling between four dashboards.
How Periscope uses the Google Ads API
Periscope calls the Google Ads API solely to retrieve keyword ideas and historical search-volume metrics from the Keyword Planner. Specifically, it uses the KeywordPlanIdeaService and invokes exactly two methods, both read-only.
POST generateKeywordIdeas
Description: Executes seed-based keyword expansion. Returns an adjacent keyword universe suggested by Google Ads based on provided seed keywords, a seed URL, or both.
HTTP Request:
POST /v21/customers/{customerId}:generateKeywordIdeas
Required Headers:
Authorization:Bearer <oauth-access-token>(or service-account access token in the legacy fallback path)developer-token:<google-ads-developer-token>login-customer-id:<manager-customer-id>Content-Type:application/json
Request Body (JSON): Provide exactly one of the following seed configurations:
{
"keywordSeed": {
"keywords": ["<string>"]
},
"urlSeed": {
"url": "https://<string>"
},
"keywordAndUrlSeed": {
"keywords": ["<string>"],
"url": "https://<string>"
}
}
POST generateKeywordHistoricalMetrics
Description Retrieves historical performance data—including average monthly search volume, competition tier, and CPC bid ranges—for a specified array of keywords.
HTTP Request
POST /v21/customers/{customerId}:generateKeywordHistoricalMetrics
Headers
Authorization:Bearer <oauth-access-token>(or service-account access token in the legacy fallback path)developer-token:<google-ads-developer-token>login-customer-id:<manager-customer-id>Content-Type:application/json
Path Parameters
customerId(string): The unique identifier for the target customer account.
Request Payload
{
"keywords": ["term one", "term two"]
}
Output
Read-only by design. No other Google Ads API service is called. Periscope does not create, read, update, or delete campaigns, ad groups, ads, budgets, conversions, audiences, or billing data. It performs zero mutations of any kind on the Google Ads account.
CLI commands that consume the Ads API
| Command | Use of Ads API |
|---|---|
periscope snapshot | Pulls Keyword Planner volume + competition for the configured keyword set |
periscope discover topics | Seeds Keyword Planner with category names + top GSC queries to surface gaps |
periscope audit articles | Compares MDX article keywords against Keyword Planner metrics |
periscope validate lps | Validates /lp/* landing pages against Keyword Planner demand |
periscope probe <url> | One-shot URL-seeded keyword ideas for a competitor URL |
periscope doctor ads | Diagnostic that verifies credentials and API reachability |
periscope auth ads | One-time OAuth setup helper. Mints a refresh token via the standard Desktop-app loopback flow and prints credentials to stdout for the operator to place in their preferred env manager. |
Authentication
Periscope authenticates to the Google Ads API via OAuth 2.0 user-flow with a developer token. The operator runs periscope auth ads once on their local machine; the tool mints a refresh token via the standard Desktop-app loopback redirect flow and prints the three resulting env vars to stdout for the operator to place in their preferred env manager (a .env file, a shell profile, 1Password CLI, Doppler — the tool does not assume).
The same operator-side credentials are then read from environment variables on every CLI invocation:
GOOGLE_ADS_DEVELOPER_TOKEN— the Basic-access developer token issued under the operator's MCCGOOGLE_ADS_CLIENT_ID— OAuth 2.0 client ID (Desktop application type)GOOGLE_ADS_CLIENT_SECRET— OAuth 2.0 client secretGOOGLE_ADS_REFRESH_TOKEN— refresh token, minted once byperiscope auth adsGOOGLE_ADS_LOGIN_CUSTOMER_ID— the operator's manager (MCC) customer IDGOOGLE_ADS_CUSTOMER_ID— the operating customer (typically a child of the MCC)
A legacy service-account fallback is preserved for environments without a domain-policy constraint (Workspace MCCs, test accounts): when GSC_SERVICE_ACCOUNT_KEY_PATH or GSC_SERVICE_ACCOUNT_JSON is set instead of the three OAuth vars above, periscope authenticates as the service account. Personal-Gmail MCCs reject service-account emails at the Ads API layer (an "allowed domains" policy that the UI tolerates but the API enforces), which is why OAuth user-flow is the primary path in v1.3+.
The OAuth user flow authenticates the operator as themselves against their own Google Ads account. The tool has no backend, no third-party identity broker, no end-user consent surface — it is local CLI tooling for a single operator at a time. No credentials are transmitted anywhere other than Google's own OAuth + Ads API endpoints.
Data handling
- Scope: Read-only access to Keyword Planner data for the developer's own Manager-account-linked accounts.
- Storage: Responses are written to a local
outputDiron the developer's machine as JSON and Markdown snapshots. Nothing is uploaded to any third-party service. - Sharing: Data is not shared with, sold to, or exposed to any third party. There are no end users.
- Retention: Snapshots are retained locally at the developer's discretion and can be deleted at any time.
- Compliance: Use complies with the Google Ads API Terms of Service, including the Required Minimum Functionality requirements for tools that surface Keyword Planner data.
Call volume
Call volume is low — on the order of dozens to low hundreds of requests per day, invoked manually from the CLI during editorial reviews. There is no automated continuous polling.
Architecture
Language and build
TypeScript in strict mode. Compiled to ESM via tsup with typed snapshot JSON contracts. Targets Node 22+; the published artifact is the dist/ directory plus the README.
CLI
Built on commander, with nested subcommands and a per-command --help that includes copy-pasteable examples. The entry point is a single bin/periscope that resolves to a small bundled script.
Engine modules
One file per upstream API under src/engines/:
gsc.ts— Google Search Consolega4.ts— Google Analytics 4bing.ts— Bing Webmaster Toolsads.ts— Google Ads Keyword Planner
Each engine takes its config explicitly (site URL, property id, API key, bot regions) rather than reading env vars itself. The orchestrator command reads env vars and a periscope.config.mjs from the consumer repo and threads them through. That's what makes the same package usable across any number of consumer projects, each with its own config.
Shared library
src/lib/* holds the cross-cutting pieces:
- Auth resolver: OAuth user-flow (primary) and service-account JWT (fallback) for Google Ads; service-account JWT for GSC + GA4
- Ads error classifier (collapses ~20 raw error codes into 8 actionable categories)
- Seed-phrase composition and topical filtering for keyword ideas
- Bucket math for keyword volume tiers
- Snapshot markdown renderer
- Config loader (zod-validated)
- ANSI color helpers
- Snapshot store (load / write / list)
- Natural-language date resolver (for the
diffcommand)
Tests
159 unit tests on vitest covering bucket math, config schema, frontmatter parser, color helpers, diagnostics, snapshot ref resolution, Ads auth resolution, error classification, retry backoff, error-budget tripping, keyword-ideas cache, GSC-first verdict scoring, and the OAuth + seed-phrase work added in v1.3 – v1.4. No live API in tests — engines are mocked at the HTTP boundary.
Distribution
Private npm package @anthonycoffey/periscope on GitHub Packages, published via a tag-triggered GitHub Actions workflow. Tags follow v* (semver from 1.0.0); consumers pin with ^1.0.0 and pick up minors with npm update.
Command surface
| Command | Description |
|---|---|
periscope snapshot --window=180 --asof=YYYY-MM-DD | Pull a multi-engine snapshot using defaults. |
periscope diff yesterday | Diff the latest snapshot against yesterday's |
periscope diff 7d | Diff the latest snapshot against ~7 days ago. |
periscope diff "last month" | Diff the latest snapshot against ~30 days ago. |
periscope diff 2026-05-10 2026-05-17 | Diff two explicit snapshot dates. |
periscope discover topics | Surface editorial topic backlog from Ads + GSC. |
periscope audit articles | Flag existing articles whose keyword fit has drifted. |
periscope validate lps | Audit landing pages against target keywords. |
periscope probe <url> | Competitor URL probe via Ads Keyword Planner. |
periscope doctor ads | Diagnose Google Ads API credentials and access. |
What's next
- An optional Ahrefs engine as a fifth data source (SPEC-022)
- Topic-gap clustering across articles (SPEC-025)
- Week-over-week movement diff (SPEC-026)
- Guided multi-property onboarding flow (SPEC-028)
