BOSSTORQUE

Sperry Tree Care — Meta Campaign Build Report

April 30, 2026  |  Spring 2026 Lead Gen

Executive Summary

Approval Page — Live
https://sperry-meta-approval-apr2026.jason-8ce.workers.dev
Status
Pending Rob Approval
3
Ad Variations
29
Zip Codes Locked
12/13
QA Checks Pass

This report documents the complete build of the Sperry Tree Care Spring 2026 Meta Lead Generation campaign — covering what was built, every error encountered and resolved, Rob's feedback alignment, the full technical stack, and the locked-in playbook for future builds.

Campaign scope: 3 ad variations, dual image formats (1:1 Feed + native 9:16 Stories/Reels), Lane + Benton County geo targeting (29 zip codes), GET_QUOTE CTA with lead form, audience 35–65+. Hosted approval page on Cloudflare Workers.

Live Page QA Results

Automated checks run against the live worker URL immediately after final deploy.

CheckResultNotes
Age Range 35–65+✓ PASSFixed unicode em-dash patch — previously displayed 25–65
GET_QUOTE CTA✓ PASSUpdated from default; all 3 ad creatives recreated with correct CTA
Lane County zip codes (97401)✓ PASS23 Lane County zips locked in adset
Benton County zip codes (97330)✓ PASS6 Benton County zips locked in adset
Native 9:16 Stories image✓ PASSVertical images generated with Imagen 4 Ultra, not stretched crops
1:1 Feed image✓ PASSSquare format shown for Feed placements
1.91:1 Right Column image✓ PASSLandscape format shown for desktop placements
Ad copy present✓ PASSAll 3 versions pulled verbatim from live Meta API
Checklist section✓ PASSPre-launch checklist included on approval page
Page title correct✓ PASS"Sperry Tree Care — Meta Campaign Approval"
No orphan warning reference✓ PASS"see ⚠️ above" removed — was dangling after warning deleted
3 ad variations✓ PASS6 "Version" refs = 3 ads × 2 (title + label); pattern too strict
Rob approval section✓ PASSApproval CTA and instructions present

Friction Log

Every error, misstep, and rework in chronological build order — with root cause and fix applied.

1

Approval page lost between sessions

Root cause: /tmp is session-scoped on Desktop Commander. File /tmp/meta-approval-v3.js built in session 1 was gone when session resumed. Full rebuild required — wasted ~90 min.
Fix: Copy all generated JS/HTML to /Users/Jason/Documents/ immediately on creation. Never rely on /tmp for files that may be needed later.
2

Zip code format rejected by Meta API

Root cause: Used {"key": "97401"}. Meta API requires country prefix: {"key": "US:97401"}. All 29 zip codes had to be reformatted and resubmitted.
Fix: All Meta zip targeting must use US:XXXXX format. Pre-validate format before API call.
3

age_max=80 rejected — Meta hard cap at 65

Root cause: Meta API INVALID_AGE_MAX error. Platform cap is 65 regardless of input. Took 2 API round trips to discover. Workaround: omit age_max entirely — Meta interprets as 35–65+ (no upper limit).
Fix: Never set age_max. Omit the field. Document in API gotchas reference.
4

asset_feed_spec + object_story_spec conflict (API error 1443048)

Root cause: Attempted to combine both in one creative to support multi-format delivery AND GET_QUOTE CTA. Meta API error 1443048: these cannot coexist in a single creative object.
Fix: GET_QUOTE CTA requires object_story_spec only. Never attempt asset_feed_spec when using lead gen CTAs.
5

Blurred/cropped 9:16 images (Rob feedback)

Root cause: 1:1 images were center-cropped or edge-blurred to fill 9:16 Stories slots. This is structurally wrong — Meta uses the native image dimensions for each placement.
Fix: Generate dedicated native 9:16 (1080×1920) images with Imagen 4 Ultra using vertical-composition-specific prompts. Composite logo at lower-left 18% width, y≈72%. Always generate separate image sets per placement format.
6

Door on truck — image hallucination (Rob feedback)

Root cause: Imagen prompt was too loose on vehicle details. Model generated a truck with an incorrect door position/style.
Fix: Image prompts must specify vehicle details explicitly. Add visual review gate before any image upload to Meta — no auto-upload.
7

adimages hashes[] param rejected for single image

Root cause: Passed image hash as a plain string. Meta API requires the hashes[] array param key even when uploading a single image.
Fix: Always use hashes[] param key for image hash submission. Documented in API gotchas.
8

Orphan "see ⚠️ above" reference

Root cause: A geo-targeting warning was removed from the approval page, but the placement table row referencing it was not updated. Left a dangling pointer with no target.
Fix: Pre-deploy content check — grep for "see above", "see ⚠️", and other reference text before every deploy.
9

Unicode em-dash patch failure — age displayed as 25–65

Root cause: Python's json.dumps() serializes "–" as \u2013. The patch script used literal string 25–65 which didn't match 25\u201365 in the JS file. Live page showed wrong age for one full deploy cycle.
Fix: All string patches on CF Worker JS must target the actual encoded form. Use Python regex to locate content before patching; verify match count before writing.
10

CF MCP OAuth token expiry mid-session

Root cause: Cloudflare MCP uses short-lived OAuth tokens that expire during a session. Multiple failed deploys before discovering the issue.
Fix: CF MCP is read-only reference only. ALL deploys use wrangler CLI via Desktop Commander exclusively. Policy locked in CLAUDE.md.
11

curl returns multipart response — raw deploy fails

Root cause: Fetching a deployed CF Worker via curl returns a multipart HTTP response with boundaries and headers, not raw JS. Attempting to deploy that content fails with a parser error.
Fix: Strip multipart wrapper in Python: find JS start after first boundary, strip trailing boundary, then patch/deploy. Standard procedure for all worker fetch-and-patch operations.
12

CTA not set to GET_QUOTE on initial ad creation (Rob feedback)

Root cause: Initial ad creatives used default CTA. GET_QUOTE with lead_gen_form_id was not set on creative build. All 3 creatives had to be deleted and recreated.
Fix: Add CTA type to creative build checklist as a required field — never leave as default.

Rob's Feedback — Alignment Log

Each piece of client feedback, its root cause, resolution, and current status.

FeedbackRoot CauseResolutionStatus
"door on truck" Imagen prompt too loose on vehicle details; model hallucinated truck door Regenerated all 3 images with Imagen 4 Ultra; explicit composition prompts for vehicle accuracy ✓ Resolved
"blurred edge frames and employees heads getting cut off" 1:1 images center-cropped/edge-blurred to fill 9:16 slots — wrong approach for Stories Generated native 9:16 (1080×1920) images; vertical-specific composition; Sperry logo composited lower-left ✓ Resolved
"should be Get Quote" Initial creatives used default CTA; GET_QUOTE not wired on creation Recreated all 3 ad creatives with object_story_spec + GET_QUOTE + lead_gen_form_id ✓ Resolved
"see ⚠️ above — there is no warning triangle above" Warning removed but placement table reference not updated Rebuilt page section; orphan reference removed in v5 rebuild ✓ Resolved
"age range should be 35–80""35–65+" age_max defaulted to 25 in initial build; Meta hard cap 65; then unicode patch missed encoded form Set age_min=35, omitted age_max (= 35–65+ in Meta); fixed unicode patch to target \u2013 encoded form ✓ Resolved
"still says 25–65" Literal patch didn't match JSON-serialized unicode em-dash \u2013 Python patch targeting encoded form; confirmed 2 occurrences replaced; redeployed; live QA verified ✓ Resolved

Technical Stack

Meta Ads API

ItemValue
API VersionGraph API v25.0
Account IDact_2556180444815953
Adset ID6981816306436
Creative IDsv1: 1639362623949691  |  v2: 987882690492875  |  v3: 895671293519348
CTA TypeGET_QUOTE with lead_gen_form_id
Token location.claude/memory/context/api-keys.md
Token renewaldevelopers.facebook.com/tools/explorer

Meta Marketing MCP (Local)

ItemValue
TypeLocal Python MCP server, registered in claude_desktop_config.json
Toolscreate_campaign, create_ad_set, create_ad, upload_image, get_account_insights, list_campaigns, update_campaign_status, list_ad_images (10 total)
LimitationDoes not expose lead_gen_form_id param — GET_QUOTE CTA requires direct API calls

Image Generation — Google AI Studio

ItemValue
ModelImagen 4 Ultra (imagen-4.0-ultra-generate-001)
API KeyStored in .claude/memory/context/api-keys.md
Endpointhttps://generativelanguage.googleapis.com/v1beta/models/imagen-4.0-ultra-generate-001:predict
AuthAPI key in URL query param ?key=...
Formats used9:16 for Stories/Reels; 1:1 for Feed
Logo compositingPIL/Pillow — lower-left, 18% width, y≈72% height
Logo source.../Sperry Tree Care/Creative and Logos/cropped-Sperry_Tree_Care_logo_2025-small-fullcolor.png

Cloudflare Workers — Approval Page

ItemValue
Worker namesperry-meta-approval-apr2026
Live URLsperry-meta-approval-apr2026.jason-8ce.workers.dev
Current Version ID8aba6891-622f-451d-aac1-9ed5c9f2b604
Worker size~304KB (base64-embedded images)
Deploy methodwrangler CLI v4.67.0 via Desktop Commander (CF MCP not used for deploys)
Deploy commandcd /Users/Jason && wrangler deploy /path/file.js --name worker-name --compatibility-date 2024-01-01
AuthOAuth via ~/.wrangler/config/default.toml

Execution Environment

ToolRoleNotes
Desktop CommanderMac-side execution: file I/O, wrangler deploys, curl, Python scriptsPrimary execution layer for all Mac operations
mcp__workspace__bashLinux sandbox: image processing, data manipulation, API calls⚠️ /tmp is session-scoped — not persistent across sessions
Meta Marketing MCPCampaign creation, ad management (read/write)No lead_gen_form_id support; use direct API for GET_QUOTE
PIL/PillowImage processing, logo compositing, format conversionInstalled at system Python

Geo Targeting

ItemValue
Total zip codes29 (23 Lane County + 6 Benton County)
Required format{"key": "US:XXXXX"} — country prefix mandatory
Lane County97401, 97402, 97403, 97404, 97405, 97408, 97477, 97478, 97448, 97424, 97426, 97439, 97487, 97431, 97455, 97452, 97463, 97437, 97438, 97461, 97488, 97489, 97492
Benton County97330, 97331, 97333, 97370, 97456, 97324

Opportunities for Improvement

AreaIssueRecommendationPriority
File persistence /tmp lost between sessions; caused full approval page rebuild (~90 min lost) Immediately copy all generated JS/HTML to /Users/Jason/Documents/ on creation. Rule: if a file took more than 5 min to build, it goes to Documents. 🔴 Critical
Image QA gate Generated images uploaded to Meta without visual review; truck door hallucination reached client Mandatory human review before upload: generate → save → view → approve → upload. No auto-upload. 🔴 Critical
Meta API validation Zip code format and age_max errors required multiple API round trips to diagnose Pre-validate all inputs before API call: zip format US:XXXXX check, age_max ≤ 65 check, CTA compatibility matrix check 🟡 High
Approval page template Built from scratch each campaign; repeated boilerplate work Lock this build as the standard CF Worker template. Future campaigns populate placeholders only — no structural rebuild. Skill created for this. 🟡 High
Image brief template Ad hoc prompts produced hallucinations; required regeneration Standardized image brief template: platform + aspect ratio (1:1 AND 9:16 separately) + composition rules + brand elements + explicit NO list (no blurred edges, no cropped faces) 🟡 High
CF deploy policy CF MCP tokens expire mid-session; caused multiple silent failures Document in CLAUDE.md: CF MCP = read only. All deploys = wrangler CLI via Desktop Commander. No exceptions. 🟡 High
Worker fetch pattern curl returns multipart; deploy of raw response fails with parser error Document standard multipart stripping procedure in CLAUDE.md. Add to skill reference. 🟠 Medium
Image storage Generated images in /tmp only; lost after session Save all generated images to Google Drive at creation: .../Sperry Tree Care/Campaigns/[campaign]/images/ 🟠 Medium
Pre-deploy QA automation Manual checks only; some issues caught post-deploy Run automated QA script against live URL after every deploy. Fail fast. Check: age range, CTA text, zip count, no orphan refs, all 3 ad sections present. 🟠 Medium
Unicode-aware patching Literal string patch missed JSON-encoded em-dash; required 2 deploy cycles to fix All CF Worker patches use Python with explicit grep for actual encoded form before replacement. Verify match count > 0 before writing. 🟠 Medium

Future Campaign Build Playbook

Locked standard process for Meta lead gen campaigns. Follow in order. Each phase has a time estimate.

Phase 1: Setup (30 min)

1

Confirm campaign brief

Service area, audience (age range, interests), imagery style, CTA type, daily/lifetime budget, campaign objective, lead form ID

2

Pull zip codes

Format as {"key": "US:XXXXX"} arrays from service area definition. Verify coverage map.

3

Validate credentials

Check Meta token at developers.facebook.com/tools/explorer. Verify Imagen API key in .claude/memory/context/api-keys.md. Confirm Meta account ID and lead form ID.

Phase 2: Image Generation (45 min)

4

Write image briefs (one per variation)

For EACH ad variation: specify platform, both aspect ratios (1:1 AND 9:16 separately), subject composition, brand elements (logo placement), explicit prohibitions (no edge blur, no cropped faces, no incorrect vehicle details)

5

Generate 1:1 images (Feed)

Imagen 4 Ultra. Save to .../Sperry Tree Care/Campaigns/[campaign]/images/ immediately — not /tmp only.

6

Generate native 9:16 images (Stories/Reels)

Separate Imagen 4 Ultra call with vertical-specific composition prompt. Subject must be visible in full frame with no edge blurring. Composite Sperry logo: lower-left, 18% width, y≈72%.

7

Visual review gate ⚠️

View all generated images before uploading. Check: no hallucinations, correct composition, logo visible, brand accurate. Do not proceed until images are approved.

Phase 3: Meta Campaign Setup (60 min)

8

Create campaign

Via Meta MCP or direct API. Objective: LEAD_GENERATION.

9

Create adset with targeting

age_min=35, omit age_max (= 35–65+). Zip targeting with US: prefix. Placements: Advantage+ Auto.

10

Upload images

Use hashes[] param key (even single image). Save all hash values for creative build.

11

Create ad creatives

Use object_story_spec ONLY. Set GET_QUOTE CTA + lead_gen_form_id. Never use asset_feed_spec with lead gen CTAs.

12

Verify via GET calls

Pull each creative, adset, and ad via API. Confirm: CTA, copy, targeting, image hash, status=PAUSED.

Phase 4: Approval Page (45 min)

13

Start from locked CF Worker template

Use the meta-social-campaign skill. Populate placeholders: campaign name, targeting summary, ad copy, zip codes, image previews (base64 embed).

14

Show all placement formats per ad

For each of the 3 ad variations: show 1:1 Feed, 9:16 Stories, 1.91:1 Right Column previews with dimensions labeled.

15

Pre-deploy QA

Grep for: age range correct, CTA text, zip count, "see above" orphan refs, all 3 ad sections present. Fix before deploying.

16

Deploy and save

wrangler deploy via Desktop Commander. Save JS to /Users/Jason/Documents/ immediately. Run automated QA on live URL after deploy.

API Gotchas — Do Not Rediscover

Zip codes: {"key": "US:XXXXX"} — country prefix mandatory. Plain zip rejected.
age_max: Hard cap at 65. Omit the field entirely for "65+" (no upper limit). Do not set to 80 or any value over 65.
GET_QUOTE CTA: Requires object_story_spec. Never combine with asset_feed_spec (error 1443048).
Image upload: Use hashes[] param key even for single image. Plain string rejected.
CF Worker fetch: curl returns multipart response. Strip wrapper before patching: find JS start, strip trailing boundary. Then patch, then deploy.
Unicode in JSON: Em-dash "–" serializes as \u2013. Literal patches won't match. Use Python targeting the raw encoded form.
CF deploys: CF MCP = read only reference. All deploys = wrangler CLI via Desktop Commander only.

Skill: meta-social-campaign

This campaign build is locked as the standard for all future Meta social campaign builds. The meta-social-campaign skill encodes this playbook, the API gotchas, the approval page template, and the image generation workflow.

Skill file saved to: /Users/Jason/Documents/meta-social-campaign-SKILL.md

To install: move to /var/folders/.../claude-hostloop-plugins/.../skills/meta-social-campaign/SKILL.md or package as a Cowork plugin.

Skill triggers on:

  • "build a Meta campaign"
  • "set up a Facebook/Instagram lead gen campaign"
  • "create an approval page for [client] Meta ads"
  • "Meta ad campaign for [client]"
  • "generate social ad images for [campaign]"
  • "launch [client] Facebook campaign"

How to Invoke

The skill triggers automatically — just describe what you need in plain English:

To force-load it explicitly regardless of phrasing, use /meta-social-campaign as a slash command in Cowork.