GSC Quick Wins in Python: Mine Position 5-30 Before You Chase New Keywords
The cheapest keyword research is the data you already own: Google Search Console queries where your site has proven relevance but sits at position 5-30 with almost no clicks. This post walks through search_performance, the GSC CLI in the zens-ink package — one-time OAuth setup, then queries, pages, and per-page filtering from pure Python stdlib — plus the quick-win filter (impressions >= 5, position 5-30) that turns raw rows into a ranked refresh list. Real runs on zens.ink's own GSC data: 621 impressions and 2 clicks over three months, 7 of 40 queries landing in the quick-win band, including a position-8 query with 138 page impressions converting at 2.2% CTR. Why position is 'personalized difficulty already measured', why the filter's other job is telling you what NOT to chase, and the honest limits of GSC's averaged position metric.
TL;DR — Before researching a single new keyword, filter your own Google Search Console data for queries with impressions >= 5 and average position 5-30. That band is where relevance is proven (Google already ranks you) but clicks haven't arrived yet — the highest-ROI targets on any site. python -m zens_ink.search_performance --queries pulls the data in pure Python stdlib, and a ten-line filter turns it into a ranked refresh list. On zens.ink's own data: 7 quick wins out of 40 queries, including one page sitting at position 7 with 138 impressions and a 2.2% CTR. One-time OAuth setup, zero API cost, pip install zens-ink.
I pulled up my own Search Console data last week and found something embarrassing: 621 impressions, 2 clicks, three months. And zero of those impressions came from the keyword I’d been planning to target next.
The site was zens.ink itself. Not a client site, not a demo — the tool site I write this journal on, which ranks for real queries but captures almost nothing from them. The 2 clicks came from “open source seo” (1 click from 85 impressions) and “bluf seo” (1 click from 6). Meanwhile the queries with the most impressions sat at position 50, 66, 86 — visible, ranked, invisible to searchers.
Buried in those 40 rows was the actual opportunity, and it took a ten-line filter to see it. That filter is what this post is about.
The problem: new-keyword bias
Everyone researching keywords has the same instinct: find queries you don’t rank for, then go get them. Autocomplete mining, volume APIs, competitor gap analysis — I’ve written about all of them (autocomplete pipeline, Bing volume API, Reddit mining). They’re prospecting tools, and prospecting has two permanent unknowns:
- Is this keyword actually relevant to my site?
- Can I rank for it at all?
Both unknowns cost weeks to resolve, because the only real test is publishing and waiting.
Your own GSC data has neither unknown. If a query shows impressions, Google already matched your page to it. If the average position is 5-30, you’re already in the fight — not winning it, but one title rewrite and a few internal links from the top half of page one. It’s personalized difficulty, already measured. Position is what SERP difficulty scoring tries to predict for strangers; in your own GSC data it’s simply reported.
That’s the whole thesis of quick-win mining: the cheapest ranking gains on any established site are in queries you already rank 5-30 for.
The tool: GSC in pure stdlib
The zens-ink package ships search_performance, a CLI wrapper around the Search Console API. No Google client libraries, no dependencies — plain urllib calls and a JSON printer.
Setup is one-time OAuth:
pip install zens-ink
# 1. Google Cloud project -> enable Search Console API
# 2. Credentials -> OAuth client ID -> Desktop app
# 3. Put client id/secret in .env, then:
python -m zens_ink.setup_gsc # browser auth, saves refresh token
# 4. Set your property in .env:
# GSC_SITE_URL=sc-domain:example.com
After that, three views of your data:
python3 -m zens_ink.search_performance --queries # by query
python3 -m zens_ink.search_performance --pages # by page
python3 -m zens_ink.search_performance --page /journal/ # queries for one page
python3 -m zens_ink.search_performance --country # by country
Every flag combination takes --start 2026-07-01 (defaults to a fixed date, so set your window), --limit N, and --json when you want raw rows for piping.
The real run: 40 queries, 621 impressions, 2 clicks
Here’s zens.ink’s own data for July-September 2026, lightly trimmed:
$ python3 -m zens_ink.search_performance --start 2026-07-01 --limit 40 --queries
================================================================================
sc-domain:zens.ink | query | 2026-07-01 - 2026-09-29
40 rows | 621 impressions | 2 clicks
================================================================================
best open source seo software imp= 187 clk= 0 CTR= 0.0% pos= 50.6
open source seo imp= 85 clk= 1 CTR= 1.2% pos= 66.7
backlink strength and keyword ... imp= 64 clk= 0 CTR= 0.0% pos= 21.6
backlink analysis report imp= 29 clk= 0 CTR= 0.0% pos= 72.6
analyze backlinks imp= 23 clk= 0 CTR= 0.0% pos= 77.4
best open source seo tools imp= 19 clk= 0 CTR= 0.0% pos= 35.4
bing keyword research api imp= 19 clk= 0 CTR= 0.0% pos= 23.6
backlinks counter imp= 13 clk= 0 CTR= 0.0% pos= 22.5
ahrefs show backlinks that other... imp= 10 clk= 0 CTR= 0.0% pos= 10.2
@deepseek-ai/dsh-mcp-client imp= 9 clk= 0 CTR= 0.0% pos= 8.6
bluf seo imp= 6 clk= 1 CTR=16.7% pos= 7.2
backlink checker extension imp= 6 clk= 0 CTR= 0.0% pos= 17.3
backlink scan imp= 3 clk= 0 CTR= 0.0% pos= 13.7
...
Read it once as a human and the eye goes to the top: 187 impressions on “best open source seo software”. Tempting — until you see position 50.6. That’s not a quick win; that’s a war of attrition against DR-90 lists, and on-page tweaks won’t move it. The same goes for the whole backlink cluster (“analyze backlinks” 77.4, “backlink analysis” 86.3): high impressions, but the backlink analysis page is nowhere near page two. Chasing those first is the classic mistake.
The money is in the middle of the table. Here’s the filter:
python3 -m zens_ink.search_performance --start 2026-07-01 --limit 40 \
--queries --json | python3 -c "
import json, sys
rows = json.load(sys.stdin).get('rows', [])
qw = [r for r in rows
if 5 <= r.get('position', 99) <= 30
and r.get('impressions', 0) >= 5]
qw.sort(key=lambda r: -r['impressions'])
print(f'quick wins: {len(qw)} of {len(rows)} queries')
for r in qw:
print(f\" pos={r['position']:5.1f} imp={r['impressions']:4d} \"
f\"clk={r.get('clicks', 0):2d} {r['keys'][0]}\")
"
Output, unedited:
quick wins: 7 of 40 queries
pos= 21.6 imp= 64 clk= 0 backlink strength and keyword difficulty
pos= 23.6 imp= 19 clk= 0 bing keyword research api
pos= 22.5 imp= 13 clk= 0 backlinks counter
pos= 10.2 imp= 10 clk= 0 ahrefs show backlinks that other domains have that mine doesn't have
pos= 8.6 imp= 9 clk= 0 @deepseek-ai/dsh-mcp-client
pos= 7.2 imp= 6 clk= 1 bluf seo
pos= 17.3 imp= 6 clk= 0 backlink checker extension
Seven targets, ranked by impressions. Two things jump out.
First: the biggest quick win isn’t a keyword I’d ever have researched. “Ahrefs show backlinks that other domains have that mine doesn’t have” is a ten-word question with 10 impressions at position 10. Nobody types that into an autocomplete miner and gets excited. But it’s a real person describing exactly the competitor gap workflow, Google has already decided my site answers it, and I’m one strong section away from owning it. Long question queries are quick-win gold precisely because they’re invisible to everyone’s keyword tools.
Second: the filter excludes more than it includes — that’s the point. Thirty-three of forty queries didn’t make the cut, including all the high-impression ones. The quick-win filter’s other job is telling you what not to work on this month. “Best open source seo software” at position 50 is a strategy conversation (links, authority, time), not a task.
Pair the query with its page
A query is only actionable once you know which page to edit. Flip the dimension:
$ python3 -m zens_ink.search_performance --start 2026-07-01 --limit 10 --pages
================================================================================
sc-domain:zens.ink | page | 2026-07-01 - 2026-09-29
10 rows | 3783 impressions | 35 clicks
================================================================================
/journal/open-source-seo-tools/ imp= 3208 clk= 6 CTR= 0.2% pos= 62.4
/ imp= 179 clk= 17 CTR= 9.5% pos= 23.8
/journal/deepseek-harness-mcp-setup/ imp= 138 clk= 3 CTR= 2.2% pos= 7.0
/journal/keyword-difficulty-checker/ imp= 106 clk= 1 CTR= 0.9% pos= 57.2
/glossary/bluf/ imp= 74 clk= 1 CTR= 1.4% pos= 51.5
/journal/seo-cli-terminal-tools/ imp= 26 clk= 2 CTR= 7.7% pos= 11.7
...
Now the “@deepseek-ai/dsh-mcp-client” query has a home: the DSH MCP setup article sits at position 7 with 138 impressions and a 2.2% CTR. Three clicks in three months from a page-one ranking. That’s the clearest quick win on the whole site — not a keyword to target, but a page to fix: the query is a package name (@deepseek-ai/dsh-mcp-client), the searcher wants installation help, and the fix is making sure the exact package name appears in the title and the first screen, plus a code block that answers the install question without scrolling.
For deeper dives, --page /journal/deepseek-harness-mcp-setup/ returns only the queries that page ranks for — the before/after view for any refresh you ship.
The math you’re playing for
Click-through rate decays brutally with position: the top three results take the majority of clicks, and by position 8-10 a typical result sees low single digits. Your own data confirms it — in the run above, “bluf seo” at position 7.2 converted 16.7% (branded-ish, exact intent match), while the DSH page at position 7.0 converted 2.2% (generic title, package name absent). Same position, 8x CTR difference — position sets the ceiling, the title and intent match set how close you get.
That’s the leverage: moving a page from position 8 to position 4, or fixing a title so the same position converts 3x better, is worth more clicks on a low-DR site than almost any new publication. And unlike a new page, the refreshed page keeps its age, its existing rankings, and its backlinks.
Two honest caveats before you over-trust the arithmetic. GSC’s position is an average — across dates, across grouped query variants, across every URL on your site that ranked. A page holding both position 4 and position 40 reports ~22. And anonymized queries never appear at all. So treat the GSC band as a screening layer: shortlist with the filter, then spend one Serper.dev call per shortlisted query to check the live position before investing a day of writing. If you’re running the intent classifier anyway, the same call tells you whether the SERP wants a guide or a tool page — refresh format included.
What to actually do with a quick win
The filter gives you a list. The playbook per item is small:
- Verify intent. One look at the live SERP (or
python -m zens_ink.serp_intent "<query>") — does the current page match what’s ranking? A how-to query stuck at position 15 with a definitional article answering it needs a rewrite of the angle, not more words. - Fix the title first. The query terms belong in the title tag and the first screen. This is the single highest-leverage edit — see the DSH example above.
- Add the missing section. Long question queries (“ahrefs show backlinks that…”) want one specific answer. Add a section that answers the literal question, with a heading close to the query wording.
- Push internal links. Point 2-3 relevant existing pages at the refreshed one with descriptive anchors. On small sites this is often what actually moves the position — see the internal linking guide.
- Wait two weeks, re-run the filter. Same command, same window length shifted forward. The query should leave the quick-win band either by position (moved up) or drop in the list (impressions shifted). If nothing moved, the diagnosis was wrong — that’s information too.
This loop — filter, verify, fix, re-measure — is a big part of what I folded into the broader SEO workflow pipeline. It’s boring, it’s incremental, and on a site with any traction it beats new content on ROI almost every time.
The honest limits
Position averaging hides multimodal reality. The single number blends good days and bad days, first page and fifth. Screen with it, verify with live SERP checks.
Small numbers are noisy. Five impressions can be one curious person on five days. That’s why the filter requires impressions >= 5 — a floor, not a guarantee of demand. Raise it for established sites, lower it (carefully) for young ones.
GSC only knows what Google showed. Queries from users who clicked nothing, anonymized queries, and traffic from other engines (Bing matters) are all outside this dataset. It’s one lens, not the territory.
Quick wins run out. A mature site exhausts its 5-30 band; then you’re back to prospecting — autocomplete, query fan-out planning, difficulty scoring. Quick wins are the fast lane, not the whole road.
Try it
pip install zens-ink
python -m zens_ink.setup_gsc # one-time OAuth
python3 -m zens_ink.search_performance --start 2026-07-01 --queries --limit 40
Pipe through the filter above, pick the top three, and fix titles before you write anything new. If you manage several properties, override per run without touching .env:
GSC_SITE_URL=sc-domain:example.com python3 -m zens_ink.search_performance --queries
The full tool list — audits, difficulty, clustering, intent, GEO — is in the open source SEO tools overview, and everything here is also exposed to AI agents through the MCP server.
FAQ
What exactly is a GSC quick win?
A query where Google Search Console shows impressions of at least 5 and an average position between 5 and 30 over your window. Position 5-30 is the band where you have proven relevance — Google already ranks the page on the first or second page of results — but you capture almost none of the clicks, because click-through rate collapses between position 3 and position 10. A content refresh, title rewrite, or internal-link push on these pages typically moves them faster than publishing something new, because the relevance threshold is already passed. Below position 5 you are already capturing most available clicks; above 30 you are looking at a linking problem, not a content problem.
Why position 5-30 specifically, and not 4-20 or 5-15?
The edges are calibration, not physics. The lower bound (5) sits just past the point where CTR flattens — positions 1-3 take the majority of clicks, and small position gains there are nice but not transformative. The upper bound (30) is roughly the end of page two: beyond it, a low-authority site rarely moves with on-page changes alone. I use 5-30 as the default band and tighten to 5-15 when the site has real authority, loosen toward 5-40 when it is young. The principle — proven relevance, unclaimed clicks — matters more than the exact edges.
How do I set up the GSC API access in Python?
One-time OAuth setup: create a Google Cloud project, enable the Search Console API, create an OAuth Desktop client, put the client ID and secret in a .env file, then run python -m zens_ink.setup_gsc. That opens a browser authorization flow and saves a refresh token locally. After that, search_performance reads the saved token, refreshes it automatically, and every query is a plain HTTPS call to the Search Console API — pure Python standard library, no client libraries to install.
Is the position number in GSC reliable?
Partially, and knowing why matters. GSC reports the average position across all dates in your window, across all query variants it groups together, and across every ranking URL on your site — so a query where you hold both position 4 and position 40 shows ~22. Anonymized queries are dropped entirely, and small-impression rows are sampled. The number is a measurement, not a guarantee: treat it as a screening signal, then verify the current live position for the shortlisted queries (one Serper.dev call each) before committing effort.
How is this different from keyword research with autocomplete or Bing volume data?
Autocomplete and volume APIs estimate demand for keywords you do not yet rank for — they are prospecting tools. GSC quick wins rank keywords you already compete in, which removes the two biggest unknowns in prospecting: whether the keyword is relevant to your site, and whether you can rank at all. Position in your own GSC data is personalized difficulty, already measured. The two work in sequence: mine quick wins first for fast gains, then use prospecting tools to expand into adjacent territory.
Want to run this analysis on your own site?
ZensInk Pro automates this pipeline. One command, from seed keywords to content plan.
Get Pro →