THE FIELD NOTES
Someone learned the hard way.
You don’t have to. 57 incidents, the lessons they left, and the recipes that account for them.
57 field notes
spend_cap: dollars in, cents out
$198,000 of unintended headroomThe first write set a $200,000 limit instead of $2,000 because the value read back was in cents.
spend_cap=0 doesn't remove the cap
The API returned success; the limit stayed.
Money-routing fields: exact match or error
$839 on the wrong pixelAn unknown funnel fell back to the account's default pixel.
Store operator choices in the launch record
A deploy mid-launch resumed with an empty config; every ad set got another brand's page.
Don't change start_time and status in one flow
start_time update failed (ad set counts as started at creation), activation succeeded — delivery started early.
One edit per object per 30s; ~4 budget changes per hour
A second POST within 30s hit error #613; frequent budget edits got blocked for an hour.
Pixel, event and goal can't change on a published ad set
Edits were rejected.
Ask Meta 'already launched?' and 'next number?'
A sync moved the counter backwards — duplicate campaign numbers, twice.
A pack is all or nothing
Auto-created packs shipped with 24 of 25 ads.
Reused code inherits its defaults
A 'create ad' helper forced PAUSED; refilled creatives never ran.
'Couldn't read' is not 'zero'
An API error read as an empty list wiped the autopilot's history.
Judge 'is it live' by effective_status
An ACTIVE ad set inside a paused campaign triggered 15 false 'not delivering' alarms a day.
New geo exclusions only hit new ad sets
~$30k at 46% ROASOld ad sets kept buying in excluded countries.
Judge a country inside the campaign's language
Country averages were wrong in 6 of 18 cases.
No kills in the first 2 days
Day-1 verdicts under ~$150/day can't tell good from bad.
Judge losses on closed cohorts
Killed creatives that ended at 106–111% ROASAbout half of 'expensive trial' flags were false.
Use a recent window for kills
Lifetime ROAS killed a recovered creative (53% lifetime vs 65.5% last 14 days).
Under a cap, slow new tests — don't cut winners
Simulation: 65.1% ROAS throttling tests vs 60.2% cutting working ad sets.
Scale by marginal return, not average
Small raises returned ~40%, large ones 58–66%.
Test share-of-budget thresholds at every budget size
A 'reduce' branch never fired: $300 floor vs $150 base budget.
Dry-run rule changes on production
One change grew a queue from 22 to 242.
Mass failure is usually tracking, not creatives
When most ad sets suddenly show zero purchases, the pixel is broken.
Start every analysis by checking the date
An analysis labeled one date was run three days later.
Join on IDs, never names
A renamed campaign kept its old UTM name: 0 trials, falsely low CPA.
Count unique converters, not purchase events
Renewals inflated counts ~3×.
Compare cohorts at the same age
Slicing by calendar date flipped the sign of a conclusion.
Watch for silent truncation
A BI API returned exactly 2,000 rows with no warning.
A skipped source is 'incomplete', not zero
A transient error showed 'FB 0 · −100%'.
Judge health by results by deadline
42 incidents in 3 weeks; the bot reported none.
Prove a change by running it
A pipe into tail hid failing tests; a deploy gate always exited 0.
Image hashes don't cross accounts
A foreign hash was accepted and rendered as a placeholder.
Setting any placement field disables Advantage+
Sending publisher_platforms turned Advantage+ placements off.
Don't send standard_enhancements
Error 3858504 appeared non-deterministically.
DLO needs a labeled link per language
A single generic link was accepted but clicks didn't route by locale.
Copy attribution exactly from the donor
1-day click and 7-day click are different universes.
Code 17 needs minutes, not seconds
After a burst of writes, /ads and /adsets edges stayed blocked 30+ minutes.
Never let an agent delete without asking
An agent deleted a 'broken' campaign; the owner wanted to choose pause, fix or delete.
Alert only on persistent failures
One-off rate limits flooded the channel and operators stopped reading.
'Already launched' = the ad exists, even at $0
Packs were nearly relaunched three times because the check looked at spend.
Hold on anomalies before acting
A 5× price spike from late data almost triggered a scale.
Backtests use only data known on the decision date
Future leakage inflated the effect ~5×.
Broad except blocks hide failures
A missing import was swallowed; account-block alerts never fired once.
Upload originals, not CDN previews
Previews were compressed and cropped.
Change a threshold everywhere at once
A threshold had copies in the bot, BI cards and scripts that drifted apart.
Every report prints its denominator
A backtest silently evaluated only 31% of decisions.
If > 50% exists, fill gaps — don't rebuild
A rebuild cost ~100 calls and a rate limit; filling gaps took ~29.
Resolve locales via search, don't guess
Guessed locale ids targeted the wrong languages.
A naming change is a schema migration
Czech CS vs CZ needed 13 code edits and 2 BI cards; DE vs GER shrank a pool to 3 of 46.
Meta sometimes returns non-JSON
response.json() threw before the error check.
Creatives only in original sizes
Auto-cropped variants underperformed and broke naming.
Explain decisions in plain words
Operators ignored decisions they couldn't read (codes like f= m=).
Name patterns need word boundaries
A number pattern matched Meta's '(2)' duplicate suffix.
Read before writing, read again after
A platform silently ignored four fields while returning success.
Exhausted credit lines stop spend silently
Accounts stopped spending with no error.
Accounts live in different time zones
Meta accounts in New York time vs another channel 7 hours ahead.
Weekends distort spend
Sunday overspend and Saturday underspend triggered brakes.
Yesterday's spend: trust the API if warehouse differs > 5%
Warehouse spend lagged a day while trials were on time.