openppc 0.1.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.
- openppc-0.1.0/.gitignore +52 -0
- openppc-0.1.0/LICENSE +21 -0
- openppc-0.1.0/PKG-INFO +224 -0
- openppc-0.1.0/README.md +198 -0
- openppc-0.1.0/examples/account_acme.json +19 -0
- openppc-0.1.0/examples/account_structure_acme.json +943 -0
- openppc-0.1.0/examples/ai_audit_sample.md +19 -0
- openppc-0.1.0/examples/keywords_acme.csv +15 -0
- openppc-0.1.0/examples/make_samples.py +121 -0
- openppc-0.1.0/examples/search_terms_acme.csv +23 -0
- openppc-0.1.0/openppc/__init__.py +3 -0
- openppc-0.1.0/openppc/__main__.py +3 -0
- openppc-0.1.0/openppc/checkfacts.py +96 -0
- openppc-0.1.0/openppc/cli.py +78 -0
- openppc-0.1.0/openppc/engine/__init__.py +1 -0
- openppc-0.1.0/openppc/engine/benchmarks.py +94 -0
- openppc-0.1.0/openppc/engine/findings.py +223 -0
- openppc-0.1.0/openppc/engine/trace.py +920 -0
- openppc-0.1.0/openppc/engine/waste_model.py +122 -0
- openppc-0.1.0/openppc/facts.py +49 -0
- openppc-0.1.0/openppc/ingest/__init__.py +3 -0
- openppc-0.1.0/openppc/ingest/account.py +126 -0
- openppc-0.1.0/openppc/ingest/google_ads_csv.py +184 -0
- openppc-0.1.0/openppc/mcp_server.py +144 -0
- openppc-0.1.0/openppc/templates/__init__.py +41 -0
- openppc-0.1.0/openppc/templates/_common.py +134 -0
- openppc-0.1.0/openppc/templates/account_read.py +124 -0
- openppc-0.1.0/openppc/templates/account_structure.py +466 -0
- openppc-0.1.0/openppc/templates/keyword_audit.py +134 -0
- openppc-0.1.0/openppc/templates/search_term_waste.py +685 -0
- openppc-0.1.0/openppc/webapi.py +157 -0
- openppc-0.1.0/pyproject.toml +48 -0
- openppc-0.1.0/rulebook/README.md +23 -0
- openppc-0.1.0/rulebook/metrics.csv +26 -0
- openppc-0.1.0/rulebook/rules.csv +141 -0
- openppc-0.1.0/rulebook/sources.csv +75 -0
- openppc-0.1.0/rulebook/taxonomy.csv +10 -0
openppc-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Keys and secrets (never commit)
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
Keys*.json
|
|
6
|
+
*.pem
|
|
7
|
+
*.key
|
|
8
|
+
credentials*.json
|
|
9
|
+
token*.json
|
|
10
|
+
client_secret*.json
|
|
11
|
+
service-account*.json
|
|
12
|
+
|
|
13
|
+
# Installed packages
|
|
14
|
+
node_modules/
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.pyc
|
|
19
|
+
|
|
20
|
+
# Build output and caches
|
|
21
|
+
dist/
|
|
22
|
+
build/
|
|
23
|
+
.next/
|
|
24
|
+
.turbo/
|
|
25
|
+
.vercel/
|
|
26
|
+
.cache/
|
|
27
|
+
coverage/
|
|
28
|
+
*.log
|
|
29
|
+
|
|
30
|
+
# OS and editor junk
|
|
31
|
+
.DS_Store
|
|
32
|
+
._*
|
|
33
|
+
Thumbs.db
|
|
34
|
+
.idea/
|
|
35
|
+
|
|
36
|
+
# Python
|
|
37
|
+
__pycache__/
|
|
38
|
+
*.py[cod]
|
|
39
|
+
.venv/
|
|
40
|
+
*.egg-info/
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
dist/
|
|
43
|
+
build/
|
|
44
|
+
|
|
45
|
+
# Real account exports never enter this repo. Put them in exports/ (ignored).
|
|
46
|
+
exports/
|
|
47
|
+
private/
|
|
48
|
+
*.real.csv
|
|
49
|
+
|
|
50
|
+
# Local editor and preview settings
|
|
51
|
+
.claude/
|
|
52
|
+
.code-review-graph/
|
openppc-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shivendra Rawat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
openppc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: openppc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read-only Google Ads audits where every number is traced back to your own export.
|
|
5
|
+
Project-URL: Homepage, https://openppc.si
|
|
6
|
+
Project-URL: Documentation, https://openppc.si/docs/
|
|
7
|
+
Project-URL: Repository, https://github.com/secondsteplabs/openppc
|
|
8
|
+
Project-URL: Issues, https://github.com/secondsteplabs/openppc/issues
|
|
9
|
+
Author: Shivendra Rawat
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: audit,fact-check,google-ads,mcp,ppc
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Office/Business
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
23
|
+
Provides-Extra: mcp
|
|
24
|
+
Requires-Dist: mcp>=1.2; extra == 'mcp'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
<h1 align="center">
|
|
28
|
+
<picture>
|
|
29
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/secondsteplabs/openppc/main/brand/png/openppc-logo-reverse-1600.png">
|
|
30
|
+
<img alt="OpenPPC" src="https://raw.githubusercontent.com/secondsteplabs/openppc/main/brand/png/openppc-logo-1600.png" width="340">
|
|
31
|
+
</picture>
|
|
32
|
+
</h1>
|
|
33
|
+
|
|
34
|
+
Read-only Google Ads audits where every number is traced back to your own export. For PPC freelancers and agencies who use AI on client accounts but won't hand it the keys, or trust its math.
|
|
35
|
+
|
|
36
|
+
## Why
|
|
37
|
+
|
|
38
|
+
AI will happily audit a Google Ads account. When we tested language models on real accounts, they read trends backwards and invented dollar figures, even after fine-tuning. So this tool splits the job: code computes every number, and a checker traces each figure in the report back to your data. It never connects to your account, never changes anything, and makes no network calls.
|
|
39
|
+
|
|
40
|
+
## Two things it does
|
|
41
|
+
|
|
42
|
+
**Audit.** Run a template on a report you exported from Google Ads. You get a markdown report where every figure is computed from your file, and the report checks itself before you see it.
|
|
43
|
+
|
|
44
|
+
**Check.** Point it at any audit, from ChatGPT, Claude, a colleague or another tool, plus your export. Every number comes back marked:
|
|
45
|
+
|
|
46
|
+
- **traced**: it matches your data, at the precision it was written
|
|
47
|
+
- **mismatch**: right number, wrong metric or row ("22 conversions" when 22 is that search term's clicks)
|
|
48
|
+
- **not in data**: nothing in your files produces it, and the flag says what the figure really is ("no: the cost of
|
|
49
|
+
'pipe repair' is 296.40")
|
|
50
|
+
- **can't check**: a target, threshold, forecast or what-if, or the audit's own working over rows an export can't
|
|
51
|
+
rebuild (brand against non-brand, "the other $3,690"). Listed apart with a prompt to ask for the working, never
|
|
52
|
+
counted against the audit
|
|
53
|
+
|
|
54
|
+
A number is only flagged when the checker knows what it claims to be: a row or match type the text names, the rows it
|
|
55
|
+
names together, the terms that never converted, or the whole account. Anything else it can't confirm is "can't check",
|
|
56
|
+
because calling the audit's own arithmetic wrong would be a guess.
|
|
57
|
+
|
|
58
|
+
Besides each row and the account totals, it checks what audits usually work out: the sums, rates and shares of the
|
|
59
|
+
rows a sentence names together, of a match type ("broad", "exact/phrase") and of the rows under a heading. It also
|
|
60
|
+
flags sentences that contradict their own numbers ("fell from 9% to 14%").
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/secondsteplabs/openppc
|
|
66
|
+
cd openppc
|
|
67
|
+
uv venv && uv pip install -e .
|
|
68
|
+
|
|
69
|
+
openppc audit search-term-waste examples/search_terms_acme.csv --industry home-services
|
|
70
|
+
openppc check --audit examples/ai_audit_sample.md --data examples/search_terms_acme.csv
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The examples are synthetic, so this runs with no account and no API keys. For your own data, export a report from Google Ads and put it in `exports/`, which git ignores.
|
|
74
|
+
|
|
75
|
+
## Run it in your browser
|
|
76
|
+
|
|
77
|
+
The web app in `web/` runs the same engine inside the browser tab (Python compiled to WebAssembly with [Pyodide](https://pyodide.org)). Your export and the audit stay on your machine: the page's security policy only lets it download its runtime, libraries and fonts from one pinned CDN (each checked against a fingerprint), so it has no way to send your files anywhere.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python3 -m http.server 8765 --directory web
|
|
81
|
+
# open http://localhost:8765 and click "Try the sample account"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
What it has:
|
|
85
|
+
|
|
86
|
+
- **Check**: paste any AI's audit next to the export it was written from, and see each number traced, mismatched or not in your data.
|
|
87
|
+
- **Audit**: run a free template on your export. The search-term waste audit comes back as cards: the headline numbers, what to do, and the wasted terms with the chance each one is bad, ready to copy as exact-match negatives or download as a .csv.
|
|
88
|
+
- **Templates**: every free template, with a link to its code.
|
|
89
|
+
- **Branded PDF**: a client-ready report under your agency's name, logo and brand color: a cover with the headline, then what we found, what we recommend, how the account compares with its industry, and how the report was made (the export's name, dates and SHA-256 fingerprint). Pages are laid out at Letter or A4 size exactly as they print: a block that does not fit moves to the next page, and long tables continue there under their own header, so nothing is cut. Before you can save, the checker reads the text of every page and confirms each number traces to the export. Save it from the browser's print dialog (Chrome and Edge keep the layout exactly). The logo stays in the browser.
|
|
90
|
+
- **Rules** (coming soon): turn a rulebook rule into a Google Ads Script you install yourself. Nothing is built for it yet.
|
|
91
|
+
|
|
92
|
+
The first visit downloads about 12 MB of runtime, which your browser then keeps. After changing anything in `openppc/` or `examples/`, rebuild the bundle the page loads (a test fails if you forget):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python tools/build_web.py
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Website
|
|
99
|
+
|
|
100
|
+
The site at [openppc.si](https://openppc.si) is built from `site/` together with the app, which it serves at `/app/`:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
python tools/build_site.py
|
|
104
|
+
python3 -m http.server 8766 --directory dist
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Pages are HTML fragments in `site/pages/` wrapped in `site/layout.html`. The build fails on any broken internal link, and the pages load nothing from other sites.
|
|
108
|
+
|
|
109
|
+
## Templates
|
|
110
|
+
|
|
111
|
+
| Template | Reads | You get |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| `search-term-waste` | Search terms report (.csv) | Zero-conversion search terms ranked by cost (Search and Performance Max apart), each with the chance it is genuinely bad, the words behind the long tail, expensive converters, terms worth adding as keywords |
|
|
114
|
+
| `keyword-audit` | Keywords report (.csv) | Keywords spending without converting, budget by match type, spend on low Quality Scores |
|
|
115
|
+
| `account-structure` | Account snapshot (.json); Google Ads Editor export next | How the account is built: broad match without Smart Bidding, brand mixed with non-brand, duplicate keywords, negatives that block your own keywords, thin or pinned ads, missing sitelinks, retired bidding. Where Google and practitioners disagree, it shows both |
|
|
116
|
+
| `account-read` | Two-period totals (.json) | What changed and why it matters: efficiency win, over-expansion, broken tracking and more |
|
|
117
|
+
|
|
118
|
+
Add `--industry` to compare against published industry averages. `openppc industries` lists them.
|
|
119
|
+
|
|
120
|
+
Add `--brand "Your Brand, Short Name"` so brand searches, and close misspellings of them, are never flagged as waste or suggested as negatives:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
openppc audit search-term-waste exports/search_terms.csv --brand "Acme Plumbing, Acme"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Premium templates are planned as a paid add-on: a full account audit, Performance Max, a branded client-ready PDF, and multi-account triage. Everything in this repository stays free under the MIT license.
|
|
127
|
+
|
|
128
|
+
## Use it inside Claude, Cursor and ChatGPT
|
|
129
|
+
|
|
130
|
+
OpenPPC is an MCP server: your assistant writes the words, and OpenPPC computes and checks the numbers.
|
|
131
|
+
|
|
132
|
+
**On your computer** (Claude Code, Claude Desktop, Cursor). It reads your exports where they are:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
claude mcp add openppc -- uvx --from "openppc[mcp] @ git+https://github.com/secondsteplabs/openppc" openppc-mcp
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For Claude Desktop or Cursor, add the same server to `claude_desktop_config.json` or `~/.cursor/mcp.json`:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{ "mcpServers": { "openppc": { "command": "uvx", "args": ["--from", "openppc[mcp] @ git+https://github.com/secondsteplabs/openppc", "openppc-mcp"] } } }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
OpenPPC isn't on PyPI yet, so these commands fetch it straight from GitHub.
|
|
145
|
+
|
|
146
|
+
**As a web connector** (ChatGPT, claude.ai), over streamable HTTP at `/mcp`:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
openppc-mcp --http --host 0.0.0.0 --port 8000
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Put it behind HTTPS and add `https://your-host/mcp` in ChatGPT (Developer mode) or claude.ai (Connectors). A hosted connector at `https://mcp.openppc.si/mcp` is coming soon. The connector's tools take the export's contents instead of a path, work in a temporary folder that is deleted when the call ends, and keep nothing. To keep an export on your computer while using ChatGPT, run OpenPPC locally and connect it through OpenAI's Secure MCP Tunnel.
|
|
153
|
+
|
|
154
|
+
Every tool is read-only: `list_templates` everywhere; `audit_account` and `check_numbers` on your computer; `audit_export` and `check_audit` on the connector. From a clone, `uv pip install -e ".[mcp]"` and then `claude mcp add openppc -- openppc-mcp`.
|
|
155
|
+
|
|
156
|
+
## How it works
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
your export (.csv)
|
|
160
|
+
-> parse skips Google's title, date and Total lines; recomputes every total from the rows
|
|
161
|
+
-> template computes each figure in code and registers it as a labeled fact
|
|
162
|
+
-> report prints registered facts only
|
|
163
|
+
-> check traces every number in the report back to a fact before you see it
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The checker compares numbers at the precision they were written: "$1.4k" matches 1,361.04 because both round to 1,400, and "$1,500" does not. It reads the words around a number, and the table column it sits in, to catch a right number attached to the wrong metric or row.
|
|
167
|
+
|
|
168
|
+
## How sure is a waste flag?
|
|
169
|
+
|
|
170
|
+
A search term with no conversions might be bad, or just unlucky. A fixed rule ("no conversions after $20") can't tell the difference. OpenPPC's waste model learns how much your account's search terms differ in conversion rate, then gives each term the chance that it truly converts at less than half your account's rate. Only terms at 90% or more are marked as negatives to add; the rest are too early to judge, and the words they share are listed instead.
|
|
171
|
+
|
|
172
|
+
We backtested this on live accounts, excluding terms that people had already added as negatives. Terms the model was 90% sure about kept converting at a small fraction of their account's rate over the next five months. Terms flagged by a fixed dollar threshold went on to convert at close to the normal rate. The sample is still small, and the method is in `openppc/engine/waste_model.py`.
|
|
173
|
+
|
|
174
|
+
## The rulebook
|
|
175
|
+
|
|
176
|
+
Every rule the tool applies is a row in [`rulebook/rules.csv`](https://github.com/secondsteplabs/openppc/blob/main/rulebook/rules.csv): what it checks, the formula, the threshold, where the idea comes from (Google's own guidance, practitioners, or our reasoning) and the line of code that runs it. A test fails if a threshold in the code and its row ever disagree, so the rulebook is always what the tool actually does. Planned rules sit in the same file. Argue with any of them in an issue or a pull request.
|
|
177
|
+
|
|
178
|
+
## What it is not
|
|
179
|
+
|
|
180
|
+
- Not connected to your account. It reads a file you exported.
|
|
181
|
+
- Not an autopilot. It never changes a campaign.
|
|
182
|
+
- Not a strategy checker. It checks numbers and their direction, not whether the advice is good.
|
|
183
|
+
|
|
184
|
+
## Known limits
|
|
185
|
+
|
|
186
|
+
- With thousands of figures in a file, a number can match one by coincidence. Every trace names the fact it matched, so read the "traced to" column instead of just counting.
|
|
187
|
+
- "Not in data" means not in the files you provided. The figure may come from another report or date range.
|
|
188
|
+
- Benchmarks are the public WordStream / LocaliQ 2026 US averages: a ballpark, not a target.
|
|
189
|
+
- Google renames export columns from time to time. If a file won't load, open an issue with its header row.
|
|
190
|
+
|
|
191
|
+
## Roadmap
|
|
192
|
+
|
|
193
|
+
- An optional writer that turns the facts into prose with your own model key, while every number still comes from code
|
|
194
|
+
- A public eval: can an AI read a Google Ads account correctly?
|
|
195
|
+
- More exports: campaigns, Performance Max, Search Console, Meta
|
|
196
|
+
|
|
197
|
+
## Configuration
|
|
198
|
+
|
|
199
|
+
None. No API keys, no `.env`.
|
|
200
|
+
|
|
201
|
+
## Contributing
|
|
202
|
+
|
|
203
|
+
Issues and pull requests are welcome. Read [CONTRIBUTING.md](https://github.com/secondsteplabs/openppc/blob/main/CONTRIBUTING.md) first, and never attach a client's export.
|
|
204
|
+
|
|
205
|
+
## Security
|
|
206
|
+
|
|
207
|
+
To report a vulnerability, see [SECURITY.md](https://github.com/secondsteplabs/openppc/blob/main/SECURITY.md).
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT. See [LICENSE](https://github.com/secondsteplabs/openppc/blob/main/LICENSE).
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
<p align="center">
|
|
216
|
+
<a href="https://github.com/secondsteplabs">
|
|
217
|
+
<picture>
|
|
218
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/secondsteplabs/.github/raw/main/assets/logo-labs-horizontal-dark.png">
|
|
219
|
+
<img alt="labs.secondstep" src="https://github.com/secondsteplabs/.github/raw/main/assets/logo-labs-horizontal-light.png" width="200">
|
|
220
|
+
</picture>
|
|
221
|
+
</a>
|
|
222
|
+
</p>
|
|
223
|
+
|
|
224
|
+
Built by [labs.secondstep](https://github.com/secondsteplabs), the open-source side of [Second Step](https://getsecondstep.com).
|
openppc-0.1.0/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/secondsteplabs/openppc/main/brand/png/openppc-logo-reverse-1600.png">
|
|
4
|
+
<img alt="OpenPPC" src="https://raw.githubusercontent.com/secondsteplabs/openppc/main/brand/png/openppc-logo-1600.png" width="340">
|
|
5
|
+
</picture>
|
|
6
|
+
</h1>
|
|
7
|
+
|
|
8
|
+
Read-only Google Ads audits where every number is traced back to your own export. For PPC freelancers and agencies who use AI on client accounts but won't hand it the keys, or trust its math.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
AI will happily audit a Google Ads account. When we tested language models on real accounts, they read trends backwards and invented dollar figures, even after fine-tuning. So this tool splits the job: code computes every number, and a checker traces each figure in the report back to your data. It never connects to your account, never changes anything, and makes no network calls.
|
|
13
|
+
|
|
14
|
+
## Two things it does
|
|
15
|
+
|
|
16
|
+
**Audit.** Run a template on a report you exported from Google Ads. You get a markdown report where every figure is computed from your file, and the report checks itself before you see it.
|
|
17
|
+
|
|
18
|
+
**Check.** Point it at any audit, from ChatGPT, Claude, a colleague or another tool, plus your export. Every number comes back marked:
|
|
19
|
+
|
|
20
|
+
- **traced**: it matches your data, at the precision it was written
|
|
21
|
+
- **mismatch**: right number, wrong metric or row ("22 conversions" when 22 is that search term's clicks)
|
|
22
|
+
- **not in data**: nothing in your files produces it, and the flag says what the figure really is ("no: the cost of
|
|
23
|
+
'pipe repair' is 296.40")
|
|
24
|
+
- **can't check**: a target, threshold, forecast or what-if, or the audit's own working over rows an export can't
|
|
25
|
+
rebuild (brand against non-brand, "the other $3,690"). Listed apart with a prompt to ask for the working, never
|
|
26
|
+
counted against the audit
|
|
27
|
+
|
|
28
|
+
A number is only flagged when the checker knows what it claims to be: a row or match type the text names, the rows it
|
|
29
|
+
names together, the terms that never converted, or the whole account. Anything else it can't confirm is "can't check",
|
|
30
|
+
because calling the audit's own arithmetic wrong would be a guess.
|
|
31
|
+
|
|
32
|
+
Besides each row and the account totals, it checks what audits usually work out: the sums, rates and shares of the
|
|
33
|
+
rows a sentence names together, of a match type ("broad", "exact/phrase") and of the rows under a heading. It also
|
|
34
|
+
flags sentences that contradict their own numbers ("fell from 9% to 14%").
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/secondsteplabs/openppc
|
|
40
|
+
cd openppc
|
|
41
|
+
uv venv && uv pip install -e .
|
|
42
|
+
|
|
43
|
+
openppc audit search-term-waste examples/search_terms_acme.csv --industry home-services
|
|
44
|
+
openppc check --audit examples/ai_audit_sample.md --data examples/search_terms_acme.csv
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The examples are synthetic, so this runs with no account and no API keys. For your own data, export a report from Google Ads and put it in `exports/`, which git ignores.
|
|
48
|
+
|
|
49
|
+
## Run it in your browser
|
|
50
|
+
|
|
51
|
+
The web app in `web/` runs the same engine inside the browser tab (Python compiled to WebAssembly with [Pyodide](https://pyodide.org)). Your export and the audit stay on your machine: the page's security policy only lets it download its runtime, libraries and fonts from one pinned CDN (each checked against a fingerprint), so it has no way to send your files anywhere.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python3 -m http.server 8765 --directory web
|
|
55
|
+
# open http://localhost:8765 and click "Try the sample account"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
What it has:
|
|
59
|
+
|
|
60
|
+
- **Check**: paste any AI's audit next to the export it was written from, and see each number traced, mismatched or not in your data.
|
|
61
|
+
- **Audit**: run a free template on your export. The search-term waste audit comes back as cards: the headline numbers, what to do, and the wasted terms with the chance each one is bad, ready to copy as exact-match negatives or download as a .csv.
|
|
62
|
+
- **Templates**: every free template, with a link to its code.
|
|
63
|
+
- **Branded PDF**: a client-ready report under your agency's name, logo and brand color: a cover with the headline, then what we found, what we recommend, how the account compares with its industry, and how the report was made (the export's name, dates and SHA-256 fingerprint). Pages are laid out at Letter or A4 size exactly as they print: a block that does not fit moves to the next page, and long tables continue there under their own header, so nothing is cut. Before you can save, the checker reads the text of every page and confirms each number traces to the export. Save it from the browser's print dialog (Chrome and Edge keep the layout exactly). The logo stays in the browser.
|
|
64
|
+
- **Rules** (coming soon): turn a rulebook rule into a Google Ads Script you install yourself. Nothing is built for it yet.
|
|
65
|
+
|
|
66
|
+
The first visit downloads about 12 MB of runtime, which your browser then keeps. After changing anything in `openppc/` or `examples/`, rebuild the bundle the page loads (a test fails if you forget):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python tools/build_web.py
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Website
|
|
73
|
+
|
|
74
|
+
The site at [openppc.si](https://openppc.si) is built from `site/` together with the app, which it serves at `/app/`:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
python tools/build_site.py
|
|
78
|
+
python3 -m http.server 8766 --directory dist
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Pages are HTML fragments in `site/pages/` wrapped in `site/layout.html`. The build fails on any broken internal link, and the pages load nothing from other sites.
|
|
82
|
+
|
|
83
|
+
## Templates
|
|
84
|
+
|
|
85
|
+
| Template | Reads | You get |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `search-term-waste` | Search terms report (.csv) | Zero-conversion search terms ranked by cost (Search and Performance Max apart), each with the chance it is genuinely bad, the words behind the long tail, expensive converters, terms worth adding as keywords |
|
|
88
|
+
| `keyword-audit` | Keywords report (.csv) | Keywords spending without converting, budget by match type, spend on low Quality Scores |
|
|
89
|
+
| `account-structure` | Account snapshot (.json); Google Ads Editor export next | How the account is built: broad match without Smart Bidding, brand mixed with non-brand, duplicate keywords, negatives that block your own keywords, thin or pinned ads, missing sitelinks, retired bidding. Where Google and practitioners disagree, it shows both |
|
|
90
|
+
| `account-read` | Two-period totals (.json) | What changed and why it matters: efficiency win, over-expansion, broken tracking and more |
|
|
91
|
+
|
|
92
|
+
Add `--industry` to compare against published industry averages. `openppc industries` lists them.
|
|
93
|
+
|
|
94
|
+
Add `--brand "Your Brand, Short Name"` so brand searches, and close misspellings of them, are never flagged as waste or suggested as negatives:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
openppc audit search-term-waste exports/search_terms.csv --brand "Acme Plumbing, Acme"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Premium templates are planned as a paid add-on: a full account audit, Performance Max, a branded client-ready PDF, and multi-account triage. Everything in this repository stays free under the MIT license.
|
|
101
|
+
|
|
102
|
+
## Use it inside Claude, Cursor and ChatGPT
|
|
103
|
+
|
|
104
|
+
OpenPPC is an MCP server: your assistant writes the words, and OpenPPC computes and checks the numbers.
|
|
105
|
+
|
|
106
|
+
**On your computer** (Claude Code, Claude Desktop, Cursor). It reads your exports where they are:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
claude mcp add openppc -- uvx --from "openppc[mcp] @ git+https://github.com/secondsteplabs/openppc" openppc-mcp
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For Claude Desktop or Cursor, add the same server to `claude_desktop_config.json` or `~/.cursor/mcp.json`:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "mcpServers": { "openppc": { "command": "uvx", "args": ["--from", "openppc[mcp] @ git+https://github.com/secondsteplabs/openppc", "openppc-mcp"] } } }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
OpenPPC isn't on PyPI yet, so these commands fetch it straight from GitHub.
|
|
119
|
+
|
|
120
|
+
**As a web connector** (ChatGPT, claude.ai), over streamable HTTP at `/mcp`:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
openppc-mcp --http --host 0.0.0.0 --port 8000
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Put it behind HTTPS and add `https://your-host/mcp` in ChatGPT (Developer mode) or claude.ai (Connectors). A hosted connector at `https://mcp.openppc.si/mcp` is coming soon. The connector's tools take the export's contents instead of a path, work in a temporary folder that is deleted when the call ends, and keep nothing. To keep an export on your computer while using ChatGPT, run OpenPPC locally and connect it through OpenAI's Secure MCP Tunnel.
|
|
127
|
+
|
|
128
|
+
Every tool is read-only: `list_templates` everywhere; `audit_account` and `check_numbers` on your computer; `audit_export` and `check_audit` on the connector. From a clone, `uv pip install -e ".[mcp]"` and then `claude mcp add openppc -- openppc-mcp`.
|
|
129
|
+
|
|
130
|
+
## How it works
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
your export (.csv)
|
|
134
|
+
-> parse skips Google's title, date and Total lines; recomputes every total from the rows
|
|
135
|
+
-> template computes each figure in code and registers it as a labeled fact
|
|
136
|
+
-> report prints registered facts only
|
|
137
|
+
-> check traces every number in the report back to a fact before you see it
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The checker compares numbers at the precision they were written: "$1.4k" matches 1,361.04 because both round to 1,400, and "$1,500" does not. It reads the words around a number, and the table column it sits in, to catch a right number attached to the wrong metric or row.
|
|
141
|
+
|
|
142
|
+
## How sure is a waste flag?
|
|
143
|
+
|
|
144
|
+
A search term with no conversions might be bad, or just unlucky. A fixed rule ("no conversions after $20") can't tell the difference. OpenPPC's waste model learns how much your account's search terms differ in conversion rate, then gives each term the chance that it truly converts at less than half your account's rate. Only terms at 90% or more are marked as negatives to add; the rest are too early to judge, and the words they share are listed instead.
|
|
145
|
+
|
|
146
|
+
We backtested this on live accounts, excluding terms that people had already added as negatives. Terms the model was 90% sure about kept converting at a small fraction of their account's rate over the next five months. Terms flagged by a fixed dollar threshold went on to convert at close to the normal rate. The sample is still small, and the method is in `openppc/engine/waste_model.py`.
|
|
147
|
+
|
|
148
|
+
## The rulebook
|
|
149
|
+
|
|
150
|
+
Every rule the tool applies is a row in [`rulebook/rules.csv`](https://github.com/secondsteplabs/openppc/blob/main/rulebook/rules.csv): what it checks, the formula, the threshold, where the idea comes from (Google's own guidance, practitioners, or our reasoning) and the line of code that runs it. A test fails if a threshold in the code and its row ever disagree, so the rulebook is always what the tool actually does. Planned rules sit in the same file. Argue with any of them in an issue or a pull request.
|
|
151
|
+
|
|
152
|
+
## What it is not
|
|
153
|
+
|
|
154
|
+
- Not connected to your account. It reads a file you exported.
|
|
155
|
+
- Not an autopilot. It never changes a campaign.
|
|
156
|
+
- Not a strategy checker. It checks numbers and their direction, not whether the advice is good.
|
|
157
|
+
|
|
158
|
+
## Known limits
|
|
159
|
+
|
|
160
|
+
- With thousands of figures in a file, a number can match one by coincidence. Every trace names the fact it matched, so read the "traced to" column instead of just counting.
|
|
161
|
+
- "Not in data" means not in the files you provided. The figure may come from another report or date range.
|
|
162
|
+
- Benchmarks are the public WordStream / LocaliQ 2026 US averages: a ballpark, not a target.
|
|
163
|
+
- Google renames export columns from time to time. If a file won't load, open an issue with its header row.
|
|
164
|
+
|
|
165
|
+
## Roadmap
|
|
166
|
+
|
|
167
|
+
- An optional writer that turns the facts into prose with your own model key, while every number still comes from code
|
|
168
|
+
- A public eval: can an AI read a Google Ads account correctly?
|
|
169
|
+
- More exports: campaigns, Performance Max, Search Console, Meta
|
|
170
|
+
|
|
171
|
+
## Configuration
|
|
172
|
+
|
|
173
|
+
None. No API keys, no `.env`.
|
|
174
|
+
|
|
175
|
+
## Contributing
|
|
176
|
+
|
|
177
|
+
Issues and pull requests are welcome. Read [CONTRIBUTING.md](https://github.com/secondsteplabs/openppc/blob/main/CONTRIBUTING.md) first, and never attach a client's export.
|
|
178
|
+
|
|
179
|
+
## Security
|
|
180
|
+
|
|
181
|
+
To report a vulnerability, see [SECURITY.md](https://github.com/secondsteplabs/openppc/blob/main/SECURITY.md).
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT. See [LICENSE](https://github.com/secondsteplabs/openppc/blob/main/LICENSE).
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
<p align="center">
|
|
190
|
+
<a href="https://github.com/secondsteplabs">
|
|
191
|
+
<picture>
|
|
192
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/secondsteplabs/.github/raw/main/assets/logo-labs-horizontal-dark.png">
|
|
193
|
+
<img alt="labs.secondstep" src="https://github.com/secondsteplabs/.github/raw/main/assets/logo-labs-horizontal-light.png" width="200">
|
|
194
|
+
</picture>
|
|
195
|
+
</a>
|
|
196
|
+
</p>
|
|
197
|
+
|
|
198
|
+
Built by [labs.secondstep](https://github.com/secondsteplabs), the open-source side of [Second Step](https://getsecondstep.com).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"currency": "USD",
|
|
4
|
+
"window": "July 2026 vs June 2026",
|
|
5
|
+
"business_model": "plumbing (lead-gen)"
|
|
6
|
+
},
|
|
7
|
+
"current": {
|
|
8
|
+
"spend": 4973.64,
|
|
9
|
+
"impressions": 9852,
|
|
10
|
+
"clicks": 732,
|
|
11
|
+
"conversions": 85
|
|
12
|
+
},
|
|
13
|
+
"prior": {
|
|
14
|
+
"spend": 5210.0,
|
|
15
|
+
"impressions": 11890,
|
|
16
|
+
"clicks": 760,
|
|
17
|
+
"conversions": 71
|
|
18
|
+
}
|
|
19
|
+
}
|