Meta ad builder skill

Meta ad builder — Poster frame: a crumpled plastic bottle with sad eyes lying in a wet gutter under a street lamp (from the novoads/agent-skills README)

Illustration · not a resultFrom the novoads/agent-skills README — shows the whole collection, not only this skill.source ↗

Publish finished creatives as live Meta (Facebook/Instagram) ads via the Meta Marketing API, plus research and ad-copy support.

by novoads·MIT license·★ 23 Stars on the repo·GitHub ↗

Use now
Meta ad builder — Poster frame: a crumpled plastic bottle with sad eyes lying in a wet gutter under a street lamp (from the novoads/agent-skills README)

Illustration · not a resultFrom the novoads/agent-skills README — shows the whole collection, not only this skill.view on GitHub

Files of Meta ad builder

novoads/main1 file shown
SKILL.md
Show the full text272 lines

Meta ad builder

Turn a finished creative — typically the output of a generative-AI skill in this workspace — into a live Meta ad. The skill covers three phases: research (optional) → copy → deploy. It talks to the Meta Marketing API directly via ported, parameterized Python scripts.

Meta credentials required before any deploy work. Run bash scripts/check-meta-env.sh first — before uploading a creative, before creating anything in the ad account, before the first Meta call of any phase. On a non-zero exit, stop and ask the user for what it printed. It has already uncommented the two required lines in .env and opened the file, so the ask is "paste these two values", not "go find the docs". Read its Live-app warning out to them: a development-mode app passes every check and then fails at ad creation. Until the check passes, nothing is uploaded, no partial ad or creative exists and nothing is charged — which is exactly why it runs first, since a deploy that dies halfway orphans uploaded assets in a live ad account. This key is deliberately lazy: the pack asks for it here, at the moment someone wants to publish, and never during setup.

When to use this skill

Trigger on phrases like:

  • "deploy this video to Meta" / "publish this as a Facebook ad"
  • "build/launch a Meta ad" / "push this creative into <ad set>"
  • "create a Meta ad with this image and copy"
  • "pull my top-performing ads" / "what are my best ads by ROAS"
  • "research competitor ads" / "pull <brand>'s ads from the Ad Library"

Do not use this skill to generate creative — that's pixar-ad, claymation-ad, chatgpt-image-ad, nano-banana-image-ad, generate-youtube-thumbnail, etc. Do not use it to write ad-table rows. This skill is the direct Meta Marketing API deployment step.

Read order

  1. This file — workflow, decision tree, safety rules.
  2. prompting/copy-guide.md — the 5-body / 5-title / 3-description frameworks and the --copy-file JSON shape.
  3. reference/deploy-patterns.md — creative spec mechanics, video polling, retry, failure modes.
  4. reference/meta-api-cheatsheet.md — the full Meta Marketing API reference (campaigns, ad sets, ads, enums, gotchas, Ad Library). Consult as needed; don't read end-to-end.

Prerequisites

REST key required. A Novoads MCP connector is not a substitute. If NOVOADS_API_KEY is missing or still the placeholder, stop before any generation work and tell the user: "Before continuing, create an API key at https://novoads.ai/dashboard/settings?tab=api and paste it into .env." That holds even when mcp__novoads__* tools are connected and authenticated in the session. Never call mcp__novoads__* tools from this repo's workflows: they are a different surface with different behavior, including the units they quote costs in. Repo installs verify with ./scripts/check-novoads-env.sh; a solo install checks NOVOADS_API_KEY in the environment.

Pack version. Every /v1 response carries X-Novoads-Pack-Version; mention a newer pack at https://github.com/novoads/agent-skills only when that header names a version NEWER than this file's metadata.packVersion — equal or older is nothing to say, and it is never a reason to stop.

The Meta app must be published (Live) — check this first

A development-mode app cannot create ad creatives. POST /act_*/ads fails with code 100 / subcode 1885183 ("Ads creative post was created by an app that is in development mode"). Every other prerequisite can be perfect and the deploy still cannot succeed.

Allowlisting the ad account under App settings → Advanced → Authorized ad account IDs does not work around it — tested live on 2026-08-03 and it does not help. Publishing the app is the only fix: App Dashboard → toggle the app from Development to Live.

No Graph endpoint reports app mode for a user token, so check-meta-env.sh cannot test this for you. Ask the user to confirm the app is Live before the first deploy against a given app.

Everything else
  • Env (in .env — see your repo's .env.example):

    • META_ACCESS_TOKEN (required) — long-lived token with ads_management scope
    • META_AD_ACCOUNT_ID (required) — with or without the act_ prefix
    • META_PAGE_ID, META_IG_USER_ID, META_PIXEL_ID (optional defaults for deploy)
    • META_API_VERSION (optional, default v23.0)
  • Python deps — install into a virtualenv:

    python3 -m venv .venv-meta
    .venv-meta/bin/python -m pip install -r scripts/requirements.txt
    # then run the scripts with .venv-meta/bin/python, not bare python3
    

    Bare python3 -m pip install fails on a Homebrew python with error: externally-managed-environment (PEP 668), and macOS is this repo's first target. Do not suggest --break-system-packages; use the venv.

  • A target ad set that already exists. The skill deploys ads into an existing ad set — it does not create campaigns or ad sets. If the user needs a new ad set, create it in Ads Manager or via the cheatsheet §3–§4 first.

  • The finished creative on disk — an image or video file path. Chat-pasted files are not accessible; ask the user for a real path.

Run bash scripts/check-meta-env.sh to verify credentials before anything else. It checks the token, the ads_management scope, ad-account reachability and the Page — a token that passes /me but carries no ads scopes is a real failure mode it now catches.

When credentials are missing it sets the user up rather than only complaining. It uncomments the two required lines in .env (they ship commented out, and a token typed onto a commented line fails the next run identically — the most likely way this path breaks in practice), opens the file, and prints where each value comes from plus the Live-app warning. Relay what it printed; it is written to be read out. It leaves the four optional keys commented, never touches a line the user has already edited, and exits 1 either way.

Workflow

Phase 1 — Research (optional, when copy should model winners)
# Rank the account's ads and pull the winning copy
python scripts/pull-top-ads.py --date-preset last_30d --min-spend 100 --limit 15

# Pull a competitor's ads from the Ad Library
python scripts/pull-competitor-ads.py --pages "BrandName" --limit 50

Both write JSON under OUTPUT_BASE (or ./outputs/meta-ads/). Read the top-ad copy fields to identify winning hook/proof/CTA patterns before writing new copy.

Phase 2 — Copy

Write a copy.json following copy-guide.md: 5 bodies (one per framework angle), 5 titles, 3 descriptions. If Phase 1 ran, mirror the voice and patterns of the winners — new creative + proven copy DNA. Save copy.json somewhere under outputs/ so it isn't committed.

Phase 3 — Deploy

Always dry-run first — it prints the creative payload in full (no truncation) plus a dedicated Advantage+ enhancement section, and makes no API calls:

.venv-meta/bin/python scripts/deploy-ad.py --dry-run \
  --adset-id <AD_SET_ID> --copy-file copy.json --link <DESTINATION_URL> \
  --image path/to/creative.png

Review the payload with the user, then deploy for real:

.venv-meta/bin/python scripts/deploy-ad.py \
  --adset-id <AD_SET_ID> --copy-file copy.json --link <DESTINATION_URL> \
  --video clip-a.mp4 --video clip-b.mp4 --cta SIGN_UP --pixel-id <PIXEL_ID>
  • --image / --video are repeatable — each becomes its own ad in the ad set.
  • Every ad is created PAUSED. Tell the user to review and un-pause in Meta Ads Manager. The skill never launches a spending ad automatically.
  • Results are written to deployment_results.json under OUTPUT_BASE: ads (created), failures (each with the verbatim Meta error), orphans, and a summary count.
Read the exit code

deploy-ad.py exits 1 if any requested ad failed, and prints each failure with Meta's own code / subcode / message / fbtrace_id. It no longer prints a DONE. line on a run that created nothing. If you are wrapping this script, branch on the exit code — do not parse stdout for success.

Advantage+ enhancements and burned-in captions

The dry run lists every degrees_of_freedom_spec enrollment Meta will apply, with a one-line note on what each one does to a shipped creative. Read that section aloud to the user before a first deploy — several enrollments modify the creative, and the defaults are opt-in.

By default the script sets --burned-in-captions, which forces to OPT_OUT the enrollments that would deface a creative with text baked into the pixels (video_uncrop, video_auto_crop, video_filtering, creative_stickers, reveal_details_over_time, show_summary, show_destination_blurbs, and the text_extraction customizations) plus text_translation, which rewrites copy the user approved verbatim. Everything this workspace produces carries burned-in text — captions on video from caption-video, typography on images from the image-ad skills — so this is the right default here.

Pass --no-burned-in-captions to send Meta's full enrollment instead. Say so explicitly when you do; it is the user's creative that gets modified.

Decision tree

User intent Phases
"Deploy this creative to Meta" + copy provided Phase 3 only
"Build a Meta ad, write the copy too" Phase 2 → 3
"Make ads modeled on my winners" Phase 1 → 2 → 3
"What are my best ads / competitor research" Phase 1 only

Safety rules

  • Ads deploy PAUSED. Never add an --active override or un-pause ads without an explicit user instruction. Confirm the user knows the ads are paused.
  • Dry-run before every real deploy. Show the payload; get a go-ahead.
  • Confirm the target ad set and destination URL with the user before deploying — an ad in the wrong ad set spends against the wrong budget.
  • Deploying ads is a shared-state, money-adjacent action. Treat the live deploy-ad.py run as something to confirm, not assume.
  • The skill creates ads only — it does not create or edit campaigns, ad sets, budgets, or audiences. Those stay manual.

Quirks and pitfalls

  • App in development mode → code 100 / subcode 1885183. See Prerequisites. This is the single most likely reason a first deploy fails.

  • Video processing is async. deploy-ad.py polls the uploaded video until Meta finishes processing before creating the ad — a video deploy can take a few minutes. See deploy-patterns.md.

  • Transient OAuthException (code 2). Retried automatically with backoff.

  • act_ prefix is added automatically if missing from META_AD_ACCOUNT_ID.

  • A failed run can leave uploaded assets behind. The video/image upload succeeds before ad creation is attempted, so a rejected ad orphans the asset. deploy-ad.py prints every orphan with a ready-to-run curl cleanup line and records them under orphans in deployment_results.json. It does not delete anything automatically — deleting objects in a live ad account is the user's call. Offer the cleanup commands; don't run them unprompted.

  • link_urls is sent for image ads only — this is deliberate, not a bug. build_image_creative puts link_urls in asset_feed_spec; build_video_creative does not. A live video deploy (2026-08-03) confirmed the ad resolves its destination correctly without it, carrying the link through object_story_spec.video_data.call_to_action instead. Leave the asymmetry alone; it is verified working.

  • TEXT_LIQUIDITY does not read back on the live creative. The skill sends text_transformation_types: ["TEXT_LIQUIDITY"] and degrees_of_freedom_type: "USER_ENROLLED", but reading the created creative back does not show either field (verified 2026-08-03). The functional part does persist — the 5/5/3 asset feed was present on the live ad. Verify by asset feed, not by flag:

    curl -s -G "https://graph.facebook.com/v23.0/<AD_ID>" \
      --data-urlencode "fields=creative{asset_feed_spec,degrees_of_freedom_spec}" \
      --data-urlencode "access_token=$META_ACCESS_TOKEN" | python3 -m json.tool
    

    Confirm asset_feed_spec.bodies/titles/descriptions carry the counts you sent. Do not claim multi-variant text rotation is enabled on the basis of the flag you sent — say what read back.

  • Account-specific data stays out of git. All output routes through outputs/ (gitignored): ad IDs, pulled spend/revenue, competitor data.

  • Special Ad Categories (credit, employment, housing, social issues) change ad-set targeting rules — flag to the user; see cheatsheet §3.5.

Cost note

The Meta Marketing API itself is free to call. Ad spend is real money — but because every ad deploys PAUSED, nothing spends until the user un-pauses it in Ads Manager. There are no per-call credits to estimate.

1---
2name: meta-ad-builder
3metadata: {packVersion: 1.3.0}
4description: >-
5 Publish finished creatives as live Meta (Facebook/Instagram) ads via the Meta
6 Marketing API, plus research and ad-copy support. Uploads an image or video,
7 builds a multi-variant creative (5 bodies / 5 titles / 3 descriptions in an
8 asset feed), and creates a PAUSED ad in an
9 existing ad set. Also pulls top-performing ads (ranked by ROAS) and competitor
10 ads from the Ad Library to inform copy. Use when the user asks to deploy /
11 publish / launch a creative as a Meta or Facebook ad, build a Meta ad, push a
12 video or image into an ad set, pull their top ads, or research competitor ads.
13 Not for generating creative (use the image/video skills) and not for writing
14 ad-table rows.
15---
16 
17# Meta ad builder
18 
19Turn a finished creative — typically the output of a generative-AI skill in this
20workspace — into a live Meta ad. The skill covers three phases: **research**
21(optional) → **copy** → **deploy**. It talks to the Meta Marketing API directly
22via ported, parameterized Python scripts.
23 
24> **Meta credentials required before any deploy work.** Run
25> `bash scripts/check-meta-env.sh` first — before uploading a creative, before
26> creating anything in the ad account, before the first Meta call of any phase.
27> On a non-zero exit, stop and ask the user for what it printed. It has already
28> uncommented the two required lines in `.env` and opened the file, so the ask
29> is "paste these two values", not "go find the docs". Read its Live-app warning
30> out to them: a development-mode app passes every check and then fails at ad
31> creation. Until the check passes, nothing is uploaded, no partial ad or
32> creative exists and nothing is charged — which is exactly why it runs first,
33> since a deploy that dies halfway orphans uploaded assets in a live ad account.
34> This key is deliberately lazy: the pack asks for it here, at the moment
35> someone wants to publish, and never during setup.
36 
37## When to use this skill
38 
39Trigger on phrases like:
40- "deploy this video to Meta" / "publish this as a Facebook ad"
41- "build/launch a Meta ad" / "push this creative into <ad set>"
42- "create a Meta ad with this image and copy"
43- "pull my top-performing ads" / "what are my best ads by ROAS"
44- "research competitor ads" / "pull <brand>'s ads from the Ad Library"
45 
46Do **not** use this skill to *generate* creative — that's `pixar-ad`,
47`claymation-ad`, `chatgpt-image-ad`, `nano-banana-image-ad`,
48`generate-youtube-thumbnail`, etc. Do not use it to write ad-table rows. This
49skill is the *direct Meta Marketing API* deployment step.
50 
51## Read order
52 
531. **This file** — workflow, decision tree, safety rules.
542. **[prompting/copy-guide.md](prompting/copy-guide.md)** — the 5-body / 5-title /
55 3-description frameworks and the `--copy-file` JSON shape.
563. **[reference/deploy-patterns.md](reference/deploy-patterns.md)** — creative
57 spec mechanics, video polling, retry, failure modes.
584. **[reference/meta-api-cheatsheet.md](reference/meta-api-cheatsheet.md)** — the
59 full Meta Marketing API reference (campaigns, ad sets, ads, enums, gotchas,
60 Ad Library). Consult as needed; don't read end-to-end.
61 
62## Prerequisites
63 
64> **REST key required. A Novoads MCP connector is not a substitute.** If
65> `NOVOADS_API_KEY` is missing or still the placeholder, stop before any
66> generation work and tell the user: "Before continuing, create an API key at
67> <https://novoads.ai/dashboard/settings?tab=api> and paste it into `.env`."
68> That holds even when `mcp__novoads__*` tools are connected and authenticated in
69> the session. Never call `mcp__novoads__*` tools from this repo's workflows: they
70> are a different surface with different behavior, including the units they quote
71> costs in. Repo installs verify with `./scripts/check-novoads-env.sh`; a solo
72> install checks `NOVOADS_API_KEY` in the environment.
73 
74**Pack version.** Every `/v1` response carries `X-Novoads-Pack-Version`; mention a newer pack at <https://github.com/novoads/agent-skills> only when that header names a version NEWER than this file's `metadata.packVersion` — equal or older is nothing to say, and it is never a reason to stop.
75 
76### The Meta app must be published (Live) — check this first
77 
78**A development-mode app cannot create ad creatives.** `POST /act_*/ads` fails
79with **code 100 / subcode 1885183** ("Ads creative post was created by an app
80that is in development mode"). Every other prerequisite can be perfect and the
81deploy still cannot succeed.
82 
83Allowlisting the ad account under **App settings → Advanced → Authorized ad
84account IDs** does **not** work around it — tested live on 2026-08-03 and it
85does not help. Publishing the app is the only fix: App Dashboard → toggle the
86app from *Development* to *Live*.
87 
88No Graph endpoint reports app mode for a user token, so `check-meta-env.sh`
89cannot test this for you. Ask the user to confirm the app is Live before the
90first deploy against a given app.
91 
92### Everything else
93 
94- **Env** (in `.env` — see your repo's `.env.example`):
95 - `META_ACCESS_TOKEN` (required) — long-lived token with `ads_management` scope
96 - `META_AD_ACCOUNT_ID` (required) — with or without the `act_` prefix
97 - `META_PAGE_ID`, `META_IG_USER_ID`, `META_PIXEL_ID` (optional defaults for deploy)
98 - `META_API_VERSION` (optional, default `v23.0`)
99- **Python deps — install into a virtualenv:**
100 
101 ```bash
102 python3 -m venv .venv-meta
103 .venv-meta/bin/python -m pip install -r scripts/requirements.txt
104 # then run the scripts with .venv-meta/bin/python, not bare python3
105 ```
106 
107 Bare `python3 -m pip install` **fails on a Homebrew python** with
108 `error: externally-managed-environment` (PEP 668), and macOS is this repo's
109 first target. Do not suggest `--break-system-packages`; use the venv.
110- **A target ad set** that already exists. The skill deploys ads into an existing
111 ad set — it does not create campaigns or ad sets. If the user needs a new ad
112 set, create it in Ads Manager or via the cheatsheet §3–§4 first.
113- **The finished creative on disk** — an image or video file path. Chat-pasted
114 files are not accessible; ask the user for a real path.
115 
116Run `bash scripts/check-meta-env.sh` to verify credentials before anything else.
117It checks the token, the `ads_management` scope, ad-account reachability and the
118Page — a token that passes `/me` but carries no ads scopes is a real failure mode
119it now catches.
120 
121**When credentials are missing it sets the user up rather than only complaining.**
122It uncomments the two required lines in `.env` (they ship commented out, and a
123token typed onto a commented line fails the next run identically — the most
124likely way this path breaks in practice), opens the file, and prints where each
125value comes from plus the Live-app warning. Relay what it printed; it is written
126to be read out. It leaves the four optional keys commented, never touches a line
127the user has already edited, and exits 1 either way.
128 
129## Workflow
130 
131### Phase 1 — Research (optional, when copy should model winners)
132 
133```bash
134# Rank the account's ads and pull the winning copy
135python scripts/pull-top-ads.py --date-preset last_30d --min-spend 100 --limit 15
136 
137# Pull a competitor's ads from the Ad Library
138python scripts/pull-competitor-ads.py --pages "BrandName" --limit 50
139```
140 
141Both write JSON under `OUTPUT_BASE` (or `./outputs/meta-ads/`). Read the top-ad
142`copy` fields to identify winning hook/proof/CTA patterns before writing new copy.
143 
144### Phase 2 — Copy
145 
146Write a `copy.json` following **[copy-guide.md](prompting/copy-guide.md)**:
1475 bodies (one per framework angle), 5 titles, 3 descriptions. If Phase 1 ran,
148mirror the voice and patterns of the winners — new creative + proven copy DNA.
149Save `copy.json` somewhere under `outputs/` so it isn't committed.
150 
151### Phase 3 — Deploy
152 
153**Always dry-run first** — it prints the creative payload **in full**
154(no truncation) plus a dedicated Advantage+ enhancement section, and makes no
155API calls:
156 
157```bash
158.venv-meta/bin/python scripts/deploy-ad.py --dry-run \
159 --adset-id <AD_SET_ID> --copy-file copy.json --link <DESTINATION_URL> \
160 --image path/to/creative.png
161```
162 
163Review the payload with the user, then deploy for real:
164 
165```bash
166.venv-meta/bin/python scripts/deploy-ad.py \
167 --adset-id <AD_SET_ID> --copy-file copy.json --link <DESTINATION_URL> \
168 --video clip-a.mp4 --video clip-b.mp4 --cta SIGN_UP --pixel-id <PIXEL_ID>
169```
170 
171- `--image` / `--video` are repeatable — each becomes its own ad in the ad set.
172- Every ad is created **PAUSED**. Tell the user to review and un-pause in Meta
173 Ads Manager. The skill never launches a spending ad automatically.
174- Results are written to `deployment_results.json` under `OUTPUT_BASE`:
175 `ads` (created), `failures` (each with the verbatim Meta error), `orphans`,
176 and a `summary` count.
177 
178#### Read the exit code
179 
180`deploy-ad.py` **exits 1 if any requested ad failed**, and prints each failure
181with Meta's own code / subcode / message / `fbtrace_id`. It no longer prints a
182`DONE.` line on a run that created nothing. If you are wrapping this script,
183branch on the exit code — do not parse stdout for success.
184 
185#### Advantage+ enhancements and burned-in captions
186 
187The dry run lists every `degrees_of_freedom_spec` enrollment Meta will apply,
188with a one-line note on what each one does to a shipped creative. Read that
189section aloud to the user before a first deploy — several enrollments **modify
190the creative**, and the defaults are opt-in.
191 
192By default the script sets `--burned-in-captions`, which forces to `OPT_OUT`
193the enrollments that would deface a creative with text baked into the pixels
194(`video_uncrop`, `video_auto_crop`, `video_filtering`, `creative_stickers`,
195`reveal_details_over_time`, `show_summary`, `show_destination_blurbs`, and the
196`text_extraction` customizations) plus `text_translation`, which rewrites copy
197the user approved verbatim. Everything this workspace produces carries burned-in
198text — captions on video from `caption-video`, typography on images from the
199image-ad skills — so this is the right default here.
200 
201Pass `--no-burned-in-captions` to send Meta's full enrollment instead. Say so
202explicitly when you do; it is the user's creative that gets modified.
203 
204## Decision tree
205 
206| User intent | Phases |
207|---|---|
208| "Deploy this creative to Meta" + copy provided | Phase 3 only |
209| "Build a Meta ad, write the copy too" | Phase 2 → 3 |
210| "Make ads modeled on my winners" | Phase 1 → 2 → 3 |
211| "What are my best ads / competitor research" | Phase 1 only |
212 
213## Safety rules
214 
215- **Ads deploy PAUSED.** Never add an `--active` override or un-pause ads without
216 an explicit user instruction. Confirm the user knows the ads are paused.
217- **Dry-run before every real deploy.** Show the payload; get a go-ahead.
218- **Confirm the target ad set and destination URL** with the user before
219 deploying — an ad in the wrong ad set spends against the wrong budget.
220- **Deploying ads is a shared-state, money-adjacent action.** Treat the live
221 `deploy-ad.py` run as something to confirm, not assume.
222- The skill creates ads only — it does **not** create or edit campaigns, ad sets,
223 budgets, or audiences. Those stay manual.
224 
225## Quirks and pitfalls
226 
227- **App in development mode → code 100 / subcode 1885183.** See Prerequisites.
228 This is the single most likely reason a first deploy fails.
229- **Video processing is async.** `deploy-ad.py` polls the uploaded video until
230 Meta finishes processing before creating the ad — a video deploy can take a
231 few minutes. See [deploy-patterns.md](reference/deploy-patterns.md).
232- **Transient `OAuthException` (code 2).** Retried automatically with backoff.
233- **`act_` prefix** is added automatically if missing from `META_AD_ACCOUNT_ID`.
234- **A failed run can leave uploaded assets behind.** The video/image upload
235 succeeds before ad creation is attempted, so a rejected ad orphans the asset.
236 `deploy-ad.py` prints every orphan with a ready-to-run `curl` cleanup line and
237 records them under `orphans` in `deployment_results.json`. It does **not**
238 delete anything automatically — deleting objects in a live ad account is the
239 user's call. Offer the cleanup commands; don't run them unprompted.
240- **`link_urls` is sent for image ads only — this is deliberate, not a bug.**
241 `build_image_creative` puts `link_urls` in `asset_feed_spec`;
242 `build_video_creative` does not. A live video deploy (2026-08-03) confirmed the
243 ad resolves its destination correctly without it, carrying the link through
244 `object_story_spec.video_data.call_to_action` instead. Leave the asymmetry
245 alone; it is verified working.
246- **`TEXT_LIQUIDITY` does not read back on the live creative.** The skill sends
247 `text_transformation_types: ["TEXT_LIQUIDITY"]` and
248 `degrees_of_freedom_type: "USER_ENROLLED"`, but reading the created creative
249 back does not show either field (verified 2026-08-03). The functional part
250 *does* persist — the 5/5/3 asset feed was present on the live ad. **Verify by
251 asset feed, not by flag:**
252 
253 ```bash
254 curl -s -G "https://graph.facebook.com/v23.0/<AD_ID>" \
255 --data-urlencode "fields=creative{asset_feed_spec,degrees_of_freedom_spec}" \
256 --data-urlencode "access_token=$META_ACCESS_TOKEN" | python3 -m json.tool
257 ```
258 
259 Confirm `asset_feed_spec.bodies/titles/descriptions` carry the counts you sent.
260 Do not claim multi-variant text rotation is enabled on the basis of the flag
261 you sent — say what read back.
262- **Account-specific data stays out of git.** All output routes through
263 `outputs/` (gitignored): ad IDs, pulled spend/revenue, competitor data.
264- **Special Ad Categories** (credit, employment, housing, social issues) change
265 ad-set targeting rules — flag to the user; see cheatsheet §3.5.
266 
267## Cost note
268 
269The Meta Marketing API itself is free to call. **Ad spend is real money** — but
270because every ad deploys PAUSED, nothing spends until the user un-pauses it in
271Ads Manager. There are no per-call credits to estimate.
272 

Discussion

Alternatives

Company valuationEstimate the intrinsic value of a public company using DCF, relative (peer multiple) and sum-of-parts (SOTP) methods, then triangulate to an implied share price with upside/downside versus the current market price. Use this skill whenever the user asks: what is AAPL worth", "valuation of NVDA", "fair value of TSLA", "intrinsic value", DCF for MSFT", "build a DCF", "discounted cash flow", "WACC", "terminal value", implied share price", "upside to fair value", "is X overvalued/undervalued", relative valuation", "peer comparison valuation", "EV/EBITDA target", "SOTP", sum of the parts", "how much is [company] worth", "price target from fundamentals", value this company", or any ticker in the context of computing intrinsic or relative valuation. Default to running ALL three methods (DCF + relative + SOTP-if-applicable) and presenting a blended implied price with a sensitivity table. Do not answer valuation questions from memory — always run the workflow. · MITCompetitor gapFind keywords a competitor ranks for that the user's site doesn't — the content gap, prioritized by volume and winnability. Use when asked "what does competitor.com rank for", "keyword gap vs X", "steal competitor keywords", or "why do they outrank us".Business & ops · MITAd angle minerMine the highest-converting ad angles from customer reviews, Reddit complaints, support tickets, and competitor ads. Extracts actual pain language, competitor weaknesses, and outcome phrases that real buyers use. Outputs a ranked angle bank with proof quotes and recommended ad formats per angle.Business & ops · MITCompetitor content intelligenceAnalyze competitor content coverage and uncover differentiated content opportunities using web search, page extraction, and DataForSEO content analysis. Use when asked for competitor content research, editorial gap analysis, content brief inputs, or topic whitespace.Business & ops · Apache-2.0