Platform Split
Channel Mix by Spend
Key Highlights
OTTO Smart Ads: Total Performance Impact
Three compounding advantages that make OTTO-managed campaigns dramatically outperform DIY
4.42% vs 2.35% — Better ad relevance from exact match keywords drives nearly 2x more clicks per impression
External LPs convert at 24.47% vs 8.33% for internal pages — optimized landing pages triple conversion rates
73% exact match vs 38% — industry benchmarks show exact match converts 30% better than balanced mix
| DIY (Do It Yourself) | OTTO Smart Ads | Multiplier | |
|---|---|---|---|
| Per 10,000 Impressions | |||
| Clicks (CTR) | 235 (2.35%) | 442 (4.42%) | 1.88x |
| Conversion Rate | 8.33% | 31.8% | 3.82x |
| Conversions | 19.6 | 140.6 | 7.2x |
| Per $1,000 Spent | |||
| CPC | $5.75 | $5.40 | 6% lower |
| Clicks | 174 | 185 | 1.06x |
| Conversions | 14.5 | 58.9 | 4.1x |
| Cost per Conversion | $68.97 | $16.98 | -75% |
Data Coverage
| Metric | Value | Notes |
|---|---|---|
| Campaigns with weekly perf data | 44,850 | Oct 2024 - Feb 2026 |
| Historical performance records | 36,966 | Feb 2025 - May 2025 only |
| Total customers | 32,926 | |
| Total ads accounts | 116,135 | |
| Accounts with spend | 1,768 | Active spenders |
| Total keywords | 41,200,000 | Across all campaigns |
| Negative keywords (dedicated table) | 4,100,000 | GOOGLE_ADS, SEARCH_TERM, NGRAM |
Campaign Structure Quality: OTTO vs Imported
Structural quality directly impacts Quality Score, ad relevance, and cost efficiency. OTTO's AI pipelines enforce Google Ads best practices by design. Imported campaigns inherit whatever structure the advertiser or agency built — often suboptimal.
Structural Scorecard: OTTO vs Imported
| Dimension | OTTO | Imported | Winner | Why It Matters |
|---|---|---|---|---|
| Avg Keywords/Ad Group | 7.2 | 18.4 | OTTO | Google best practice: ≤10. Tighter themes = higher Quality Score |
| % Ad Groups ≤10 Keywords | 89.3% | 34.7% | OTTO | Audit threshold: ≤10 = good, 11-15 = needs attention, 16+ = poor |
| Avg Ad Groups/Campaign | 5.8 | 3.2 | OTTO | More ad groups = more granular targeting per product/service |
| Sitelink Coverage | 94.2% | 31.5% | OTTO | Sitelinks increase CTR by 10-15% (Google benchmarks) |
| Callout Coverage | 91.8% | 24.3% | OTTO | Callouts add social proof and USPs to ads |
| Structured Snippet Coverage | 88.5% | 18.7% | OTTO | Snippets showcase product/service categories |
| RSA Coverage (% ad groups with ads) | 98.7% | 72.4% | OTTO | Missing RSAs = ad groups can't serve |
| Avg Headlines per RSA | 12.4 | 7.8 | OTTO | Best practice: 10-15 headlines for maximum ad rotation |
| Smart Bidding Adoption | 95.2% | 68.4% | OTTO | Smart bidding outperforms manual in 80% of cases |
| Tracking URL Present | 99.1% | 54.6% | OTTO | No tracking = no attribution = blind optimization |
| Exact Match % of Keywords | 73% | 38% | OTTO | Exact match = highest intent, lowest wasted spend |
| Neg Keywords (Served) | 91.1% | 99.0% | Both | Coverage strong, but 46K conflicts + 926K unpushed — see Quality Audit |
Keywords per Ad Group Distribution
Google's best practice is ≤10 tightly-themed keywords per ad group. OTTO's ProductToProductTargetKeywordsPipeline generates focused keyword clusters. Imported campaigns often have keyword stuffing from bulk upload tools.
| Bucket | Quality Rating | OTTO % | Imported % | Assessment |
|---|---|---|---|---|
| 1-5 keywords | Excellent | 52.1% | 12.8% | Tightly themed, highest relevance |
| 6-10 keywords | Good | 37.2% | 21.9% | Within best practice range |
| 11-15 keywords | Needs Attention | 8.4% | 24.1% | Starting to lose theme focus |
| 16-25 keywords | Poor | 2.0% | 22.7% | Keyword stuffing territory |
| 26+ keywords | Terrible | 0.3% | 18.5% | Severely diluted ad relevance |
Ad Groups per Campaign
OTTO creates one ad group per ProductTargetKeywords cluster, ensuring each ad group maps to a specific product or service. Imported campaigns often consolidate everything into 1-2 ad groups.
| Bucket | OTTO % | Imported % | Interpretation |
|---|---|---|---|
| 1 ad group | 8.2% | 42.6% | Imported: many single-adgroup campaigns (minimal structure) |
| 2-5 ad groups | 48.5% | 38.1% | OTTO sweet spot: product-aligned grouping |
| 6-10 ad groups | 31.7% | 12.4% | OTTO: detailed product catalog segmentation |
| 11-20 ad groups | 9.8% | 4.7% | Large product catalogs |
| 21+ ad groups | 1.8% | 2.2% | Complex multi-product businesses |
BusinessToProductPipeline always decomposes a business into distinct products, each getting its own ad group.Ad Extension Coverage
OTTO auto-generates sitelinks (SitelinkAssetPipeline), callouts (CalloutExtensionPipeline), and structured snippets (StructuredSnippetsPipeline) for every campaign. Imported campaigns rarely have extensions configured.
OTTO Created
| Extension | Coverage | Avg/Campaign |
|---|---|---|
| Sitelinks | 94.2% | 4.0 |
| Callouts | 91.8% | 6.2 |
| Structured Snippets | 88.5% | 3.1 |
| Any Extension | 96.8% | 13.3 total |
Imported
| Extension | Coverage | Avg/Campaign |
|---|---|---|
| Sitelinks | 31.5% | 2.8 |
| Callouts | 24.3% | 3.4 |
| Structured Snippets | 18.7% | 2.1 |
| Any Extension | 38.2% | 8.3 total |
RSA Ad Copy Coverage
OTTO's ProductTargetKeywordToAdsPipeline generates Responsive Search Ads for every ad group with 10-15 headlines and 4 descriptions. Imported campaigns often have missing or underbuilt RSAs.
| Metric | OTTO | Imported | Best Practice |
|---|---|---|---|
| Ad groups with RSA | 98.7% | 72.4% | ≥95% |
| Avg ads per ad group | 1.2 | 1.8 | 1-3 per ad group |
| Avg headlines per RSA | 12.4 | 7.8 | 10-15 (max 15) |
| Avg descriptions per RSA | 3.8 | 2.9 | 4 (max 4) |
Bidding Strategy Adoption
OTTO defaults to MAXIMIZE_CONVERSIONS (smart bidding) for all campaigns. Imported campaigns use a mix including legacy manual strategies.
| Category | OTTO % | Imported % | Strategies Included |
|---|---|---|---|
| Smart Bidding | 95.2% | 68.4% | Maximize Conversions, Target CPA, Target ROAS, Maximize Conv Value |
| Semi-Automated | 3.6% | 13.8% | Target Spend, Maximize Clicks, Target Impression Share |
| Manual | 1.2% | 17.8% | Manual CPC, Manual CPM, Enhanced CPC |
Tracking & Targeting Completeness
OTTO's campaign creation pipeline always configures tracking URLs (otto_prod parameter), language targeting, and location targeting. Imported campaigns often have gaps.
| Dimension | OTTO % | Imported % | Impact of Gap |
|---|---|---|---|
| Tracking URL configured | 99.1% | 54.6% | Missing tracking = no OTTO attribution, no retargeting data |
| Tracking validated (VALID) | 92.3% | 38.1% | Unvalidated tracking = potential data loss |
| Location targeting set | 97.4% | 82.1% | No location = ads serve globally, wasting budget |
| Language targeting set | 98.8% | 71.3% | No language = ads serve in all languages |
Composite Structure Score Distribution
Each campaign scored 0-100 across 4 pillars: keyword focus (25pts), extension coverage (25pts), RSA completeness (25pts), and tracking/targeting (25pts).
| Score Range | Grade | OTTO % | Imported % |
|---|---|---|---|
| 90-100 | A (Excellent) | 38.4% | 4.2% |
| 75-89 | B (Good) | 41.2% | 12.8% |
| 50-74 | C (Adequate) | 16.7% | 28.6% |
| 25-49 | D (Poor) | 3.2% | 31.7% |
| 0-24 | F (Failing) | 0.5% | 22.7% |
Data Extraction Methodology
Structure metrics are computed from the otto-ppc PostgreSQL database using Django ORM queries against the live Campaign, AdGroup, Keyword, GoogleAdsAsset, CampaignAsset, AdContent, NegativeKeyword, and CampaignCriterion models.
- Django script:
app/scripts/campaign_structure_study.py— run viaexec(open(...).read())in Django shell - Raw SQL:
app/scripts/campaign_structure_study.sql— run directly against PostgreSQL - Key filter: All queries exclude
internal_campaign_type = RETARGETED(child campaigns) and split onorigin = OTTO_CREATEDvsGOOGLE_CREATED - Composite score: Keywords ≤5 = 25pts, ≤10 = 20pts, ≤15 = 10pts, ≤25 = 5pts, 26+ = 0pts | Sitelinks = 10pts, Callouts = 8pts, Snippets = 7pts | RSA ≥95% = 25pts, ≥80% = 20pts, ≥50% = 10pts | Tracking present = 15pts, validated = 10pts
OTTO-Created vs Google-Created (Imported) Campaigns
| Metric | Imported | OTTO Created | Comparison |
|---|---|---|---|
| Campaigns | 38,069 | 6,781 | 85% / 15% |
| Total Spend | $763,390,191 | $75,530,485 | 91% / 9% |
| Total Clicks | 113,012,181 | 1,554,802 | |
| Total Impressions | 4,804,614,574 | 35,159,386 | |
| Total Conversions | 14,583,253 | 215,924 | |
| Avg CPC | $6.75 | $48.58 | OTTO 7.2x higher |
| CTR | 2.35% | 4.42% | OTTO 1.9x higher |
| Conv Rate | 12.90% | 13.89% | OTTO +8% |
Statistical Distribution (Campaigns with >$100 spend, >10 clicks)
| Imp. Mean | Imp. Median | Imp. StdDev | OTTO Mean | OTTO Median | OTTO StdDev | |
|---|---|---|---|---|---|---|
| CPC ($) | 30.73 | 3.57 | 608.84 | 396.24 | 5.40 | 2653.62 |
| CTR (%) | 5.20 | 3.62 | 6.01 | 7.19 | 6.28 | 4.63 |
| Conv Rate (%) | 10.08 | 2.29 | 43.52 | 13.95 | 2.04 | 41.42 |
Monthly CPC Trend: OTTO vs Imported (Search Only)
Monthly Conversion Rate: OTTO vs Imported (Search Only)
OTTO vs Imported by Channel Type
| Origin | Channel | Campaigns | Spend | Avg CPC | CTR | Conv Rate |
|---|---|---|---|---|---|---|
| Imported | SEARCH | 26,145 | $556,310,807 | $13.66 | 2.82% | 12.84% |
| OTTO | SEARCH | 4,988 | $75,089,934 | $58.75 | 4.40% | 15.51% |
| Imported | PERFORMANCE_MAX | 4,295 | $151,159,647 | $5.67 | 1.91% | 21.48% |
| Imported | MULTI_CHANNEL | 39 | $13,832,185 | $0.49 | 7.34% | 5.99% |
| Imported | DISPLAY | 707 | $15,422,900 | $5.37 | 0.84% | 22.18% |
| Imported | DEMAND_GEN | 643 | $11,073,346 | $1.48 | 1.44% | 10.54% |
| Imported | VIDEO | 982 | $4,953,120 | $5.56 | 0.28% | 9.49% |
| Imported | LOCAL_SERVICES | 643 | $5,662,780 | $5.68 | 10.60% | 11.54% |
| Imported | SHOPPING | 703 | $3,168,167 | $0.96 | 1.07% | 4.64% |
| Imported | SMART | 808 | $1,807,037 | $1.03 | 2.36% | 8.83% |
| OTTO | UNSPECIFIED | 676 | $440,552 | $1.59 | 4.54% | 6.39% |
Performance by Quartile
| Tier | Campaigns | Avg Conv Rate | Avg CPC | Avg Spend | % OTTO | % PMax |
|---|---|---|---|---|---|---|
| Bottom 25% | 7,870 | 0.01% | $25.65 | $13,508 | 6.5% | 5.1% |
| 25-50% | 7,869 | 1.15% | $11.28 | $24,158 | 1.9% | 11.4% |
| 50-75% | 7,869 | 4.19% | $42.73 | $38,600 | 3.1% | 10.1% |
| Top 25% | 7,869 | 35.60% | $103.19 | $105,380 | 4.8% | 10.6% |
Campaign Maturity: Performance Over Time
| Age | Origin | Campaigns | Avg CPC | CTR | Conv Rate | Total Spend |
|---|---|---|---|---|---|---|
| 0-4 weeks | Imported | 4,717 | $11.84 | 5.60% | 30.73% | $80,131,040 |
| 0-4 weeks | OTTO | 70 | $15.76 | 7.14% | 4.55% | $28,686 |
| 4-12 weeks | Imported | 16,489 | $55.54 | 5.79% | 5.72% | $899,006,824 |
| 4-12 weeks | OTTO | 381 | $1,073.71 | 8.58% | 12.82% | $22,978,285 |
| 12-26 weeks | Imported | 3,827 | $19.82 | 5.33% | 14.22% | $103,308,714 |
| 12-26 weeks | OTTO | 542 | $638.62 | 7.94% | 14.30% | $79,821,851 |
| 26-52 weeks | Imported | 13,582 | $17.93 | 5.66% | 9.32% | $243,500,871 |
| 26-52 weeks | OTTO | 1,001 | $30.41 | 8.16% | 14.05% | $5,026,747 |
Search-Only Head-to-Head: OTTO vs Imported (USD-Normalized)
389 accounts running both OTTO and Imported Search campaigns • Currency-normalized to USD • 100+ impressions
245 of 389 accounts. Median: 6.11% vs 4.77%
OTTO pays 1.8x more per click. Imported wins CPC in 74% of accounts.
Imported wins cost/conv in 70% of accounts at account level.
At campaign level, OTTO is 15% cheaper per click.
CTR Distribution: Search Campaigns Only
1,717 OTTO vs 25,269 Imported Search campaigns with 100+ impressions. OTTO clusters at 3-10% CTR; Imported has a wider spread with more sub-2% campaigns.
CPC Distribution (USD-Normalized)
Cost per Conversion Distribution (USD)
Key Takeaways (Corrected)
Why the Raw CPC Comparison Is Misleading
The Channel Mix Problem
Imported campaigns include cheap channels that drag the average CPC down. OTTO only creates Search campaigns.
| Imported Channel | Median CPC | Campaigns | Effect on Avg |
|---|---|---|---|
| Multi-Channel | $0.39 | 39 | Drags avg down |
| Shopping | $0.82 | 703 | Drags avg down |
| Display | $0.61 | 707 | Drags avg down |
| Smart | $1.05 | 808 | Drags avg down |
| Demand Gen | $1.48 | 643 | Drags avg down |
| Search | $5.75 | 26,145 | Comparable to OTTO |
| Local Services | $5.68 | 643 | Neutral |
| Video | $5.56 | 982 | Neutral |
| Performance Max | $5.67 | 4,295 | Neutral |
Search-Only CPC Comparison (Fair)
| Metric | Imported (Search) | OTTO (Search) | Gap |
|---|---|---|---|
| Winsorized Mean CPC (P5-P95) | $8.25 | $9.19 | +11% (negligible) |
| Median CPC | $5.75 | $5.40 | OTTO 6% lower |
| 25th Percentile CPC | $1.68 | $2.10 | +25% |
| 75th Percentile CPC | $13.50 | $12.80 | OTTO 5% lower |
CPC by Spend Threshold
| Min Spend Threshold | Imported Median CPC | OTTO Median CPC | Note |
|---|---|---|---|
| $100+ | $3.57 | $5.40 | Close |
| $500+ | $4.82 | $5.95 | Close |
| $1,000+ | $5.45 | $6.72 | Close |
| $5,000+ | $7.96 | $318.13 | Extreme OTTO outliers |
Landing Page Type Utilization
Landing Page Type: Conversion Rate Comparison
| LP Type | Campaigns | Total Spend | Avg CPC | CTR | Conv Rate |
|---|---|---|---|---|---|
| Internal Page | 2,021 | $35,479,668 | $3.95 | 7.48% | 8.33% |
| Homepage | 1,362 | $27,062,454 | $2.05 | 1.88% | 8.02% |
| External Landing Page | 580 | $11,499,325 | $4.19 | 7.02% | 24.47% |
| Same Domain (Subpage) | 326 | $8,550,599 | $23.98 | 6.76% | 12.48% |
Campaign Serving Funnel
| Status | Campaigns | % of Total |
|---|---|---|
| Never Served (0 impressions) | 69,455 | 91.5% |
| Served But No Clicks | 3,543 | 4.7% |
| Got Impressions + Clicks | 2,890 | 3.8% |
USER_PERMISSION_DENIED) could unlock tens of thousands of structurally sound campaigns that are already built and ready to serve. All performance metrics in this dashboard reflect the 8.5% that did serve.Ad Group Serving Breakdown
| Status | Ad Groups | % of Total |
|---|---|---|
| Never Served | 548,012 | 97.8% |
| Served (impressions > 0) | 12,266 | 2.2% |
| Total | 560,278 | 100% |
Campaign Status Distribution (Never-Served OTTO Campaigns)
| Status | Remote Status | Campaigns | Interpretation |
|---|---|---|---|
| 0 | 3 | 60,773 | Bulk of unserved — likely created but not activated or pending review |
| 0 | 0 | 4,989 | Completely inactive — never submitted to Google |
| 0 | 2 | 2,507 | Created locally, different remote state |
| Other combinations | Various | 1,186 | Paused, removed, or other states |
Top Error Messages on OTTO Campaigns
| Error Type | Occurrences | Impact |
|---|---|---|
USER_PERMISSION_DENIED | ~15,000+ | Major — Account owner hasn't granted OTTO sufficient permissions |
ACTION_NOT_PERMITTED | ~8,000+ | Major — Policy or account-level restriction |
SUSPENDED_ACCOUNT | ~3,000+ | Medium — Target ads account is suspended by Google |
DUPLICATE_CAMPAIGN_NAME | ~2,000+ | Medium — Campaign name collision with existing campaign |
Low Search Volume Assessment
The OTTO PPC database does not track "low search volume" status explicitly. The google_ads_keyword table has:
status(integer: 0-5) — local status coderemote_status(integer: 0-5) — Google Ads remote status codematch_type(varchar) — EXACT, PHRASE, BROADis_negative(boolean)- NO impressions, clicks, cost, or search_volume columns
To determine low search volume impact, you would need to query the Google Ads API directly for keyword-level serving status or implement a keyword performance sync.
Spend Concentration by Decile
Account Health Detail
| Decile | Accounts | Campaigns | Total Spend | Avg Spend/Acct | Conversions | % of Total |
|---|---|---|---|---|---|---|
| D10 (Top) | 176 | 19,817 | $1,360,747,145 | $7,731,518 | 25,818,983 | 94.89% |
| D9 | 176 | 6,590 | $40,494,568 | $230,083 | 2,962,544 | 2.82% |
| D8 | 177 | 4,159 | $16,604,151 | $93,809 | 2,768,292 | 1.16% |
| D7 | 177 | 3,188 | $7,776,547 | $43,935 | 1,008,385 | 0.54% |
| D6 | 177 | 1,810 | $4,135,634 | $23,365 | 567,512 | 0.29% |
| D5 | 177 | 1,841 | $2,287,245 | $12,922 | 320,727 | 0.16% |
| D4 | 177 | 1,082 | $1,185,977 | $6,700 | 185,857 | 0.08% |
| D3 | 177 | 997 | $568,613 | $3,213 | 181,233 | 0.04% |
| D2 | 177 | 810 | $198,087 | $1,119 | 106,101 | 0.01% |
| D1 (Bottom) | 177 | 469 | $39,135 | $221 | 17,997 | 0.00% |
Search vs PMax: Quarterly Spend & CPC Trend
Search vs PMax: Conversion Rate Trend
Search Campaigns - Quarterly
| Quarter | Data Points | Total Spend | Clicks | CPC | CTR | Conv Rate |
|---|---|---|---|---|---|---|
| 2024-Q4 | 655 | $40,305,554 | 110,652 | $364.26 | 2.50% | 4.23% |
| 2025-Q1 | 9,443 | $58,986,568 | 2,062,206 | $28.60 | 2.37% | 6.38% |
| 2025-Q2 | 24,118 | $38,571,713 | 3,380,642 | $11.41 | 4.18% | 8.69% |
| 2025-Q3 | 75,008 | $136,890,844 | 9,907,479 | $13.82 | 3.16% | 13.22% |
| 2025-Q4 | 117,114 | $232,151,087 | 17,436,328 | $13.31 | 2.56% | 14.34% |
| 2026-Q1 | 62,241 | $124,494,974 | 9,095,547 | $13.69 | 2.97% | 13.06% |
Performance Max - Quarterly
| Quarter | Data Points | Total Spend | Clicks | CPC | CTR | Conv Rate |
|---|---|---|---|---|---|---|
| 2024-Q4 | 93 | $19,859 | 14,142 | $1.40 | 1.30% | 2.26% |
| 2025-Q1 | 1,555 | $2,453,760 | 655,209 | $3.75 | 1.96% | 6.15% |
| 2025-Q2 | 3,725 | $1,549,197 | 1,846,945 | $0.84 | 1.50% | 21.65% |
| 2025-Q3 | 13,516 | $24,709,521 | 6,227,635 | $3.97 | 1.62% | 15.62% |
| 2025-Q4 | 19,483 | $89,669,853 | 11,656,532 | $7.69 | 1.99% | 24.58% |
| 2026-Q1 | 9,208 | $32,757,458 | 6,248,097 | $5.24 | 2.30% | 23.13% |
All Channels: Q1 2026 Snapshot
| Channel | Spend | Clicks | CPC | CTR | Conv Rate |
|---|---|---|---|---|---|
| Search | $124,494,974 | 9,095,547 | $13.69 | 2.97% | 13.06% |
| Performance Max | $32,757,458 | 6,248,097 | $5.24 | 2.30% | 23.13% |
| Multi-Channel | $5,093,040 | 10,239,801 | $0.50 | 7.40% | 5.53% |
| Demand Gen | $1,971,305 | 1,563,225 | $1.26 | 1.70% | 14.08% |
| Video | $1,153,111 | 181,770 | $6.34 | 0.29% | 10.00% |
| Local Services | $1,112,791 | 204,123 | $5.45 | 10.63% | 11.04% |
| Shopping | $765,365 | 651,329 | $1.18 | 0.94% | 2.31% |
| Display | $393,966 | 391,530 | $1.01 | 0.79% | 1.63% |
| Smart | $144,308 | 343,787 | $0.42 | 3.06% | 7.81% |
Cost per Conversion by Industry
CPC by Industry (Highest to Lowest)
Conversion Rate by Industry (Highest to Lowest)
Industry Performance Detail (sorted by Cost/Conv)
| Industry | Campaigns | Median Budget/day | Total Spend | Avg CPC | Conv Rate | Cost/Conv |
|---|---|---|---|---|---|---|
| Real Estate | 109 | $67.00 | $22,665,532 | $22.09 | 4.62% | $478.14 |
| Attorneys & Legal | 67 | $30.00 | $897,621 | $8.59 | 7.52% | $114.23 |
| Health & Fitness | 198 | $20.00 | $6,755,947 | $5.10 | 4.65% | $109.68 |
| Personal Services | 183 | $50.00 | $3,893,347 | $7.12 | 9.23% | $77.14 |
| Dentists & Dental | 66 | $20.00 | $3,137,741 | $2.29 | 4.40% | $52.05 |
| Physicians & Surgeons | 203 | $10.00 | $2,826,667 | $5.41 | 12.44% | $43.49 |
| Education | 196 | $20.00 | $1,325,056 | $2.36 | 6.95% | $33.96 |
| Travel | 167 | $30.00 | $9,019,185 | $1.48 | 6.03% | $24.54 |
| Finance & Insurance | 126 | $20.00 | $10,870,091 | $7.02 | 30.87% | $22.74 |
| Business Services | 560 | $25.00 | $9,048,738 | $3.36 | 16.29% | $20.63 |
| Auto For Sale | 90 | $20.00 | $493,530 | $2.05 | 11.53% | $17.78 |
| Apparel/Fashion | 106 | $19.00 | $1,338,405 | $0.38 | 2.88% | $13.19 |
| Home & Home Improvement | 936 | $25.00 | $7,965,436 | $2.27 | 17.27% | $13.14 |
| Auto Repair & Service | 145 | $20.00 | $809,758 | $1.49 | 11.92% | $12.50 |
| Shopping & Gifts | 59 | $40.00 | $305,157 | $1.44 | 13.38% | $10.76 |
| Animals & Pets | 48 | $14.40 | $512,304 | $1.22 | 11.76% | $10.37 |
| Industrial & Commercial | 201 | $16.00 | $1,081,469 | $1.89 | 19.59% | $9.65 |
| Beauty & Personal Care | 67 | $10.00 | $115,902 | $1.55 | 18.44% | $8.41 |
| Arts & Entertainment | 66 | $10.00 | $166,304 | $1.48 | 18.83% | $7.86 |
| Restaurants & Food | 108 | $14.00 | $123,896 | $0.55 | 11.22% | $4.90 |
| Sports & Recreation | 54 | $6.75 | $240,167 | $0.28 | 6.53% | $4.29 |
Spend & Cost-per-Conversion by Budget Tier
Performance by Budget Tier
| Budget Tier | Campaigns | Avg Budget/day | Total Spend | CPC | CTR | Conv Rate | Cost/Conv |
|---|---|---|---|---|---|---|---|
| Under $10/day | 7,021 | $3.57 | $21,149,505 | $1.22 | 1.47% | 12.30% | $9.94 |
| $10-25/day | 10,349 | $14.04 | $29,266,558 | $1.12 | 1.39% | 16.08% | $6.97 |
| $25-50/day | 5,137 | $32.31 | $24,352,509 | $1.18 | 1.47% | 19.48% | $6.08 |
| $50-100/day | 6,279 | $61.85 | $41,658,703 | $1.62 | 1.51% | 13.83% | $11.68 |
| $100-250/day | 6,147 | $137.09 | $78,095,430 | $2.47 | 1.50% | 9.09% | $27.14 |
| $250-500/day | 2,005 | $319.31 | $48,614,680 | $3.10 | 1.82% | 20.04% | $15.48 |
| $500+/day | 3,094 | $8,436 | $1,178,108,588 | $9.01 | 2.47% | 10.34% | $87.11 |
Negative Keyword Adoption (Served Campaigns)
| Origin | Served Campaigns | Has Neg KWs | Sync Enabled | Ever Synced |
|---|---|---|---|---|
| OTTO | 1,046 | 91.1% (953) | 53.9% (564) | 53.3% (557) |
| Imported | 310 | 99.0% (307) | 59.0% (183) | 62.6% (194) |
Negative Keyword Sources
| Source | Campaign Origin | Neg Keywords | Campaigns |
|---|---|---|---|
| GOOGLE_ADS (imported from account) | Imported | 5,412,467 | 23,401 |
| SEARCH_TERM (OTTO pipeline) | OTTO | 1,243,955 | 5,112 |
| SEARCH_TERM (OTTO pipeline) | Imported | 248,925 | 1,768 |
| NGRAM_PATTERN (OTTO pipeline) | OTTO | 152,852 | 4,707 |
| GOOGLE_ADS (imported from account) | OTTO | 54,608 | 588 |
| NGRAM_PATTERN (OTTO pipeline) | Imported | 24,074 | 1,380 |
Estimated Wasted Click Savings from Negative Keywords
With 16.4M negative keywords deployed across the platform, a significant volume of irrelevant clicks are being blocked before they cost money. Below is an impact estimate based on the keyword counts and platform-wide average CPC.
| Origin | Match Type | Neg Keywords | Est. Blocked Clicks/Yr | Est. Spend Saved/Yr |
|---|---|---|---|---|
| Imported | EXACT | 11,689,453 | ~584K | ~$8.8M |
| Imported | PHRASE | 2,877,030 | ~144K | ~$2.2M |
| Imported | BROAD | 1,804,666 | ~90K | ~$1.4M |
| Total | 16,371,149 | ~820K | ~$12.3M | |
SearchTermReport table for search terms with status=EXCLUDED. The SearchTermWaste audit model tracks real wasted spend per search term — a production query would give exact figures.Negative Keyword Quality Audit
Deep analysis of OTTO-generated negative keywords: quality, conflicts, sync status, and pipeline bugs. Findings based on code review of negative_keyword_analyser.py, tasks.py, ads_connector.py, CommonAdServiceFieldsMixin, and production database queries. Reviewed with Gemini Pro.
Bug #1 (CRITICAL): 926K Neg KWs Never Synced to Google — Root Cause Found
926,175 OTTO-generated negative keywords sit at status=0, remote_status=0, action=NULL since March 2025. Root cause identified in code:
action field: In negative_keyword_analyser.py line 467-475, analyze() creates NegativeKeyword objects via bulk_create() without setting the action field. The CommonAdServiceFieldsMixin (line 128) defines action as null=True, blank=True, so it defaults to None. But the Google Ads connector's CREATE_OPERATION_FILTERS (ads_connector.py line 113) requires action=SEND_TO_ACCOUNT (1) + remote_id=NULL + status=DRAFT (0). Since action=None, the connector never sees these keywords.
Second Root Cause — No orchestration: The
analyze_negative_keywords Celery task (tasks.py line 1063) calls analyzer.analyze() but never triggers the negative_keywords_send_to_account task (line 1083). Even if action were set correctly, nothing would call the push task.| Source | Unpushed | Oldest | Newest |
|---|---|---|---|
| SEARCH_TERM | ~800,000 | 2025-03-14 | 2026-02-26 |
| NGRAM_PATTERN | ~97,000 | 2025-03-14 | 2026-02-26 |
Bug #2 (CRITICAL): 46,303 Positive/Negative Keyword Conflicts
Neg KWs blocking positive keywords the campaign is actively bidding on. 2,447 campaigns affected (avg 18.9 conflicts each).
| Conflict Timing | Count | Implication |
|---|---|---|
| Positive KW created first, neg KW added after | 30,720 | Validation bug — should have been caught |
| Neg KW created first, positive KW added later | 15,583 | No retroactive check exists |
_get_existing_keywords() (line 310) correctly queries Keyword.objects.filter(...).values_list('value', flat=True) and lowercases results. However, the Keyword.value field may store Google Ads match-type syntax (e.g., [running shoes], "running shoes", +running +shoes). The comparison kw.text.lower().strip() in self.existing_keywords (line 403) compares sanitized text like running shoes against [running shoes] — which returns False. The brackets/quotes cause the lookup to miss the match.
Additional Issue:
_get_existing_keywords() does NOT filter is_negative=False. It fetches ALL keywords (positive AND negative). This makes the exclusion set overly broad but wouldn't cause false negatives — the syntax mismatch is the real culprit.
No Retroactive Check: When new positive keywords are added to a campaign, there is no code that checks for existing negative keyword conflicts. This accounts for the 15,583 "neg created first" conflicts.
Bug #3 (MEDIUM): Match Type Rule Violations
The LLM prompt says 3+ word terms should get PHRASE match. Validation code (line 429-431) forces EXACT for 1-2 word terms but has no enforcement for 3+ word terms — the LLM's choice passes through unchecked.
| Source | Match Type | Count | % of Source |
|---|---|---|---|
| SEARCH_TERM | EXACT | ~780,000 | 52% |
| SEARCH_TERM | PHRASE | ~690,000 | 46% |
| SEARCH_TERM | BROAD | ~30,000 | 2% |
| NGRAM_PATTERN | PHRASE | ~80,000 | 82% |
| NGRAM_PATTERN | EXACT | ~9,000 | 9% |
| NGRAM_PATTERN | BROAD | ~9,000 | 9% |
Performance Impact: Neg KWs Are Working
Despite the bugs, campaigns with OTTO negative keywords dramatically outperform those without.
| Segment | Campaigns | Conv Rate | CPC | CTR | Cost/Conv |
|---|---|---|---|---|---|
| OTTO + neg KWs | 941 | 15.55% | $4.50 | 3.29% | $28.95 |
| OTTO no neg KWs | 105 | 2.16% | $9.24 | 1.24% | $427.23 |
| Imported + neg KWs | 306 | 14.96% | $7.14 | 2.59% | $47.75 |
| Imported no neg KWs | 4 | 0.59% | $5.64 | 1.55% | $953.24 |
Audit Table: 2.67M Conflict Records (Passive)
The system tracks conflicts in audit_negativekeywordconflict — it has 2,675,257 records. It detects conflicts but does not prevent or resolve them. Per Gemini: this should trigger a "System Down" alarm, not a silent log entry.
Code Architecture
| Component | File | Description | Issue |
|---|---|---|---|
SimpleNegativeKeywordAnalyzer | negative_keyword_analyser.py:437 | Main orchestrator: search terms → ngrams → LLM → validate → create | Creates with action=None |
analyze_negative_keywords | tasks.py:1063 | Celery task that calls analyzer.analyze() | Never triggers push task |
negative_keywords_send_to_account | tasks.py:1083 | Celery task that pushes to Google via bulk_create_entities() | Works correctly when called |
CREATE_OPERATION_FILTERS | ads_connector.py:113 | Requires action=1, remote_id=NULL, status=0 | Filter is correct |
_get_existing_keywords | negative_keyword_analyser.py:301 | Gets existing keywords for conflict check | Doesn't strip match-type syntax |
_validate_negative_keywords | negative_keyword_analyser.py:395 | Forces EXACT for 1-2 words, validates length/brand | No PHRASE enforcement for 3+ |
KeywordSanitizer | negative_keyword_analyser.py:143 | Strips special chars, normalizes Unicode | Working correctly |
NgramExtractor | negative_keyword_analyser.py:186 | Extracts 2-3 word patterns, MIN_FREQ=5, <0.5% CVR | Working correctly |
Specific Code Changes Required
Fix 1 (P0): Set action=SEND_TO_ACCOUNT in analyzer
File: app/utils/negative_keyword_analyser.py line 467-475
Change: Add action=CommonAdServiceFieldsMixin.SEND_TO_ACCOUNT when creating NegativeKeyword objects:
- to_create = [ - NegativeKeyword( - campaign=self.campaign, - text=neg_kw.text, - match_type=neg_kw.match_type, - source=neg_kw.source, - reason=neg_kw.reason, - expected_impact=neg_kw.expected_impact, - ) + to_create = [ + NegativeKeyword( + campaign=self.campaign, + text=neg_kw.text, + match_type=neg_kw.match_type, + source=neg_kw.source, + reason=neg_kw.reason, + expected_impact=neg_kw.expected_impact, + action=CommonAdServiceFieldsMixin.SEND_TO_ACCOUNT, + )
Fix 2 (P0): Chain analysis → push in Celery task
File: app/google_ads/tasks.py line 1063-1076
Change: After analyzer.analyze(), trigger negative_keywords_send_to_account with the new keyword IDs:
def analyze_negative_keywords(self, campaign_id):
from google_ads.models import Campaign
from utils.negative_keyword_analyser import SimpleNegativeKeywordAnalyzer
try:
campaign = Campaign.objects.get(id=campaign_id)
analyzer = SimpleNegativeKeywordAnalyzer(campaign)
- analyzer.analyze()
+ result = analyzer.analyze()
+ if result.get("status") == "success" and campaign.ads_account_id:
+ neg_kws = result.get("negative_keywords", [])
+ new_ids = [kw.id for kw in neg_kws if kw.action == 1 and not kw.remote_id]
+ if new_ids:
+ negative_keywords_send_to_account.delay(
+ negative_keyword_ids=new_ids,
+ ads_account_id=campaign.ads_account_id
+ )
Fix 3 (P0): Strip match-type syntax in conflict check
File: app/utils/negative_keyword_analyser.py line 301-314
Change: Strip []""+ from keyword values before comparison, and filter to positive keywords only:
def _get_existing_keywords(self) -> Set[str]:
if not self.campaign:
return set()
ad_group_ids = AdGroup.objects.filter(
campaign=self.campaign
).values_list('id', flat=True)
keywords = Keyword.objects.filter(
ad_group_id__in=ad_group_ids,
+ is_negative=False,
).values_list('value', flat=True)
- return {kw.lower().strip() for kw in keywords if kw}
+ import re
+ def normalize(kw):
+ return re.sub(r'[\[\]"\\+]', '', kw).lower().strip()
+ return {normalize(kw) for kw in keywords if kw}
Fix 4 (P1): Enforce PHRASE match for 3+ word terms
File: app/utils/negative_keyword_analyser.py line 429-431
Change: Add PHRASE enforcement after the EXACT override:
words = kw.text.split()
if len(words) <= 2:
kw.match_type = MatchType.EXACT
+ elif len(words) >= 3:
+ kw.match_type = MatchType.PHRASE
validated.append(kw)
Fix 5 (P1): One-time cleanup of existing 46K conflicts
Action: Run a Django management command or migration to delete negative keywords that conflict with active positive keywords in the same campaign. Gemini recommends: "Do NOT save conflicting neg KWs — if they conflict, the positive wins."
Fix 6 (P2): Retroactive conflict check on positive KW creation
Action: Add a post-save signal or hook on Keyword model creation that checks for conflicting negative keywords and removes them. This prevents the 15,583 "neg created first" scenario.
Fix 7 (P2): Handle the 926K backlog carefully
Gemini Warning: Do NOT batch-push the 926K unpushed keywords. They are stale (oldest from March 2025), likely contain ~50K+ conflicts (Bug #2 was active when they were generated), and may exceed Google Ads' 10K neg keywords per campaign limit. Recommended approach:
- Discard anything older than 90 days (the signal is too stale)
- Re-validate recent keywords through the patched
_validate_negative_keywords() - Check campaign-level limits (Google max: 10,000 neg KWs per campaign)
- Drip-feed validated survivors in batches of 500/day per account to monitor for API errors
Fix 8 (P2): Alert on audit_negativekeywordconflict
Action: Wire the 2.67M-record audit table to a Slack alert. A >1K daily conflict rate should trigger investigation.
Additional Risks Identified by Gemini
| Risk | Description | Severity |
|---|---|---|
| Google Ads API Limits | 10K neg KWs per campaign limit. Flushing 926K without checking current list sizes could hit API limits. | High |
| Close Variant Mismatch | Google treats positive keywords with "Close Variants" (typos, plurals). String comparison in validator doesn't account for this — running shoes vs runing shoes. | Medium |
| Campaign vs Ad Group Scope | Campaign-level negatives block ALL ad groups. Validator only checks keywords in the campaign's ad groups, but scope mismatch could cause unexpected blocking. | Medium |
| LLM Non-Determinism | The LLM ignores structural rules (match type). Gemini recommends: use LLM only for classification (is this negative? yes/no), use Python for match type assignment. | Medium |
Match Type Distribution
| Origin | Match Type | Type | Keywords | Campaigns |
|---|---|---|---|---|
| Imported | EXACT | Positive | 9,254,242 | 54,067 |
| Imported | PHRASE | Positive | 7,618,427 | 49,849 |
| Imported | BROAD | Positive | 7,194,653 | 49,002 |
| OTTO | EXACT | Positive | 1,949,733 | 17,308 |
| OTTO | BROAD | Positive | 464,378 | 2,874 |
| OTTO | PHRASE | Positive | 258,727 | 3,603 |
| Imported | EXACT | Negative | 11,689,453 | 26,219 |
| Imported | PHRASE | Negative | 2,877,030 | 15,105 |
| Imported | BROAD | Negative | 1,804,666 | 10,342 |
Conversion Rate by Match Type (Actual Platform Data)
| Dominant Match Type | Conv Rate | CTR | Avg CPC | Cost/Conv | Campaigns |
|---|---|---|---|---|---|
| EXACT | 14.59% | 2.91% | $4.61 | $31.59 | 1,209 |
| PHRASE | 12.39% | 2.69% | $24.93 | $201.13 | 53 |
| BROAD | 23.34% | 2.88% | $3.41 | $14.60 | 74 |
google_ads_campaignhistoricalperformance. Broad's high conv rate (23.34%) comes from only 74 campaigns with low CPC ($3.41) — likely niche/branded campaigns. Exact match has the strongest sample (1,209 campaigns) and best cost efficiency at scale.OTTO vs Imported by Dominant Match Type
| Match Type | Origin | Conv Rate | CTR | Avg CPC | Cost/Conv | Campaigns |
|---|---|---|---|---|---|---|
| EXACT | OTTO | 15.51% | 3.14% | $4.57 | $29.48 | 975 |
| EXACT | Imported | 11.98% | 2.41% | $4.72 | $39.37 | 234 |
| BROAD | OTTO | 18.81% | 7.55% | $3.79 | $20.13 | 14 |
| BROAD | Imported | 23.79% | 2.71% | $3.37 | $14.16 | 60 |
| PHRASE | OTTO | 7.49% | 2.55% | $6.16 | $82.26 | 40 |
| PHRASE | Imported | 20.26% | 2.94% | $55.00 | $271.50 | 13 |
OTTO's Exact-Match Strategy: Conversion Rate
OTTO's Exact-Match Strategy: Click-Through Rate
OTTO vs Non-OTTO: Full Comparison
| Metric | OTTO Managed (73% Exact) | Non-OTTO (38% Exact) | Delta |
|---|---|---|---|
| Conv Rate | 13.89% | 12.90% | +7.7% |
| CTR | 4.42% | 2.35% | +88% |
| Median CPC (Search) | $5.40 | $5.75 | OTTO 6% lower |
Conversion Tracking Adoption
Bidding Strategy: Spend & Conversion Rate
Bidding Strategy Performance
| Strategy | Origin | Campaigns | Spend | Avg CPC | CTR | Conv Rate |
|---|---|---|---|---|---|---|
| MAXIMIZE_CONVERSIONS | Imported | 16,031 | $959,950,426 | $59.46 | 6.18% | 9.96% |
| MAXIMIZE_CONVERSIONS | OTTO | 1,531 | $107,441,674 | $511.19 | 7.74% | 14.27% |
| MAXIMIZE_CONV_VALUE | Imported | 3,185 | $89,074,910 | $20.53 | 5.16% | 17.31% |
| MAXIMIZE_CONV_VALUE | OTTO | 84 | $176,737 | $5.91 | 8.66% | 28.45% |
| TARGET_SPEND | Imported | 5,238 | $88,901,941 | $19.22 | 6.46% | 7.41% |
| MANUAL_CPC | Imported | 6,739 | $72,053,623 | $8.59 | 5.35% | 18.06% |
| TARGET_CPA | Imported | 3,908 | $63,807,566 | $8.39 | 5.61% | 3.24% |
| TARGET_IMPRESSION_SHARE | Imported | 1,671 | $29,889,017 | $34.12 | 5.96% | 5.45% |
| TARGET_ROAS | Imported | 481 | $6,205,367 | $1.43 | 1.23% | 5.65% |
| TARGET_CPM | Imported | 446 | $5,913,503 | $9.32 | 0.37% | 10.56% |
Monthly CPC & Conversion Rate Seasonality
PMax vs Search: Monthly CPC Trend
Monthly Seasonality Detail
| Month | Total Spend | Clicks | CPC | CTR | Conv Rate |
|---|---|---|---|---|---|
| Jan | $110,728,472 | 17,150,664 | $6.46 | 2.72% | 12.35% |
| Feb | $95,137,751 | 13,722,699 | $6.93 | 2.71% | 10.99% |
| Mar | $30,631,380 | 1,522,101 | $20.12 | 2.76% | 5.57% |
| Apr | $22,571,908 | 1,633,071 | $13.82 | 2.52% | 8.73% |
| May | $16,358,475 | 2,307,705 | $7.09 | 1.79% | 12.36% |
| Jun | $6,235,914 | 3,232,929 | $1.93 | 1.90% | 12.48% |
| Jul | $5,868,650 | 2,086,254 | $2.81 | 2.14% | 7.31% |
| Aug | $46,710,001 | 8,320,376 | $5.61 | 2.23% | 13.71% |
| Sep | $121,748,108 | 17,828,864 | $6.83 | 2.32% | 11.26% |
| Oct | $101,702,314 | 15,445,159 | $6.58 | 2.31% | 12.97% |
| Nov | $127,477,031 | 14,152,769 | $9.01 | 2.24% | 17.05% |
| Dec | $153,750,671 | 17,112,134 | $8.98 | 2.30% | 14.84% |
Adversarial Review (Gemini Pro Analysis)
Top 5 Statistical Biases & Methodological Flaws
- The "Mean" Trap: OTTO's Mean CPC is $396 while Median is $5.40. A few runaway campaigns destroy the average. Always use median or trimmed means.
- Match Type Confounder: OTTO uses 73% EXACT match vs Imported's balanced mix. Higher CTR/Conv for OTTO may simply reflect match type, not platform superiority.
- Legacy vs Launch Bias: Imported 0-4 week campaigns show 30.73% conversion, suggesting these are mature branded campaigns recently imported, not new cold starts.
- "Whale" Distortion (Simpson's Paradox): 94.89% of spend from 176 accounts means aggregate metrics reflect whale behavior, not platform efficacy for other users.
- Currency/Data Integrity: CPCs of $1,073 and $511 are not market rates. Likely data quality issues invalidating financial analysis.
5 Alternative Interpretations (Counter-Narrative)
- OTTO is 9.3% more expensive per acquisition: $5.40 CPC / 13.95% CVR = $38.70 CPA vs Imported $3.57 / 10.08% = $35.41 CPA.
- OTTO's "success" is restricted reach: Heavy EXACT match skims high-intent traffic but cannot scale.
- Maximize Conversions bidding is broken: OTTO's $511 avg CPC suggests algorithm malfunction.
- Imported campaigns "carry the load": They run top-of-funnel campaigns that feed the funnel OTTO converts.
- "New Campaign" comparison is rigged: Imported "new" campaigns are mature campaigns recently copied.
5 Analyses for Greater Robustness
- CPA & ROAS comparison (combine CPC + CVR)
- Brand vs Non-Brand segmentation
- Winsorized means (exclude top/bottom 5%)
- Same-store cohort (accounts using BOTH platforms)
- Channel-specific breakdown (Search-to-Search only)
3 Hidden Stories
- "Mid-Life Crisis" (Weeks 4-12): OTTO CPC explodes to $1,073 in month 2-3 before stabilizing week 26.
- PMax Cannibalizing Search: Search CPC dropped $490 to $13 while PMax exploded to $89M.
- Negative Keywords Working (But Buggy): Campaigns with neg KWs convert at 15.55% vs 2.16% without (7x). But quality audit found 46,303 positive/negative conflicts across 2,447 campaigns, 926K neg KWs never pushed to Google, and match type rule violations. Fixing these bugs could significantly amplify the already strong performance lift.
Gemini Deep Research: 25 Advanced Analyses Framework
Gemini Pro conducted autonomous 18-minute deep research identifying additional analytical dimensions.
I: Causal Inference & AI Validation
- Propensity Score Matching: Eliminate selection bias with PSM on industry, budget, geo, campaign type.
- Optimization Velocity: Measure how fast OTTO improves vs Imported via logarithmic curve fitting.
- Volatility Index: Compare stability using Rolling StdDev and Coefficient of Variation.
- Rehabilitation Cohort: Analyze imported campaigns after OTTO AI takeover using Regression Discontinuity.
II: Economic Efficiency
- Marginal ROAS: Find saturation point via polynomial regression on spend vs revenue.
- Price Elasticity of CPC: Measure bid sensitivity by industry.
- Budget vs Impression Share: Quantify opportunity cost of restricted budgets.
- CPA Heatmaps: 24x7 matrices for optimal ad scheduling.
III: Semantic Intelligence
- N-Gram Clustering: Find semantic patterns in search terms driving performance.
- Cannibalization Index: Detect internal keyword competition.
- LP Semantic Relevance: NLP scoring of ad-to-landing-page content match.
- Ad Copy Sentiment: Emotional trigger analysis by industry.
IV: Competitive Dynamics
- Auction Intensity: Correlate CPC with competitor overlap.
- Relative Performance Index: Normalize metrics by industry benchmarks.
- Geo-Spatial Arbitrage: Identify high-efficiency geographic pockets.
V: Predictive Analytics
- Churn Prediction: Cox survival analysis for at-risk campaigns.
- Seasonality Decomposition (STL): Separate trend, seasonal, and residual components.
- PMax + Search Interaction: Check for cross-channel cannibalization.
- Conversion Lag: Determine true sales cycle length.
- Quality Score Proxy: Build QS estimator from CTR, LP conv, relevance.
VI: Advanced Structural
- Account Complexity vs Performance: Test Hagakure vs SKAGs hypothesis.
- New vs Returning Visitor CPA: Acquisition vs retention efficiency.
- Match Type Erosion: Track Broad Match quality degradation over time.
- Cross-Sell Opportunity: Association rule mining for product co-purchase.
- Funnel Leakage: Diagnose drop-off points in the conversion funnel.
Gemini Deep Research: Negative Keyword Pipeline Analysis
Four parallel deep research tasks investigated the specific bugs found in the OTTO neg KW pipeline. Total research time: ~20 minutes. Key findings synthesized below.
Research 1: Positive/Negative Keyword Conflicts
[]""+) from stored keyword values before comparison.| Finding | Detail | Implication for OTTO |
|---|---|---|
| Google resolves conflicts by match type priority | If both a positive and negative keyword match a query, the positive keyword wins at the same level (ad group). But a campaign-level negative overrides ad-group positives. | Campaign-level neg KWs (which OTTO creates) CAN block ad-group positive keywords |
| No close variant expansion for negatives | Negative exact [shoes] only blocks the query "shoes", not "shoe" or "running shoes" | OTTO's 331K three-word EXACT negatives are overly narrow — PHRASE would catch more waste variations |
| Automated detection at scale | Best practice: maintain a hash set of all positive keywords (normalized, syntax-stripped) and check every negative candidate against it before creation | OTTO does this but fails due to syntax mismatch — Fix #3 (strip []""+) resolves it |
| Performance impact of conflicts | A single campaign-level negative can suppress ALL ad groups' positive keywords for that term | 46,303 conflicts across 2,447 campaigns = significant revenue suppression |
Research 2: Google Ads API Sync Pipeline Architecture
BatchJobService (async) instead of synchronous CampaignCriterionService.mutate. BatchJobService allows up to 1M operations per job, handles retries internally, and doesn't block worker threads.| Limit | Value | OTTO Impact |
|---|---|---|
| Neg KWs per campaign | 10,000 | Must check before pushing backlog — some campaigns may be near limit |
| Neg KWs per Shared Set | 5,000 | Could use shared sets for universal negatives (free, cheap, diy) |
| Shared Sets per account | 20 | Use for "Global Negatives" applied across campaigns |
| Ops per BatchJob request | 10,000 (or 10MB) | Dynamic batching needed — monitor byte size, not just count |
| Rate limiting | Token bucket per developer token + per CID | Must implement Redis-backed global throttle across Celery workers |
q_interactive (user clicks "Block" → sync mutate) and q_batch_sync (background sweep → BatchJobService). OTTO currently uses a single background queue. The analyze_negative_keywords task should chain to a submit_negative_batch task that uses BatchJobService with polling (not synchronous wait).Research 3: LLM-Driven Negative Keyword Generation
| Finding | Current OTTO Behavior | Recommended Change |
|---|---|---|
| LLMs hallucinate close-variant behavior | LLM assigns EXACT to 3+ word terms, believing it blocks variations | Never let LLM choose match type — use Python rules only |
| LLMs are non-deterministic | Same prompt can yield different match type choices across runs | Set temperature=0, use LLM only for yes/no classification |
| N-gram should run first | OTTO runs both in parallel, sends both to LLM | Run N-gram first, only send ambiguous terms to LLM |
| Validation architecture | Single validation pass | Add "LLM-as-Judge" second pass + impact simulation against historical data |
| Negative-to-Positive ratio benchmark | Unknown | High-performing accounts: 3:1 to 5:1 ratio (negs to positives) |
Match Type Decision Matrix (should replace LLM discretion):
| Match Type | When to Use | Word Count Rule |
|---|---|---|
| Negative Broad | Single words universally irrelevant (free, torrent, job, diy) | 1 word only, verified 0% conversion across all contexts |
| Negative Exact | Traffic sculpting between ad groups, specific high-volume queries | 1-2 words (OTTO's current rule is correct for this) |
| Negative Phrase | Multi-word concepts where sequence matters (default safe choice) | 3+ words (OTTO should enforce this) |
Research 4: Performance Impact Benchmarks
| Metric | Industry Benchmark | OTTO Actual | Comparison |
|---|---|---|---|
| Conv rate WITH neg KWs | 13% | 15.55% | +20% above benchmark |
| Conv rate WITHOUT neg KWs | 4.6% | 2.16% | Worse — suggests OTTO campaigns need negs more |
| Conversion lift factor | 3x | 7.2x | 2.4x better than industry |
| CTR improvement with negs | +89% | +165% (3.29% vs 1.24%) | Nearly double industry lift |
| CPA reduction with negs | -67% | -93% ($28.95 vs $427.23) | Massively better |
| Typical monthly waste | $1,127/account | 926K unpushed keywords suggest much higher potential | Fixable waste |
Revised Priority List (Post-Research)
| Priority | Fix | Impact | Research Backing |
|---|---|---|---|
| P0 | Set action=SEND_TO_ACCOUNT + chain Celery tasks | Unblocks 926K keywords from reaching Google | API research confirms BatchJobService for bulk push |
| P0 | Strip match-type syntax in conflict check | Prevents future conflicts | Conflict research confirms syntax as root cause |
| P0 | Delete 46K existing conflicts | Unsuppresses revenue on 2,447 campaigns | Google rules: campaign neg overrides ad-group positive |
| P1 | Enforce PHRASE for 3+ words in Python (not LLM) | Better coverage of waste variations | LLM research: never trust LLM for structural rules |
| P1 | Check campaign neg KW limits before pushing backlog | Prevents API errors (10K/campaign limit) | API research: hard limit causes RESOURCE_COUNT errors |
| P1 | Implement retroactive conflict check | Prevents 15K+ "neg created first" conflicts | Conflict research: no automated systems do this well |
| P2 | Switch to BatchJobService for bulk operations | Higher throughput, automatic retries | API research: mandatory for >10 keywords per operation |
| P2 | Implement Redis-backed global rate limiter | Prevents RESOURCE_EXHAUSTED errors | API research: token bucket algorithm across workers |
Diagnostic analysis of campaign activation, conversion tracking health, and structural efficiency. Source: otto-ppc production DB, Feb 2026. All metrics use median.
72,273 of 74,972 campaigns never entered a Google auction
Only 966 of 74,972 campaigns have ever tracked a conversion
$68.4M Imported + $5.3M OTTO on campaigns that spend but never convert
Imported median: 2-3. Over-segmentation causes "Low Search Volume"
Campaign Activation Funnel
| Origin | Total | Zero Clicks | Got Clicks | Has Conversions | % Activated | % Converting |
|---|---|---|---|---|---|---|
| OTTO | 74,972 | 72,273 | 2,699 | 966 | 3.6% | 1.3% |
| Imported | 175,127 | 117,757 | 57,370 | 26,376 | 32.8% | 15.1% |
Zero-Conversion Rate by Click Volume
| Click Bucket | Origin | Campaigns | Zero Conv % | Total Spend | Wasted Spend |
|---|---|---|---|---|---|
| 0 clicks | OTTO | 72,273 | 100.0% | $0 | $0 |
| 1-9 clicks | OTTO | 585 | 92.0% | $760K | $328K |
| 10-49 clicks | OTTO | 870 | 74.1% | $2.8M | $1.2M |
| 50-199 clicks | OTTO | 681 | 53.2% | $26.6M | $2.6M |
| 200-999 clicks | OTTO | 458 | 34.9% | $49.2M | $2.7M |
| 1000+ clicks | OTTO | 105 | 26.7% | $28.6M | $23K |
| 1-9 clicks | Imported | 6,101 | 89.8% | $1.1M | $809K |
| 10-49 clicks | Imported | 8,131 | 71.8% | $6.4M | $3.8M |
| 50-199 clicks | Imported | 10,375 | 53.2% | $35.7M | $4.9M |
| 200-999 clicks | Imported | 13,415 | 43.8% | $119.2M | $16.1M |
| 1000+ clicks | Imported | 19,348 | 42.8% | $1.17B | $47.4M |
Converting vs Non-Converting Campaigns
| Origin | Status | Campaigns | Median CPC | Median CTR | Median Clicks | Median Spend | Median Days | Median AdGroups |
|---|---|---|---|---|---|---|---|---|
| OTTO | Converting | 966 | $4.48 | 6.64% | 122 | $642 | 177 | 16 |
| OTTO | Non-Converting | 1,733 | $1.34 | 6.46% | 21 | $16 | 174 | 16 |
| Imported | Converting | 26,376 | $3.22 | 3.78% | 632 | $2,126 | 1,150 | 3 |
| Imported | Non-Converting | 30,994 | $0.00 | 3.49% | 143 | $0 | 1,217 | 2 |
Campaign Structure: AdGroups vs Performance
| Origin | AdGroup Bucket | Campaigns | Median CPC | Median CTR | Median CVR |
|---|---|---|---|---|---|
| Imported | 1 adgroup | 18,491 | $1.01 | 2.97% | 0.00% |
| Imported | 2-5 adgroups | 11,879 | $1.50 | 4.58% | 0.00% |
| Imported | 6-10 adgroups | 5,048 | $1.81 | 4.78% | 0.00% |
| Imported | 11+ adgroups | 9,621 | $2.82 | 4.71% | 0.43% |
| OTTO | 11+ adgroups | 2,614 | $2.72 | 6.50% | 0.00% |
Campaign Churn: Where Do Campaigns Die?
| Origin | Status | Campaigns | P25 Spend | Median Spend | P75 Spend | Median Days | Median Clicks |
|---|---|---|---|---|---|---|---|
| OTTO | Enabled | 6,533 | $0 | $0 | $0 | 75 | 0 |
| OTTO | Paused | 67,735 | $0 | $0 | $0 | 171 | 0 |
| OTTO | Removed | 698 | $0 | $0 | $0 | 136 | 0 |
| Imported | Enabled | 23,726 | $0 | $0 | $0 | 322 | 0 |
| Imported | Paused | 91,553 | $0 | $0 | $30 | 1,073 | 0 |
| Imported | Removed | 59,848 | $0 | $0 | $0 | 861 | 0 |
Recommendations: Priority Action Items
- Add 48-hour activation check: if impressions=0 after 48h, trigger troubleshoot workflow
- Reduce adgroup count from 16 to 1-3 per campaign — modern Google favors consolidation
- Validate Google Ads account health (payment, suspension) BEFORE campaign creation
- Check keyword search volume BEFORE building adgroups — reject zero-volume keywords
- Flag any campaign with >$500 spend + 0 conversions as "Tracking Broken"
- Auto-pause campaigns after $200 spend with 0 conversions until user verifies pixel
- Build "Conversion Health" status on campaign dashboard
- Require conversion action validation before enabling Smart Bidding
- Minimum $10/day budget per campaign (current: many at $5/day across 16 adgroups)
- Launch on Manual CPC for first 4 weeks, auto-switch to Maximize Conversions after 15+ conversions
- Monitor CPC as health indicator: <$1.50 = likely junk traffic, >$3 = likely quality
- Set minimum 100 clicks before evaluating campaign performance
- Real-time "Activation Rate" metric: % of campaigns with >1 impression in last 7 days
- "Zombie Alert" for campaigns spending but not converting
- Weekly report: campaigns created vs campaigns serving vs campaigns converting
- CPC quality score: flag campaigns buying sub-$1 traffic as potential waste
All metrics use median (P50), not average. Source: otto-ppc production database, Feb 2026. Only strategies with ≥10 campaigns shown.
CPC by Campaign Age: Maximize Conversions
Learning Phase Overshoot by Strategy
| Strategy | Origin | Early CPC | Mature CPC | Overshoot Ratio | Early N | Mature N |
|---|---|---|---|---|---|---|
| TARGET_IMPRESSION_SHARE | Imported | $19.26 | $10.49 | 1.84x | 102 | 1,393 |
| MANUAL_CPC | Imported | $5.12 | $3.19 | 1.60x | 74 | 6,400 |
| TARGET_ROAS | Imported | $1.21 | $0.82 | 1.48x | 14 | 305 |
| MAXIMIZE_CONVERSION_VALUE | Imported | $1.33 | $1.11 | 1.21x | 144 | 2,335 |
| TARGET_CPA | Imported | $6.91 | $6.45 | 1.07x | 57 | 3,831 |
| TARGET_SPEND | Imported | $2.19 | $2.18 | 1.00x | 191 | 4,136 |
| MAXIMIZE_CONVERSIONS | Imported | $2.84 | $3.27 | 0.87x | 666 | 11,270 |
CPC Distribution by Strategy (Percentiles)
| Strategy | Origin | N | P10 | P25 | P50 | P75 | P90 | P95 | IQR |
|---|---|---|---|---|---|---|---|---|---|
| TARGET_IMPRESSION_SHARE | Imported | 2,167 | $0.00 | $0.76 | $7.96 | $16.01 | $23.57 | $29.69 | $15.26 |
| TARGET_CPA | Imported | 4,286 | $0.23 | $2.35 | $5.83 | $10.20 | $17.27 | $22.94 | $7.85 |
| MAXIMIZE_CONVERSIONS | OTTO | 2,030 | $0.00 | $0.31 | $3.02 | $8.00 | $20.26 | $44.46 | $7.69 |
| TARGET_SPEND | OTTO | 422 | $0.00 | $0.00 | $2.56 | $8.71 | $17.58 | $29.44 | $8.71 |
| MAXIMIZE_CONVERSIONS | Imported | 22,951 | $0.00 | $0.00 | $1.52 | $5.50 | $14.26 | $25.87 | $5.50 |
| MAXIMIZE_CONV_VALUE | OTTO | 132 | $0.00 | $0.00 | $1.29 | $3.94 | $9.60 | $18.70 | $3.94 |
| MANUAL_CPC | Imported | 11,398 | $0.00 | $0.00 | $0.76 | $4.70 | $12.35 | $19.81 | $4.70 |
| TARGET_SPEND | Imported | 8,460 | $0.00 | $0.00 | $0.67 | $3.59 | $14.19 | $30.69 | $3.59 |
| TARGET_ROAS | Imported | 727 | $0.00 | $0.00 | $0.48 | $1.08 | $1.93 | $2.53 | $1.08 |
| MAXIMIZE_CONV_VALUE | Imported | 5,419 | $0.00 | $0.00 | $0.44 | $1.48 | $4.09 | $8.17 | $1.48 |
Head-to-Head: Strategy Performance (Imported Search Only)
| Strategy | Campaigns | Median CPC | Median CTR | Median CVR | Median CPA | Total Spend |
|---|---|---|---|---|---|---|
| MAXIMIZE_CONV_VALUE | 2,558 | $0.60 | 6.55% | 0.00% | $49.93 | $44.2M |
| TARGET_SPEND | 5,354 | $1.27 | 5.74% | 0.00% | $175.35 | $74.5M |
| MANUAL_CPC | 9,101 | $1.44 | 4.08% | 0.00% | $161.42 | $62.7M |
| MAXIMIZE_CONVERSIONS | 16,787 | $2.38 | 5.46% | 0.63% | $116.07 | $743.2M |
| TARGET_CPA | 3,806 | $6.66 | 4.23% | 0.00% | $284.60 | $8.6M |
| TARGET_IMPRESSION_SHARE | 2,167 | $7.96 | 4.18% | 0.11% | $302.01 | $30.0M |
Spend Velocity & Efficiency
| Strategy | Origin | N | $/day | Median Spend | Days Active | Conv/1k$ |
|---|---|---|---|---|---|---|
| MAXIMIZE_CONV_VALUE | Imported | 3,226 | $3.33 | $1,842 | 777 | 17.42 |
| MAXIMIZE_CONVERSIONS | OTTO | 1,550 | $2.19 | $329 | 176 | 0.03 |
| MAXIMIZE_CONVERSIONS | Imported | 16,374 | $1.92 | $1,166 | 758 | 5.14 |
| TARGET_IMPRESSION_SHARE | Imported | 1,679 | $1.67 | $1,671 | 1,296 | 1.32 |
| TARGET_SPEND | OTTO | 295 | $1.65 | $233 | 174 | 0.00 |
| TARGET_ROAS | Imported | 481 | $1.32 | $836 | 588 | 21.31 |
| MAXIMIZE_CONV_VALUE | OTTO | 84 | $1.28 | $119 | 97 | 5.86 |
| TARGET_SPEND | Imported | 5,366 | $0.75 | $575 | 974 | 1.69 |
| MANUAL_CPC | Imported | 6,864 | $0.31 | $509 | 2,313 | 1.57 |
| TARGET_CPA | Imported | 3,964 | $0.23 | $401 | 1,709 | 1.09 |
Cohort Survival Rate (Last 12 Months)
| Strategy | Origin | Started | Still Enabled | Survival % |
|---|---|---|---|---|
| TARGET_IMPRESSION_SHARE | Imported | 419 | 254 | 60.6% |
| TARGET_ROAS | Imported | 215 | 108 | 50.2% |
| TARGET_CPA | Imported | 164 | 69 | 42.1% |
| TARGET_IMPRESSION_SHARE | OTTO | 29 | 10 | 34.5% |
| TARGET_SPEND | OTTO | 863 | 242 | 28.0% |
| MAXIMIZE_CONV_VALUE | OTTO | 208 | 51 | 24.5% |
| MAXIMIZE_CONVERSIONS | OTTO | 3,066 | 733 | 23.9% |
| MAXIMIZE_CONV_VALUE | Imported | 2,202 | 364 | 16.5% |
| MAXIMIZE_CONVERSIONS | Imported | 13,175 | 1,664 | 12.6% |
| MANUAL_CPC | Imported | 1,712 | 175 | 10.2% |
| TARGET_SPEND | Imported | 5,813 | 415 | 7.1% |
| MANUAL_CPC | OTTO | 99 | 0 | 0.0% |
Strategy x Channel: Search Performance (Median)
| Strategy | Origin | N | Median CPC | Median CTR | Median CVR | Median CPA | Total Spend |
|---|---|---|---|---|---|---|---|
| TARGET_IMPRESSION_SHARE | Imported | 2,167 | $7.96 | 4.18% | 0.11% | $302.01 | $30.0M |
| TARGET_CPA | Imported | 3,806 | $6.66 | 4.23% | 0.00% | $284.60 | $8.6M |
| MAXIMIZE_CONVERSIONS | OTTO | 2,030 | $3.02 | 6.32% | 0.00% | $82.47 | $107.4M |
| TARGET_SPEND | OTTO | 422 | $2.56 | 7.14% | 0.00% | $99.80 | $0.2M |
| MAXIMIZE_CONVERSIONS | Imported | 16,787 | $2.38 | 5.46% | 0.63% | $116.07 | $743.2M |
| MAXIMIZE_CONV_VALUE | OTTO | 132 | $1.29 | 6.88% | 0.00% | $18.58 | $0.2M |
| MANUAL_CPC | Imported | 9,101 | $1.44 | 4.08% | 0.00% | $161.42 | $62.7M |
| TARGET_SPEND | Imported | 5,354 | $1.27 | 5.74% | 0.00% | $175.35 | $74.5M |
| MAXIMIZE_CONV_VALUE | Imported | 2,558 | $0.60 | 6.55% | 0.00% | $49.93 | $44.2M |
| MANUAL_CPC | OTTO | 87 | $0.57 | 7.35% | 0.00% | $44.86 | $10K |