Skip to main content

adsense

Put Google ads on a content site and read what they earn — the whole job, from the terminal. Two halves. (1) DISPLAY: the copy-pasteable ad markup (AdSense loader, Auto Ads, manual units, and the Google Publisher Tag / GPT alternative), where to inject it, how to reserve slot height so ads don't wreck Core Web Vitals (CLS), the Content-Security-Policy allowlist ads need (the real blocker on a locked-down site), Consent Mode v2 + a certified CMP for EEA/UK traffic, and the ads.txt file. (2) REPORTING: query the AdSense Management API (earnings, RPM, top pages, country, ad-unit, site approval, policy issues, payments) with curl + an OAuth token — no MCP server, no npm install. Use when the user asks to add / set up / place Google ads or AdSense, monetize pages, choose ad sizes or placements, check ad revenue/RPM/earnings, why ads aren't showing, AdSense approval/policy/consent/ads.txt, or GPT / Google Ad Manager on a website.

Source facts

Repository
coffeegrind123/adsense-skill
Last source activity
July 22, 2026 at 02:06
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
adsense
description
Put Google ads on a content site and read what they earn — the whole job, from the terminal. Two halves. (1) DISPLAY: the copy-pasteable ad markup (AdSense loader, Auto Ads, manual units, and the Google Publisher Tag / GPT alternative), where to inject it, how to reserve slot height so ads don't wreck Core Web Vitals (CLS), the Content-Security-Policy allowlist ads need (the real blocker on a locked-down site), Consent Mode v2 + a certified CMP for EEA/UK traffic, and the ads.txt file. (2) REPORTING: query the AdSense Management API (earnings, RPM, top pages, country, ad-unit, site approval, policy issues, payments) with curl + an OAuth token — no MCP server, no npm install. Use when the user asks to add / set up / place Google ads or AdSense, monetize pages, choose ad sizes or placements, check ad revenue/RPM/earnings, why ads aren't showing, AdSense approval/policy/consent/ads.txt, or GPT / Google Ad Manager on a website.
when_to_use
Use when the user wants to add or configure Google ads / AdSense / Google Ad Manager (GPT) on a site, decide ad placements or sizes, set up ads.txt or a consent banner for ads, diagnose why ads don't serve, or read ad earnings/RPM/impressions/approval-status/policy-issues. Examples: "put ads on every page", "set up AdSense", "where should the ad units go", "why are my ads blank", "is my site approved", "how much did ads make last week", "add ads.txt", "do I need a cookie consent banner for ads". Do NOT use for the Google Ads API (buying ads / campaigns / keywords / bids) — that is the advertiser side, a different product. Do NOT use for on-page SEO audits — use the seo-audit skills.
allowed-tools
Bash(curl:*) Bash(jq:*) Bash(python3:*) Read Write Edit Grep
# Google AdSense / Ad Manager (display + reporting) Two jobs live here and they are unrelated except that both say "Google ads": 1. **Display** — putting ads on pages. This is **HTML/JS you emit**, plus one security-header change and two small files. There is no API and no MCP for it; an ad unit is a `<script>` and an `<ins>`/`<div>`. The hard parts are not the snippet — they are **CSP**, **CLS**, **consent**, and **policy**. 2. **Reporting** — reading earnings/RPM/approval/policy. This is the **AdSense Management API**, and like Search Console it is `curl` + a token, not a server. **Why not an MCP server.** The AdSense reporting MCPs on GitHub (`AppsYogi-com/ adsense-mcp-server`, `vishmathpati/adsense-mcp`, `Raffaele86/adsense-mcp` — audited by reading their source, not READMEs) each wrap **one** report endpoint plus a few `list` calls behind ~10 "tools", a native OS-keychain dependency (`keytar`), and a SQLite cache. The setup burden (a Google Cloud OAuth client + a human consent click) is **identical whether or not you use them** — a server saves the agent writing curl and saves you nothing. No install hooks or telemetry were found in any of them, so they're safe; they're just unnecessary. This skill keeps the recipes and drops the packaging, exactly like the `search-console` skill. **If `LOCAL.md` exists in this skill's directory, read it first** — it carries the publisher ID, the live setup state, and the site-specific injection points and gotchas that make everything below concrete. It is gitignored (per-install). --- ## The auth reality — AdSense is NOT like Search Console This is the single most important auth fact and it is the opposite of GSC: **The AdSense Management API does not support service accounts.** It requires **OAuth 2.0 user consent.** (Confirmed by reading three independent MCP servers: two have no service-account path at all; the third ships one but it is copied GA4/GSC boilerplate that authenticates and then returns zero accounts, because AdSense accounts are tied to a *personal* Google identity and AdSense is not a Workspace-domain-delegatable API — there is no admin who can grant the delegation.) So there is **one unavoidable human click**: a one-time consent flow to mint a **refresh token**. After that it is fully headless — refresh token → access token, re-minted whenever a call 401s. Scope: **`https://www.googleapis.com/auth/adsense.readonly`** (read-only covers 100% of reporting; there is no write surface worth having). ### One-time setup 1. [console.cloud.google.com](https://console.cloud.google.com) → a project → **APIs & Services → Library** → enable **"AdSense Management API"**. 2. **Credentials → Create credentials → OAuth client ID → Desktop app.** Copy the **Client ID** and **Client Secret**. 3. **OAuth consent screen** → add your Google account (the one that owns the AdSense account) as a **Test user** (fine to leave the app in "Testing"). 4. Do the consent exchange once to get a refresh token (below), then store all three values in `~/.adsense_oauth.json` (0600, outside any repo): ```json { "client_id": "...", "client_secret": "...", "refresh_token": "..." } ``` **Minting the refresh token once** (paste the URL into a browser, approve, paste the `code` back): ```bash CID=...; CSECRET=... # 1. open this, approve, copy the ?code= from the redirect (urn:...:oob shows it on-page) echo "https://accounts.google.com/o/oauth2/v2/auth?client_id=$CID\ &redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code\ &scope=https://www.googleapis.com/auth/adsense.readonly&access_type=offline&prompt=consent" # 2. exchange the code for a refresh token curl -s https://oauth2.googleapis.com/token \ -d client_id="$CID" -d client_secret="$CSECRET" \ -d code="PASTE_CODE_HERE" -d grant_type=authorization_code \ -d redirect_uri="urn:ietf:wg:oauth:2.0:oob" | jq -r .refresh_token ``` (If Google has retired `oob` for your client, use `redirect_uri=http://localhost` and copy the `code` query param from the failed-to-load localhost redirect. Same result.) --- ## Reporting: the curl recipes Everything read-only. The whole product is **one** `reports:generate` endpoint (parameterized by `dimensions`) plus a handful of `list` calls. ```bash # token: refresh -> access. Re-run on a 401 (access tokens are short-lived). adsense_token() { python3 - <<'PY' import json,urllib.request,urllib.parse,pathlib c=json.loads(pathlib.Path.home().joinpath('.adsense_oauth.json').read_text()) d=urllib.parse.urlencode({"client_id":c["client_id"],"client_secret":c["client_secret"], "refresh_token":c["refresh_token"],"grant_type":"refresh_token"}).encode() print(json.load(urllib.request.urlopen("https://oauth2.googleapis.com/token",d))["access_token"]) PY } TOKEN=$(adsense_token); A="Authorization: Bearer $TOKEN"; B=https://adsense.googleapis.com/v2 # account name -> accounts/pub-XXXXXXXXXXXXXXXX (run first; proves auth) ACCT=$(curl -s -H "$A" "$B/accounts" | jq -r '.accounts[0].name') # earnings summary (totals only) for a preset range curl -s -H "$A" "$B/$ACCT/reports:generate?dateRange=MONTH_TO_DATE\ &metrics=ESTIMATED_EARNINGS&metrics=CLICKS&metrics=IMPRESSIONS\ &metrics=PAGE_VIEWS&metrics=PAGE_VIEWS_CTR&metrics=PAGE_VIEWS_RPM" # any breakdown = same call + a dimension curl -s -H "$A" "$B/$ACCT/reports:generate?dateRange=LAST_7_DAYS\ &dimensions=COUNTRY_NAME&metrics=ESTIMATED_EARNINGS&metrics=IMPRESSIONS\ &orderBy=-ESTIMATED_EARNINGS&limit=15" # custom date range (flat params, no dateRange) curl -s -H "$A" "$B/$ACCT/reports:generate?dimensions=DATE&metrics=ESTIMATED_EARNINGS\ &startDate.year=2026&startDate.month=7&startDate.day=1\ &endDate.year=2026&endDate.month=7&endDate.day=22" # the non-report facts curl -s -H "$A" "$B/$ACCT/sites" # approval state per site curl -s -H "$A" "$B/$ACCT/alerts" # INFO/WARNING/SEVERE curl -s -H "$A" "$B/$ACCT/policyIssues" # WARNED / AD_SERVING_RESTRICTED / _DISABLED curl -s -H "$A" "$B/$ACCT/payments" # payment history ``` **Report vocabulary** (real enum names). Repeated params (`metrics=…&metrics=…`) is how the API takes arrays. - **Metrics:** `ESTIMATED_EARNINGS`, `TOTAL_EARNINGS`, `COST_PER_CLICK`, `PAGE_VIEWS`, `AD_REQUESTS`, `MATCHED_AD_REQUESTS`, `IMPRESSIONS`, `INDIVIDUAL_AD_IMPRESSIONS`, `CLICKS`, `AD_REQUESTS_COVERAGE`, `PAGE_VIEWS_CTR`, `AD_REQUESTS_CTR`, `IMPRESSIONS_CTR`, `PAGE_VIEWS_RPM`, `AD_REQUESTS_RPM`, `IMPRESSIONS_RPM`, `ACTIVE_VIEW_VIEWABILITY`, `ACTIVE_VIEW_MEASURABILITY`. (`RPM` = revenue per 1,000 — the number to watch. `COVERAGE` = matched/requested; low coverage = unfilled slots.) - **Dimensions:** `DATE`, `WEEK`, `MONTH`, `PAGE_URL`, `DOMAIN_NAME`, `AD_UNIT_NAME`, `AD_UNIT_ID`, `AD_FORMAT_NAME`, `AD_PLACEMENT_NAME`, `COUNTRY_NAME`, `PLATFORM_TYPE_NAME` (mobile/desktop), `TRAFFIC_SOURCE_NAME`, `CUSTOM_CHANNEL_NAME`, `URL_CHANNEL_NAME`, `OWNED_SITE_DOMAIN_NAME`. Combine a few; put the big metric in `orderBy=-METRIC`. - **Other params:** `dateRange` ∈ {`TODAY`,`YESTERDAY`,`MONTH_TO_DATE`,`YEAR_TO_DATE`, `LAST_7_DAYS`,`LAST_28_DAYS`,`LAST_30_DAYS`,`CUSTOM`}; `limit` (cap 100000); `currencyCode`; `reportingTimeZone=ACCOUNT_TIME_ZONE`. CSV: `reports:generateCsv`. **Report analysis recipes** (all one call + which dimension): | Question | dimension(s) + read | |---|---| | Which pages/silos earn? | `PAGE_URL`, order by `ESTIMATED_EARNINGS`; bucket by path prefix (`/maps/` vs `/modes/` vs landers) | | Where is RPM highest/lowest? | `PAGE_URL` + `PAGE_VIEWS_RPM` — a high-traffic low-RPM page is a placement/format problem, not a traffic one | | Is a slot going unfilled? | `AD_UNIT_NAME` + `AD_REQUESTS_COVERAGE` — low coverage = kill or resize the unit | | Mobile vs desktop money | `PLATFORM_TYPE_NAME` — most content traffic (and most CLS pain) is mobile | | Which country pays? | `COUNTRY_NAME` + RPM — cross-check against where your locale investment went | | Format performance | `AD_FORMAT_NAME` — which of display/in-article/multiplex actually earns | --- ## Display, part 1 — the ad markup (copy-paste, plain HTML) All snippets below are the raw markup the popular React libraries ultimately emit (`hustcc/react-adsense`, `scttcper/react-adsense`) and the official Google samples (`googleads/google-publisher-tag-samples`) — extracted so they drop into static HTML with no framework. ### Loader (once, in `<head>`) ```html <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXXXXXXXXX" crossorigin="anonymous"></script> ``` Include `?client=ca-pub-…` so the *same* tag also powers Auto Ads. ### Auto Ads (Google places everything; the one-line switch) ```html <script> (window.adsbygoogle = window.adsbygoogle || []).push({ google_ad_client: "ca-pub-XXXXXXXXXXXXXXXX", enable_page_level_ads: true }); </script> ``` No `<ins>` slots of your own — Google auto-inserts anchor/vignette/in-content ads. **Lowest effort, least control, highest CLS risk** (Google injects wherever it likes). Modern AdSense increasingly drives this from the account UI toggle; this push is the programmatic equivalent. You can mix Auto Ads with manual units. ### Manual display unit (responsive) ```html <ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-XXXXXXXXXXXXXXXX" data-ad-slot="1234567890" data-ad-format="auto" data-full-width-responsive="true"></ins> <script>(adsbygoogle = window.adsbygoogle || []).push({});</script> ``` One `push({})` per unit. Per ad type: | Type | attributes | |---|---| | Display (responsive) | `data-ad-format="auto"` `data-full-width-responsive="true"` | | In-article | `data-ad-format="fluid"` `data-ad-layout="in-article"` | | In-feed | `data-ad-format="fluid"` `data-ad-layout="in-feed"` `data-ad-layout-key="<from your account>"` | | Multiplex (grid) | `data-ad-format="autorelaxed"` | | Fixed | `style="display:inline-block;width:728px;height:90px"` + drop the responsive attrs | ### The GPT alternative (often the better fit — see the recommendation) Loader is a different origin: ```html <script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js" crossorigin="anonymous"></script> <script>window.googletag = window.googletag || { cmd: [] };</script> ``` Slot + single-request + display: ```html <div id="banner-ad" style="width:728px;height:90px"></div> <script> googletag.cmd.push(() => { googletag.defineSlot("/NETWORK/AdUnit", [728,90], "banner-ad").addService(googletag.pubads()); googletag.setConfig({ singleRequest: true }); googletag.enableServices(); googletag.display("banner-ad"); }); </script> ``` Responsive size mapping (viewport → allowed creative sizes; largest first, `[0,0]→[]` = no ad on tiny screens): ```js const map = googletag.sizeMapping() .addSize([1024,768], [[728,90],[750,200]]) .addSize([640,480], [300,250]) .addSize([0,0], []).build(); slot.defineSizeMapping(map); ``` Native lazy-load (no IntersectionObserver of your own — GPT only): ```js googletag.setConfig({ lazyLoad: { fetchMarginPercent:500, renderMarginPercent:200, mobileScaling:2.0 }, singleRequest:true }); ``` **Recommendation.** On a **static, CWV-budgeted site, prefer GPT over AdSense manual
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub