Every operator step between "all eight phases merged" and "the engine is spending, optimizing, and reporting on its own." Work top to bottom — each phase gates the next. Checkboxes save automatically in this browser.
← Back to the main user guideDo these first — the live publish, the digest emails, and prod webhooks all depend on them.
https://{domain}/c/{code}. Phase 5's live publish is blocked until an ad's destination URL is a real tracker link on this domain. The currently-approved test creative points at the fake ads.test.local — it must be re-pointed..env.local is complete; production is missing two. In the Vercel project → Settings → Environment Variables add:
ADS_WEBHOOK_SECRET — provider callbacks (kie/heygen) won't ingest in prod without it.BLOB_READ_WRITE_TOKEN — creative asset uploads to Vercel Blob.ADS_TELEGRAM_BOT_TOKEN, ADS_SLACK_WEBHOOK_URL (see the deep dives below), then run pnpm ads:bootstrap once so they're seeded as encrypted provider credentials.aa-transactional and aa-bulk./api/webhooks/ses so suppressions stay accurate.Phase 8 shipped default-safe: digests skip with "no recipients" until you add one.
/ads/settings → Reporting & alerts/api/ads/digest/preview?date=YYYY-MM-DD (logged in). It renders the email in-browser without sending. Right now you'll see the empty-state digest — that's correct. The response header x-digest-skip tells you whether the real cron would have skipped that day.The first-ever real Meta write. Everything is deliberately paranoid here: structures are created PAUSED, and you activate manually once.
/ads/settingsdailySpendCap is currently NULL — set it (e.g. $10). Switch autonomyMode to assisted. Confirm publishEnabled is on./ads/queue, approve a creative whose destination is a live tracker link on the verified domain (regenerate or re-point the existing test creative). This doubles as the still-owed full-cost generation run (kie/heygen paid pipeline end-to-end).ads-publish run, then activate manuallyCRON_SECRET bearer). It creates campaign → ad set → ad PAUSED by design. Go to Ads Manager and flip the campaign/ad set to Active — a one-time manual step, by spec.ad_publish_jobs (unique idempotency key per creative × ad set) and Ads Manager.ad_attribution_daily row for that ad/day). This closes the loop: Meta spend on one side, first-party truth on the other.specialAdCategories in /ads/settings to include Financial products and services. Meta requires it for insurance/investment/annuity ads and will reject or flag ads without it. It restricts age/gender/zip targeting — expected. Have compliance/brand pages live on the verified domain; regulated offers get heavier ad review. Meta: special ad categories ↗Spec §12 step 1: trust the numbers before letting anything act on them.
/ads insights vs Ads Manager daily for ~a weekads-sync runs every 4h. Spot-check spend, impressions, clicks, conversions per day. This also validates the hand-built test fixtures against reality for the first time.ad_report_runs row exists (second run reports already_ran).Read every decision the optimizer would make before letting it make any.
autonomyMode = shadow, set optimizeEnabled = true. The 14:00 UTC optimize run now writes an ad_actions row for every decision — none touch Meta.targetCpa / killCpaMultiple until you agree with every intended decision.assisted with maxKillsPerRun = 1. Raise caps only as confidence justifies. Full autonomous mode comes much later, if ever.Reddit gates commercial Data API use behind its Responsible Builder policy; our request is pending.
REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET, REDDIT_USER_AGENT into .env.local and Vercel.pnpm ads:bootstrap to seed the encrypted credential.researchEnabled = true, then in the brand kit configure subreddits and keywords. The YouTube field takes video IDs, not channel names (v1 limitation). The 06:00 UTC cron then feeds /ads/research.Adding your partner for the annuity campaign, done the right way — as a person in your Business Manager with scoped asset access.
act_1735117560747528) → enable Manage campaigns (Advertiser).619087824010534 → view or manage.act_… ID the engine keys on never changes. The asset that does deserve naming care is the Page — if "Affiliate Challenge" isn't the brand you want on a financial offer, create/rename a Page for that brand, assign it to the System User, and update metaPageId before publishing.Telegram carries alerts only (spend spikes, sync failures, token expiry…) — never the daily digest. That's deliberate: a digest on your phone at 7am is noise; a spend-spike ping is signal.
/newbot → pick a display name (e.g. "AA Ads Alerts") and a unique username ending in bot. BotFather replies with the bot token — a string like 1234567890:AAF…. Treat it like a password.https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
In the JSON, find "chat":{"id": …}. A DM id is a positive number; a group id is negative (often starting -100…). Shortcut for DMs: message @userinfobot ↗ and it replies with your id.ADS_TELEGRAM_BOT_TOKEN in .env.local and Vercel, then run pnpm ads:bootstrap — it lands encrypted in ad_provider_credentials./ads/settings → Reporting & alerts → Telegram chat id.ad_sync_state.consecutive_failures = 3 via a tsx one-off) and run the alerts cron with the bearer token — one Telegram message should arrive; running it again in the same hour sends nothing (dedupe).Same job as Telegram — alerts only, for the team view. Uses a plain incoming webhook; no Slack app installation gymnastics beyond creating it once.
#ads-alerts — make a dedicated channel so pings are unambiguous) → Allow.https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
This URL is the credential — anyone holding it can post to that channel. Treat it as a secret.ADS_SLACK_WEBHOOK_URL in .env.local and Vercel, run pnpm ads:bootstrap — stored encrypted in ad_provider_credentials. Then tick "slack" under alert channels in /ads/settings.hooks.slack.com over https, and routes the request through the SSRF-safe fetcher — a URL pointing anywhere else (internal IPs, lookalike domains) is rejected before any network call. If Slack ever rotates your webhook (they're revocable from the app config page), update the env var and re-run bootstrap.