adloop 0.8.1__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {adloop-0.8.1 → adloop-0.9.0}/PKG-INFO +27 -27
  2. {adloop-0.8.1 → adloop-0.9.0}/README.md +25 -25
  3. {adloop-0.8.1 → adloop-0.9.0}/pyproject.toml +2 -2
  4. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/__init__.py +1 -1
  5. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ads/forecast.py +27 -9
  6. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/cli.py +24 -3
  7. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/__main__.py +0 -0
  8. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/_mcp_patches.py +0 -0
  9. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ads/__init__.py +0 -0
  10. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ads/client.py +0 -0
  11. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ads/currency.py +0 -0
  12. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ads/enums.py +0 -0
  13. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ads/gaql.py +0 -0
  14. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ads/pmax.py +0 -0
  15. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ads/read.py +0 -0
  16. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ads/write.py +0 -0
  17. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/auth.py +0 -0
  18. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/bundled_credentials.json +0 -0
  19. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/config.py +0 -0
  20. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/crossref.py +0 -0
  21. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/diagnostics.py +0 -0
  22. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ga4/__init__.py +0 -0
  23. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ga4/client.py +0 -0
  24. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ga4/reports.py +0 -0
  25. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/ga4/tracking.py +0 -0
  26. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/rules/__init__.py +0 -0
  27. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/rules/adloop.md +0 -0
  28. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/rules/commands/analyze-performance.md +0 -0
  29. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/rules/commands/budget-plan.md +0 -0
  30. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/rules/commands/create-ad.md +0 -0
  31. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/rules/commands/create-campaign.md +0 -0
  32. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/rules/commands/diagnose-tracking.md +0 -0
  33. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/rules/commands/optimize-campaign.md +0 -0
  34. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/rules_install.py +0 -0
  35. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/safety/__init__.py +0 -0
  36. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/safety/audit.py +0 -0
  37. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/safety/guards.py +0 -0
  38. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/safety/preview.py +0 -0
  39. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/server.py +0 -0
  40. {adloop-0.8.1 → adloop-0.9.0}/src/adloop/tracking.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: adloop
3
- Version: 0.8.1
4
- Summary: Stop switching between Google Ads, GA4, and your code editor to figure out why conversions dropped.
3
+ Version: 0.9.0
4
+ Summary: The AI command center for Google Ads, GA4, and tracking code.
5
5
  Keywords: mcp,google-ads,google-analytics,ga4,cursor,marketing
6
6
  Author: Daniel Klose
7
7
  Author-email: Daniel Klose <info@daniel-klose.com>
@@ -26,7 +26,7 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  # AdLoop
28
28
 
29
- **Stop switching between Google Ads, GA4, and your code editor to figure out why conversions dropped.**
29
+ **The AI command center for Google Ads, GA4, and tracking code.**
30
30
 
31
31
  [![PyPI](https://img.shields.io/pypi/v/adloop.svg)](https://pypi.org/project/adloop/)
32
32
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
@@ -196,16 +196,23 @@ AdLoop manages real ad spend, so safety is not optional.
196
196
 
197
197
  ## Setup
198
198
 
199
- ### Quick Start (Recommended)
199
+ > **⚠️ Built-in OAuth credentials are temporarily unavailable while Google verification is pending.**
200
+ > Google limits unverified OAuth apps to 100 users, and AdLoop has reached that cap. New users will see a **"This app is blocked"** error if they pick the built-in option in the wizard.
201
+ >
202
+ > **What this means for you:** until Google completes verification, **bring your own Google Cloud project** — it takes ~5 minutes, has no user cap, and the `adloop init` wizard guides you through it. Status updates: [Discussion #13](https://github.com/kLOsk/adloop/discussions/13).
203
+ >
204
+ > *(Existing users whose tokens were already issued before the cap continue to work — only first-time sign-ins are blocked.)*
205
+
206
+ ### Install
200
207
 
201
- **Option A — Install from PyPI:**
208
+ **From PyPI:**
202
209
 
203
210
  ```bash
204
211
  pip install adloop
205
212
  adloop init
206
213
  ```
207
214
 
208
- **Option B — Install from source:**
215
+ **From source:**
209
216
 
210
217
  ```bash
211
218
  git clone https://github.com/kLOsk/adloop.git
@@ -214,21 +221,19 @@ uv sync
214
221
  uv run adloop init
215
222
  ```
216
223
 
217
- The `adloop init` wizard walks you through everything. AdLoop ships with built-in Google OAuth credentials, so you don't need to create a Google Cloud project.
224
+ ### What `adloop init` does
218
225
 
219
- > **⚠️ Built-in credentials temporarily unavailable Google verification pending.**
220
- > Google limits unverified OAuth apps to 100 users. AdLoop has reached that cap while awaiting Google's app verification. Until verification is complete, the built-in credentials will show a **"This app is blocked"** error for new users.
221
- >
222
- > **Workaround:** set up your own Google Cloud project using the [Advanced Setup](#advanced-setup-custom-google-cloud-project) instructions below (takes ~5 minutes). Your own project has no user cap and is the recommended setup path in the meantime.
226
+ The wizard defaults to the "bring your own Google Cloud project" path while verification is pending. It walks you through:
223
227
 
224
- The wizard:
228
+ 1. **Google Cloud setup** — creates a project, enables the three APIs, generates an OAuth client (see [Custom Google Cloud Project Setup](#custom-google-cloud-project-setup) below for the exact steps the wizard refers you to)
229
+ 2. **Developer token** — from your Google Ads MCC ([API Center](https://ads.google.com/aw/apicenter))
230
+ 3. **MCC Account ID** — your Manager Account ID (top bar in the MCC UI)
231
+ 4. **OAuth sign-in** — opens a browser to sign in with Google (or prints a URL for headless servers)
232
+ 5. **Auto-discovers your accounts** — finds your GA4 properties and Ads accounts automatically
233
+ 6. **Safety defaults** — budget cap and dry-run preference
234
+ 7. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code
225
235
 
226
- 1. **Developer token** from your Google Ads MCC ([API Center](https://ads.google.com/aw/apicenter))
227
- 2. **MCC Account ID** — your Manager Account ID (top bar in the MCC UI)
228
- 3. **OAuth sign-in** — opens a browser to sign in with Google (or prints a URL for headless servers)
229
- 4. **Auto-discovers your accounts** — finds your GA4 properties and Ads accounts automatically
230
- 5. **Safety defaults** — budget cap and dry-run preference
231
- 6. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code
236
+ The wizard does still offer AdLoop's built-in credentials as a non-default option for existing users whose tokens predate the cap. Picking that option for a brand-new Google account will fail at the consent screen — the wizard warns you about this before you choose.
232
237
 
233
238
  ### Requirements
234
239
 
@@ -258,12 +263,9 @@ A developer token is **always required** — even when using AdLoop's built-in O
258
263
 
259
264
  Running on a server without a browser (VMs, Docker, SSH)? The wizard automatically detects this and falls back to a manual flow: it prints an authorization URL you can open on any device, then you paste the redirect URL back into the terminal.
260
265
 
261
- ### Advanced Setup (Custom Google Cloud Project)
262
-
263
- <details>
264
- <summary>Click to expand — only needed if you want to use your own GCP project instead of AdLoop's built-in credentials</summary>
266
+ ### Custom Google Cloud Project Setup
265
267
 
266
- When you run `adloop init`, choose "No" when asked about built-in credentials. The wizard will guide you through:
268
+ This is the default path while built-in credentials are blocked by Google's 100-user cap. The wizard refers to these steps — do them in your browser before running `adloop init` (or while it waits at the OAuth prompt).
267
269
 
268
270
  #### Step 1 — Google Cloud Project
269
271
 
@@ -335,8 +337,6 @@ If you'd rather manage things by hand instead, copy `.claude/rules/adloop.md` an
335
337
 
336
338
  **Claude Desktop / claude.ai** has no programmatic rules location. Run `adloop install-rules` and it will print the rules content for you to paste into Project settings → Custom instructions on claude.ai.
337
339
 
338
- </details>
339
-
340
340
  ### Use It
341
341
 
342
342
  Ask your AI assistant things like:
@@ -411,7 +411,7 @@ What's been shipped and what's next:
411
411
  - ~~Claude Code support~~ ✓ — `CLAUDE.md`, `.mcp.json`, `.claude/rules/`, `.claude/commands/`, CLI wizard snippets
412
412
  - **Claude Desktop one-click install** — `adloop install claude-desktop` (and/or a `.dxt` extension bundle) that writes the AdLoop MCP entry into `claude_desktop_config.json` automatically, so Claude Desktop + Cowork users don't have to hand-edit JSON
413
413
  - ~~PyPI package~~ ✓ — `pip install adloop`
414
- - ~~Bundled OAuth credentials~~ ✓ — no Google Cloud project required, auto-discovery of GA4/Ads accounts (currently capped at 100 users pending Google verification use [Advanced Setup](#advanced-setup-custom-google-cloud-project) in the meantime)
414
+ - ~~Bundled OAuth credentials~~ ✓ — no Google Cloud project required (**currently blocked at 100-user cap** pending Google verification; `adloop init` defaults to the [Custom Google Cloud Project Setup](#custom-google-cloud-project-setup) path until verification completes)
415
415
  - ~~Headless server support~~ ✓ — manual URL copy-paste flow for servers without a browser
416
416
  - ~~Behavioral eval suites~~ ✓ — 28 prompt-and-expectation tests covering read, write, tracking, and planning workflows
417
417
  - **Community launch** — HN, Indie Hackers, r/cursor, Twitter
@@ -433,7 +433,7 @@ AdLoop runs entirely on your machine. No data is collected, stored, or transmitt
433
433
 
434
434
  <div align="center">
435
435
 
436
- **If AdLoop saves you from switching between Google Ads, GA4, and your code editor — [give it a star](https://github.com/kLOsk/adloop).**
436
+ **If AdLoop helps you run Google Ads, GA4, and tracking code from one place — [give it a star](https://github.com/kLOsk/adloop).**
437
437
 
438
438
  Made by [@kLOsk](https://github.com/kLOsk) | [Privacy Policy](PRIVACY.md)
439
439
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # AdLoop
4
4
 
5
- **Stop switching between Google Ads, GA4, and your code editor to figure out why conversions dropped.**
5
+ **The AI command center for Google Ads, GA4, and tracking code.**
6
6
 
7
7
  [![PyPI](https://img.shields.io/pypi/v/adloop.svg)](https://pypi.org/project/adloop/)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
@@ -172,16 +172,23 @@ AdLoop manages real ad spend, so safety is not optional.
172
172
 
173
173
  ## Setup
174
174
 
175
- ### Quick Start (Recommended)
175
+ > **⚠️ Built-in OAuth credentials are temporarily unavailable while Google verification is pending.**
176
+ > Google limits unverified OAuth apps to 100 users, and AdLoop has reached that cap. New users will see a **"This app is blocked"** error if they pick the built-in option in the wizard.
177
+ >
178
+ > **What this means for you:** until Google completes verification, **bring your own Google Cloud project** — it takes ~5 minutes, has no user cap, and the `adloop init` wizard guides you through it. Status updates: [Discussion #13](https://github.com/kLOsk/adloop/discussions/13).
179
+ >
180
+ > *(Existing users whose tokens were already issued before the cap continue to work — only first-time sign-ins are blocked.)*
181
+
182
+ ### Install
176
183
 
177
- **Option A — Install from PyPI:**
184
+ **From PyPI:**
178
185
 
179
186
  ```bash
180
187
  pip install adloop
181
188
  adloop init
182
189
  ```
183
190
 
184
- **Option B — Install from source:**
191
+ **From source:**
185
192
 
186
193
  ```bash
187
194
  git clone https://github.com/kLOsk/adloop.git
@@ -190,21 +197,19 @@ uv sync
190
197
  uv run adloop init
191
198
  ```
192
199
 
193
- The `adloop init` wizard walks you through everything. AdLoop ships with built-in Google OAuth credentials, so you don't need to create a Google Cloud project.
200
+ ### What `adloop init` does
194
201
 
195
- > **⚠️ Built-in credentials temporarily unavailable Google verification pending.**
196
- > Google limits unverified OAuth apps to 100 users. AdLoop has reached that cap while awaiting Google's app verification. Until verification is complete, the built-in credentials will show a **"This app is blocked"** error for new users.
197
- >
198
- > **Workaround:** set up your own Google Cloud project using the [Advanced Setup](#advanced-setup-custom-google-cloud-project) instructions below (takes ~5 minutes). Your own project has no user cap and is the recommended setup path in the meantime.
202
+ The wizard defaults to the "bring your own Google Cloud project" path while verification is pending. It walks you through:
199
203
 
200
- The wizard:
204
+ 1. **Google Cloud setup** — creates a project, enables the three APIs, generates an OAuth client (see [Custom Google Cloud Project Setup](#custom-google-cloud-project-setup) below for the exact steps the wizard refers you to)
205
+ 2. **Developer token** — from your Google Ads MCC ([API Center](https://ads.google.com/aw/apicenter))
206
+ 3. **MCC Account ID** — your Manager Account ID (top bar in the MCC UI)
207
+ 4. **OAuth sign-in** — opens a browser to sign in with Google (or prints a URL for headless servers)
208
+ 5. **Auto-discovers your accounts** — finds your GA4 properties and Ads accounts automatically
209
+ 6. **Safety defaults** — budget cap and dry-run preference
210
+ 7. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code
201
211
 
202
- 1. **Developer token** from your Google Ads MCC ([API Center](https://ads.google.com/aw/apicenter))
203
- 2. **MCC Account ID** — your Manager Account ID (top bar in the MCC UI)
204
- 3. **OAuth sign-in** — opens a browser to sign in with Google (or prints a URL for headless servers)
205
- 4. **Auto-discovers your accounts** — finds your GA4 properties and Ads accounts automatically
206
- 5. **Safety defaults** — budget cap and dry-run preference
207
- 6. **Editor config snippets** — prints MCP configuration for both Cursor and Claude Code
212
+ The wizard does still offer AdLoop's built-in credentials as a non-default option for existing users whose tokens predate the cap. Picking that option for a brand-new Google account will fail at the consent screen — the wizard warns you about this before you choose.
208
213
 
209
214
  ### Requirements
210
215
 
@@ -234,12 +239,9 @@ A developer token is **always required** — even when using AdLoop's built-in O
234
239
 
235
240
  Running on a server without a browser (VMs, Docker, SSH)? The wizard automatically detects this and falls back to a manual flow: it prints an authorization URL you can open on any device, then you paste the redirect URL back into the terminal.
236
241
 
237
- ### Advanced Setup (Custom Google Cloud Project)
238
-
239
- <details>
240
- <summary>Click to expand — only needed if you want to use your own GCP project instead of AdLoop's built-in credentials</summary>
242
+ ### Custom Google Cloud Project Setup
241
243
 
242
- When you run `adloop init`, choose "No" when asked about built-in credentials. The wizard will guide you through:
244
+ This is the default path while built-in credentials are blocked by Google's 100-user cap. The wizard refers to these steps — do them in your browser before running `adloop init` (or while it waits at the OAuth prompt).
243
245
 
244
246
  #### Step 1 — Google Cloud Project
245
247
 
@@ -311,8 +313,6 @@ If you'd rather manage things by hand instead, copy `.claude/rules/adloop.md` an
311
313
 
312
314
  **Claude Desktop / claude.ai** has no programmatic rules location. Run `adloop install-rules` and it will print the rules content for you to paste into Project settings → Custom instructions on claude.ai.
313
315
 
314
- </details>
315
-
316
316
  ### Use It
317
317
 
318
318
  Ask your AI assistant things like:
@@ -387,7 +387,7 @@ What's been shipped and what's next:
387
387
  - ~~Claude Code support~~ ✓ — `CLAUDE.md`, `.mcp.json`, `.claude/rules/`, `.claude/commands/`, CLI wizard snippets
388
388
  - **Claude Desktop one-click install** — `adloop install claude-desktop` (and/or a `.dxt` extension bundle) that writes the AdLoop MCP entry into `claude_desktop_config.json` automatically, so Claude Desktop + Cowork users don't have to hand-edit JSON
389
389
  - ~~PyPI package~~ ✓ — `pip install adloop`
390
- - ~~Bundled OAuth credentials~~ ✓ — no Google Cloud project required, auto-discovery of GA4/Ads accounts (currently capped at 100 users pending Google verification use [Advanced Setup](#advanced-setup-custom-google-cloud-project) in the meantime)
390
+ - ~~Bundled OAuth credentials~~ ✓ — no Google Cloud project required (**currently blocked at 100-user cap** pending Google verification; `adloop init` defaults to the [Custom Google Cloud Project Setup](#custom-google-cloud-project-setup) path until verification completes)
391
391
  - ~~Headless server support~~ ✓ — manual URL copy-paste flow for servers without a browser
392
392
  - ~~Behavioral eval suites~~ ✓ — 28 prompt-and-expectation tests covering read, write, tracking, and planning workflows
393
393
  - **Community launch** — HN, Indie Hackers, r/cursor, Twitter
@@ -409,7 +409,7 @@ AdLoop runs entirely on your machine. No data is collected, stored, or transmitt
409
409
 
410
410
  <div align="center">
411
411
 
412
- **If AdLoop saves you from switching between Google Ads, GA4, and your code editor — [give it a star](https://github.com/kLOsk/adloop).**
412
+ **If AdLoop helps you run Google Ads, GA4, and tracking code from one place — [give it a star](https://github.com/kLOsk/adloop).**
413
413
 
414
414
  Made by [@kLOsk](https://github.com/kLOsk) | [Privacy Policy](PRIVACY.md)
415
415
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "adloop"
3
- version = "0.8.1"
4
- description = "Stop switching between Google Ads, GA4, and your code editor to figure out why conversions dropped."
3
+ version = "0.9.0"
4
+ description = "The AI command center for Google Ads, GA4, and tracking code."
5
5
  readme = "README.md"
6
6
  authors = [
7
7
  { name = "Daniel Klose", email = "info@daniel-klose.com" }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import sys
4
4
 
5
- __version__ = "0.8.1"
5
+ __version__ = "0.9.0"
6
6
 
7
7
 
8
8
  def main() -> None:
@@ -98,30 +98,43 @@ def estimate_budget(
98
98
  response = kp_service.generate_keyword_forecast_metrics(request=request)
99
99
  metrics = response.campaign_forecast_metrics
100
100
 
101
+ # KeywordForecastMetrics fields are ``optional`` in v23, so the SDK
102
+ # returns ``None`` for unset fields and the actual integer (including
103
+ # 0) otherwise. Falsy checks like ``int(v) if v else None`` would
104
+ # silently map a real 0-click or 0-cost forecast to None and the
105
+ # caller couldn't tell "no data" apart from "data says zero" — same
106
+ # bug class as discover_keywords (issue Bug 2). Use ``is not None``
107
+ # throughout and the shared ``_micros_to_currency`` helper for the
108
+ # micros→currency conversions.
101
109
  clicks = getattr(metrics, "clicks", None)
102
110
  impressions = getattr(metrics, "impressions", None)
103
111
  avg_cpc_micros = getattr(metrics, "average_cpc_micros", None)
104
112
  cost_micros = getattr(metrics, "cost_micros", None)
105
113
  ctr = getattr(metrics, "click_through_rate", None)
106
114
 
107
- total_cost = round(cost_micros / 1_000_000, 2) if cost_micros else None
108
- avg_cpc = round(avg_cpc_micros / 1_000_000, 2) if avg_cpc_micros else None
115
+ total_cost = _micros_to_currency(cost_micros)
116
+ avg_cpc = _micros_to_currency(avg_cpc_micros)
109
117
 
110
118
  days = max(forecast_days, 1)
111
119
  daily = {
112
- "clicks": round(clicks / days, 1) if clicks else None,
113
- "impressions": round(impressions / days, 1) if impressions else None,
114
- "cost": round(total_cost / days, 2) if total_cost else None,
120
+ "clicks": round(clicks / days, 1) if clicks is not None else None,
121
+ "impressions": (
122
+ round(impressions / days, 1) if impressions is not None else None
123
+ ),
124
+ "cost": round(total_cost / days, 2) if total_cost is not None else None,
115
125
  }
116
126
 
117
127
  insights = []
128
+ # Only emit the headline insight when there's actually something to
129
+ # report (real clicks AND real cost) — a zero-click forecast gets its
130
+ # own dedicated insight below, and a None forecast shouldn't trigger
131
+ # either path.
118
132
  if total_cost is not None and clicks is not None and clicks > 0:
119
- effective_cpa_budget = total_cost / clicks * 10
120
133
  insights.append(
121
134
  f"Estimated {clicks:.0f} clicks over {forecast_days} days at "
122
135
  f"~{avg_cpc} avg CPC. Total estimated cost: {total_cost:.2f}."
123
136
  )
124
- if daily_budget > 0 and daily["cost"] is not None:
137
+ if daily_budget > 0 and daily["cost"] is not None and daily["cost"] > 0:
125
138
  if daily_budget < daily["cost"]:
126
139
  capture_pct = round(daily_budget / daily["cost"] * 100)
127
140
  insights.append(
@@ -134,7 +147,12 @@ def estimate_budget(
134
147
  f"most available traffic (estimated daily cost: {daily['cost']:.2f})."
135
148
  )
136
149
 
137
- if impressions is not None and clicks is not None and impressions > 0 and clicks == 0:
150
+ if (
151
+ impressions is not None
152
+ and clicks is not None
153
+ and impressions > 0
154
+ and clicks == 0
155
+ ):
138
156
  insights.append(
139
157
  "Forecast shows impressions but zero clicks — keywords may be too "
140
158
  "generic or CPCs too low for competitive positions."
@@ -149,7 +167,7 @@ def estimate_budget(
149
167
  "estimated_impressions": impressions,
150
168
  "estimated_cost": total_cost,
151
169
  "estimated_avg_cpc": avg_cpc,
152
- "estimated_ctr": round(ctr, 4) if ctr else None,
170
+ "estimated_ctr": round(ctr, 4) if ctr is not None else None,
153
171
  "daily_estimates": daily,
154
172
  "keywords_used": len([kw for kw in keywords if kw.get("text")]),
155
173
  "insights": insights,
@@ -319,12 +319,33 @@ def run_init_wizard() -> None:
319
319
  return fallback
320
320
 
321
321
  # Step 1: Credentials mode
322
+ #
323
+ # We used to default to AdLoop's bundled OAuth credentials (zero-GCP
324
+ # setup, "Use built-in credentials? (recommended)" Y/N with Y as default).
325
+ # That stopped being safe once Google's 100-user cap on unverified OAuth
326
+ # apps was reached — new users picking the bundled option now hit a
327
+ # "This app is blocked" error at the consent screen, leaving them
328
+ # stranded mid-wizard with no path forward.
329
+ #
330
+ # Until Google completes verification, the wizard defaults to the
331
+ # "bring your own Google Cloud project" path (which has no cap and
332
+ # works immediately) and only offers the bundled path as an opt-in
333
+ # for existing users whose tokens predate the cap. We surface the
334
+ # status explicitly so people understand WHY the default flipped
335
+ # rather than wondering whether the wizard is broken.
322
336
  _step_header(1, "Google Credentials")
323
- _print(" AdLoop includes built-in Google OAuth credentials so you")
324
- _print(" don't need to create your own Google Cloud project.")
337
+ _print(" AdLoop's built-in Google OAuth is currently blocked at")
338
+ _print(" Google's 100-user cap (pending OAuth verification).")
339
+ _print(" New sign-ins will fail with \"This app is blocked\".")
340
+ _print(" Status: https://github.com/kLOsk/adloop/discussions/13")
341
+ _print()
342
+ _print(" The wizard will set you up with your own Google Cloud")
343
+ _print(" project instead — takes ~5 minutes, no cap, works today.")
325
344
  _print()
326
345
  use_bundled = _prompt_bool(
327
- "Use built-in credentials? (recommended)", default=True
346
+ "Use built-in credentials anyway? (only works if you already "
347
+ "authorized AdLoop before the cap)",
348
+ default=False,
328
349
  )
329
350
 
330
351
  credentials_path = ""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes