Claude Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download ericrisco-rsc-harness-skills_ads-953fef5.zip · 13 KB
Part of ericrisco/rsc-harness — 46 skills

Install

skills CLI npx skills add https://github.com/ericrisco/rsc-harness/tree/main/skills/ads
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ericrisco-rsc-harness@llmmart
Git 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
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.
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_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.
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.

No comments yet.

Reviews (0)

No reviews yet.

Related