Skill: alirezarezvani/cs-ad-creative (version 1.0.0)

--- name: "ad-creative" description: "When the user needs to generate, iterate, or scale ad creative for paid advertising. Use when they say 'write ad copy,' 'generate headlines,' 'create ad variations,' 'bulk creative,' 'iterate on ads,' 'ad copy validation,' 'RSA headlines,' 'Meta ad copy,' 'LinkedIn ad,' or 'creative testing.' This is pure creative production — distinct from paid-ads (campaign strategy). Use ad-creative when you need the copy, not the campaign plan." license: MIT metadata: version: 1.0.0 author: Alireza Rezvani category: marketing updated: 2026-03-06 --- # Ad Creative You are a performance creative director who has written thousands of ads. You know what converts, what gets rejected, and what looks like it should work but doesn't. Your goal is to produce ad copy that passes platform review, stops the scroll, and drives action — at scale. ## Before Starting **Check for context first:** If `marketing-context.md` exists, read it before asking questions. Use that context and only ask for information not already covered. Gather this context (ask if not provided): ### 1. Product & Offer - What are you advertising? Be specific — product, feature, free trial, lead magnet? - What's the core value prop in one sentence? - What does the customer get and how fast? ### 2. Audience - Who are you writing for? Job title, pain point, moment in their day - What do they already believe? What objections will they have? ### 3. Platform & Stage - Which platform(s)? (Google, Meta, LinkedIn, Twitter/X, TikTok) - Funnel stage? (Awareness / Consideration / Decision) - Any existing copy to iterate from, or starting fresh? ### 4. Performance Data (if iterating) - What's currently running? Share current copy. - Which ads are winning? CTR, CVR, CPA? - What have you already tested? --- ## How This Skill Works ### Mode 1: Generate from Scratch Starting with nothing. Build a complete creative set from brief to ready-to-upload copy. **Workflow:** 1. Extract the core message — what changes in the customer's life? 2. Map to funnel stage → select creative framework 3. Generate 5–10 headlines per formula type 4. Write body copy per platform (respecting character limits) 5. Apply quality checks before handing off ### Mode 2: Iterate from Performance Data You have something running. Now make it better. **Workflow:** 1. Audit current copy — what angle is each ad taking? 2. Identify the winning pattern (hook type, offer framing, emotional appeal) 3. Double down: 3–5 variations on the winning theme 4. Open new angles: 2–3 tests in unexplored territory 5. Validate all against platform specs and quality score ### Mode 3: Scale Variations You have a winning creative. Now multiply it for testing or for multiple audiences/platforms. **Workflow:** 1. Lock the core message 2. Vary one element at a time: hook, social proof, CTA, format 3. Adapt across platforms (reformat without rewriting from scratch) 4. Produce a creative matrix: rows = angles, columns = platforms --- ## Platform Specs Quick Reference | Platform | Format | Headline Limit | Body Copy Limit | Notes | |----------|--------|---------------|-----------------|-------| | Google RSA | Search | 30 chars (×15) | 90 chars (×4 descriptions) | Max 3 pinned | | Google Display | Display | 30 chars (×5) | 90 chars (×5) | Also needs 5 images | | Meta (Facebook/Instagram) | Feed/Story | 40 chars (primary) | 125 chars primary text | Image text <20% | | LinkedIn | Sponsored Content | 70 chars headline | 150 chars intro text | No click-bait | | Twitter/X | Promoted | 70 chars | 280 chars total | No deceptive tactics | | TikTok | In-Feed | No overlay headline | 80–100 chars caption | Hook in first 3s | See [references/platform-specs.md](references/platform-specs.md) for full specs including image sizes, video lengths, and rejection triggers. --- ## Creative Framework by Funnel Stage ### Awareness — Lead with the Problem They don't know you yet. Meet them where they are. **Frame:** Problem → Amplify → Hint at Solution - Lead with the pain, not the product - Use the language they use when complaining to a colleague - Don't pitch. Relate. **Works well:** Curiosity hooks, stat-based hooks, "you know that feeling" hooks ### Consideration — Lead with the Solution They know the problem. They're evaluating options. **Frame:** Solution → Mechanism → Proof - Explain what you do, but through the lens of the outcome they want - Show that you work differently (the mechanism matters here) - Social proof starts mattering here: reviews, case studies, numbers **Works well:** Benefit-first headlines, comparison frames, how-it-works copy ### Decision — Lead with Proof They're close. Remove the last objection. **Frame:** Proof → Risk Removal → Urgency - Testimonials, case studies, results with numbers - Remove risk: free trial, money-back, no credit card - Urgency if you have it — but only real urgency, not fake countdown timers **Works well:** Social proof headlines, guarantee-first, before/after See [references/creative-frameworks.md](references/creative-frameworks.md) for the full framework catalog with examples by platform. --- ## Headline Formulas That Actually Work ### Benefit-First `[Verb] [specific outcome] [timeframe or qualifier]` - "Cut your churn rate by 30% without chasing customers" - "Ship features your team actually uses" - "Hire senior engineers in 2 weeks, not 4 months" ### Curiosity `[Surprising claim or counterintuitive angle]` - "The email sequence that gets replies when your first one fails" - "Why your best customers leave at 90 days" - "Most agencies won't tell you this about Meta ads" ### Social Proof `[Number] [people/companies] [outcome]` - "1,200 SaaS teams use this to reduce support tickets" - "Trusted by 40,000 developers across 80 countries" - "How [similar company] doubled activation in 6 weeks" ### Urgency (done right) `[Real scarcity or time-sensitive value]` - "Q1 pricing ends March 31 — new contracts from April 1" - "Only 3 onboarding slots open this month" - No: "🔥 LIMITED TIME DEAL!! ACT NOW!!!" — gets rejected and looks desperate ### Problem Agitation `[Describe the pain vividly]` - "Still losing 40% of signups before they see value?" - "Your ads are probably running, your budget is definitely spending, and you're not sure what's working" --- ## Iteration Methodology When you have performance data, don't just write new ads — learn from what's working. ### Step 1: Diagnose the Winner - What hook type is it? (Problem / Benefit / Curiosity / Social Proof) - What funnel stage is it serving? - What emotional driver is it hitting? (Fear, ambition, FOMO, frustration, relief) - What's the CTA asking for? (Click / Sign up / Learn more / Book a call) ### Step 2: Extract the Pattern Look for what the winner has that others don't: - Specific numbers vs. vague claims - First-person customer voice vs. brand voice - Direct benefit vs. emotional appeal ### Step 3: Generate on Theme Write 3–5 variations that preserve the winning pattern: - Same hook type, different angle - Same emotional driver, different example - Same structure, different product feature ### Step 4: Test a New Angle Don't just exploit. Also explore. Pick one untested angle and generate 2–3 ads. ### Step 5: Validate and Submit Run all new copy through the quality checklist (see below) before uploading. --- ## Quality Checklist Before submitting any ad copy, verify: **Platform Compliance** - [ ] All character counts within limits (use `scripts/ad_copy_validator.py`) - [ ] No ALL CAPS except acronyms (Google and Meta both flag it) - [ ] No excessive punctuation (!!!, ???, …. all trigger rejection) - [ ] No "click here," "buy now," or platform trademarks in copy - [ ] No first-person platform references ("Facebook," "Insta," "Google") **Quality Standards** - [ ] Headline could stand alone — doesn't require the description to make sense - [ ] Specific claim over vague claim ("save 3 hours" > "save time") - [ ] CTA is clear and matches the landing page offer - [ ] No claims you can't back up (#1, best-in-class, etc.) **Audience Check** - [ ] Would the ideal customer stop scrolling for this? - [ ] Does the language match how they talk about this problem? - [ ] Is the funnel stage right for the audience targeting? --- ## Proactive Triggers Surface these without being asked: - **Generic headlines detected** ("Grow your business," "Save time and money") → Flag and replace with specific, measurable versions - **Character count violations** → Always validate before presenting copy; mark violations clearly - **Stage-message mismatch** → If copy is showing proof content to cold audiences, flag and adjust - **Fake urgency** → If copy uses countdown timers or "limited time" with no real constraint, flag the risk of trust damage and platform rejection - **No variation in hook type** → If all 10 headlines use the same formula, flag the testing gap - **Copy lifted from landing page** → Ad copy and landing page need to feel connected but not identical; flag verbatim duplication --- ## Output Artifacts | When you ask for... | You get... | |---------------------|------------| | "Generate RSA headlines" | 15 headlines organized by formula type, all ≤30 chars, with pinning recommendations | | "Write Meta ads for this campaign" | 3 full ad sets (primary text + headline + description) for each funnel stage | | "Iterate on my winning ads" | Winner analysis + 5 on-theme variations + 2 new angle tests | | "Create a creative matrix" | Table: angles × platforms with full copy per cell | | "Validate my ad copy" | Line-by-line validation report with character counts, rejection risk flags, and quality score (0-100) | | "Give me LinkedIn ad copy" | 3 sponsored content ads with intro text ≤150 chars, plus headlines ≤70 chars | --- ## Communication All output follows the structured communication standard: - **Bottom line first** — lead with the copy, explain the rationale after - **Platform specs visible** — always show character count next to each line - **Confidence tagging** — 🟢 tested formula / 🟡 new angle / 🔴 high-risk claim - **Rejection risks flagged explicitly** — don't make the user guess Format for presenting ad copy: ``` [AD SET NAME] | [Platform] | [Funnel Stage] Headline: "..." (28 chars) 🟢 Body: "..." (112 chars) 🟢 CTA: "Learn More" Notes: Benefit-first formula, tested format for consideration stage ``` --- ## Related Skills - **paid-ads**: Use for campaign strategy, audience targeting, budget allocation, and platform selection. NOT for writing the actual copy (use ad-creative for that). - **copywriting**: Use for landing page and long-form web copy. NOT for platform-specific character-constrained ad copy. - **ab-test-setup**: Use when planning which ad variants to test and how to measure significance. NOT for generating the variants (use ad-creative for that). - **content-creator**: Use for organic social content and blog content. NOT for paid ad copy (different constraints, different voice). - **copy-editing**: Use when polishing existing copy. NOT for bulk generation or platform-specific formatting.

path: references/creative-frameworks.md
content: # Creative Frameworks — Headline and Copy Formulas by Platform and Funnel Stage

A working catalog of the copy frameworks that consistently outperform generic ads. Use these as starting points, not templates to fill in blindly.

---

## The Golden Rule

Every ad has one job: get the right person to stop, read, and take one action. If the copy is trying to do three things, it does none of them well. One message, one CTA, one next step.

---

## Framework Index

1. PAS — Problem, Agitate, Solution
2. BAB — Before, After, Bridge
3. FAB — Feature, Advantage, Benefit
4. AIDA — Attention, Interest, Desire, Action
5. Social Proof Frame
6. Contrarian Frame
7. Specificity Frame
8. How-It-Works Frame

---

## 1. PAS — Problem, Agitate, Solution

**Best for:** Awareness and consideration stage. Cold audiences who don't know your solution.

**Structure:**
- Problem: Name the pain in their words
- Agitate: Make them feel how bad it is (don't make it worse than reality — they'll know)
- Solution: Position your product as the obvious fix

**Example (SaaS — project management):**
```
Primary text: "Your team is shipping, but nobody knows who owns what. Deadlines are "this week" 
not "Tuesday at 3pm." By the time the stand-up is over, everyone has a different version of 
the plan. [Product] replaces the chaos with a single source of truth. Try it free for 14 days."

Headline: "Stop running projects in Slack threads"
```

**Length guidance:** PAS works long and short. Short for cold feed, long for warm retargeting.

---

## 2. BAB — Before, After, Bridge

**Best for:** Consideration and decision stage. Audiences who know the problem and are evaluating.

**Structure:**
- Before: Where they are now (the frustrating state)
- After: Where they want to be (the goal)
- Bridge: How your product gets them from here to there

**Example (B2B analytics tool):**
```
Headline: "From data chaos to clear answers"
Body: "Before [Product]: 6 spreadsheets, 4 dashboards, nobody agrees on the numbers.
After [Product]: One source of truth, automated weekly reports, decisions in minutes.
The bridge: connect your data sources once, and [Product] does the rest."
```

**Note:** BAB works especially well for case studies and social proof ads where you can show a real before/after with numbers.

---

## 3. FAB — Feature, Advantage, Benefit

**Best for:** Decision stage, retargeting, people who have visited your product page.

**Structure:**
- Feature: What it does (the thing you built)
- Advantage: How it works better than the alternative
- Benefit: What the customer actually gets in their life/work

**Common mistake:** Stopping at the feature. "Two-factor authentication" is a feature. "Bank-level security" is an advantage. "Sleep at night knowing your customer data is protected" is the benefit.

**Example (HR software):**
```
Feature: "Automated payroll that syncs with your accounting software"
Advantage: "No more manual data entry between systems"
Benefit: "Close the books on time, every time — without staying late"

Ad copy: "Payroll that closes itself. Automated payroll synced directly to QuickBooks — 
no double entry, no reconciliation hell. Every month. On time. [Product] — 
start your free trial."
```

---

## 4. AIDA — Attention, Interest, Desire, Action

**Best for:** Video ads, longer copy (LinkedIn, email), awareness campaigns.

**Structure:**
- Attention: Hook (first 3 seconds / first sentence)
- Interest: Why this matters to them specifically
- Desire: Make them want the outcome
- Action: One clear CTA

**Example (video script outline for SaaS):**
```
[0–3s] ATTENTION: "[Hook visual/statement]" — "Most companies spend 8 hours a week 
on reports nobody reads."
[3–10s] INTEREST: "If you're a head of marketing, that's 32 hours of your team's time 
each month — time they could spend on campaigns that actually drive revenue."
[10–20s] DESIRE: "[Product] automates the reporting. Your team gets that time back. 
Your manager gets the data they asked for, without the nagging."
[20–25s] ACTION: "Start your 14-day free trial. No credit card."
```

---

## 5. Social Proof Frame

**Best for:** Decision stage. Works especially well for retargeting.

**Formulas:**

**Customer voice:**
```
"[Customer quote that speaks to the exact result — specific numbers preferred]"
— [Name, Title, Company]
[Product name] — [CTA]
```

**Results-led:**
```
Headline: "[Company] saved [X] hours per week with [Product]"
Body: "Before [Product], [Company] was manually tracking [problem]. Today, 
they [specific result] — in [timeframe]. Here's how they did it."
```

**Volume proof:**
```
Headline: "[Number] teams trust [Product] to [outcome]"
Body: "From 5-person startups to Fortune 500 companies. Start your free trial."
```

**Note:** Specificity makes social proof work. "Many customers love it" → weak. "4,200 teams use [Product] to eliminate weekly reports" → strong.

---

## 6. Contrarian Frame

**Best for:** Awareness stage on saturated topics. Breaks through category fatigue.

**Structure:** Challenge the conventional wisdom in your category. Then reframe with your perspective.

**Example (email marketing tool):**
```
Headline: "More emails isn't the answer"
Body: "Everyone says send more emails. Better segmentation. More automation. More sequences.
But if your email is boring, more of it just means more unsubscribes.
[Product] helps you write emails people actually want to open — then send them 
to the people most likely to act. Less volume. More revenue."
```

**Warning:** The contrarian frame needs substance behind it. If your product is "like everyone else but better," don't use this frame. Use it when you genuinely have a different approach.

---

## 7. Specificity Frame

**Best for:** All stages. Works everywhere. The most underused framework.

**Principle:** Specific claims outperform vague claims on every metric. Not "save time" — "save 3 hours per week." Not "grow your business" — "increase trial-to-paid conversion by 22%."

**Upgrade examples:**
| Vague | Specific |
|-------|---------|
| "Save time on reporting" | "Cut reporting time from 8 hours to 45 minutes" |
| "Trusted by leading companies" | "Used by 3,200+ growth teams in 60 countries" |
| "Improve your team's performance" | "Teams using [Product] ship 40% more features per quarter" |
| "Get better results" | "Average customer sees 28% higher conversion within 90 days" |
| "Easy to use" | "Set up in 15 minutes — no engineering required" |

If you don't have specific numbers: get them. Talk to 5 customers, ask for their before/after. One real number beats 10 marketing adjectives.

---

## 8. How-It-Works Frame

**Best for:** Consideration stage. Audiences who are curious but not yet convinced.

**Structure:** Show the mechanism — how your product produces the result. Remove mystery, reduce skepticism.

**Example (automation tool):**
```
Headline: "How [Product] works in 3 steps"
Body:
"1. Connect your tools (10 minutes, no coding)
2. Set your conditions ("when a lead scores over 80, do this")
3. Watch it run — 24/7, without your team touching it"

The result? Leads followed up in minutes, not days. Teams that spend time on deals, 
not on data entry.
[CTA: See it in action — free demo]
```

---

## Platform-Specific Framework Match

| Platform | Best Frameworks | Why |
|----------|---------------|-----|
| Google RSA | Specificity, Benefit-first | Intent-driven — they searched for it |
| Meta Feed (cold) | PAS, Contrarian | Interrupt and engage fast |
| Meta Feed (retargeting) | BAB, Social Proof | They know you — sell the outcome |
| LinkedIn | AIDA, FAB, How-It-Works | Longer attention span, B2B mindset |
| TikTok | PAS (compressed), Hook-first | 3-second hook is everything |
| Twitter | Contrarian, Specificity | Opinionated content performs |

---

## Funnel Stage → Framework Selector

| Stage | Goal | Top Frameworks |
|-------|------|---------------|
| **Awareness** | Interrupt → Relevant → Curious | PAS, Contrarian, Specificity |
| **Consideration** | Educate → Differentiate → Trust | AIDA, FAB, How-It-Works, BAB |
| **Decision** | Prove → Remove risk → Action | Social Proof, BAB, Specificity + guarantee |
| **Retention/Upsell** | Remind value → Expand → Deepen | BAB, Feature highlight, milestone-based |

---

## Headline Formula Quick Reference

| Formula | Structure | Example |
|---------|-----------|---------|
| Benefit-First | [Verb] [outcome] [qualifier] | "Ship twice as fast without breaking prod" |
| Problem-Led | [Pain point they recognize] | "Still manually exporting to CSV every Monday?" |
| Number-Led | [Number] [thing] [result] | "14 days. Zero code. Full automation." |
| Curiosity | [Counterintuitive or unexpected] | "The feature nobody builds that triples retention" |
| How-To | "How [persona] [achieves outcome]" | "How growth teams cut CAC by 35% in one quarter" |
| Social Proof | "[Number] [people/teams] [do/use/trust]" | "31,000 marketers use this to skip the daily stand-up" |
| Objection-Lead | [Address the #1 reason they don't buy] | "No, you don't need an engineer to set this up" |
| Direct Comparison | "[Vs. their current approach]" | "Cheaper than Salesforce. More powerful than your spreadsheet." |

---

## Anti-Patterns to Avoid

| Anti-Pattern | Why It Fails | Fix |
|-------------|-------------|-----|
| "We are the #1 platform for..." | Unsubstantiated, overused, ignored | Lead with proof, not ranking |
| "Solutions for modern teams" | Meaningless — who isn't a modern team? | Name the specific team + specific problem |
| "Powerful yet easy to use" | Every product says this | Show the result — don't describe the product |
| "Unlock your potential" | Zero specificity, total fluff | What potential, specifically? Show it. |
| "Join thousands of happy customers" | Vague and dated | "3,400 companies use [Product] to [specific outcome]" |
| Emoji abuse | Looks desperate on LinkedIn, clutters Google | One emoji max, only if it adds meaning |


path: references/platform-specs.md
content: # Platform Specs — Ad Copy Character Limits and Format Requirements

Full specifications for each major ad platform. Use this when generating or validating ad copy.

---

## Google Ads

### Responsive Search Ads (RSA)
| Element | Limit | Count | Notes |
|---------|-------|-------|-------|
| Headline | 30 chars | Up to 15 (min 3) | At least 3 unique, Google mixes them |
| Description | 90 chars | Up to 4 (min 2) | Google selects 2 to show |
| Display URL path | 15 chars each | 2 path fields | Auto-appended to domain |
| Final URL | No char limit | 1 | Must match domain in display URL |

**Pinning:** You can pin headlines to position 1, 2, or 3. Only pin when critical (e.g., brand name, compliance disclaimer). Pinning reduces Google's optimization.

**Ad Strength:** Google scores RSAs: Poor / Average / Good / Excellent. Target "Good" or "Excellent" by:
- Using all 15 headline slots
- Making headlines unique (no repeats, no same keywords)
- Including your main keyword in at least 3 headlines
- Using descriptions that complement, not repeat, headlines

### Performance Max (PMax)
| Element | Limit | Count |
|---------|-------|-------|
| Headline | 30 chars | Up to 5 |
| Long headline | 90 chars | Up to 5 |
| Description | 90 chars | Up to 5 |
| Short description | 60 chars | 1 |

### Display Ads (Responsive)
| Element | Limit | Count |
|---------|-------|-------|
| Short headline | 30 chars | 1 |
| Long headline | 90 chars | 1 |
| Description | 90 chars | 1 |
| Business name | 25 chars | 1 |

---

## Meta (Facebook & Instagram)

### Feed Ads (Single Image / Carousel)
| Element | Limit | Notes |
|---------|-------|-------|
| Primary text | 125 chars (preview) / 2200 max | First 125 shown before "See more" |
| Headline | 40 chars | Shown below image |
| Description | 30 chars | Optional, below headline |
| Link description | 20 chars | URL preview |

**Image text rule:** Images with >20% text surface area get reduced distribution. Meta's tool at meta.com/ads/inspector/ checks this. Keep text minimal on images — put copy in the primary text field.

### Story / Reel Ads
| Element | Limit | Notes |
|---------|-------|-------|
| Primary text overlay | 90 chars | Auto-placed if used |
| No traditional headline | — | Overlay text is the copy |

### Carousel Ads
| Element | Limit | Notes |
|---------|-------|-------|
| Primary text | 125 chars (preview) | Shared across cards |
| Headline per card | 40 chars | Each card has own headline |
| Description per card | 20 chars | Optional |
| Cards | 2–10 | |

**Rejection triggers (Meta):**
- "Facebook" or "Instagram" in ad copy
- Guarantees of specific financial outcomes ("Make $10k/month")
- Before/after comparison (health/beauty)
- Excessive use of first-person pronouns targeting users ("you," "your" in a way that implies personal attributes)
- ALL CAPS in any significant portion
- Exaggerated health claims
- Click-bait phrasing ("You won't believe...", "Click to find out...")

---

## LinkedIn

### Sponsored Content (Single Image)
| Element | Limit | Notes |
|---------|-------|-------|
| Intro text | 150 chars (preview) / 600 max | First 150 visible before "See more" |
| Headline | 70 chars | |
| Description | 100 chars | Optional |

### Message Ads (InMail)
| Element | Limit | Notes |
|---------|-------|-------|
| Subject line | 60 chars | |
| Body | 1,500 chars | First 500 most critical |
| CTA button | 20 chars | |

### Conversation Ads
| Element | Limit | Notes |
|---------|-------|-------|
| Intro message | 500 chars | |
| CTA per branch | 25 chars | Up to 5 buttons |
| Message body per branch | 500 chars | |

**LinkedIn-specific rules:**
- No "Click here" as standalone CTA
- No images with more than 20% text
- No misleading job descriptions or recruitment bait
- Avoid generic corporate language — LinkedIn users are saturated with it
- B2B works better when you lead with a specific insight or stat, not a product pitch

---

## Twitter/X

### Promoted Tweets
| Element | Limit | Notes |
|---------|-------|-------|
| Tweet text | 280 chars total | URL counts as 23 chars |
| Usable copy | ~257 chars | After URL deduction |
| Image | Any ratio | 1200×628 recommended |

**Twitter-specific notes:**
- Copy + URL + image works in the feed
- Lead with the hook — first 15 words matter most (above-the-fold on mobile)
- Hashtags are optional for paid — they distract from the CTA

---

## TikTok

### In-Feed Ads
| Element | Limit | Notes |
|---------|-------|-------|
| Ad text / caption | 100 chars | Overlaid on video |
| Video length | 5–60 seconds (optimal 15–30s) | |
| Mention/hashtag | Avoid branded hashtags | Policy restriction |

**TikTok-specific notes:**
- Hook must land in the first 3 seconds — after that, thumb stops
- Native-feeling content outperforms polished ads (not always better to use brand assets)
- Text on screen increases time-watched
- CTA button text: "Shop Now," "Learn More," "Download," "Sign Up" are options

---

## Common Rejection Triggers (All Platforms)

| Trigger | Why It Gets Rejected | Fix |
|---------|---------------------|-----|
| ALL CAPS words | Flagged as aggressive/spam | Use title case or sentence case |
| Excessive punctuation | !!!, ???, ... — looks spammy | One at most |
| "#1" claims | Superlatives require proof | Remove or qualify |
| "Guaranteed" | Financial/result guarantees restricted | "Proven to" or show results data |
| Trademarked terms | Platform + competitor names | Remove or get written permission |
| Profanity | Obvious | Remove |
| "Click here" | Considered low-quality bait | Use specific CTA |
| Personal attributes | "You are depressed," "For single people" | Rephrase without identifying attributes |
| Misleading discounts | "90% off" without context | Substantiate or remove |

---

## Platform Comparison — Which Platform for Which Creative?

| Use case | Best platform | Why |
|----------|-------------|-----|
| High intent, search-driven | Google Search RSA | Users are already looking |
| Visual product with broad audience | Meta Feed | Best visual reach, lowest CPM for B2C |
| B2B decision-makers | LinkedIn | Job title + company size targeting |
| Young consumer audience, viral potential | TikTok | Organic-native feel, high engagement |
| Real-time relevance, news-adjacent | Twitter/X | Timely content performs |
| Retargeting across the web | Google Display | Broad reach, cheap retargeting |


path: scripts/ad_copy_validator.py
content: #!/usr/bin/env python3
"""
ad_copy_validator.py — Validates ad copy against platform specs.

Checks: character counts, rejection triggers (ALL CAPS, excessive punctuation,
trademarked terms), and scores each ad 0-100.

Usage:
    python3 ad_copy_validator.py                  # runs embedded sample
    python3 ad_copy_validator.py ads.json         # validates a JSON file
    echo '{"platform":"google_rsa","headlines":["My headline"]}' | python3 ad_copy_validator.py

JSON input format:
    {
      "platform": "google_rsa" | "meta_feed" | "linkedin" | "twitter" | "tiktok",
      "headlines": ["...", ...],
      "descriptions": ["...", ...],   # for google
      "primary_text": "...",          # for meta, linkedin, twitter, tiktok
      "headline": "...",              # for meta headline field
      "intro_text": "..."             # for linkedin
    }
"""

import json
import re
import sys
from collections import defaultdict

# ---------------------------------------------------------------------------
# Platform specifications
# ---------------------------------------------------------------------------
PLATFORM_SPECS = {
    "google_rsa": {
        "name": "Google RSA",
        "headline_max": 30,
        "headline_count_max": 15,
        "headline_count_min": 3,
        "description_max": 90,
        "description_count_max": 4,
        "description_count_min": 2,
    },
    "google_display": {
        "name": "Google Display",
        "headline_max": 30,
        "description_max": 90,
    },
    "meta_feed": {
        "name": "Meta (Facebook/Instagram) Feed",
        "primary_text_max": 125,   # preview limit; 2200 absolute max
        "headline_max": 40,
        "description_max": 30,
    },
    "linkedin": {
        "name": "LinkedIn Sponsored Content",
        "intro_text_max": 150,     # preview limit; 600 absolute max
        "headline_max": 70,
        "description_max": 100,
    },
    "twitter": {
        "name": "Twitter/X Promoted",
        "primary_text_max": 257,   # 280 - 23 chars for URL
    },
    "tiktok": {
        "name": "TikTok In-Feed",
        "primary_text_max": 100,
    },
}

# ---------------------------------------------------------------------------
# Rejection triggers
# ---------------------------------------------------------------------------
TRADEMARKED_TERMS = [
    "facebook", "instagram", "google", "youtube", "tiktok", "twitter",
    "linkedin", "snapchat", "whatsapp", "amazon", "apple", "microsoft",
]

PROHIBITED_PHRASES = [
    "click here",
    "limited time offer",  # allowed if real — flagged for review
    "guaranteed",
    "100% free",
    "act now",
    "best in class",
    "world's best",
    "#1 rated",
    "number one",
]

# Financial / health claim patterns
SUSPICIOUS_PATTERNS = [
    r"\$\d{3,}[k+]?\s*per\s*(day|week|month)",   # "make $1,000 per day"
    r"\d{3,}%\s*(return|roi|profit|gain)",         # "300% return"
    r"(cure|treat|heal|eliminate)\s+\w+",          # health claims
    r"lose\s+\d+\s*(pound|lb|kg)",                 # weight loss claims
]

# ---------------------------------------------------------------------------
# Validation logic
# ---------------------------------------------------------------------------

def count_chars(text):
    return len(text.strip())


def check_all_caps(text):
    """Returns True if more than 30% of alpha chars are uppercase — not counting acronyms."""
    words = text.split()
    violations = []
    for word in words:
        alpha = re.sub(r'[^a-zA-Z]', '', word)
        if len(alpha) > 3 and alpha.isupper():
            violations.append(word)
    return violations


def check_excessive_punctuation(text):
    """Flags repeated punctuation (!!!, ???, ...)."""
    return re.findall(r'[!?\.]{2,}', text)


def check_trademark_mentions(text):
    lowered = text.lower()
    return [term for term in TRADEMARKED_TERMS if re.search(r'\b' + term + r'\b', lowered)]


def check_prohibited_phrases(text):
    lowered = text.lower()
    return [phrase for phrase in PROHIBITED_PHRASES if phrase in lowered]


def check_suspicious_claims(text):
    hits = []
    for pattern in SUSPICIOUS_PATTERNS:
        if re.search(pattern, text, re.IGNORECASE):
            hits.append(pattern)
    return hits


def score_ad(issues):
    """
    Score 0-100. Start at 100, deduct per issue category.
    """
    score = 100
    deductions = {
        "char_over_limit": 20,
        "all_caps": 15,
        "excessive_punctuation": 10,
        "trademark_mention": 25,
        "prohibited_phrase": 15,
        "suspicious_claim": 30,
        "count_too_few": 10,
        "count_too_many": 5,
    }
    for category, items in issues.items():
        if items:
            score -= deductions.get(category, 5) * (1 if isinstance(items, bool) else min(len(items), 3))
    return max(0, score)


def validate_google_rsa(ad):
    spec = PLATFORM_SPECS["google_rsa"]
    issues = defaultdict(list)
    report = []

    headlines = ad.get("headlines", [])
    descriptions = ad.get("descriptions", [])

    # Count checks
    if len(headlines) < spec["headline_count_min"]:
        issues["count_too_few"].append(f"Need ≥{spec['headline_count_min']} headlines, got {len(headlines)}")
    if len(headlines) > spec["headline_count_max"]:
        issues["count_too_many"].append(f"Max {spec['headline_count_max']} headlines, got {len(headlines)}")
    if len(descriptions) < spec["description_count_min"]:
        issues["count_too_few"].append(f"Need ≥{spec['description_count_min']} descriptions, got {len(descriptions)}")

    # Character checks per headline
    for i, h in enumerate(headlines):
        length = count_chars(h)
        status = "✅" if length <= spec["headline_max"] else "❌"
        if length > spec["headline_max"]:
            issues["char_over_limit"].append(f"Headline {i+1}: {length} chars (max {spec['headline_max']})")
        report.append(f"  Headline {i+1}: {status} '{h}' ({length}/{spec['headline_max']} chars)")

        # Rejection trigger checks on each headline
        caps = check_all_caps(h)
        if caps:
            issues["all_caps"].extend(caps)
        punct = check_excessive_punctuation(h)
        if punct:
            issues["excessive_punctuation"].extend(punct)
        trademarks = check_trademark_mentions(h)
        if trademarks:
            issues["trademark_mention"].extend(trademarks)
        prohibited = check_prohibited_phrases(h)
        if prohibited:
            issues["prohibited_phrase"].extend(prohibited)

    for i, d in enumerate(descriptions):
        length = count_chars(d)
        status = "✅" if length <= spec["description_max"] else "❌"
        if length > spec["description_max"]:
            issues["char_over_limit"].append(f"Description {i+1}: {length} chars (max {spec['description_max']})")
        report.append(f"  Description {i+1}: {status} '{d}' ({length}/{spec['description_max']} chars)")

        suspicious = check_suspicious_claims(d)
        if suspicious:
            issues["suspicious_claim"].extend(suspicious)

    return report, dict(issues)


def validate_meta_feed(ad):
    spec = PLATFORM_SPECS["meta_feed"]
    issues = defaultdict(list)
    report = []

    primary = ad.get("primary_text", "")
    headline = ad.get("headline", "")

    if primary:
        length = count_chars(primary)
        status = "✅" if length <= spec["primary_text_max"] else "⚠️ (preview truncated)"
        report.append(f"  Primary text: {status} ({length}/{spec['primary_text_max']} preview chars)")
        if length > spec["primary_text_max"]:
            issues["char_over_limit"].append(f"Primary text {length} chars exceeds {spec['primary_text_max']}-char preview")

        for check_fn, key in [
            (check_all_caps, "all_caps"),
            (check_excessive_punctuation, "excessive_punctuation"),
            (check_trademark_mentions, "trademark_mention"),
            (check_prohibited_phrases, "prohibited_phrase"),
            (check_suspicious_claims, "suspicious_claim"),
        ]:
            result = check_fn(primary)
            if result:
                issues[key].extend(result if isinstance(result, list) else [str(result)])

    if headline:
        length = count_chars(headline)
        status = "✅" if length <= spec["headline_max"] else "❌"
        if length > spec["headline_max"]:
            issues["char_over_limit"].append(f"Headline {length} chars (max {spec['headline_max']})")
        report.append(f"  Headline: {status} '{headline}' ({length}/{spec['headline_max']} chars)")

    return report, dict(issues)


def validate_linkedin(ad):
    spec = PLATFORM_SPECS["linkedin"]
    issues = defaultdict(list)
    report = []

    intro = ad.get("intro_text", ad.get("primary_text", ""))
    headline = ad.get("headline", "")

    if intro:
        length = count_chars(intro)
        status = "✅" if length <= spec["intro_text_max"] else "⚠️ (preview truncated)"
        report.append(f"  Intro text: {status} ({length}/{spec['intro_text_max']} preview chars)")
        if length > spec["intro_text_max"]:
            issues["char_over_limit"].append(f"Intro text {length} chars exceeds {spec['intro_text_max']}-char preview")

        for check_fn, key in [
            (check_all_caps, "all_caps"),
            (check_excessive_punctuation, "excessive_punctuation"),
            (check_trademark_mentions, "trademark_mention"),
        ]:
            result = check_fn(intro)
            if result:
                issues[key].extend(result if isinstance(result, list) else [str(result)])

    if headline:
        length = count_chars(headline)
        status = "✅" if length <= spec["headline_max"] else "❌"
        if length > spec["headline_max"]:
            issues["char_over_limit"].append(f"Headline {length} chars (max {spec['headline_max']})")
        report.append(f"  Headline: {status} '{headline}' ({length}/{spec['headline_max']} chars)")

    return report, dict(issues)


def validate_generic(ad, platform_key):
    spec = PLATFORM_SPECS.get(platform_key, {})
    issues = defaultdict(list)
    report = []

    text = ad.get("primary_text", ad.get("text", ""))
    max_chars = spec.get("primary_text_max", 280)

    if text:
        length = count_chars(text)
        status = "✅" if length <= max_chars else "❌"
        if length > max_chars:
            issues["char_over_limit"].append(f"Text {length} chars (max {max_chars})")
        report.append(f"  Text: {status} ({length}/{max_chars} chars)")

        for check_fn, key in [
            (check_all_caps, "all_caps"),
            (check_excessive_punctuation, "excessive_punctuation"),
            (check_trademark_mentions, "trademark_mention"),
            (check_prohibited_phrases, "prohibited_phrase"),
        ]:
            result = check_fn(text)
            if result:
                issues[key].extend(result if isinstance(result, list) else [str(result)])

    return report, dict(issues)


def validate_ad(ad):
    platform = ad.get("platform", "").lower()

    if platform == "google_rsa":
        return validate_google_rsa(ad)
    elif platform == "meta_feed":
        return validate_meta_feed(ad)
    elif platform == "linkedin":
        return validate_linkedin(ad)
    elif platform in ("twitter", "tiktok"):
        return validate_generic(ad, platform)
    else:
        return [f"  ⚠️  Unknown platform '{platform}' — using generic validation"], {}


# ---------------------------------------------------------------------------
# Reporting
# ---------------------------------------------------------------------------

def format_report(ad, char_lines, issues):
    platform = ad.get("platform", "unknown")
    spec = PLATFORM_SPECS.get(platform, {})
    platform_name = spec.get("name", platform.upper())

    score = score_ad(issues)
    grade = "🟢 Excellent" if score >= 85 else "🟡 Needs Work" if score >= 60 else "🔴 High Risk"

    lines = []
    lines.append(f"\n{'='*60}")
    lines.append(f"Platform: {platform_name}")
    lines.append(f"Quality Score: {score}/100  {grade}")
    lines.append(f"{'='*60}")

    lines.append("\nCharacter Counts:")
    lines.extend(char_lines)

    if issues:
        lines.append("\nIssues Found:")
        category_labels = {
            "char_over_limit": "❌ Over character limit",
            "all_caps": "⚠️  ALL CAPS words",
            "excessive_punctuation": "⚠️  Excessive punctuation",
            "trademark_mention": "🚫 Trademarked term",
            "prohibited_phrase": "🚫 Prohibited phrase",
            "suspicious_claim": "🚨 Suspicious claim (review required)",
            "count_too_few": "⚠️  Too few elements",
            "count_too_many": "⚠️  Too many elements",
        }
        for category, items in issues.items():
            label = category_labels.get(category, category)
            lines.append(f"  {label}: {', '.join(str(i) for i in items)}")
    else:
        lines.append("\n✅ No rejection triggers found.")

    lines.append("")
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Sample data (embedded — runs with zero config)
# ---------------------------------------------------------------------------

SAMPLE_ADS = [
    {
        "platform": "google_rsa",
        "headlines": [
            "Cut Reporting Time by 80%",                   # 26 chars ✅
            "Automated Reports, Zero Effort",              # 31 chars ❌ over limit
            "Your Data. Your Way. Every Week.",            # 33 chars ❌ over limit
            "Save 8 Hours Per Week on Reports",            # 32 chars ❌ over limit
            "Try Free for 14 Days",                        # 21 chars ✅
            "No Code. No Complexity. Just Results.",        # 38 chars ❌
            "5,000 Teams Use This",                        # 21 chars ✅
            "Replace Your Weekly Standup Deck",            # 32 chars ❌
            "Connect Your Tools in 15 Minutes",            # 32 chars ❌
            "Instant Dashboards for Your Team",            # 32 chars ❌
            "Start Free — No Credit Card",                 # 28 chars ✅
            "Built for Growth Teams",                      # 22 chars ✅
            "See Your KPIs at a Glance",                   # 25 chars ✅
            "Data-Driven Decisions, Made Easy",            # 32 chars ❌
            "GUARANTEED Results — Try Now!!!",             # 31 chars ❌ + ALL CAPS + excessive punct
        ],
        "descriptions": [
            "Connect your tools, set your KPIs, and let the platform handle the weekly reporting. Free 14-day trial.",  # 103 chars ❌
            "Stop wasting Monday mornings on spreadsheets. Automated reports your whole team actually reads.",           # 94 chars ❌
        ],
    },
    {
        "platform": "meta_feed",
        "primary_text": "Your team is shipping features, but nobody can see the impact. [Product] connects your tools and shows you exactly what's working — in one dashboard, updated automatically. Start free today.",
        "headline": "See Your Impact, Automatically",
    },
    {
        "platform": "linkedin",
        "intro_text": "Growth teams at 3,200+ companies use [Product] to replace their manual weekly reports with automated dashboards.",
        "headline": "Automated Reporting for Growth Teams",
    },
    {
        "platform": "twitter",
        "primary_text": "Stop spending 8 hours on a report nobody reads. [Product] automates it — connect your tools, set your KPIs, and it runs itself. Free trial → [link]",
    },
]


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    import argparse

    parser = argparse.ArgumentParser(
        description="Validates ad copy against platform specs. "
                    "Checks character counts, rejection triggers, and scores each ad 0-100."
    )
    parser.add_argument(
        "file", nargs="?", default=None,
        help="Path to a JSON file containing ad data. "
             "If omitted, reads from stdin or runs embedded sample."
    )
    args = parser.parse_args()

    # Load from file or stdin, else use sample
    ads = None

    if args.file:
        try:
            with open(args.file) as f:
                data = json.load(f)
                ads = data if isinstance(data, list) else [data]
        except Exception as e:
            print(f"Error reading file: {e}", file=sys.stderr)
            sys.exit(1)
    elif not sys.stdin.isatty():
        raw = sys.stdin.read().strip()
        if raw:
            try:
                data = json.loads(raw)
                ads = data if isinstance(data, list) else [data]
            except Exception as e:
                print(f"Error reading stdin: {e}", file=sys.stderr)
                sys.exit(1)
        else:
            print("No input provided — running embedded sample ads.\n")
            ads = SAMPLE_ADS
    else:
        print("No input provided — running embedded sample ads.\n")
        ads = SAMPLE_ADS

    # Aggregate results for JSON output
    results = []
    all_output = []

    for ad in ads:
        char_lines, issues = validate_ad(ad)
        score = score_ad(issues)
        report_text = format_report(ad, char_lines, issues)
        all_output.append(report_text)
        results.append({
            "platform": ad.get("platform"),
            "score": score,
            "issues": {k: v for k, v in issues.items()},
            "passed": score >= 70,
        })

    # Human-readable output
    for block in all_output:
        print(block)

    # Summary
    avg_score = sum(r["score"] for r in results) / len(results) if results else 0
    passed = sum(1 for r in results if r["passed"])
    print(f"\nSUMMARY: {passed}/{len(results)} ads passed (avg score: {avg_score:.0f}/100)")

    # JSON output to stdout (for programmatic use) — write to separate section
    print("\n--- JSON Output ---")
    print(json.dumps(results, indent=2))


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)