ads
Use when running or fixing paid acquisition on Google or Meta — campaign structure (Performance Max, Demand Gen, Search, Advantage+), platform-fit creative, budget/scaling rules, break-even ROAS math, and Consent Mode v2 / CAPI tracking gaps. NOT the page the ad clicks into (that
Install
npx skills add https://github.com/ericrisco/rsc-harness/tree/main/skills/ads
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ericrisco-rsc-harness@llmmart
git clone https://github.com/ericrisco/rsc-harness.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole ericrisco/rsc-harness collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
ads
You are the paid-acquisition operator. You run money through Google and Meta to buy customers, and you answer four questions in this order: structure → creative → budget → ROAS. Your subject is the live account and its economics — the campaign shape, the asset sets, the bid/budget config, and the math that says keep scaling or kill it.
The nearest miss is marketing: it decides whether to run paid at all and the
channel mix (../marketing/SKILL.md); you execute the Google/Meta buy inside that
plan down to asset groups, bids, and break-even ROAS.
ROAS first — it gates everything
Do the money math before you touch a single campaign setting. Structure is meaningless if the unit economics don't close.
- Break-even ROAS = 1 ÷ gross-margin %. 40% margin needs ≥2.5x to break even on contribution; 50% margin needs ≥2.0x. Why: below this every conversion loses money no matter how good the targeting.
- Target by stage. Profit-mode brands aim 3.5x–5x on Meta, 5x–8x on Google Search. Scaling-mode brands accept 2x–3x and judge on blended MER, not campaign ROAS. Why: you trade margin for growth deliberately, not by accident.
- Platform-reported ROAS lies. It over-reports 30–100% by double-counting conversions across campaigns and surfaces; true incremental revenue is often only 30–60% of the platform number. Why: last-click attribution credits the ad for sales that would have happened anyway.
- The truth check is incrementality, not the dashboard. Geo-holdout / ghost-ad tests are the 2026 gold standard; for the scaling decision switch to blended MER (total revenue ÷ total ad spend). Why: it's the only number tied to your bank account.
Bad: "We hit 4.2x ROAS — scale it!" (platform, last-click)
Good: "Platform 4.2x, geo-holdout incremental 2.1x, break-even 2.5x.
Incremental is BELOW break-even — we're losing money. Cut."
Full worked math, the platform-vs-MER-vs-incrementality table, a geo-holdout test
design, and the scale/hold/kill rule live in references/roas-model.md.
Pick the surface
Choose by goal, how much creative/audience control you need, and how much conversion data the account already produces. Don't default to the most-automated option just because it exists.
| Platform | Surface | Use when |
|---|---|---|
| Performance Max | Full-funnel, you'll cede control for reach, and the account already has steady conversion volume to feed the algorithm. | |
| Demand Gen | You need creative + audience control PMax won't give: preview exact combinations, opt out of optimized targeting, report by placement/audience/asset. | |
| Search | Capturing existing high-intent demand; keyword/query control matters more than discovery reach. | |
| Meta | Advantage+ Shopping/Sales | Acquiring new customers at volume, you can feed 15–20+ creatives, and the daily budget clears the learning floor. |
| Meta | Manual (ABO/CBO) | Tight audience control, small budgets, or testing a specific segment the algorithm would dilute. |
Structure
- Consolidate to feed the learning phase. A campaign needs enough conversions to exit learning; many tiny campaigns each starve. Why: the algorithm can't optimize on noise.
- Split budget by job: broad/prospecting, a manual test slice, and retargeting — not eight clones of the same campaign. Why: each slice answers a different question.
- Cap existing customers on Advantage+ at 20–30%. Without the cap, Meta defaults to cheap retargeting conversions and you stop acquiring while the dashboard looks great. Why: easy reconversions inflate ROAS and hide that growth stalled.
- Protect the learning phase: hold structure ≥4 weeks. Budget changes >20%, bid-strategy switches, or adding asset groups all restart learning. Why: every reset throws away the data you paid to collect.
Bad: 8 campaigns × $20/day, each restarted twice this week.
Good: 1 prospecting campaign above the conversion-data floor, untouched 4 weeks,
then act on the data.
PMax allows max 25 asset groups per campaign — start with 1–2. Full structure
detail and the Google Ads API version note for scripting are in
references/platform-specs.md.
Creative
Write the ad-surface copy only. It must obey the brand's voice (../brand-voice/SKILL.md)
and click into a page you do not write (../landing-copy/SKILL.md).
Per-surface caps (summary — full tables, image/video orientations and sizes, and the
Low/Good/Best rotation playbook in references/platform-specs.md):
| Surface | Headlines | Descriptions | Media |
|---|---|---|---|
| PMax (per asset group) | 15 × 30 char + 1 long × 90 char | 5 × 90 char | 20 images, 5 videos |
| Demand Gen | 5 × 40 char | 5 × 90 char | per format |
| Search (RSA) | 15 × 30 char | 4 × 90 char | — |
| Meta Advantage+ | feed 15–20+ creative variations | — | mixed orientations |
- Feed 15–20+ variations on Advantage+. With 3–5 creatives the algorithm can't test and you've built an expensive manual campaign. Why: automation needs raw material to compare.
- Refresh on cadence to fight fatigue. Google rates each asset Low / Good / Best; replace Low assets after 4–6 weeks. Why: a dead creative drags the whole asset group's rating and delivery.
- Never overflow a platform limit. A 33-char "30-char" headline gets truncated or
rejected and tanks the asset rating. Why: the limit is hard, not advisory — lint
before you ship (see
scripts/verify.sh).
Budget & scaling
- Meta Advantage+ floor ≈ 50× target CPA, with a practical minimum around $100/day; below ~$50/day the algorithm can't exit learning. Why: it needs ~50 conversions/week to optimize.
- Scale ≤ 20% per week. Bigger jumps reset the learning phase and you start over at a worse CPA. Why: the algorithm re-explores after a large budget shock.
target CPA $40 → Advantage+ floor ≈ 50 × $40 = $2,000/day
(or ramp in ≤20%/week steps to get there)
Measurement setup gate
Conversions you can't track don't count, and Smart Bidding degrades without them. Run this gate before judging any campaign:
- Consent Mode v2 (Advanced) — mandatory for EEA/UK since 2024-03-06.
- Enhanced Conversions on Google — hashed first-party email/phone to recover modeled conversions.
- Meta CAPI — the server-side equivalent; most stores need both it and Enhanced Conversions.
- Account updated for the unified
ad_storageparameter before 2026-06-15 — after that, un-updated accounts risk attribution gaps and bidding degradation.
Hand the reporting/dashboards to the analytics / dashboard skills — you set up
the signal; they build the read-out.
Anti-patterns
| Anti-pattern | Why it fails | Do instead |
|---|---|---|
| Scaling on platform ROAS | Over-reports 30–100% via double-counting | Validate with geo-holdout / blended MER first |
| Fragmenting budget across many tiny campaigns | None gets enough data to exit learning | Consolidate above the conversion-data floor |
| No existing-customer cap on Advantage+ | Meta drifts to cheap retargeting; acquisition stops | Cap existing customers at 20–30% |
| Launching with 3–5 creatives | Algorithm can't test; it's a manual campaign in disguise | Feed 15–20+ variations, refresh weekly |
| Tweaking budget/bids/assets every few days | Each >20% change resets the learning phase | Hold structure ≥4 weeks, then act on data |
| Target ROAS set below break-even | Every conversion loses money | Set target ≥ 1÷margin; profit-mode 3.5x–8x |
| Ignoring Consent Mode v2 / CAPI | Conversions go unattributed; Smart Bidding degrades | Advanced consent + Enhanced Conversions + CAPI |
| Copy that overflows the platform char limit | Truncated/rejected assets, Low rating | Lint headlines/descriptions to per-surface caps |
Handoff
- Real experiment design (sample size, significance) → the
ab-testingskill. - Blended/next-quarter revenue projection → the
forecastingskill. - Top-of-funnel B2B prospect lists (not paid media) → the
lead-genskill.
Files (rsc-harness)
-
evals
-
cases.yaml 4 KB
skill: ads # Prompts that MUST load `ads`. The skill owns the paid-media BUY on Google and # Meta: campaign structure, platform-fit creative, budget/scaling, and the # break-even/incremental ROAS decision. It does NOT own the landing page # (landing-copy), the voice (brand-voice), the experiment stats (ab-testing), or # the channel-mix decision (marketing). should_trigger: - prompt: "Set up our Google Ads from scratch for a DTC skincare brand." why: "Standing up account structure (which campaign types, budget split, signal) is the skill's core job." - prompt: "Our Performance Max campaign spends a fortune but barely converts — what's wrong with the structure?" why: "Diagnosing and restructuring a PMax campaign (asset groups, learning phase, signal) is owned here." - prompt: "Platform says 4.2x ROAS but the bank account disagrees — are we actually profitable?" why: "Non-obvious: reads like a finance/bookkeeping question, but it's incrementality vs break-even ROAS on a paid buy — the skill's ROAS gate." - prompt: "How much should each Meta campaign get per day, and how fast can I scale the winners?" why: "Daily budget floors (50x CPA) and the <=20%/week scaling rule are the budget-and-scaling section, even though the skill is never named." - prompt: "Qué ROAS necesito en Google Search para no perder dinero con un margen del 45%?" why: "Spanish break-even ROAS math (1 ÷ margin) — a core deliverable, phrased in Spanish without the skill name." - prompt: "La campanya de Meta no converteix des que vam activar el Consent Mode v2 — què s'ha trencat?" why: "Catalan: conversions dropped after Consent Mode v2 — the measurement-setup gate (Advanced consent + Enhanced Conversions + CAPI) is owned here." should_not_trigger: - prompt: "Write the landing page these ads point to." route_to: landing-copy why: "The destination page is the click target, not the ad-surface buy; landing-copy owns the page words." - prompt: "Design an A/B test with proper sample size and statistical significance for our checkout." route_to: ab-testing why: "Experiment methodology — power, sample size, significance — is ab-testing's domain, not the media buy." - prompt: "Should we even invest in paid this quarter, or focus on content and SEO?" route_to: marketing why: "The whether-to-run-paid / channel-mix decision sits one level up in marketing; ads only executes the paid buy inside that plan." - prompt: "Build the dashboard that tracks our ad spend and revenue over time." route_to: dashboard why: "Constructing the reporting surface is dashboard's job; ads sets up the conversion signal but doesn't build the read-out." - prompt: "Define the tone and voice our ad copy should speak in across every channel." route_to: brand-voice why: "The voice system the copy must obey is brand-voice; ads writes ad-surface copy that conforms to it." capability: - scenario: "Given a product (gross margin 50%, target CPA $40) and the goal of scaling Meta acquisition, produce a launch plan." must_include: - "Picks a Meta surface (Advantage+ vs manual) with a one-line why tied to conversion-data volume / creative count." - "Computes break-even ROAS = 1 ÷ 0.50 = 2.0x and sets a target ROAS at or above break-even (states scaling-mode 2x-3x or profit-mode higher explicitly)." - "Derives the daily budget floor from ~50x target CPA (≈$2,000/day) or a staged ramp toward it." - "States an existing-customer budget cap of 20-30% on Advantage+ and why (else delivery drifts to cheap retargeting)." - "Asks for 15-20+ creative variations within platform char/count limits (no overflow)." - "Includes a measurement-setup note: Consent Mode v2 (Advanced) + Enhanced Conversions + Meta CAPI." - "Names incrementality / blended MER (not platform ROAS) as the scale-vs-kill decision, and the <=20%/week scaling rule." - "Hands off destination to landing-copy, voice to brand-voice, experiment stats to ab-testing, reporting to analytics/dashboard." -
README.md 1 KB
# Evals — ads `cases.yaml` has three blocks. **should_trigger** and **should_not_trigger** are routing checks: feed each `prompt` to the router and confirm it picks `ads` for the triggers (including the non-obvious "the bank account disagrees" finance-looking one and the Spanish/Catalan phrasings) and the named real sibling for each near-miss (`landing-copy`, `ab-testing`, `marketing`, `dashboard`, `brand-voice`). The near-miss cases pass only when the router prefers the named sibling over `ads`. The **capability** block is an LLM- or human-graded rubric: run the scenario with the skill loaded and check the produced launch plan hits every `must_include` line — break-even computed (2.0x), target ≥ break-even, ~50× CPA budget floor, the 20–30% existing-customer cap, 15–20+ in-spec creatives, the Consent Mode v2 / Enhanced Conversions / CAPI gate, incrementality/MER (not platform ROAS) as the kill switch, and the handoffs. There's no automated runner here — grade by reading the output against the list, or wire it into your eval harness of choice.
-
-
references
-
platform-specs.md 3 KB
# Platform asset specs Hard limits per surface. The character counts are not advice — overflow gets the asset truncated or rejected, which drags the asset-group rating. Lint against these with `scripts/verify.sh` before shipping. *All counts/limits dated 2026-06-02. Sources: Google Ads Help answer/14528220 (PMax), answer/13704860 (Demand Gen); digitalapplied.com PMax 2026 guide; groas.com Demand Gen 2026 guide; adligator.com / attnagency.com Advantage+ guides.* ## Google Performance Max (per asset group) | Asset | Count | Limit | |---|---|---| | Headlines | up to 15 | 30 char each | | Long headline | 1 | 90 char | | Descriptions | up to 5 | 90 char each (one short ≤60 recommended) | | Images | up to 20 | landscape 1.91:1, square 1:1, portrait 4:5 | | Logos | up to 5 | 1:1 and 4:1 | | Videos | up to 5 | ≥10s; add your own — Google auto-generates poor ones if you don't | | Business name | 1 | 25 char | - **Asset groups:** max **25 per campaign**; start with **1–2** and only split when a group has a distinct audience/theme and enough budget to feed it. - **Asset rating:** Google labels each asset **Low / Good / Best**. Replace **Low** assets after **4–6 weeks** — a Low asset suppresses the whole group's reach. ## Google Demand Gen | Asset | Count | Limit | |---|---|---| | Headlines | up to 5 | 40 char each | | Descriptions | up to 5 | 90 char each | | Images | per format | 1.91:1, 1:1, 4:5 | | Videos | per format | landscape / square / vertical | Demand Gen gives the control PMax withholds: **preview exact combinations**, **opt out of optimized targeting**, and **report by placement / audience / asset**. Reach for it when you need creative and audience control, not maximum automation. ## Google Search — Responsive Search Ads | Asset | Count | Limit | |---|---|---| | Headlines | up to 15 | 30 char each | | Descriptions | up to 4 | 90 char each | Pin sparingly — pinning everything removes the algorithm's ability to test combinations. ## Meta Advantage+ (Shopping / Sales) - Feed **15–20+ creative variations** across mixed orientations (1:1, 4:5, 9:16) so the algorithm has material to compare; 3–5 creatives make it a manual campaign in disguise. - Primary text, headline, and description fields exist per placement; Meta truncates primary text in-feed around ~125 char — front-load the hook. - Set the **existing-customer budget cap at 20–30%**, or delivery drifts to cheap retargeting reconversions and acquisition stalls. ## Meta manual (ABO / CBO) Use for tight audience control, small budgets, or isolating a segment the algorithm would dilute. Same creative fields; you control the audience and budget split per ad set. ## Google Ads API version note (for scripting) If you script against the API rather than the UI: Google moved to a **monthly release cadence in 2026**. Latest is **v24.1 (released 2026-05-13)**, preceded by v24 (2026-04-22) and v23 (2026-01-28). Pin a version explicitly and watch the deprecation window. *Source: developers.google.com/google-ads/api/docs/release-notes — accessed 2026-06-02.* -
roas-model.md 3.1 KB
# ROAS model The money math that gates every campaign decision. Do this before structure. *Dated 2026-06-02. Sources: triplewhale.com break-even ROAS guide; bennettfinancials.com contribution-margin guide; kleene.ai ROAS guide.* ## Break-even ROAS ```text break-even ROAS = 1 ÷ gross-margin % ``` Gross margin here is **contribution margin** — price minus COGS, payment fees, shipping, and any per-order variable cost. Use the real number, not the headline markup. | Gross margin | Break-even ROAS | |---|---| | 30% | 3.33x | | 40% | 2.50x | | 50% | 2.00x | | 60% | 1.67x | | 70% | 1.43x | A campaign whose target ROAS sits **below** the break-even row is losing money on every conversion. There is no targeting fix for that — change the offer, the margin, or the target. ## Target ROAS by stage | Mode | Meta target | Google Search target | Judge on | |---|---|---|---| | Profit | 3.5x–5x | 5x–8x | Campaign ROAS, validated incremental | | Scaling | 2x–3x | 2x–3x | Blended MER (you trade margin for growth on purpose) | Always set **target ≥ break-even**. Scaling-mode targets near break-even are a deliberate growth bet, not an accident — name it as such. ## Why the platform number lies | Number | What it measures | Trust for | |---|---|---| | **Platform ROAS** | Last-click, per-platform; double-counts across surfaces | Nothing on its own — over-reports 30–100% | | **Blended MER** | Total revenue ÷ total ad spend, all channels | The scaling decision | | **Incremental ROAS** | Revenue that would NOT have happened without the ad | The truth — usually 30–60% of the platform number | Platform-reported ROAS credits the ad for sales that would have closed anyway (brand searches, returning customers, organic-assisted). Incrementality strips that out. ## Geo-holdout test design The 2026 gold standard for validating real lift: 1. Split comparable regions into **test** (ads on) and **control** (ads off / held out). Match on baseline revenue and seasonality. 2. Run for a clean window (≥2–4 weeks, longer than the purchase cycle). 3. Incremental revenue = test-region revenue − control-region revenue (scaled to equal population/baseline). 4. **Incremental ROAS = incremental revenue ÷ test-region ad spend.** 5. Compare that incremental ROAS to break-even — not the platform number. Ghost-ad / PSA-holdout tests are the on-platform equivalent when geo-splits aren't feasible. For the statistical design (power, significance, sample size) hand off to the `ab-testing` skill — this is the media-side framing, not the experiment math. ## Scale / hold / kill rule | Incremental ROAS vs break-even | Decision | |---|---| | Comfortably above break-even AND stable ≥2 weeks | **Scale** — ≤20%/week, don't reset learning | | Near break-even or noisy | **Hold** — keep structure ≥4 weeks, gather data, fix creative/signal first | | Below break-even after the learning phase | **Kill** — or fix margin/offer; no bid tweak rescues negative unit economics | Never scale on the platform dashboard. Scale on incremental ROAS or blended MER, and only after the learning phase has stabilized.
-
-
scripts
-
verify.sh 8.1 KB
#!/usr/bin/env bash # # verify.sh — paid-ads asset/plan linter for the `ads` skill. # # WHAT IT DOES (read-only; never edits a file) # Lints the ad-asset/plan files the skill produces against hard platform limits # and ROAS sanity. Looks for simple, machine-checkable tags in *.md / *.txt / # *.yaml / *.yml / *.csv files. Every tag is optional — a file with none is # simply skipped, so this never false-fails on an unrelated repo. # # Checks (a finding is a FAILURE only when copy overflows a hard limit or a # target ROAS sits below break-even — those are not judgement calls): # 1. Headline length per surface tag: # [PMAX-HEADLINE] / [SEARCH-HEADLINE] limit 30 char # [DG-HEADLINE] (Demand Gen) limit 40 char # and description tags: # [DESCRIPTION] / [DG-DESCRIPTION] / [PMAX-DESCRIPTION] limit 90 char # 2. Headline COUNT per surface, per file: # PMax/Search <= 15 headlines, Demand Gen <= 5 headlines. # 3. ROAS sanity: a line tagged # [ROAS] target=<x> margin=<0..1 or pct> # must have target >= break-even (1 / margin). Below break-even FAILS. # 4. Advantage+ existing-customer cap: a file that mentions Advantage+ # (case-insensitive) but has no [EXISTING-CUSTOMER-CAP] / "existing customer # cap" line gets a WARNING (acquisition risk), not a failure. # # HOW TO RUN (inside YOUR project, not the skills repo) # ./verify.sh # scan ./ for ad asset/plan files # ./verify.sh --path plans # scan a subdirectory # ./verify.sh --strict # treat any warning as a failure (exit 1) # # EXIT CODES # 0 clean, or warnings only without --strict (also: nothing to check) # 1 a hard-limit overflow / below-break-even ROAS, or --strict with a warning # 2 bad usage # # Runs on stock macOS bash 3.2 — no mapfile, no associative arrays. set -euo pipefail if [ -t 1 ]; then RED=$'\033[31m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; NC=$'\033[0m' else RED=''; GREEN=''; YELLOW=''; NC='' fi ok_count=0; skip_count=0; warn_count=0; fail_count=0 ok() { printf '%s[ ok ]%s %s\n' "$GREEN" "$NC" "$*"; ok_count=$((ok_count + 1)); } skip() { printf '%s[skip]%s %s\n' "$YELLOW" "$NC" "$*"; skip_count=$((skip_count + 1)); } warn() { printf '%s[warn]%s %s\n' "$YELLOW" "$NC" "$*"; warn_count=$((warn_count + 1)); } fail() { printf '%s[fail]%s %s\n' "$RED" "$NC" "$*"; fail_count=$((fail_count + 1)); } usage() { sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'; } SCAN_PATH="." STRICT=0 while [ $# -gt 0 ]; do case "$1" in --path) SCAN_PATH="${2:?--path needs a value}"; shift 2 ;; --strict) STRICT=1; shift ;; -h|--help) usage; exit 0 ;; *) printf '%sUnknown argument: %s%s\n\n' "$RED" "$1" "$NC"; usage; exit 2 ;; esac done if [ ! -e "$SCAN_PATH" ]; then printf '%sPath not found: %s%s\n' "$RED" "$SCAN_PATH" "$NC"; exit 2 fi have() { command -v "$1" >/dev/null 2>&1; } # Collect candidate files (asset/plan-bearing extensions only). TMPDIR_V="$(mktemp -d 2>/dev/null || printf '/tmp/ads-verify.%s' "$$")" mkdir -p "$TMPDIR_V" 2>/dev/null || true cleanup() { rm -rf "$TMPDIR_V" 2>/dev/null || true; } trap cleanup EXIT FILES="$TMPDIR_V/files" find "$SCAN_PATH" -type f \ \( -name '*.md' -o -name '*.txt' -o -name '*.yaml' -o -name '*.yml' -o -name '*.csv' \) \ 2>/dev/null > "$FILES" || true if [ ! -s "$FILES" ]; then skip "no asset/plan files (*.md *.txt *.yaml *.yml *.csv) under: $SCAN_PATH" printf '\nok=%d skip=%d warn=%d fail=%d\n' "$ok_count" "$skip_count" "$warn_count" "$fail_count" exit 0 fi # visible length of the text after a tag on a line (strip the tag, trim, drop a # leading "= " or ": ", strip surrounding quotes), printed as an integer. text_after_tag() { # $1 = full line, $2 = tag token (e.g. [PMAX-HEADLINE]) line="$1"; tag="$2" rest="${line#*"$tag"}" # drop a leading separator rest="${rest#:}"; rest="${rest#=}" # trim leading/trailing whitespace rest="$(printf '%s' "$rest" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')" # strip one layer of surrounding quotes rest="${rest#\"}"; rest="${rest%\"}" rest="${rest#\'}"; rest="${rest%\'}" printf '%s' "$rest" } # --- per-tag length checks --------------------------------------------------- # args: <tag> <limit> <human label> check_len_tag() { tag="$1"; limit="$2"; label="$3" found=0; bad=0 while IFS= read -r f; do [ -z "$f" ] && continue while IFS= read -r ln; do case "$ln" in *"$tag"*) : ;; *) continue ;; esac found=1 txt="$(text_after_tag "$ln" "$tag")" n=${#txt} if [ "$n" -gt "$limit" ]; then fail "$label over $limit char ($n): $f :: $txt" bad=1 fi done < "$f" done < "$FILES" if [ "$found" -eq 0 ]; then skip "no $tag tags to check" elif [ "$bad" -eq 0 ]; then ok "$label: all within $limit char" fi } check_len_tag "[PMAX-HEADLINE]" 30 "PMax headline" check_len_tag "[SEARCH-HEADLINE]" 30 "Search headline" check_len_tag "[DG-HEADLINE]" 40 "Demand Gen headline" check_len_tag "[DESCRIPTION]" 90 "Description" check_len_tag "[PMAX-DESCRIPTION]" 90 "PMax description" check_len_tag "[DG-DESCRIPTION]" 90 "Demand Gen description" # --- headline COUNT per surface, per file ------------------------------------ # args: <tag> <max> <label> check_count_tag() { tag="$1"; max="$2"; label="$3" any=0 while IFS= read -r f; do [ -z "$f" ] && continue c="$( { grep -F -c "$tag" "$f" 2>/dev/null || true; } | tr -dc '0-9')" [ -z "$c" ] && c=0 [ "$c" -eq 0 ] && continue any=1 if [ "$c" -gt "$max" ]; then fail "$label count $c exceeds max $max: $f" fi done < "$FILES" if [ "$any" -eq 0 ]; then skip "no $tag tags to count"; fi } check_count_tag "[PMAX-HEADLINE]" 15 "PMax headline" check_count_tag "[SEARCH-HEADLINE]" 15 "Search headline" check_count_tag "[DG-HEADLINE]" 5 "Demand Gen headline" # --- ROAS sanity: target >= 1/margin ---------------------------------------- # Matches a line containing [ROAS] with target=<num> and margin=<num|pct>. roas_any=0 while IFS= read -r f; do [ -z "$f" ] && continue while IFS= read -r ln; do case "$ln" in *"[ROAS]"*) : ;; *) continue ;; esac target="$(printf '%s' "$ln" | sed -n 's/.*[Tt]arget[[:space:]]*=[[:space:]]*\([0-9.]*\).*/\1/p')" margin_raw="$(printf '%s' "$ln" | sed -n 's/.*[Mm]argin[[:space:]]*=[[:space:]]*\([0-9.]*%\{0,1\}\).*/\1/p')" [ -z "$target" ] && continue [ -z "$margin_raw" ] && continue roas_any=1 # normalise margin to a fraction 0..1 case "$margin_raw" in *%) mfrac="$(awk -v m="${margin_raw%\%}" 'BEGIN{printf "%.6f", m/100}')" ;; *) mfrac="$(awk -v m="$margin_raw" 'BEGIN{ if (m>1) printf "%.6f", m/100; else printf "%.6f", m }')" ;; esac # break-even = 1/mfrac; guard divide-by-zero verdict="$(awk -v t="$target" -v m="$mfrac" 'BEGIN{ if (m<=0) { print "skip"; exit } be=1/m; if (t+0.0001 < be) printf "fail %.2f", be; else printf "ok %.2f", be; }')" case "$verdict" in fail*) be="${verdict#fail }"; fail "target ROAS $target below break-even $be (margin $margin_raw): $f" ;; ok*) be="${verdict#ok }"; ok "target ROAS $target >= break-even $be (margin $margin_raw)" ;; *) warn "ROAS line has non-positive margin, cannot check: $f" ;; esac done < "$f" done < "$FILES" if [ "$roas_any" -eq 0 ]; then skip "no [ROAS] target/margin lines to check"; fi # --- Advantage+ existing-customer cap presence ------------------------------- adv_any=0 while IFS= read -r f; do [ -z "$f" ] && continue if grep -iE 'advantage\+|advantage plus' "$f" >/dev/null 2>&1; then adv_any=1 if grep -iE 'existing[- ]customer cap|\[EXISTING-CUSTOMER-CAP\]' "$f" >/dev/null 2>&1; then ok "Advantage+ plan declares an existing-customer cap: $f" else warn "Advantage+ plan with no existing-customer cap (acquisition will drift to retargeting): $f" fi fi done < "$FILES" if [ "$adv_any" -eq 0 ]; then skip "no Advantage+ plan to check for an existing-customer cap"; fi printf '\nok=%d skip=%d warn=%d fail=%d\n' "$ok_count" "$skip_count" "$warn_count" "$fail_count" if [ "$fail_count" -gt 0 ]; then exit 1; fi if [ "$STRICT" -eq 1 ] && [ "$warn_count" -gt 0 ]; then exit 1; fi exit 0
-
-
SKILL.md 8.7 KB
--- name: ads description: "Use when running or fixing paid acquisition on Google or Meta — campaign structure (Performance Max, Demand Gen, Search, Advantage+), platform-fit creative, budget/scaling rules, break-even ROAS math, and Consent Mode v2 / CAPI tracking gaps. NOT the page the ad clicks into (that is `landing-copy`), NOT the channel-mix plan (that is `marketing`)." tags: [paid-ads, google-ads, meta-ads, roas, performance-max, ppc, paid-acquisition] recommends: [marketing, landing-copy, brand-voice, ab-testing, analytics, dashboard, forecasting, lead-gen] origin: risco --- # ads You are the paid-acquisition operator. You run money through Google and Meta to buy customers, and you answer four questions in this order: **structure → creative → budget → ROAS**. Your subject is the live account and its economics — the campaign shape, the asset sets, the bid/budget config, and the math that says *keep scaling* or *kill it*. The nearest miss is `marketing`: it decides *whether to run paid at all and the channel mix* (`../marketing/SKILL.md`); you execute *the Google/Meta buy inside that plan* down to asset groups, bids, and break-even ROAS. ## ROAS first — it gates everything Do the money math before you touch a single campaign setting. Structure is meaningless if the unit economics don't close. - **Break-even ROAS = 1 ÷ gross-margin %.** 40% margin needs ≥**2.5x** to break even on contribution; 50% margin needs ≥**2.0x**. *Why:* below this every conversion loses money no matter how good the targeting. - **Target by stage.** Profit-mode brands aim **3.5x–5x on Meta**, **5x–8x on Google Search**. Scaling-mode brands accept **2x–3x** and judge on blended MER, not campaign ROAS. *Why:* you trade margin for growth deliberately, not by accident. - **Platform-reported ROAS lies.** It over-reports **30–100%** by double-counting conversions across campaigns and surfaces; true incremental revenue is often only **30–60%** of the platform number. *Why:* last-click attribution credits the ad for sales that would have happened anyway. - **The truth check is incrementality, not the dashboard.** Geo-holdout / ghost-ad tests are the 2026 gold standard; for the scaling decision switch to **blended MER** (total revenue ÷ total ad spend). *Why:* it's the only number tied to your bank account. ```text Bad: "We hit 4.2x ROAS — scale it!" (platform, last-click) Good: "Platform 4.2x, geo-holdout incremental 2.1x, break-even 2.5x. Incremental is BELOW break-even — we're losing money. Cut." ``` Full worked math, the platform-vs-MER-vs-incrementality table, a geo-holdout test design, and the scale/hold/kill rule live in `references/roas-model.md`. ## Pick the surface Choose by goal, how much creative/audience control you need, and how much conversion data the account already produces. Don't default to the most-automated option just because it exists. | Platform | Surface | Use when | |---|---|---| | Google | **Performance Max** | Full-funnel, you'll cede control for reach, and the account already has steady conversion volume to feed the algorithm. | | Google | **Demand Gen** | You need creative + audience control PMax won't give: preview exact combinations, opt out of optimized targeting, report by placement/audience/asset. | | Google | **Search** | Capturing existing high-intent demand; keyword/query control matters more than discovery reach. | | Meta | **Advantage+ Shopping/Sales** | Acquiring new customers at volume, you can feed 15–20+ creatives, and the daily budget clears the learning floor. | | Meta | **Manual (ABO/CBO)** | Tight audience control, small budgets, or testing a specific segment the algorithm would dilute. | ## Structure - **Consolidate to feed the learning phase.** A campaign needs enough conversions to exit learning; many tiny campaigns each starve. *Why:* the algorithm can't optimize on noise. - **Split budget by job:** broad/prospecting, a manual test slice, and retargeting — not eight clones of the same campaign. *Why:* each slice answers a different question. - **Cap existing customers on Advantage+ at 20–30%.** Without the cap, Meta defaults to cheap retargeting conversions and you stop acquiring while the dashboard looks great. *Why:* easy reconversions inflate ROAS and hide that growth stalled. - **Protect the learning phase: hold structure ≥4 weeks.** Budget changes >20%, bid-strategy switches, or adding asset groups all **restart** learning. *Why:* every reset throws away the data you paid to collect. ```text Bad: 8 campaigns × $20/day, each restarted twice this week. Good: 1 prospecting campaign above the conversion-data floor, untouched 4 weeks, then act on the data. ``` PMax allows max **25 asset groups** per campaign — start with **1–2**. Full structure detail and the Google Ads API version note for scripting are in `references/platform-specs.md`. ## Creative Write the ad-surface copy only. It must obey the brand's voice (`../brand-voice/SKILL.md`) and click into a page you do not write (`../landing-copy/SKILL.md`). Per-surface caps (summary — full tables, image/video orientations and sizes, and the Low/Good/Best rotation playbook in `references/platform-specs.md`): | Surface | Headlines | Descriptions | Media | |---|---|---|---| | PMax (per asset group) | 15 × 30 char + 1 long × 90 char | 5 × 90 char | 20 images, 5 videos | | Demand Gen | 5 × 40 char | 5 × 90 char | per format | | Search (RSA) | 15 × 30 char | 4 × 90 char | — | | Meta Advantage+ | feed 15–20+ creative variations | — | mixed orientations | - **Feed 15–20+ variations on Advantage+.** With 3–5 creatives the algorithm can't test and you've built an expensive manual campaign. *Why:* automation needs raw material to compare. - **Refresh on cadence to fight fatigue.** Google rates each asset **Low / Good / Best**; replace **Low** assets after **4–6 weeks**. *Why:* a dead creative drags the whole asset group's rating and delivery. - **Never overflow a platform limit.** A 33-char "30-char" headline gets truncated or rejected and tanks the asset rating. *Why:* the limit is hard, not advisory — lint before you ship (see `scripts/verify.sh`). ## Budget & scaling - **Meta Advantage+ floor ≈ 50× target CPA**, with a practical minimum around **$100/day**; below ~$50/day the algorithm can't exit learning. *Why:* it needs ~50 conversions/week to optimize. - **Scale ≤ 20% per week.** Bigger jumps reset the learning phase and you start over at a worse CPA. *Why:* the algorithm re-explores after a large budget shock. ```text target CPA $40 → Advantage+ floor ≈ 50 × $40 = $2,000/day (or ramp in ≤20%/week steps to get there) ``` ## Measurement setup gate Conversions you can't track don't count, and Smart Bidding degrades without them. Run this gate before judging any campaign: - [ ] **Consent Mode v2 (Advanced)** — mandatory for EEA/UK since 2024-03-06. - [ ] **Enhanced Conversions** on Google — hashed first-party email/phone to recover modeled conversions. - [ ] **Meta CAPI** — the server-side equivalent; most stores need both it and Enhanced Conversions. - [ ] Account updated for the unified `ad_storage` parameter before **2026-06-15** — after that, un-updated accounts risk attribution gaps and bidding degradation. Hand the reporting/dashboards to the `analytics` / `dashboard` skills — you set up the signal; they build the read-out. ## Anti-patterns | Anti-pattern | Why it fails | Do instead | |---|---|---| | Scaling on platform ROAS | Over-reports 30–100% via double-counting | Validate with geo-holdout / blended MER first | | Fragmenting budget across many tiny campaigns | None gets enough data to exit learning | Consolidate above the conversion-data floor | | No existing-customer cap on Advantage+ | Meta drifts to cheap retargeting; acquisition stops | Cap existing customers at 20–30% | | Launching with 3–5 creatives | Algorithm can't test; it's a manual campaign in disguise | Feed 15–20+ variations, refresh weekly | | Tweaking budget/bids/assets every few days | Each >20% change resets the learning phase | Hold structure ≥4 weeks, then act on data | | Target ROAS set below break-even | Every conversion loses money | Set target ≥ 1÷margin; profit-mode 3.5x–8x | | Ignoring Consent Mode v2 / CAPI | Conversions go unattributed; Smart Bidding degrades | Advanced consent + Enhanced Conversions + CAPI | | Copy that overflows the platform char limit | Truncated/rejected assets, Low rating | Lint headlines/descriptions to per-surface caps | ## Handoff - **Real experiment design** (sample size, significance) → the `ab-testing` skill. - **Blended/next-quarter revenue projection** → the `forecasting` skill. - **Top-of-funnel B2B prospect lists** (not paid media) → the `lead-gen` skill.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.