---
name: meta-grey-ops
description: "Launches, monitors and operates Meta (Facebook/Instagram) ads for any vertical and offer through the metaops CLI (workspace.json + JSON specs): doctor, plan, apply --confirm SPEND (creates ACTIVE, spends), verify, review/monitor, insights/stats pulls, pause, comment hiding, BM invites, pixels/CAPI, and tokens (System User, or DevTools EAAB/EAAG/EAAd/EAAH/EAAI with cookies + proxy). Grey-market survival: antidetect/proxy hygiene, rejects and disabled accounts, review layer and cloaking, catalog/PWA funnels, restricted-vertical gates, vertical playbooks (casino/iGaming, nutra, crypto/trading, news-to-Telegram). Triggers: 'launch FB ads', 'metaops', 'workspace.json', 'bulk launch on N accounts', 'ad rejected Online Gambling', 'account disabled', 'check spend/stats on FB'. Clean Ads Manager theory is meta-ads; tracker/postback metrics are tracker-ops; portfolio and kill/scale decisions are senior-buyer-ops."
---

# Meta Grey Ops

Launch and operate via API on your own tokens (= autolaunch SaaS without the UI, `13`) + grey survival.
Sibling skills (same `meta/` folder): "buy well" → `../meta-ads` · "count & sync" → `../tracker-ops` · "portfolio, kill and
scale decisions" → `../senior-buyer-ops` · "is this difference real" → `../measurement-experimentation-ops`. In references,
`meta-ads/14`, `tracker-ops/03`, `senior-buyer-ops/01` etc. mean `<skill>/references/NN-*.md` (link `../../<skill>/references/NN-*.md`
from a references file, `../<skill>/references/NN-*.md` from this file); bare numbers (`02`, `16`) are this skill's
`references/NN-*.md`.

## Start here

| You are… | Read | Then |
|---|---|---|
| In a directory with `workspace.json` | `references/16-metaops-agent-cli.md` | `metaops --workspace … --json` (or the project's wrapper); before `apply --confirm SPEND` read `00` §6–7.5 |
| Handed a token / asked to launch anything via API | `references/00-launch-runbook.md` | a workspace in a project dir outside the skill (`scripts/specs/example-workspace.json`), validate, then `metaops` |
| Setting up access, minting/handing over a token | `references/02-access-tokens-and-mcp.md` | back to `00` |
| Hit an error, a gate, a rejection, a dead token | `00` § "When a step fails" | the one file it names |
| Ad account disabled, or ads "Spam"-rejected right after approval | `references/23-disabled-account-triage.md` | `05`, `03` |
| Launching by hand in Ads Manager | `references/24-manual-launch-ads-manager.md` | n/a |
| Own BM from scratch, new account after a ban, buying accounts | `22` · `26` · `27` | `02`, `03` |

**The agent writes a JSON spec; `metaops` controls the lifecycle; `launch.py` writes the Graph payloads.** Never
hand-assemble a payload or call a Graph mutation directly. A new shape → extend `metaops` and its tests.

## Strategy belongs to the project

Structure (1-5-1, 5-3-1, ABO/CBO…), budgets, bid, optimization event (registration first or Purchase from day one), start
time, account tz and edit policy (in-place scaling vs set-and-forget) depend on offer and geo: there is no right answer
here. Read them from the project's notes/AGENTS file or ask the owner before a launch; this skill gives options (`04`
Structures, `playbooks/`, `senior-buyer-ops/01`), not a default. Mechanics that serve any strategy:
- Campaign creates on one account are spaced by `METAOPS_CREATE_GAP_HOURS` (default 0.1 = 6 min; raise it on weak/fresh accounts).
  Several campaigns on one account at once → `METAOPS_CREATE_GAP_HOURS=0` for that batch (keeps the throttle cooldown;
  `METAOPS_PACE_OVERRIDE=1` drops it, so keep it for stopping spend). Leave `start_time` out of the spec (start at create).
- New stories into a live campaign (after rejects): spec `"campaign": {"id": "<id>"}` adds ad sets; the campaign is read and checked,
  never edited; no create gap; single-account only (`16` § Single-account lifecycle).
- A budget above the account's daily spend limit (`adtrust_dsl`) is a valid choice (the limit grows within hours): don't flag it.
- Judge on full days (deps lag); kill/scale figures → `senior-buyer-ops/01`.

Standing rules that hold in every project:
- **Rejects:** never edit or resubmit a rejected ad, never re-upload the same creative/angle/PWA into that account (`23`).
  Switching rejected ads / dead campaigns off is the owner's manual job unless the project says otherwise.
- **Comments: hide, never delete** (`comments delete` is disabled).
- **Backup admin** on every BM before work: an ADMIN invite to the owner's backup email (`business user invite --role
  ADMIN`), re-issued before the 30-day expiry; PENDING_INTEGRITY_REVIEW = no email went out, the owner confirms in the UI
  (`22`).
- **No brand/casino names** in campaign / ad set / ad names, headline, description or display link (`lint` checks only
  headline/description; names and display link by hand). Longread copy: `playbooks/casino.md`.

## Launch defaults (detail and enforcement: `16` § Launch defaults)

| Setting | Default |
|---|---|
| Status at create | `ACTIVE`; `apply`/`bulk-apply` need the literal `--confirm SPEND`. Opt-out `"create_status": "PAUSED"` → `activate --confirm-ui REVIEWED --confirm SPEND` |
| Attribution | 1d click / 1d engaged-view / 1d view; **casino Purchase = 7d click / 1d view, set explicitly** (immutable after create) |
| Advantage+ creative | every enhancement `OPT_OUT`; `contextual_multi_ads: OPT_OUT`. Ads Manager-built creatives keep "Show spotlights" OPT_IN (`24`) |
| Advantage+ audience | explicit `true/false`; `true` caps `age_min` at 25 and forces `age_max` 65 → a hard age (US gambling 21-65) = `false` |
| Placements | `"fb_ig_all"` (FB + IG, mobile, no AN / Messenger / Threads) or `"fb_ig_feeds"`; Instagram mandatory |
| Bid | an uncapped conversion goal → `plan` WARNS; `"bid_policy": "require_cap"` makes a cap mandatory; the cap amount goes on ad sets, never on the campaign |
| Link-ad copy | headline neutral ≤40 chars ("Read before you judge"), `cta: "SEE_DETAILS"` (the launcher default is LEARN_MORE), display link empty/root, `lint: "strict"` |
| Budget | CBO or ABO, never both; integer minor units (whole units on no-offset currencies); `currency` in the spec |
| API pacing | a throttle (17/613/4/32/80xxx) is never retried, ≥30 min account cooldown; one campaign create per account per create gap (§ Strategy); no polling loops |
| Identity | Page + PBIA (`instagram_user_id: "auto"`); no PBIA → Ads Manager draft → Identity → Use Facebook Page (`18`) |
| Tracking | spec-level `url_tags` on every ad; PWA builders that pick the pixel from the URL need `fbp=<pixel>` (`11`) |
| Required | `special_ad_categories` explicit; EU/EEA `dsa_beneficiary` + `dsa_payor` |

Not in the scripts: Advertising Settings "test new optimizations", billing/card binding, appeals, BM and Page creation.

## Non-negotiables

1. **One identity = one egress profile, always through its proxy** (a persona reached by a script without its proxy or cookies goes
   straight to a verification / selfie checkpoint). Scripts refuse without `META_PROXY` (only `defaults.allow_no_proxy: true`
   waives it, for a persona whose own browser lives on that IP) and, before the first Facebook call, check the proxy answers and
   exits in `META_PROXY_COUNTRY` / `defaults.proxy_country` when set. A dead or wrong-country proxy = nothing sent (`01`, `02`).
2. **DevTools tokens need their session:** in `token_mode: devtools` every call needs the profile's `c_user` + `xs` cookies
   and UA (else code 1). System User / own-app tokens need none, but rule 1 still applies.
3. **A token rides a login session.** Logout / password change / checkpoint kills every token of it (`02`).
4. **Restriction / checkpoint = FREEZE.** No re-logins, token regen, profile edits. Set `META_LOCKED='<reason>'` in the profile's
   env file: every call refuses until the owner clears it (`01`).
5. **Never touch a work identity from a personal browser or IP.**
6. **Bulk = one creative per account** (the same creative across accounts competes with itself; `bulk.py` warns).
7. **A launch runs on a plan file, not memory.** The `19` template → your plan file; tick only against a read-back.
8. **Pace like a human operator** (an account with a code-17 retry storm was disabled for "automation", siblings lived): one
   read-back after create, stop on any throttle, leave that account ≥30 min (`16` § Pacing).

## References

| Need | Reference |
|---|---|
| **Ordered launch path: START HERE** · plan file template | `references/00-launch-runbook.md` · `19-launch-plan.md` |
| Proxies, IP/session discipline, checkpoints, domain/pixel rotation | `01-infra-and-identity.md` |
| Tokens: classes (EAAB/EAAG/EAAd/EAAH/EAAI), devtools mode, scopes, System User, token death | `02-access-tokens-and-mcp.md` |
| Agency setups, BMs, asset sharing, bought accounts, billing, naming, replacements | `03-agency-accounts-and-bm.md` |
| Why the scripts do what they do: bid strategies, objective traps, creative fields, media, warm-up, re-moderation, catalog quirks | `04-mass-launch-api.md` |
| API errors and the grey response (canonical code→fix: `meta-ads/14`) · why accounts die | `05-api-error-catalog.md` · `06-portfolio-forensics.md` |
| Review layer, cloaking, DLO/unicode/CTM tricks (vendor configs → `07a`) | `07-review-layer-and-cloaking.md` |
| Location fees, tz/currency lock, sanctioned targeting, WABA | `08-geo-fees-and-waba.md` |
| Verification gates, gambling/crypto/financial authorization · does this vertical have a path at all | `09-verification-gates.md` · `10-no-path-and-permissions.md` |
| PWA funnel builders (SKAK, pwa.bot, …): join macros, postbacks, CAPI, QA gates | `11-pwa-funnel-builders.md` |
| **metaops CLI**: commands, JSON contract, locks, pacing, launch-default enforcement | `16-metaops-agent-cli.md` |
| Fanpage lifecycle, PBIA · attribution windows by vertical | `18-fanpage-lifecycle-and-shielding.md` · `21-attribution-market-defaults.md` |
| Own BM from scratch · disabled account / "Spam" after approval · manual launch · placements | `22` · `23` · `24` · `25` |
| New account after a ban · buying accounts and keeping them alive | `26` · `27` |
| Vertical playbooks (casino, nutra, crypto-trading, news-tg): priors, replace with live data | `playbooks/` |

Optional (load only when the project uses it):

| Lane | Where |
|---|---|
| **Catalog / swap funnels**: launch order in `17` § Field scenario (EAAH = the catalog token, white items, new white sets per campaign, `{{product.*}}` macros, `assets swap --watch` right after `apply`) | `17-catalog-via-google-sheets.md`, `00` §9.7, `16` § Catalog lifecycle, `playbooks/casino.md` § Review traps |
| **Keitaro** (cost push, per-ad join, campaign-name contract) | `16` § Keitaro, `../tracker-ops/references/01-keitaro.md` |
| AdsPower Local API · Meta Ads MCP (tool inventory) · Meta Ads CLI · autolaunch SaaS parity | `20` · `15` · `12` · `13` |

Declared gaps: no dating or loans playbook; `APP_PROMOTION` only directional in `playbooks/casino.md`.

## Implementation boundary and env

`metaops` is the only agent write boundary: it rejects workspaces inside the skill directory, and the low-level scripts'
writes run only through a validated, workspace-bound `metaops` process. Generated files belong in the project. After a code
change run `16`'s isolated checks, then `uv tool install --reinstall ~/.claude/skills/meta/meta-grey-ops`: the `metaops` on
PATH is a copy, not a link (fixes sit unused until the reinstall).

Tokens (`token_mode: "devtools"`, default for new workspaces, `02` §5.1): `META_TOKEN` (EAAB ads), `META_TOKEN_BUSINESS`
(EAAG), `META_TOKEN_EVENTS` (EAAd pixel/CAPI), `META_TOKEN_CATALOG` (EAAH), `META_TOKEN_RULES` (EAAI), optional
`META_TOKEN_ANALYTICS` (BM System User token, GET-only: policy in `02` §5.1a, i.e. every scheduled read uses ONLY the per-BM
System User token with no persona-token fallback, and DevTools tokens serve on-demand reads), `META_COOKIES`, `META_USER_AGENT`,
`META_PROXY` (socks5h), `META_PROXY_COUNTRY` (ISO2), `META_LOCKED`. `metaops token import` files a pasted FB Helper block by
class; `doctor` prints the table. Also: `META_APP_SECRET`, `META_API_VERSION` (pinned `v26.0`), `GSHEETS_JSON_KEY_FILE`
(Google Sheet catalog feed), `TG_BOT_TOKEN`+`TG_CHAT_ID` (`monitor --telegram`), `METAOPS_CREATE_GAP_HOURS`. Never pass a
token on argv.

Graph quirk: `{adset}/ads`, `{campaign}/ads` and nested `adsets{ads{}}` can answer EMPTY while `act_ID/ads` lists the ads;
read the flat account edge with `filtering` on `adset.id` / `campaign.id`.
