ads-google
Skill1 starsChanged 4 days agoMITView on GitHub
Use when setting up Google Ads plus GA4 conversion tracking, launching a small Search campaign or paid test, debugging zero conversions, building Customer Match audiences, or validating a Measurement Protocol secret.
- Reads credentials
11 other projects use a version of this.
Adopted by
Most recently taken up by
- oivanfelipe/claude-skillstheir own version, first seen Nov 8, 2026
- pooriaarab/skillsword for word, first seen Nov 7, 2026
- Hainrixz/claude-adstheir own version, first seen Nov 1, 2026
- AgriciDaniel/claude-adstheir own version, first seen Oct 25, 2026
- hackjpn/claude-skillstheir own version, first seen Oct 11, 2026
- chriswestt/claude-skillstheir own version, first seen Oct 9, 2026
Used with
Projects with it also use ads-math, ads-test and 4 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
chriswesttSkillads-math
PPC financial calculator and modeling tool. CPA, ROAS, CPL calculations, break-even analysis, impression share opportunity sizing, budget forecasting, LTV:CAC ratio analysis, and MER (Marketing Efficiency Ratio) assessment. Requires zero API access. Works with pasted data from exports. Use when user says PPC math, ad calculator, break-even, budget forecast, ROAS calculator, CPA calculator, impression share, LTV CAC, or MER.
Also in 3 projects that use this
chriswesttSkillads-test
A/B test design and experiment planning for paid advertising. Structured hypothesis framework, statistical significance calculator, test duration estimator, sample size calculator, and platform-specific experiment setup guides (Meta Experiments, Google Experiments, LinkedIn A/B). Use when user says A/B test, split test, experiment design, test hypothesis, statistical significance, sample size, or test duration.
Also in 3 projects that use this
- +4 more from chriswestt
Other versions11 projects use a version of this.
Word for word · 1
- pooriaarab/skills★ 1100% same
Edited · 0
None found.
Wrote their own version · 10
- AgriciDaniel/claude-ads★ 10ksame name
- Hainrixz/claude-ads★ 102same name
- coopersimson96/ai-content-system★ 17same name
- megastep/codex-skills★ 4same name
- Maxymize/maxym-ai-ads★ 2same name
- get-proofpilot/claude-skills-catalog★ 1same name
- Biznomad/claude-skills★ 0same name
- chriswestt/claude-skills★ 0same name
- hackjpn/claude-skills★ 0same name
- oivanfelipe/claude-skills★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository325 other files it gives its agents.
- AGENTS.mdAGENTS.md
- accept-x402-paymentsSkill
- ad-auto-optimizerSkill
- ad-conversion-hubSkill
- ad-creative-generationSkill
- ad-creative-templatesSkill
- ad-experimentsSkill
- ad-image-prompt-librarySkill
- adobe-express-addonSkill
- ad-platform-credentialsSkill
- ads-amazonSkill
- ads-apple-searchSkill
- ads-baiduSkill
- ads-bilibiliSkill
- ads-chatgptSkill
- ads-copilotSkill
- ads-criteoSkill
- ads-discordSkill
- ads-douyinSkill
- ads-hotstarSkill
- ads-kakaoSkill
- ads-kuaishouSkill
- ads-lgSkill
- ads-lineSkill
- ads-linkedinSkill
- ads-mercadolibreSkill
- ads-meta-aiSkill
- ads-metaSkill
- ads-microsoftSkill
- ads-mojSkill
- ads-naverSkill
- ads-nextdoorSkill
- ads-outbrainSkill
- ads-perplexitySkill
- ads-pinterestSkill
- ads-quoraSkill
- ads-redditSkill
- ads-rokuSkill
- ads-samsungSkill
- ads-sharechatSkill
- ads-shopeeSkill
- ads-snapchatSkill
- ads-spotifySkill
- ads-stackadaptSkill
- ads-stravaSkill
- ads-taboolaSkill
- ads-tencentSkill
- ads-tiktokSkill
- ads-trade-deskSkill
- ads-twitchSkill
- ads-vkSkill
- ads-wazeSkill
- ads-weiboSkill
- ads-xiaohongshuSkill
- ads-xSkill
- ads-yahoo-japanSkill
- ads-yandex-directSkill
- ads-yelpSkill
- agent-avatar-systemSkill
- agent-browser-consolesSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
6 rules. 0 are common, 4 are distinctive, and 2 are very specific to this project.
0
Common
Most of the 10 edited versions we read keep these.
4
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 86
Prefer the offline-upload path when your server already captures gclid and you have an UPLOAD_CLICKS conversion action.
- Very specific: line 107
Use partialFailure: true, but always inspect per-row errors and exit non-zero when any row fails.
- Distinctive: line 127
Set the campaign conversion goal explicitly: Don't leave it on "account default: no goals" — point it at the action you care about (e.g.
- Distinctive: line 136
If you drive campaign creation through the Google Ads API rather than the UI:
- Distinctive: line 171
Create a service account; add it as a Viewer on the GA4 property (Admin → Property Access Management).
- Distinctive: line 174
Never print retrieved secrets: When reading an MP secret back through the Admin API, use it, don't log it — treat it like any bearer …
“Common” is measured: kept by at least 6 in 10 of the 10 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
ads-google
Google Ads conversion tracking for a web app has three layers, and they stack — get the lower ones right or the top one has nothing to count:
- GA4 client tag (
gtag.js) — firespurchasein the browser. Easy to see in a real browser, easy to miss server-side. - GA4 server-side Measurement Protocol — your backend posts the same
purchaseto GA4 after the charge actually succeeds (e.g. a payment webhook), so you don't lose conversions to ad-blockers, closed tabs, or client failures. - Google Ads conversion import from GA4 — Google Ads doesn't track the purchase itself; it imports the GA4
purchaseevent as a conversion action. If GA4 never recorded the event, there is nothing to import.
You configure these in order. Skipping layer 2 is the usual reason "it worked in testing but production shows nothing."
When to use
- Triggers: 'Google Ads conversion tracking', 'GA4 Measurement Protocol', '0 conversions', 'import GA4 conversion', 'small Search campaign', 'Customer Match', 'GA4 pageviews zero'.
- Three layers: GA4 client gtag, GA4 server Measurement Protocol (an unset api_secret silently no-ops), and Google Ads import of a GA4 conversion marked Primary. Verification via the GA4 Data API and GAQL.
- API launch nuances (containsEuPoliticalAdvertising, text-only search ads, OAuth project enablement); Customer Match via crm_based_user_list and OfflineUserDataJob (about 1,000-member floor); a gtag page_view ordering bug that zeroes GA4 pageviews.
- Small-budget Search setup: defaults that burn money (Display Network, Search partners, all languages, no negatives), bidding with no history (Manual CPC or Maximize Clicks), long-tail vs head terms, and a search-volume check before spend.
GA4 client tag (gtag)
Standard GA4 purchase on the confirmation/thank-you page:
gtag('event', 'purchase', {
transaction_id: '<order-or-session-id>',
value: 25,
currency: 'USD',
items: [{ item_id: 'plan_pro', item_name: 'Pro plan', price: 25, quantity: 1 }]
});transaction_idis your dedup key — GA4 de-duplicates apurchasebytransaction_id, so send the same value from client and server (below).value+currencyare what Smart Bidding optimizes toward; always send both.- Sequence
gtag('js')→gtag('config')→ any manualpage_viewin one effect. If apage_viewis queued from a different effect/hook than thejs/configinit, it can execute beforeconfiglands and gtag silently drops it — GA4 then shows sessions but 0 pageviews. Same-effect ordering fixes it.
GA4 server-side Measurement Protocol
Post the same event from your backend after the charge is confirmed:
POST https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXX&api_secret=<API_SECRET>
Content-Type: application/json{
"client_id": "<GA4 client_id from the browser _ga cookie>",
"events": [{
"name": "purchase",
"params": {
"transaction_id": "<same id as the client event>",
"value": 25,
"currency": "USD",
"items": [{ "item_id": "plan_pro", "price": 25, "quantity": 1 }]
}
}]
}api_secretis NOT the measurement id. The measurement id is the publicG-XXXXXXX. Theapi_secretis a separate server credential created per data stream in GA4 Admin → Data Streams → (your stream) → Measurement Protocol API secrets → Create. It is a secret — keep it server-side only.- If
api_secretis unset, the serverpurchasesilently no-ops. The Measurement Protocol accepts the call shape but sends nothing to the property without a valid secret — you get 0 server conversions and no error. This is the #1 pitfall here. Anyif (!apiSecret) returnguard in your code makes it worse: the code runs, the guard takes the empty branch, nothing is sent. - Use the browser's GA4
client_idso the server event joins the same user as the client session. Read it from the first-party_gacookie (value looks likeGA1.1.1234567890.1700000000; the client_id is the last two dotted segments,1234567890.1700000000) and carry it to the backend. A random/new client_id fragments the user and can drop the join.
Google Ads: import the GA4 purchase as a conversion action
In Google Ads → Goals → Conversions → New conversion action → Import → Google Analytics 4 → Web, pick the GA4 purchase event.
- A conversion action can be HIDDEN — imported but not counted. It must be enabled and set to Primary for Smart Bidding to optimize toward it. A "Secondary" action is observed only, never bid on.
- Attributed conversions require a real ad click. With no ads running (or clicks that don't match), the conversion action correctly reports 0 — that's expected, not a broken tag. The tag can be firing perfectly and Ads still shows 0 because nobody clicked an ad.
Click id
Google's GA4-import path attributes on the GA4 client_id / gclid, so you generally do not need to capture gclid yourself when using conversion import. Capturing gclid (from the landing-page query param, first-touch) is only needed if you later switch to offline/enhanced conversions that key on it. Optional for the import flow.
Conversion tracking & Enhanced Conversions for Leads
Enhanced Conversions for Leads (EC-for-Leads) has two implementation paths:
- Google tag / GTM: captures first-party user data in the browser with the lead conversion.
- Manual/offline upload: sends hashed identifiers with an offline click conversion through the API.
Prefer the offline-upload path when your server already captures gclid and you have an UPLOAD_CLICKS conversion action. It keeps the conversion record and match data on the server.
Enabling EC-for-Leads is UI-only. customer.conversion_tracking_setting.enhanced_conversions_for_leads_enabled is Output-only in the Google Ads API. A CLI can build a mutate request for it, but the API rejects that request. Enable the setting in the Google Ads UI, through Data Manager or Conversions settings. accepted_customer_data_terms is a separate prerequisite and is usually handled in the UI.
For offline click uploads, call customers:uploadClickConversions. Each ClickConversion includes both the click id and user identifiers:
{
"gclid": "<click-id>",
"conversionAction": "customers/<customer-id>/conversionActions/<action-id>",
"conversionDateTime": "2026-01-15 18:30:00+00:00",
"conversionValue": 25,
"currencyCode": "USD",
"userIdentifiers": [{
"userIdentifierSource": "FIRST_PARTY",
"hashedEmail": "<sha256-hash>"
}]
}- Send
gclidanduserIdentifierstogether. Use UTCconversionDateTimeinyyyy-MM-dd HH:mm:ss+00:00format. The email value is a SHA-256 hash of the lowercase, trimmed email only. Do not remove Gmail dots or+tags; Google canonicalizes those server-side, and over-normalizing lowers match rate. - Use
partialFailure: true, but always inspect per-row errors and exit non-zero when any row fails. The endpoint can return HTTP 200 with partial errors. This is a classic silent-failure money-path trap. - Google deduplicates on
(gclid, conversionAction, conversionDateTime).ONE_PER_CLICKreinforces this rule, so re-running the same upload is safe. - A HIDDEN conversion action is not a primary goal. Un-hide it before expecting Smart Bidding to optimize toward it.
Customer Match (audience expansion — there is no lookalike object)
Google's classic "similar audiences" was deprecated (Aug 2023) — you can't create a lookalike object. To reach people similar to your users:
- Upload a Customer Match list: a
crm_based_user_list, populated via anOfflineUserDataJobwithhashedEmailmembers (emails normalized trim + lowercase, then SHA-256). - Let optimized targeting and value-based Smart Bidding expand from that list — the "lookalike" behavior is a bidding/targeting property, not a separate audience.
- A Customer Match list needs ~1,000 members to serve. A tiny seed won't activate — a high-intent signup segment beats a tiny paying-customer-only seed. Seed-sizing and the PII-export authorization boundary are in
ad-experiments.
Campaign setup (search): defaults that quietly burn a small budget
Learned launching a live small-budget ($50-scale) search test. A new Search campaign's defaults are tuned for spend, not for a clean signal — fix these before you turn it on:
- Display Network + Search partners default ON. A new "Search" campaign is created targeting Google Search plus Search partners plus the Display Network by default, so a search-intent budget bleeds onto junk Display placements. Set it to Google Search only (
targetContentNetwork=false,targetSearchNetwork=falseon the campaign'sNetworkSettings) unless you deliberately want partners/Display. - Language defaults to "All languages" → you serve users who can't read the ad. Restrict targeting to your target market's language.
- Add negative keywords up front. PHRASE/BROAD match will match junk ("free …", "jobs", "how to …", "reviews", "hire a …", off-topic and competitor terms) and eat the budget before you can read any signal. Put a campaign negative-keyword list in place at launch, not after you've spent.
- Bid strategy on a tiny budget with no conversion history: Manual CPC (full control) or Maximize Clicks (volume, to fill the top of the funnel so you can read clicks → signups). Smart Bidding (Maximize Conversions / tCPA / tROAS) needs a conversion action wired up and roughly 15-30 conversions to exit the learning phase — a small test won't produce that, so it can't optimize. Switch to Smart Bidding only once conversions have accumulated.
- Set the campaign conversion goal explicitly. Don't leave it on "account default: no goals" — point it at the action you care about (e.g. signup) so bidding optimizes toward it once you do move to Smart Bidding.
- Value rules apply only to value-based / tROAS bidding — skip them for a signup- or clicks-based test.
- Head terms are unaffordable on a small budget — broad category terms get bid up by large advertisers (think tens of dollars per click). Ride long-tail, vertical, high-intent phrases instead.
- Check search volume before you spend. Pull exact-match monthly volume (Keyword Planner, or the API's
generateKeywordHistoricalMetrics) — a hyper-long-tail term in a tiny geo can have too little volume to spend even a small budget in your window, which starves the test of a read. Confirm there's enough volume to actually spend before you launch.
Then run tightly-scoped experiments on top of this setup — see the ad-experiments skill for the methodology (hypothesis → narrow audience×geo×creative → cheapest conversion first → server-side verification).
Google Ads API launch nuances (generic)
If you drive campaign creation through the Google Ads API rather than the UI:
containsEuPoliticalAdvertisingis now REQUIRED on campaign create — aCampaignOperationcreate mutate returns a 400 without it. Set the declaration field explicitly (typically "does not contain").- Search ads are text-only. A responsive search ad takes headlines + descriptions; there is no image asset on a search ad. Don't try to attach an image to one.
- The API must be enabled on the OAuth client's GCP project — the project behind your OAuth credentials needs the Google Ads API enabled and a developer token, or calls fail before they reach your account.
Customer Match audience upload (the trap)
Uploading a first-party customer list for lookalike/retargeting is the single most failure-prone part of Google Ads automation. Expect a multi-step yak-shave:
- The classic Ads API
OfflineUserDataJobpath may be blocked —CUSTOMER_NOT_ALLOWLISTEDif the dev token / account isn't approved for Customer Match. Teams then pivot to the Data Manager API (datamanager.googleapis.com,audienceMembers:ingest) with SHA-256-hashedCONTACT_INFO. - Batch the ingest — chunk to roughly ≤10k members per request, sequentially. One oversized POST silently truncates (only the first slice lands) with no error surfaced — an undocumented per-request member ceiling.
- NEVER trust the ingest success response as proof of membership. A
200+ returned request IDs can still leave the list effectively empty. Always follow with a size-verification query against the destinationuser_list(size_for_display,size_range_for_display) before treating the upload as done. Silent partial/zero success is a real, common failure mode here — not hypothetical. - The empty-list trap: a Customer Match list can report a non-zero
match_rate_percentageyetsize_for_display = 0/LESS_THAN_FIVE_HUNDRED, and never serve.match_ratealone does not prove a servable, populated list. Things to check when a list stays empty:- members not landing in the Ads-queryable
user_listat all — the ingest may be populating a Data-Manager audience surface distinct from the targetable Adsuser_list(verify which resource actually holds membership before assuming success); - the account isn't approved/eligible for Customer Match (a policy/spend-history gate);
- missing consent fields on ingest (
ad_user_data/ad_personalization) dropping members from the servable pool; - size sometimes only computes once a live campaign targets the list — the truest verification is to point a tiny campaign at it and see if it serves, not to stare at the size field.
- members not landing in the Ads-queryable
- A "5,000-email list matched at 82%" that shows size 0 is broken, not warming up. Waiting 24–48h does not fix a structural ingest/eligibility problem — verify size, and if it's still 0 after a full day, it's not timing.
- Bring-your-own analytics ≠ native dashboard. Some site builders only offer a connect-your-own-GA4-tag setup, not native visitor analytics — a fact worth respecting in ad copy (don't promise a dashboard the product doesn't have; see the
ad-experimentstruthfulness gate).
Verification (server-side truth, not "the tag is on the page")
- GA4 Data API
runReport— confirm GA4 actually recorded purchases. Requires: (a) a service account added as a Viewer on the GA4 property, (b) the Analytics Data API enabled on its project, and (c) the numeric property id (found in GA4 Admin → Property Settings), not theG-XXXXXXXmeasurement id. Query theconversions/eventCountmetrics foreventName == purchase. - Google Ads GAQL — list conversion action state and totals:
SELECT conversion_action.name, conversion_action.status, conversion_action.primary_for_goal, metrics.all_conversions FROM conversion_actionstatusshows ENABLED vs HIDDEN/REMOVED;all_conversionscounts even without an ad click, so it separates "tag firing" from "no ad clicks yet". - Validate the MP secret without waiting — post to the debug endpoint
https://www.google-analytics.com/debug/mp/collect?measurement_id=...&api_secret=...with the same body. It returnsvalidationMessagessynchronously; an empty array means the event (and the secret) are accepted. This is how you prove the secret is live in seconds instead of waiting for reporting to populate.
Access setup (read + MP secret management)
- Create a service account; add it as a Viewer on the GA4 property (Admin → Property Access Management).
- Enable the Analytics Data API (for
runReport) and the Analytics Admin API (to read/create Measurement Protocol secrets programmatically) on its project. - JWT scopes:
analytics.readonlyfor reporting; addanalytics.editif you create/read MP secrets via the Admin API. - Never print retrieved secrets. When reading an MP secret back through the Admin API, use it, don't log it — treat it like any bearer credential.
Cross-cutting lessons (the ones that actually bite)
- Timing: a conversion count of 0 over a window that predates the tracking deploy is expected, not a bug. Check the deploy date before debugging — you can't count conversions from before the tag existed.
- Build-scoped env: if the
api_secret/ measurement id are baked into a generated module at build time (common on serverless/edge), setting them in your dashboard does nothing until the next deploy. Redeploy after adding — match wherever your other working analytics secrets are imported from rather than assuming a runtimeprocess.envread works. - Ground truth: cross-check Google Ads / GA4
purchasecounts against your payment provider's actual succeeded-charge count. Real succeeded charges > 0 with GA4 purchases = 0 means tracking is broken, full stop — the payment provider is the source of truth, the analytics layer is the thing under test. - Silent failure is the norm: these integrations fail by sending nothing (an early return on a missing
api_secret, a HIDDEN conversion action), not by throwing. Verify with server-side truth — the Data API, GAQL, the payment provider — never "I saw the pixel load in the browser."
Common pitfalls
- Confusing
api_secretwith the measurement id — one is a publicG-XXXXXXX, the other a per-stream server secret. Serverpurchasesilently no-ops without the secret. - Using the numeric property id where the code wants
G-XXXXXXX, or vice-versa — Data API wants the numeric id,gtag/MP wantG-XXXXXXX. - Leaving the imported conversion action HIDDEN or Secondary, then wondering why Smart Bidding ignores it — enable it and mark it Primary.
- Debugging "0 conversions" that's actually just "no ad clicks yet" — check
all_conversionsand whether any campaign is live before assuming the tag is broken. - Generating a fresh
client_idserver-side instead of reusing the browser's_gavalue — fragments the user and can break attribution. - Campaign-create 400s from the Google Ads API because
containsEuPoliticalAdvertisingwasn't declared, or the API isn't enabled on the OAuth project.
Security — secret handling
The GA4 api_secret is a server-side secret. Source it from an environment variable (e.g. $GA4_API_SECRET) — never hardcode, commit, or log it, and never echo its value back to the user. It appears in the Measurement Protocol query string because Google's /mp/collect and /debug/mp/collect endpoints require it there; always send over HTTPS and redact it from any captured request logs. The placeholders in this doc (<API_SECRET>, ...) are not real values.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

