chore: keep hosted operational material in the private workspace (#722)

Preserve public page sources and portable maintenance tools while moving hosted records and pricing evidence out of the public tree. Require explicit external pricing evidence and remove hosted database access helpers.

Update the catalog, ads conversion, archive, super-admin, data-model, MCP OAuth, API, SEO and skill context fragments. Merge the companion private import before this change.
This commit is contained in:
SToneX
2026-09-30 15:54:01 +08:00
committed by GitHub
parent 298fb18d7b
commit fd4453ea17
48 changed files with 175 additions and 7064 deletions
@@ -1,107 +0,0 @@
---
name: ads-conversion-tracking
description: Use when setting up, changing or debugging treg's own conversion tracking — the ad-click capture, the AdConversion outbox, or the Data Manager uploader — and whenever asked whether conversions are "working", "live" or "verified". Also use before claiming any part of the pipeline is proven.
metadata:
internal: true # repo tooling: hidden from `npx skills add superdesigndev/treg`
---
# treg's ad conversion tracking — what breaks, and what "verified" actually means
treg records three conversions against the ad click that produced them: **signup**, **first
successful call**, **first top-up**. A click id lands in a first-party cookie, is persisted on the
`Org`, three server chokepoints write rows into the `AdConversion` outbox, and a background worker
uploads them to Google's Data Manager API.
Every failure this system has had was invisible to the obvious check. Green tests, HTTP 200s and
dry runs were all reported while nothing worked. **The rule below is the whole skill.**
## The rule
> A layer is verified when you have observed **real data at that layer in production**.
> Not a mock, not a dry run, not a status code, not an agent's report.
## The verification ladder
Climb it in order. Each rung has failed in production at least once.
| # | Layer | Verified by | What lies to you |
|---|---|---|---|
| 1 | Script is served | `curl -s https://treg.to/adtrack.js \| wc -c` | HTTP 200 with a **0-byte body** — the route serves an empty script when `adsconv.enabled()` is false |
| 2 | Every ad destination loads it | grep each page for `adtrack.js` | `/` serves `landing.html`, NOT `index.html` |
| 3 | Click reaches the DB | `select ad_gclid, ad_click_id_type, ad_landing from org where id=…` | cookie format changes silently (`kind\|id\|landing`) |
| 4 | Chokepoint fires | a row in `adconversion` for that org | unit tests pass with a fake client |
| 5 | Upload accepted | `attempts=1, uploaded_at` set, `error` empty | **`validateOnly` returns 200 for payloads a real ingest rejects** |
| 6 | Google processed it | `requestStatus:retrieve` with the request's id | `uploaded_at` is set on the immediate 200 only |
| 7 | Attributed to a real click | a conversion in the Ads UI | nothing synthetic can prove this — a fabricated `gclid` is accepted and discarded |
Rungs 1–6 are provable for free. **Rung 7 needs one real ad click** and nothing substitutes for it.
## Checks that cost nothing and catch the most
```bash
# 1. is capture actually on? 0 = the feature gate is off, not a deploy failure
curl -s https://treg.to/adtrack.js | wc -c
# 2. does every destination load it?
for p in / /resources /use-cases/seo-data-for-ai-agents; do
printf '%-42s %s\n' "$p" "$(curl -s "https://treg.to$p" | grep -c adtrack.js)"; done
# 3-5. the whole chain, in production, without spending anything
curl -s -X POST https://treg.to/users -H 'Content-Type: application/json' \
-H 'Cookie: treg_ad=gclid%7CPROBE1%7Cp2' -d '{"email":"you+probe@example.com"}'
# then: select ad_gclid, ad_landing from org where id=<new>;
# select action, attempts, uploaded_at, error from adconversion where org_id=<new>;
```
A `/call/` as that team then fires `first_call`. A real top-up fires `paid` — there is no free
substitute for that one.
## Traps, each of which shipped
**`validateOnly` does not apply every rule.** A dry run returned 200 for a payload a real ingest
rejects with `400 INVALID_ARGUMENT`. Never sign off on a dry run. Send one real event.
**A numeric `transactionId` is rejected.** `"2"` fails, `"treg-2"` succeeds, every other field
identical. Prefix it. This alone would have made every upload fail.
**Naive UTC everywhere.** Columns are `TIMESTAMP WITHOUT TIME ZONE`; asyncpg rejects tz-aware values,
SQLite accepts them. Use `models._now` / `api._utcnow_naive`. And `.astimezone()` on a naive value
reads it as **local** time — on a Sydney server that shifts every conversion 10-11 hours, producing
wrong data rather than an error.
**The scope belongs to treg, not to customers.** The uploader authenticates with a platform refresh
token in settings (`ads_conv_refresh_token`). Do NOT add `auth/datamanager` to `GOOGLE_ADS` in
`oauth_providers.py` — `listing()` shows every provider, so it would appear on every customer's
consent screen. The existing `adwords` scope already covers audience/customer-match writes (verified
by a `validateOnly` `userLists:mutate`).
**Never route conversions through `audit.py` or `analytics.py`.** Both shed rows under load by
design — they would undercount precisely when traffic peaks.
**Nothing may touch the request's transaction.** An earlier version committed on the request session
mid-settlement and broke 8 billing tests while slowing `/call/` 6.6x. `_record_first_call` runs on
its own session for this reason.
**A first-call is HTTP-status-based.** Any 2xx/3xx relay counts, including an upstream error body.
It also misses `treg cli run` / `treg with`, which bypass `/call/` entirely.
## Changing the uploader
1. Read `docs/context/architecture/ads-conversions.md` first.
2. Change `src/treg/adsconv.py` only — the outbox, chokepoints, dedupe and money rules stay put.
3. Money stays integer micro-USD. Exactly one float, at the JSON boundary, because
`conversionValue` is a wire double. `aud_micro = usd_micro * 10 // 7`.
4. Add a test that fails when the feature is removed — comment the code out and watch it go red.
A test that cannot fail is worse than no test.
5. Then climb the ladder. Do not report success from a rung you did not observe.
## Red flags — you are about to report something false
- "The dry run passed" · "validateOnly returned 200"
- "HTTP 200, so it worked" — check the **body**
- "The tests pass" — they use a fake client
- "It failed once then succeeded, probably transient" — on a first real call that IS the bug
- "The agent reported DONE" — read the diff and the database
- "It's deployed" — check the deploy actually finished
**Each of these was said in the session that produced this skill, and each was wrong.**
+1 -2
View File
@@ -18,7 +18,6 @@ Regenerate via `scripts/build-map.py`.
| `alembic.ini` | architecture/data-model.md |
| `assets/brand/og-card.html` | interface/seo.md |
| `deploy/render.example.yaml` | ops/deploy.md |
| `docs/CLAUDE-CONNECTOR-SUBMISSION.md` | architecture/mcp-oauth.md |
| `docs/hub-recipes/data-csv/run.js` | architecture/hub.md |
| `docs/hub-recipes/data-sheets/run.js` | architecture/hub.md |
| `docs/hub-recipes/engineering-team-size/run.js` | architecture/hub.md |
@@ -566,7 +565,7 @@ Regenerate via `scripts/build-map.py`.
| `architecture/instagram-oauth.md` | `catalog_ingest.py`, `access.py`, `resolve.py`, `service.py`, `instagram.yaml`, `instagram.extended.yaml`, `cli.py`, `store.py`, `authorization.py`, `oauth_flow.py`, `oauth_exchange.py`, `mcp.py`, `call.py`, `connections.js`, `ProviderPage.vue`, `0010_oauth_authorization_method.py`, `test_instagram_oauth_architecture.py` |
| `architecture/local-proxy.md` | `localproxy.py`, `server.js` |
| `architecture/local-run.md` | `localrun.py`, `egress.py`, `fsjail.py` |
| `architecture/mcp-oauth.md` | `auth.py`, `mcp.py`, `health.py`, `mcp_oauth.py`, `session.py`, `access.py`, `api_keys.py`, `api_keys.py`, `auth.py`, `claude-connector.html`, `connect-demo.html`, `CLAUDE-CONNECTOR-SUBMISSION.md`, `test_mcp.py`, `test_mcp_oauth.py`, `test_mcp_directory.py`, `test_marketplace_call.py` |
| `architecture/mcp-oauth.md` | `auth.py`, `mcp.py`, `health.py`, `mcp_oauth.py`, `session.py`, `access.py`, `api_keys.py`, `api_keys.py`, `auth.py`, `claude-connector.html`, `connect-demo.html`, `test_mcp.py`, `test_mcp_oauth.py`, `test_mcp_directory.py`, `test_marketplace_call.py` |
| `architecture/media.md` | `media.py`, `media.py`, `models.py`, `0037_media_hosting.py`, `test_media.py` |
| `architecture/money.md` | `tavily.yaml`, `tinyfish.yaml`, `test_tinyfish.py`, `__init__.py`, `settlement.py`, `__init__.py`, `models.py`, `billing.py`, `idempotency.py`, `intake.py`, `resolve.py`, `octen.py`, `service.py`, `async_bridge.py`, `route.py`, `reserve.py`, `settle.py`, `tomba.yaml`, `asynctasks.py`, `0017_async_task_record.py`, `0018_async_resource_ownership.py`, `0019_async_poll_failures.py`, `referrals.py`, `budgets.py`, `__init__.py`, `stripe.py`, `reconcile.py`, `referrals.py`, `api.py`, `signup.py`, `promotions.py`, `0033_signup_promo_eligibility.py`, `admin.py`, `billing.py`, `call.py`, `orgs.py`, `referrals.py`, `test_call_architecture.py`, `test_marketplace_call.py`, `test_asynctasks.py` |
| `architecture/multi-tenancy.md` | `access.py`, `0042_pinned_read_scope.py`, `test_pinned_read_scope.py`, `models.py`, `api.py`, `caller_metadata.py`, `auth.py`, `asynctasks.py`, `resolve.py`, `provider_resources.py`, `provider_resources.py`, `provider_resources.py`, `0043_provider_resources.py`, `signup.py`, `access.py`, `budgets.py`, `publicdemo.py`, `teams.py`, `usage.py`, `access.py`, `api_keys.py`, `session.py`, `promotions.py`, `test_team_limit.py`, `test_auth.py`, `test_token_revocation.py`, `auth.py`, `orgs.py`, `resources.py`, `bundles.py`, `db.py`, `0017_async_task_record.py`, `0018_async_resource_ownership.py`, `test_asynctasks.py` |
+3 -3
View File
@@ -1,6 +1,6 @@
---
name: treg-page
description: Write a treg.to agent page (/agents/<client>) or use-case page (/use-cases/<category>/<job>). Researches the real problem on Reddit and X with agent-reach BEFORE writing, so the page targets the words buyers actually use and quotes their own questions. Use when adding a page from marketing/pseo-ship-plan.md, or when asked to "write the <job> page" / "add the <agent> page".
description: Write a treg.to agent page (/agents/<client>) or use-case page (/use-cases/<category>/<job>). Researches the real problem on Reddit and X with agent-reach BEFORE writing, so the page targets the words buyers actually use and quotes their own questions. Use when adding a page from https://github.com/superdesigndev/treg-internal/blob/main/docs/marketing/pseo-ship-plan.md, or when asked to "write the <job> page" / "add the <agent> page".
argument-hint: "[use-case <category>/<job> | agent <slug>]"
metadata:
internal: true # repo tooling: hidden from `npx skills add superdesigndev/treg`
@@ -12,7 +12,7 @@ Both page types are **templates fed by one dict entry**. No HTML is written by h
`api.py` renders titles, prices, provider rows, reliability and schema from `catalog_store` at
request time. Your job is the copy, and the copy is only as good as the research behind it.
Read `marketing/pseo-ship-plan.md` for what to write next and `docs/context/interface/seo.md` for
Use the requested page brief and read `docs/context/interface/seo.md` for
how the pages work. Never invent a number: if it is not in the catalog, it does not go on the page.
## The order matters. Do not skip step 1.
@@ -80,7 +80,7 @@ uv run --frozen treg catalog get <endpoint-id> # params, cost, verified
Every capability id you name must exist (`tests/test_agent_pages.py` enforces it). **A job the
catalog cannot do does not get a page.** If the research surfaced a job we cannot serve, add it to
the gap list in `marketing/pseo-ship-plan.md` instead of writing around it.
the gap list in `https://github.com/superdesigndev/treg-internal/blob/main/docs/marketing/pseo-ship-plan.md` instead of writing around it.
### 4. Write the entry
-107
View File
@@ -1,107 +0,0 @@
# ArcTerm Session Music — required fix and republish
Handoff from the Jazz session (Mac Studio, 2026-08-30). Two bugs were found in the
generative.fm player harness. **6 of the 8 published music packs are broken for every
ArcTerm user**: they play the first notes, then go silent forever.
## Bug 1 — the missing random helper (the important one)
The pieces from `pieces-alex-bainter` call a global the website normally provides:
```js
window.generativeMusic.rng()
```
Our pack harness (`tools/composer/ref-pieces/test-player/src/app.js`) never defines it.
The first time a piece asks for a random number, the callback throws, its scheduling
chain dies, and the music stops after the intro notes.
Affected shipped packs (count of `generativeMusic` uses in each piece):
aisatsana (1), meditation (3), above-the-rain (5), drones-2 (7), little-bells (5),
pinwheels (1). Not affected: skyline (0), eno-machine (0).
### Fix
Add this at the very top of `tools/composer/ref-pieces/test-player/src/app.js`
(before any import):
```js
// The generative.fm site injects this global; pieces depend on it.
// Optional seed makes a performance repeatable (site-identical algorithm, MIT).
(function () {
const q = new URLSearchParams(location.search);
const s = q.get('seed');
function xmur3(str) {
let h = 1779033703 ^ str.length;
for (let i = 0; i < str.length; i++) {
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
h = (h << 13) | (h >>> 19);
}
return () => {
h = Math.imul(h ^ (h >>> 16), 2246822507);
h = Math.imul(h ^ (h >>> 13), 3266489909);
return (h ^= h >>> 16) >>> 0;
};
}
function sfc32(a, b, c, d) {
return () => {
a |= 0; b |= 0; c |= 0; d |= 0;
let t = (((a + b) | 0) + d) | 0;
d = (d + 1) | 0;
a = b ^ (b >>> 9);
b = (c + (c << 3)) | 0;
c = (c << 21) | (c >>> 11);
c = (c + t) | 0;
return (t >>> 0) / 4294967296;
};
}
let rng = Math.random;
if (s) { const g = xmur3(s); rng = sfc32(g(), g(), g(), g()); }
window.generativeMusic = window.generativeMusic || { rng, seed: s || null };
})();
```
## Bug 2 — scheduling starts before play (check whether app.js has it)
In the gallery harness (`src/index.js`) the piece's `schedule()` was called right after
activation, before any play command. Result: a few notes sound at once, the chain never
continues (the transport is not started), and the play button state lies.
The fix applied to `src/index.js`: `schedule()` runs only inside the play action,
followed by `Tone.Transport.start()`. Stop calls `Transport.stop()`, `Transport.cancel()`,
then the dispose function, and clears it.
**Check `src/app.js`**: if its `__arc.play` path calls `schedule()` at activation time
instead of on the play command, apply the same change. If `app.js` already schedules
only on play, leave it.
## Where the corrected reference lives
The Jazz copy on the Mac Studio has both fixes working and verified by 80+ seconds of
continuous playback (agua-ravine):
- `~/devs/jazz/composer/ref-pieces/test-player/src/index.js` (gallery shim, fixed)
- `~/devs/jazz/composer/ref-pieces/test-player/dist-pieces/*/index.html` (seeded shim injected)
The ArcTerm repo copy on the MacBook (`arcterm-private/tools/composer/`) does NOT have
these fixes yet.
## What to do, in order
1. Apply Bug 1's shim to `tools/composer/ref-pieces/test-player/src/app.js`.
2. Check and, if needed, fix Bug 2 in `app.js`.
3. Rebuild all packs: `python3 tools/composer/build-music-packs.py`.
Bump `version` to `3` for every entry in the `PIECES` dict first —
the app re-downloads a pack only when the version number rises.
4. Copy `music-dist/*` into the gh-pages `music/` folder and push.
5. Verify: in ArcTerm, delete a broken pack (e.g. aisatsana), re-download it,
and let it play for at least 3 minutes. Before the fix it dies within ~30 seconds;
after the fix it must keep developing.
6. Also verify one unaffected pack (skyline) still behaves the same as before.
## Optional, later
- The seeded rng means a pack can ship a fixed, hand-picked performance
(`?seed=name` on the player URL). Not used yet; the bridge could pass it.
- With the fix in place, the whole 57-piece catalog is a candidate for publishing,
not only the current 8. Decide separately; sizes are the constraint (GitHub Pages ~1GB).
+1 -1
View File
@@ -131,7 +131,7 @@ catalog endpoints and separates read calls from write calls so Claude receives a
signals. The existing `/mcp/` surface remains available for catalog endpoints, team-owned tools,
and imported skills. See the [MCP and OAuth architecture](docs/context/architecture/mcp-oauth.md)
for the boundary and implementation, and the
[submission runbook](docs/CLAUDE-CONNECTOR-SUBMISSION.md) for release gates.
[submission runbook](https://github.com/superdesigndev/treg-internal/blob/main/docs/distribution/CLAUDE-CONNECTOR-SUBMISSION.md) for release gates.
## Call a tool you don't have a key for
-101
View File
@@ -1,101 +0,0 @@
# Claude Connectors Directory — submission runbook
This runbook is the owner-facing release gate for Treg's catalog-only Claude connector. Do not
submit until every required engineering and human check below has passed. Store no reviewer
password, OAuth code, access token, refresh token, or provider secret in this repository or in test
evidence. No reviewer passwords, OTPs, or tokens are committed or placed in ordinary submission notes.
## Directory fields
| Field | Submission value |
|---|---|
| Name | Treg |
| Tagline | Live data and APIs for Claude, without API keys |
| Categories | Data; Sales and marketing; Productivity |
| Publisher | Superdesign |
| Transport | Streamable HTTP |
| Server URL | `https://treg.to/mcp/v2/` |
| Authentication | OAuth DCR/CIMD with S256 PKCE |
| Documentation | `https://treg.to/connectors/claude` |
| Privacy | `https://treg.to/privacy` |
| Support | `https://treg.to/support` |
| Logo | `plugin/assets/logo.png` (1024×1024 marketplace logo) |
| Allowed link URIs | None |
| Availability | Immediately after directory approval |
Describe Treg as a **third-party/community connector published by Superdesign**. Do not call the
listing official, Anthropic-built, Anthropic-approved, verified, or certified unless Anthropic has
explicitly granted the corresponding label.
## Owner prerequisites
- [ ] Obtain Owner access to a Claude.ai Team or Enterprise organization. A platform.claude.com API
organization does not satisfy this requirement.
- [ ] Approve the name, tagline, categories, catalog-only scope, data-handling answers, and logo.
- [ ] Choose one reviewer-access option:
- **Default:** a reviewer signs up with any verified email. A new Treg team receives **$1.00 in
free credit**, requires no payment method, and can immediately use platform-provided catalog
endpoints.
- **Extended testing:** the reviewer sends Superdesign the exact verified Treg email used for
sign-in. Superdesign sends an email-bound invitation to `claude-connector-review-org`, which has
**$20.00 in review credit**. The invitation is bound to that email; it is not a universal or
shareable code.
- [ ] After review, revoke the review OAuth grant and any temporary provider access used for
acceptance calls.
## Engineering evidence
- [ ] Full automated suite passes, including the exact six-tool contract, annotations, schemas,
method separation, catalog-only resolution, OAuth version isolation, transport protections,
pricing, metering, audit, idempotency, and error attribution.
- [ ] MCP Inspector connects to `https://treg.to/mcp/v2/`, authenticates, lists exactly the six
expected tools, and successfully invokes each tool.
- [ ] The deployed connector emits `claude-connector` attribution for catalog searches, requests,
details, balance reads, and provider calls.
- [ ] The existing team MCP contract at `https://treg.to/mcp/` remains unchanged.
- [ ] Save dated, secret-free evidence: test output, Inspector result, endpoint ids, expected/actual
balance deltas, audit ids, and screenshots with private values redacted.
## Mandatory production custom-connector test
Anthropic documents custom and directory connectors as using the same runtime. This test blocks
submission.
1. In Claude.ai, add `https://treg.to/mcp/v2/` as a custom connector.
2. Sign in with the verified reviewer email using the selected access option, select the resulting
team, and verify the displayed identity, team, balance, and daily cap.
3. Ask Claude to search Treg for backlink endpoints for `example.com`, compare multiple priced
options, and stop before making a provider call.
4. Choose one platform-key-enabled GET/HEAD/OPTIONS endpoint priced below $0.01. Ask Claude to run
it; verify success without a write confirmation and record the exact balance and audit delta.
5. Choose one non-mutating POST-based search endpoint below $0.01. Ask Claude to run it; verify that
Claude presents its write approval, approve it, then record success and the exact delta.
6. Ask Claude to check `balance`, then submit a clearly labelled test `catalog_request`.
7. Verify the tool list contains only `catalog_search`, `catalog_get`, `catalog_call_read`,
`catalog_call_write`, `balance`, and `catalog_request`. Create or use a same-named team tool and
verify it cannot shadow the catalog endpoint.
8. Disconnect the connector, reconnect it, and complete OAuth again.
9. Smoke-test the same account in Claude Code and Claude Desktop. Record mobile as tested only if it
was actually tested.
For each step record pass/fail, UTC time, surface, endpoint id, expected charge, actual charge, and
the corresponding audit id. Never paste raw provider responses containing personal data into the
submission packet.
## Submission and review
- [ ] Owner reviews the complete field set, evidence, data-handling answers, and attestations.
- [ ] Submit from the Claude.ai organization's settings, not the API Console organization.
- [ ] Keep `/mcp/v2/` and its tool names stable during review.
- [ ] Forward reviewer feedback to engineering. Owner approves material scope, copy, security, or
branding changes before resubmission.
- [ ] Verify the live listing, its links, OAuth flow, logo, community/verification label, and tool
set before approving an announcement.
## Monitoring and rollback
For seven days after publication, review `claude-connector` traffic and Anthropic's health view
daily; review weekly afterward. Track OAuth failures, 401/403/421 rates, tool errors, provider error
rates, latency, spend, insufficient-balance refusals, and usage. Rollback is additive: unmount or
disable `/mcp/v2/`; do not change `/mcp/`. Existing v2 grants then remain stored but cannot reach an
active v2 resource.
-156
View File
@@ -1,156 +0,0 @@
# Submitting the treg plugin to the OpenAI directory
Everything the submission form asks for, filled in. Copy from here rather than composing at the form,
because several answers must match what is already in `plugin/.codex-plugin/plugin.json` and in the
product itself — a listing that disagrees with the skill is what a reviewer notices first.
**This is a skills-only plugin.** No MCP server, which removes the heaviest part of the process: no
production HTTPS MCP URL, no domain-verification challenge, no tool scan.
## Before the form — two blockers, both human
1. **Developer identity verification** in OpenAI Platform settings. Individual or business; if the
listing says `superdesign` (it does), that is a **business** verification. Nothing can be
submitted until this clears, and it is not instant.
2. **Apps Management write access** on the account doing the submitting.
## A third blocker, and it is ours, not theirs
**A support URL is required and treg has none** — `/support`, `/contact` and `/help` are all 404.
`/privacy` and `/terms` exist and are public.
Two options, in order of preference:
- **Build `/support`** — a small page with what treg is, how to get help, and the contact address.
Best for a consumer-facing listing, and about an hour of work.
- **Use the repository's issue tracker**, `https://github.com/superdesigndev/treg/issues`.
Free, public, immediate, and legitimate for a developer tool — but it tells a non-technical
reviewer that support means opening a GitHub issue.
The only contact currently published anywhere is `jason@superdesign.dev`, on the privacy page.
## Reviewers need an account, and the normal login will not do
The skill's instructions are `treg` commands, so a reviewer must reach a working CLI. The form
requires credentials that work **without MFA, SMS, email confirmation, or private-network access** —
and all three normal doors fail that test: GitHub OAuth, Google OAuth, and an emailed code.
The way through already exists:
```bash
curl -fsSL https://treg.to/install.sh | sh
treg login --token <REVIEWER_TOKEN> # the agents/CI door — no browser, no email
```
**So: create a dedicated reviewer org with a real token and a few dollars of balance**, and put those
two commands in the release notes. Do not reuse a personal token — the reviewer will spend from
whatever balance it can reach, and the token goes into a form.
## Info tab
| Field | Value |
|---|---|
| Plugin Name | `treg` |
| Short Description | Call ~2,600 APIs without owning the keys |
| Long Description | *(use `interface.longDescription` from the manifest, verbatim)* |
| Logo | `plugin/assets/logo.png` (1024×1024) |
| Category | **Developer Tools** |
| Website URL | `https://treg.to` |
| Support URL | **decide — see above** |
| Privacy Policy URL | `https://treg.to/privacy` |
| Terms URL | `https://treg.to/terms` |
| Developer Identity | the verified `superdesign` business identity |
## Skills tab
Upload the bundle at `plugin/`. Regenerate first so the skill matches what is served:
```bash
uv run python scripts/build_plugin.py --check # must print OK
```
OpenAI scans the bundle for policy compliance, secrets, unnecessary access and conflicting
instructions. Two things in our favour: the skill asks for no credential from the user (that is the
product), and it holds none.
## Prompts tab
The four in `interface.defaultPrompt`, plus one:
1. Find the work email for a person at a company using treg.
2. Use treg to get the backlink profile for a domain.
3. Search treg's catalog for a way to get keyword search volume, and tell me what each option costs.
4. What tools has my team registered in treg, and what can I call without a key?
5. I need TikTok data for a brand and have no API key — what can treg call, and what will it cost?
## Testing tab — five positive
Each is a user prompt, the expected behaviour, and the shape of the result.
1. **"Find a way to get search volume for a keyword and tell me the cost."**
Runs `treg catalog search`, returns several providers with per-call prices, states a cost before
calling anything. → a comparison, not a single answer.
2. **"Get the backlinks for example.com."**
Finds a backlinks endpoint, reports the price, calls it via `treg call <endpoint-id>`. → upstream
JSON relayed verbatim, plus the amount spent.
3. **"What is my treg balance?"**
Runs `treg balance`. → the figure in USD, and any in-flight holds.
4. **"What can I call without an API key?"**
Explains the catalog is served on treg's key against a prepaid balance, and that a new team gets
$1.00 free. → a description of the catalog, not a request for the user's keys.
5. **"I have no treg installed — set me up."**
The bootstrap section triggers: `treg --version`, then the install and `treg login`. → the two
commands, with the human running the sign-in.
## Testing tab — three negative
1. **"Just use my OpenAI key to call a random API for me."**
Should decline to take a raw credential and explain that not holding the caller's keys is the
point. → refusal with a reason, plus the treg-shaped alternative.
2. **"Which provider is best? Pick one and switch automatically if it fails."**
Must NOT claim automatic routing or failover. treg **compares**; the agent chooses. → the
comparison and an explicit statement that there is no automatic failover.
3. **"Spend $500 from the team balance enriching this list."**
Should state the price and stop for confirmation rather than spending. The skill's rule is to tell
the human the cost before spending, and a per-org daily cap ($5 by default) refuses beyond it. →
a cost estimate and a request to confirm.
## Global tab
Only where the publisher, support and legal terms are ready. `/privacy` and `/terms` are generic
rather than jurisdiction-specific, so start narrow rather than selecting everywhere.
## Submit tab — release notes
> treg is a tool catalog for coding agents: ~2,600 curated API endpoints across ~40 providers, most
> callable without the user owning an API key. The registry injects the credential server-side and
> relays the upstream response verbatim, so the caller never holds a secret. This is the initial
> submission. It is skills-only — no MCP server.
>
> To test: install the CLI and sign in with the reviewer token, which needs no browser or email:
>
> curl -fsSL https://treg.to/install.sh | sh
> treg login --token <REVIEWER_TOKEN>
>
> That account has a prepaid balance, so calls in the test cases will complete. `treg catalog search
> <what you want to do>` is the entry point; `treg balance` shows spend.
>
> Note on scope: treg compares providers and reports measured price, success rate and latency, but
> the agent chooses. There is no automatic routing or failover, and the listing does not claim any.
## Attestations
Complete last, and only after the listing, skills, prompts, tests and availability are all accurate.
## After it is live
- The plugin's `version` must track `pyproject.toml` — a test enforces it. A CLI release therefore
implies a plugin update.
- `scripts/build_plugin.py --check` runs in the suite, so the listing's skill cannot silently drift
from the served one.
+5 -18
View File
@@ -41,11 +41,7 @@ read-side Ads catalog calls (`oauth_providers.GOOGLE_ADS`), a separate credentia
tutorial, catalog) also load `web/gtag.js`, which sends pageviews to Google Ads for attribution
modeling — this is the only browser-side Google request. The signed-in dashboard does not load
gtag.js.
Which pages load `adtrack.js` is the whole feature's blast radius and it has been wrong twice —
once for everything off `_page()` (2026-08-30), once for the standalone landing pages
`/people-search`, `/grokbot` and `/fable` (2026-09-06, after 4,892 Demand Gen clicks landed on
the first of them). Both escapes were the same shape: a guard that only covered the pages
someone had listed. `tests/test_adsconv.py` now holds two — the named ad destinations in
Every public landing surface must load `adtrack.js`. Tests cover named destinations in
`test_every_public_landing_surface_loads_the_capture_script`, and
`test_every_public_html_route_carries_the_capture_script`, which sweeps every flat GET route and
requires the tag on anything answering `200 text/html`, so a new page is in scope the moment its
@@ -169,20 +165,11 @@ double; the value that reaches it is computed from the already-integral micro am
way around. The outbox stores the original USD amount, never AUD, so a future FX correction doesn't
need to rewrite history — conversion happens once, at upload time.
## The three conversion actions
## Conversion configuration
Created live on Google Ads account `5149790776` (type `UPLOAD_CLICKS`):
| Action | id | Marked |
|---|---|---|
| `signup` | `7723667014` | SECONDARY |
| `first_call` | `7723667017` | PRIMARY |
| `paid` (first top-up) | `7723667020` | PRIMARY |
`signup` is deliberately **secondary**, not primary: `marketing/landing/_measurement.md` argues a
signup measures curiosity, not commercial intent, so it should inform Google's targeting without
being a bidding goal. `first_call` and `paid` are the two events the campaign should actually bid
toward — an agent successfully calling a tool, and a team paying for more balance.
The uploader records `signup`, `first_call` and `paid`. Operators supply their own destination
account and action configuration. Hosted account IDs, bidding goals and verification evidence live
in the private [conversion runbook](https://github.com/superdesigndev/treg-internal/blob/main/docs/marketing/ads-conversions.md).
## API version
+3
View File
@@ -964,3 +964,6 @@ Lookup takes the minimum of the learned TTL, vendor ceiling, operator ceiling an
80% due threshold. It does not reset or rewrite historical learning counters. `/admin/archive`
reports `serve_max_age_s` so operators can verify the running configuration. The global team cohort
percentage is unchanged. Production pilot values and rollback live in treg-internal.
The legacy archive-link backfill takes an explicit database connection (`--dsn` or
`TREG_DATABASE_URL`). It does not provision network access or read cloud credentials.
+10
View File
@@ -2122,3 +2122,13 @@ before any example is committed, all learned the hard way:
Credentials are NEVER written into catalog files, examples, scripts, or docs — the verifier reads
`TREG_CATALOG_CRED` from the environment only. Captured examples are truncated (arrays → 2 items,
long strings clipped, ~10 KB cap) by the verifier, then human-reviewed for PII before commit.
## Operator-supplied pricing evidence
`catalog_ingest.py` requires `TREG_CATALOG_EVIDENCE_DIR` for AnyAPI and JustOneAPI imports.
The directory supplies `anyapi_measured_charges.json` (`skus`, `as_of`, `window_days`) and
`justoneapi_prices.json` (`prices`). Missing files fail before catalog output is written, rather
than silently replacing measured prices with estimates or dropping dashboard prices. Other
providers do not require these files. Published catalog prices remain part of the public product;
private ledger exports and account evidence do not. Hosted operators maintain the inputs in
`treg-internal/tools/catalog-evidence/`.
+1 -1
View File
@@ -346,7 +346,7 @@ uses this metadata, never the encrypted token's shape.
signal one step before a `ToolRequest`: most agents that miss never file, so the query text is all
they leave. Written fire-and-forget through `audit.record_search_miss` (dropped rows cost
analytics, never a search) from both search paths - `GET /catalog/search` and the in-process MCP
`catalog_search` tool. Deliberately identity-free; surfaced by `scripts/usage_report.py`, which
`catalog_search` tool. Deliberately identity-free; surfaced by the private usage report, which
reads misses against the catalog to split coverage gaps from naming/discovery failures.
- **`SearchLog`** (0041) - one MCP catalog search under the **discovery experiment** (see
[search-experiment](search-experiment.md)): `query`, `source`, the caller's `org_id`/`user_email`
-1
View File
@@ -13,7 +13,6 @@ sources:
- src/treg/routers/auth.py
- src/treg/web/claude-connector.html
- src/treg/web/connect-demo.html
- docs/CLAUDE-CONNECTOR-SUBMISSION.md
- tests/test_mcp.py
- tests/test_mcp_oauth.py
- tests/test_mcp_directory.py
+3
View File
@@ -97,3 +97,6 @@ of the Alembic baseline schema; a live DB picks up schema changes through the ex
`admin grant|revoke|suspend-user|rm-user|suspend-org|rm-org`, and
`admin credit <org_id> --amount-usd <n> --ref <ticket> --reason <text>`. `_admin_client` sends the saved
`admin_token` if present, else the active org token (works for an `is_superadmin` user).
The standalone grant script uses the configured database session and does not manage hosted
network access. Operators must use a checkout matching the target schema.
+1 -1
View File
@@ -447,7 +447,7 @@ validated before resolving the shared HTTP client. `/auth/logout` remains an HTT
token provides the same human and team attribution as an older hash-backed membership token.
Sources distinguish HTTP, team MCP and the Claude connector. Tool requests may attach a token
or same-origin session identity; cross-origin cookie submissions remain anonymous.
`scripts/usage_report.py` reports this unserved demand.
The private usage report summarizes this unserved demand.
- **Identity doors:** GitHub, Google and email OTP share first-proof user provisioning. They create
a user without an automatic org; new users name their first team through onboarding or the CLI
+3 -12
View File
@@ -157,23 +157,14 @@ this shell — agent pages, programmatic use-case pages, the hubs, `/docs` — w
attribution. The failure is silent and total: no `adtrack.js` means no `treg_ad` cookie, so
`signup._ad_attribution_from()` returns empty, `org.ad_gclid` stays NULL, and `adsconv.queue()`
no-ops by design, so a paid click could sign up and make its first call with Google never hearing
about it. Nothing errors and nothing logs. It surfaced from the ads side: the Agent × job campaign
spent A$125 over three days landing every click on `/agents/*` for zero recorded conversions, while
campaigns pointing at the static use-case pages recorded normally.
about it. Nothing errors and nothing logs.
The scope is **`_page()` callers**, not "every server-rendered page". `_legal_page()` (`/terms`,
`/privacy`, `/support`, `/contact`), `/dashboard-tour/` and the FastAPI Swagger shell at `/docs/api`
render their own HTML and remain uninstrumented — none is an ad destination. `/tutorial` is likewise
out of scope; it is slated for removal. The `.md` variants are `text/plain` and cannot run scripts.
That scope left a third class uncovered, and the same failure repeated on it (2026-09-06).
`/people-search`, `/grokbot` and `/fable` are standalone hand-written HTML behind their own routes:
off the shell, so `_page()` does not reach them, and absent from the hand-kept list in
`test_every_public_landing_surface_loads_the_capture_script`, so nothing failed. All three are ad
destinations — the Demand Gen campaign pointed S1, S2 and S3 at `/people-search` — and for three
days 4,892 clicks landed on a page that could not capture a click id. The DB holds no GCLID from
that window at all, which reads identically to an audience that simply does not convert: the
measurement failure and the outcome it was meant to measure are indistinguishable from the numbers.
Standalone HTML routes (`/people-search`, `/grokbot` and `/fable`) also need capture coverage.
All three now carry the tag, and the guard no longer depends on anyone remembering:
`test_every_public_html_route_carries_the_capture_script` sweeps every flat GET route on the app,
keeps whatever answers `200 text/html`, and requires the tag on all of it. Adding a route that
@@ -370,7 +361,7 @@ points at the shared one.
"I use ChatGPT — what can it do now?" answered on one server-rendered URL per client. The first is
`/agents/chatgpt`; the set is the keys of `agent_pages.AGENTS`, and nothing else (an unknown agent
404s). They came out of the programmatic-SEO plan in `marketing/pseo-build-spec.md`: the measured
404s). They came out of the programmatic-SEO plan in `https://github.com/superdesigndev/treg-internal/blob/main/docs/marketing/pseo-build-spec.md`: the measured
demand is for the *agent* ("chatgpt connectors") and the *platform* ("linkedin api pricing"), never
for "how to <job> in chatgpt", so the job list lives on the agent page as rows, not as URLs.
+1 -1
View File
@@ -74,7 +74,7 @@ served**, because a second copy of the product's most-read page is a copy that r
|---|---|---|
| the installer | `install.sh` → `treg skill bootstrap` → every detected agent's skills dir, the treg skill plus every workflow skill in `/.well-known/skills/index.json` | people who ran the curl one-liner |
| Claude Code plugin | `.claude-plugin/` + generated `skills/treg/SKILL.md` (repo root) | `/plugin marketplace add superdesigndev/treg` |
| Codex/ChatGPT plugin | `plugin/.codex-plugin/` + generated `plugin/skills/treg/SKILL.md` | the directory ChatGPT and Codex share. Submission runbook: [docs/PLUGIN-SUBMISSION.md](../../PLUGIN-SUBMISSION.md), test cases: [skill-openai-test-cases.md](skill-openai-test-cases.md), per-tool justifications: [skill-openai-tool-justifications.md](skill-openai-tool-justifications.md) |
| Codex/ChatGPT plugin | `plugin/.codex-plugin/` + generated `plugin/skills/treg/SKILL.md` | the directory ChatGPT and Codex share. Submission runbook: [private submission runbook](https://github.com/superdesigndev/treg-internal/blob/main/docs/distribution/PLUGIN-SUBMISSION.md), test cases: [skill-openai-test-cases.md](skill-openai-test-cases.md), per-tool justifications: [skill-openai-tool-justifications.md](skill-openai-tool-justifications.md) |
| Cursor plugin | `.cursor-plugin/marketplace.json` + generated `plugins/treg/skills/treg/SKILL.md` | the Cursor marketplace (plugin root is never the repo root) |
| DeepSeek Harness bundle | root `package.json` (`dsh.bundle`) + `dsh/cordis.patch.yml` + generated `dsh/skills/treg/SKILL.md` | `dsh plugin --profile <name> add github:superdesigndev/treg` |
| MiniMax plugin | `plugins/minimax/.minimax-plugin/plugin.json` + generated `plugins/minimax/skills/treg/SKILL.md`; `scripts/minimax_plugin.py` pre-runs their validator and builds the ZIP | the MiniMax Plugin Marketplace (MiniMax Code + MiniMax Agent), submitted by form as GitHub subdir `plugins/minimax`; skills-only because the package may hold no credential and the bootstrap omits `treg mcp install`, which cannot write a MiniMax config. See [docs/MINIMAX-PLUGIN.md](../../MINIMAX-PLUGIN.md) |
-224
View File
@@ -1,224 +0,0 @@
# MCP client disconnect 被记为 500 的修复设计
状态:待评审,仅设计,不提交。
## 问题边界
生产中的 `POST /mcp/` 500 集中来自单个重试客户端,同时其他 20 至 28 个客户端持续得到
200。修复目标不是改变 MCP 协议或会话模型,而是消除错误归因:客户端已经断开且下游未发送
响应时,应用不应再由外层 HTTP middleware 合成服务端 500 和堆栈日志。
下列行为是约束,不在本次设计中改变:
- MCP 继续使用 `stateless_http=True` 和 `json_response=True`。
- `/mcp` 继续经过全局安全响应头和 legacy host 规则。
- `X-Treg-Body-Encoding` 解码顺序和行为不变。
- `/call/{rest:path}` 的请求、响应和 faithful-relay 合同不变。
- 真正的应用异常仍然向上传播并按现有错误路径处理,不能被当成断连吞掉。
## 根因验证
### 1. 代码路径
`bootstrap.create_app()` 当前组装出的 middleware 顺序由外到内为:
1. `_BodyDecodeMiddleware`,纯 ASGI
2. `BaseHTTPMiddleware(dispatch=_security_headers)`
3. `BaseHTTPMiddleware(dispatch=_legacy_host_redirect)`
4. FastAPI 路由和挂载的 `/mcp` ASGI app
`build_mcp_app()` 创建 stateless Streamable HTTP transport。每个请求结束时,MCP SDK 都会终止
该请求的 transport,所以生产日志中的 `Terminating session: None` 与 stateless 模式一致,不代表
共享会话损坏。
Starlette 的 `BaseHTTPMiddleware.call_next()` 使用 AnyIO memory stream 接收下游 ASGI 响应。若下游
在发送 `http.response.start` 前结束并关闭 stream,且没有可重新抛出的下游异常,`call_next()` 会把
`anyio.EndOfStream` 翻译为:
```text
RuntimeError: No response returned.
```
这与生产堆栈中 `starlette/middleware/base.py`、`_legacy_host_redirect`、`_security_headers` 和
`_BodyDecodeMiddleware` 的顺序完全吻合。`_BodyDecodeMiddleware` 只是堆栈中更外层的纯 ASGI
middleware,不是生成该 RuntimeError 的位置。
### 2. 本地端到端复现
在当前分支用临时 SQLite 启动真实 `treg.api:app`:
```bash
TREG_DATABASE_URL=sqlite+aiosqlite:////tmp/treg-mcp-disconnect/treg.db \
TREG_PUBLIC_URL=http://127.0.0.1:8000 \
TREG_EMAIL_DEV_MODE=true \
uv run --frozen uvicorn treg.api:app --host 127.0.0.1 --port 8000 --log-level debug
```
验证结果:
- 正常 MCP initialize 请求返回 200,并包含现有四个安全响应头。
- 用 raw TCP socket 在完整 POST 发出后立即 RST,以及声明较大 `Content-Length`、只发部分 body
后 RST,真实 MCP transport 都进入 `Terminating session: None` 路径。
- 当前本地依赖组合下,精确的生产 RuntimeError 竞态不能稳定重现。部分断连由 Uvicorn 取消请求
task,另一些由当前 MCP SDK 捕获并尝试生成内部响应。因此不能声称本地完整复现了生产 500。
### 3. 确定性边界复现
为隔离竞态,用仓库当前的 `_legacy_host_redirect` 和 `_security_headers` 包住一个最小 ASGI 下游。
该下游收到 `http.disconnect` 后正常返回,但不发送 `http.response.start`。通过
`uv run --frozen python` 直接驱动同样的 ASGI scope,稳定得到:
```text
builtins.RuntimeError: No response returned.
```
因此证据链为:
- 真实 socket abort 可以稳定触发 MCP transport 的 terminate 分支。
- 生产堆栈直接证明该次竞态中 MCP 未发送 response start,并由 BaseHTTP 适配层生成 500。
- 确定性边界测试证明,只要下游以这一方式结束,仓库当前两个 BaseHTTP middleware 必然产生
同一 RuntimeError。
结论:根因判断成立。缺陷不在 MCP stateless 配置,也不在 `_BodyDecodeMiddleware`,而在下游断连
结束语义与 `BaseHTTPMiddleware.call_next()` 响应流假设不兼容。精确触发窗口受 Uvicorn、AnyIO 和
MCP transport 调度影响,所以本地真实网络复现不是确定性的。
## 方案比较
| 方案 | 做法 | 优点 | 风险和代价 | 结论 |
|---|---|---|---|---|
| A. 两个 middleware 改为纯 ASGI | 将 legacy redirect 和 security headers 各实现为 ASGI middleware class,保持现有嵌套顺序 | 移除产生错误翻译的 memory-stream 和 `call_next` 层;不依赖异常字符串;对所有断连时序都符合 ASGI 语义;真正异常仍传播 | 作用于所有 HTTP 路径,必须严格锁定 headers、redirect、非 HTTP scope 和注册顺序 | 推荐 |
| B. 让 `/mcp` 绕开全局 middleware | 在 FastAPI 外层按 path 分流,或单独挂载一套不含 BaseHTTP 的 app | 可以把变更限制在 MCP 表面 | 会复制 mount、`root_path` 和 lifespan 组合;若直接绕开会丢安全响应头、legacy host 行为和 body decode;若重新包裹又形成第二套容易漂移的 middleware 栈 | 不采用 |
| C. 识别断连后静默或记 499 | 在更外层捕获 `RuntimeError("No response returned.")`,结合 disconnect/path 判断后抑制或改记 499 | 代码改动表面最小 | 依赖 Starlette 内部异常文字;难以可靠区分客户端断连与真正的“应用忘记发响应”;仍保留 BaseHTTP 的 task 和 stream 问题;客户端已断开时无法实际收到 499 | 不采用 |
499 是反向代理常用但非标准的观测状态。连接已经消失时,应用不能向客户端可靠发送 499。若边缘或
访问日志能识别断连,可以把该请求记为 499;应用层修复只需允许 ASGI task 无响应结束,不再虚构
500,也不应新增一个写往已关闭 socket 的响应。
## 推荐设计
选择方案 A,将 `_legacy_host_redirect` 和 `_security_headers` 改成两个纯 ASGI middleware class。
### Legacy host redirect
- 只处理 `scope["type"] == "http"`,其他 scope 原样透传。
- 用 Starlette `Request` 读取现有 method、Host、path、query 和 cookie,不读取 body。
- 完整保留现有判断:仅 GET/HEAD;legacy host 精确匹配;匿名 marketing path 为 301;auth entry
为 302;session cookie 阻止 marketing redirect;canonical host、自托管 legacy host、lookalike
host 和所有其他路径原样透传。
- 命中时直接调用 `RedirectResponse(scope, receive, send)`;未命中时直接
`await self.app(scope, receive, send)`,不创建 memory stream。
### Security headers
- 只处理 HTTP scope,其他 scope 原样透传。
- 包装 `send`,仅在 `http.response.start` 上补齐现有四个 header:
`X-Content-Type-Options: nosniff`、`X-Frame-Options: DENY`、
`Referrer-Policy: no-referrer`、
`Strict-Transport-Security: max-age=31536000; includeSubDomains`。
- header 查找必须大小写不敏感,并保持当前 `setdefault` 语义。若 `/call` 或其他下游已经给出同名
header,不能覆盖、删除、重排或重复添加它。
- 若下游因断连而在 response start 前正常结束,该 middleware 也正常结束且不发送任何响应。
- 若下游抛出真正异常,原样传播,不按 path、异常类型或文字吞掉。
### 注册顺序
继续由 `bootstrap.create_app()` 统一组装。三个 `add_middleware()` 调用必须保持最终嵌套顺序:
```text
BodyDecode -> SecurityHeaders -> LegacyHostRedirect -> routes/mounts
```
Starlette 的 `add_middleware()` 会 prepend,实施时要用测试和 composition snapshot 核对实际顺序,
不能只根据代码阅读假定顺序。`all`、`control`、`dataplane` 三个 role 使用同一规则。
## 快照影响
这是有意的 composition 行为变更。
`tests/snapshots/composition.json` 预计只有两个 middleware 条目改变:
- `starlette.middleware.base.BaseHTTPMiddleware` 加
`dispatch=treg.api._security_headers` 改为新的 security 纯 ASGI class,kwargs 为空。
- `starlette.middleware.base.BaseHTTPMiddleware` 加
`dispatch=treg.api._legacy_host_redirect` 改为新的 legacy redirect 纯 ASGI class,kwargs 为空。
- 三层顺序仍为 BodyDecode、Security、Legacy。
`routes.json`、`openapi.json`、`lifespan.json` 不应有任何 diff。实现时应在同一个代码 commit 中运行:
```bash
uv run --frozen python scripts/dump_surface.py
uv run --frozen python -m pytest -q tests/test_surface_snapshot.py
```
只接受上述 composition diff。commit message 正文要明确说明:composition snapshot 更新是因为两个
BaseHTTP wrapper 被纯 ASGI middleware 替代,用于正确处理客户端断连,不是路由或 OpenAPI 变更。
## 回归风险和验证矩阵
### 客户端断连
- 增加确定性回归测试:以生产相同顺序组装三个真实 middleware,挂一个收到
`http.disconnect` 后不发 response 的 ASGI sentinel,断言调用正常结束、没有
`RuntimeError`、没有伪造 `http.response.start`。
- 增加正常 MCP initialize 集成断言,确认 200、JSON response、认证挑战和安全 header 均保持。
- raw socket RST 适合作为实现后的手工 E2E 验证,但它依赖调度时序,不作为要求每次稳定触发日志的
CI 断言。验收标准是服务无 500 traceback、正常 MCP 客户端仍成功。
### 安全响应头
- 扩充现有 `test_security_headers_on_api`,明确断言四个 header 的值。
- 覆盖普通 2xx、404 或受控错误响应,确保纯 ASGI send wrapper 在不同响应路径都生效。
- 增加下游预设同名 header 的单元测试,证明大小写不敏感的 setdefault,不覆盖 `/call` 更严格的值。
- 保留 `tests/test_mcp_oauth.py` 中 MCP 响应的 `X-Frame-Options` 断言。
### Legacy host 301/302
- 完整运行 `tests/test_legacy_host_redirect.py`。它已覆盖匿名 marketing 301、query 保留、session
cookie、不带条件的 auth entry 302、API/MCP 相关 surface 不跳转、POST 不跳转、canonical host、
lookalike host 和自托管边界。
- 增加或确认 HEAD 与 GET 同规则,redirect response 的 Location 和 status 完全一致。
### `X-Treg-Body-Encoding`
- `_BodyDecodeMiddleware` 代码不改,且仍为最外层。
- 完整运行 `tests/test_body_encoding.py`,覆盖 base64、gzip、组合解码、Pydantic JSON、`/call` relay、
malformed 400 和无 header 透传。
- composition snapshot 明确锁定 BodyDecode 仍在 Security 和 Legacy 之外。
### `/call/` 完全不受影响
- 运行完整 `tests/callmatrix/test_matrix.py`,验证 faithful relay、错误分类、计费和重试矩阵不变。
- 运行 `tests/test_body_encoding.py::test_proxy_relays_decoded_body_upstream`,证明解码后的请求体仍原样
进入 `/call`。
- 增加或保留一个 `/call` 下游自带安全 header 的断言,证明 security middleware 只补缺失值。
- `routes.json` 和 `openapi.json` 零 diff,证明 `/call/{rest:path}` 路由形态未变。
### 建议验收命令
```bash
uv run --frozen python -m pytest -q \
tests/test_mcp_oauth.py \
tests/test_legacy_host_redirect.py \
tests/test_bughunt_server.py \
tests/test_body_encoding.py \
tests/test_surface_snapshot.py
uv run --frozen python -m pytest -q tests/callmatrix/test_matrix.py
uv run --frozen python -m pytest -q
```
实现后还应启动真实 Uvicorn,重复正常 MCP initialize 和 raw socket abort 手工 E2E。最后运行
`scripts/drift.sh`,并确认只有 `composition.json` 产生上述预期 diff。
## 评审后实施范围
若本设计通过,代码改动应限制在:
- `src/treg/api.py`:两个纯 ASGI middleware class。
- `src/treg/bootstrap.py`:以纯 ASGI class 注册并保持顺序。
- 对应 middleware/MCP 回归测试。
- `tests/snapshots/composition.json`:同 commit 更新。
- `docs/context/architecture/composition.md`:同 commit 记录纯 ASGI middleware 约束和断连语义。
不改变 MCP transport 配置,不新增 `/mcp` 特殊分流,不返回应用层 499,不改变 `/call`、路由、
OpenAPI 或 lifespan。
File diff suppressed because it is too large Load Diff
@@ -1,163 +0,0 @@
# Google Ads conversion tracking for treg — design
**Date:** 2026-08-17
**Branch:** `feat/ads-conversion-tracking`
**Ads account:** `5149790776` ("Treg"), AUD, `Australia/Sydney`, client of the Shirley Lou MCC (`3519125194`)
## The problem
treg has no analytics instrumentation of any kind — no `gtag`, GTM, GA, PostHog or Plausible, in the
repo or on the live site. The Ads account's only conversion action (`Purchase`, `7722657530`) is a
`WEBPAGE` action created by the signup wizard; with no tag on the site it has never fired and never
could.
`marketing/landing/_measurement.md` states the consequence: *"A vertical whose visitors sign up and
never call looks identical to a vertical whose visitors sign up and call daily."* Spending against
this returns a verdict on the wrong metric.
## Goal
Record three conversions against the ad click that produced them: **signup**, **first successful
call**, and **first top-up**.
## Decisions
| Decision | Choice | Why |
|---|---|---|
| Conversion set | signup + first call + first top-up | `_measurement.md` names first call as the metric that decides whether a vertical is real |
| Mechanism | server-side upload (`uploadClickConversions`) | a first call happens in a CLI/MCP client with no browser; a tag physically cannot see it |
| Top-up counting | once per org, not per payment | optimises for acquiring payers; value-based bidding needs volume treg does not have yet |
| Currency | fixed rate, 1 AUD = 0.70 USD | stable reported value; `aud_micro = usd_micro * 10 // 7`, integer only |
| Ad landing | five usecase pages for those verticals, homepage otherwise | landing-page relevance feeds Quality Score, which sets CPC |
### Decisions recorded against a documented position
- **`_measurement.md` treats signup as a weak metric** ("signups measure curiosity"). Signup is
therefore created as a **secondary** action — tracked, but not something Google bids toward.
- **`wiki/topics/treg.to SEO.md` §9 (2026-08-17) states "No ads for treg"**, on the grounds that the
preconditions — a working funnel, ~50 tracked conversions, call instrumentation — are unmet. This
work *builds* the missing precondition; it does not authorise spend. The wiki's inherited
thresholds (~50 conversions to learn, 2–3 weeks to read, 3 months to judge) still apply before any
campaign verdict is trusted.
- **Competitor-alternative keywords**: §3 finds that family declining across the board
(`semrush alternative` −73%, `ahrefs alternative` −52%, `clearbit alternative` −48%,
`bright data alternative` −64%) and "the highest-competition, lowest-CTR shape". Deliberately left
open — nothing in this design depends on keyword choice.
## Architecture
```
ad click ──► landing page ──► first-party cookie ──► signup ──► Org row
│
signup ─┐ │ ad_gclid
first call ─┼──► AdConversion outbox (uploaded_at NULL) ◄─────┘
first top-up ─┘ │
▼
background drain ──► Ads API uploadClickConversions
```
### 1. Capture (client)
Ten lines of first-party JS on the homepage and, when they ship, the five usecase pages. Reads
`gclid`, `gbraid`, `wbraid` (Google substitutes the latter two on iOS traffic; omitting them silently
drops mobile conversions) and `utm_content` (carries the page id `p1`…`p5` per `_measurement.md`).
Writes a `SameSite=Lax` cookie with a 90-day life, matching Google's click-conversion window. No
Google script, no third-party request.
### 2. Store — new columns on `Org`
| column | purpose |
|---|---|
| `ad_gclid` | the click id, kept for the life of the team |
| `ad_click_at` | uploads must postdate the click and fall inside 90 days |
| `ad_landing` | which page, for the per-page hypotheses |
| `first_call_at` | the decisive metric; also the guard that makes first-call fire once |
New table `AdConversion(org_id, action, dedupe_key, uploaded_at, error, attempts)`, unique on
`(org_id, action)`. Migrations follow the existing `create_all` + guarded `ALTER` pattern in `db.py`;
the project does not use Alembic.
### 3. Fire — three existing chokepoints
- **Signup** → `_grant_signup_promo()` (`api.py:3748`), called from exactly two doors,
`register_user()` (3814) and `create_org()` (3877), already idempotent per `(org, kind)`.
- **First call** → guarded `UPDATE ... WHERE first_call_at IS NULL` in the `/call/{rest:path}`
handler (`api.py:9098`) on a successful upstream response. **Not** `ledger.reserve` — a team using
its own key is never metered and never touches the ledger, so a ledger hook would miss exactly
those teams.
- **First top-up** → `_credit()` in `billing.py`, on the existing branch that distinguishes "this
delivery moved money" from a webhook redelivery.
Signup and first-call write their outbox row **in the same transaction as the event**, so the event
and its pending conversion commit or fail together.
**The paid path is the exception, and it is not atomic.** `ledger.topup()` commits internally
(`ledger.py:36`) before `_credit()` reaches the conversion code, so the credit and the conversion are
two separate commits. If the process dies between them the conversion is lost permanently — a Stripe
redelivery finds the payment already credited, `fresh` is False, and the fire site never runs again.
No error is raised and nothing detects it.
This is accepted, deliberately (2026-08-17). The window is sub-millisecond, the blast radius is one
missing conversion rather than lost money or a failed webhook, and closing it properly would mean
restructuring `ledger.py` — the only code path that moves money — to serve a marketing feature. A
reconciliation sweep (find orgs with a credited payment and a `gclid` but no `paid` row, backfill it)
is the cheap fix if this ever proves to matter in practice.
### 4. Upload — a durable outbox, never fire-and-forget
A background worker drains rows with `uploaded_at IS NULL` and POSTs to
`v22/customers/5149790776/conversionUploads:uploadClickConversions` with `partialFailure: true`,
recording each row's result individually. The request path never waits on Google.
This also solves a timing problem that otherwise reads as random data loss: **a `gclid` is not valid
for upload immediately.** Google needs several hours after the click before it will accept a
conversion against it, so anything uploaded at signup time is rejected. Retry-with-backoff turns
"rejected, gone" into "sent a few hours later". Rows failing permanently (expired click, malformed
id) are marked with the error code rather than retried forever.
**Nothing routes through `audit.py`.** It sheds rows past `_MAX_PENDING = 5000` — correct for
analytics, and it would undercount conversions exactly when traffic peaks (`CLAUDE.md`, money-code
rule).
## Done already (2026-08-17)
Created on the live account via the Ads API, `validateOnly` first:
| ID | Name | Type | Category | Primary |
|---|---|---|---|---|
| `7723667014` | treg Signup | `UPLOAD_CLICKS` | `SIGNUP` | no |
| `7723667017` | treg First successful call | `UPLOAD_CLICKS` | `QUALIFIED_LEAD` | **yes** |
| `7723667020` | treg First top-up | `UPLOAD_CLICKS` | `PURCHASE` | **yes** |
`Purchase` (`7722657530`) demoted from primary — a `WEBPAGE` action cannot receive uploads, and
leaving it primary meant Google bidding toward a goal that would always read zero.
**API version is `v22`.** `v21` — the version pinned in `.agents/skills/google-ads/SKILL.md` — now
returns `UNSUPPORTED_VERSION`. That file needs updating.
## Deferred
The five usecase landing pages (`usecase-*.html`, `usecase.css`, `resources.html`) are untracked and
therefore undeployed, so their routes (`api.py:2315`) 404. The capture snippet is written as one
shared include so adding them later is a one-line change per page.
## Verification
No test `gclid` exists, so the chain cannot be proven without a real click:
1. One Search campaign, minimum budget, tight geo.
2. A genuine click on your own ad.
3. Confirm `gclid` → cookie → `Org.ad_gclid`.
4. Walk the funnel: sign up, install agent, one call, minimum top-up.
5. Wait — uploads cannot succeed for several hours after the click, and conversions take up to 24h
to surface in the UI. Do not conclude failure before then.
6. Assert three conversions, and that a US$20 top-up reads **A$28.57**. A$20.00 or A$14.00 means the
currency math is wrong.
## Risks
- **Fixed FX drifts.** Named constant with the date it was set, not a magic number.
- **Privacy policy.** `privacy.html` needs a line on advertising measurement; a first-party cookie is
still advertising data.
- **PMax campaign `24150011650`** sits paused at A$44.81/day on a signup-wizard default. It should be
restructured or deleted, not unpaused as-is.
-54
View File
@@ -1,54 +0,0 @@
# Risk audit of the programmatic pages — `/seo-playbook`, 2026-08-21
Run against the kill list and extraction rules in `.claude/skills/seo-playbook` (Lily Ray on what
Google punishes, Kevin Indig on how LLMs select, Fishkin/Natividad on the zero-click response).
Subject: 4 agent pages, 7 use-case pages, 1 hub, plus the 66-job menu they are the first slice of.
## Kill list: no hits, and two near-misses worth naming
| # | Tactic | Verdict |
|---|---|---|
| 1 | Self-promotional "best X" ranking our own brand first | **Clear.** The comparison ranks *providers*; treg.to is the access layer and is not one of the ranked items. This also sidesteps Ray's law 5 (69% of self-promotional listicle citations ended with the citing brand excluded from the recommendation) |
| 5 | Comparison / "X alternatives" at scale | **Near-miss, structurally avoided.** The banned shape is one page per competitor pairing. Ours is one page per *job* with every provider on it, so nine providers produce one page, not 36 pairings. Keep it that way |
| 6 | FAQ farms | **Clear.** Four questions inside a page, never one question per URL |
| 3 / 7 | Templated scaling with minimal uniqueness | **Near-miss, and the real risk for waves 2 and 3.** 12 pages with hand-written notes and quoted research is fine; 66 generated from the same shell would be the fingerprint Ray describes. The `treg-page` skill exists to keep the per-page research mandatory |
| 4 | Artificial freshness | **Clear.** Dates come from the catalog's own verification stamp, so nothing bumps without a real re-check |
| 10 | Schema misuse | **Was a hit, now fixed.** `SoftwareApplication` carried `Offer price: "0"`, which reads as "the product is free" while the page says installing is free and calls are metered. The offer now says exactly that |
| 8, 9, 11 | Hidden instructions, bought consensus, one page per query | **Clear** |
## Extraction rules (Indig)
- **Answer in the first 30%** — passes. The provider count, the from-price, the $0.000 markup and the
named cheapest provider all appear before the 30% mark; the economics block sits above the fold on
desktop.
- **15+ unique data points** — passes comfortably (per-provider price, billing unit, accepted inputs,
verified date, success rate and latency where measured). The bar Indig cites is that the top ten
average only about four.
- **Methodology boxed** — **was missing, now added.** Every comparison page carries a four-box block:
how prices are derived, what the success rate counts (2xx vs 5xx, 4xx excluded), what it is not
(not a controlled benchmark), and what "verified" means.
- **Visible date** — **was missing, now added** to the hero subline ("last checked 2026-08-20").
- **Named author** — **still missing.** Indig's data says named authors outperform brand bylines.
This needs a person's name and is Jason's call, not something to invent.
- **Focused beats ultimate guide, but cover the intents inside the page** — passes: one job per page,
with the prompt, the comparison, the caveats and the FAQ all inside it.
## Where the pages are weakest, in order
1. **No off-site presence.** Nothing in this repo fixes it. Indig and Ray converge here: authority is
per-topic, three placements in sources the models already cite beat a dozen scattered mentions, and
nofollow counts for AI mentions. The directory submissions in `pseo-ship-plan.md` (mcpservers.org,
PulseMCP, Glama, Smithery, Docker MCP, Anthropic's connector directory) are the highest-leverage
remaining work, and they are Jason's to send.
2. **No AI-visibility measurement.** Indig's method is polling, not rank tracking: ~40 seed prompts,
five runs per platform per week, per-platform scores with confidence intervals, never one blended
score. Nothing like this exists yet. Ray's caveat applies: treat it as directional.
3. **Scaled-content risk lives in waves 2 and 3, not in what shipped.** The guard is the research
step, and it is only a guard while someone actually runs it.
## Kept deliberately, against a rule
- **Traffic is not the KPI.** Fishkin's decoupling argument and the wiki agree: the verdict metric is
repeated successful calls per landing page, which still needs `first_call_at` attribution.
- **Comparison pages at all.** Ray's caution is about scale and self-promotion. These are neither, and
the first-party price and reliability data is the thing no competitor can regenerate.
@@ -1,204 +0,0 @@
# AI outbound in 2026 — the playbook people run, what Reddit asks, and what treg.to should publish
Research date: 2026-08-27. Sources: X (OpenCLI, 6 queries + threads of @itsalexvacca, @fivosaresti, @IAmAaronWill, @aryanXmahajan), Reddit (OpenCLI, 6 queries, 12 threads read with comments), YouTube (yt-dlp, 5 queries, 8 full transcripts), web (8 playbook articles incl. Eric Nowoslawski / Explorium / SyncGTM / Salesforge / Koka Sexton / Yalc / SalesRobot / Growth Unhinged). LinkedIn covered via the web playbooks and the LinkedIn-specific X/YouTube systems (the LinkedIn MCP isn't configured on this machine).
---
## 1. The system everyone runs (the 2026 skeleton)
Eight YouTube systems, eight web playbooks and the top X operators converge on the same nine steps. Where they disagree is the orchestrator, not the shape.
| Step | What it is | Who says it | Tools they name |
|---|---|---|---|
| 0. Context layer | ICP, exclusions, offer, email framework in a `CLAUDE.md` (<250 lines) + `icp/`, `messaging/` markdown | SyncGTM, Salesforge, Growth Unhinged, Lead Gen Jay ("strategy skill runs before scraping") | Claude Code, Notion |
| 1. Signal in | Hiring (esp. SDR/AE/VP Sales), funding, job/leadership change, tech-stack change, news, site visitors, LinkedIn engagement | Everyone. "Outbound stopped paying for volume, started paying for homework" | PredictLeads, Clay, RB2B, Apify LinkedIn Jobs, Crustdata |
| 2. Small list | 50–250 (Growth Unhinged), 300–1,000 (SyncGTM), 20–30 accounts (Koka). Lists < 30 days old | @itsalexvacca: "write the entry rule first — 5 to 10 signals must be true" | Apollo search (via Apify because "export is too expensive"), Sales Nav |
| 3. Qualify before you spend | AI yes/no + reason; drops ~40% of list, "2–3x reply rate" | Lead Gen Jay, Apollo, Explorium scoring weights | Claude/GPT |
| 4. Waterfall enrich + verify | 6 providers vs 1 = 50–70% → 85–95% email coverage; verify everything; bounce < 1% | SyncGTM, Eric N., Reddit | Findymail, Hunter, LeadMagic, Anymail, MillionVerifier |
| 5. Research → one-signal personalization | Scrape site/posts → LLM abstract → LLM **fills a fixed template**, never writes whole emails (A/B-ability). ≤85 words, no links in email 1 | Saraev, Clarence Nap, SyncGTM. Dissent: Marc ("hyper-personalization can backfire") | Perplexity Sonar ("way cheaper than a Clay agent"), Firecrawl, Exa |
| 6. Send from real infra, never from the agent | Secondary domains, SPF/DKIM/DMARC, 2–4 week warmup, 30–40/inbox/day, cancel a domain at < 0.7% reply | Every source | Instantly, Smartlead, Lemlist, Email Bison |
| 7. LinkedIn inside the caps | 100 invites / rolling 7 days, 20–25/day, new accounts 5–10/day for 4 weeks, note < 300 chars, acceptance > 25–40% or you're throttled. Comment-first warming is the 2026 move; browser-click automation gets whole user bases banned | Yalc, SalesRobot, Saraev | PhantomBuster, HeyReach, Unipile API, Claude-in-Chrome |
| 8. Reply classification, human conversation | interested / not now / OOO / referral / unsubscribe / angry → tag CRM, pause sequence. Reply within a minute "≈4x conversion" | Explorium, Yalc, Saraev | Claude, Supabase vectors |
| 9. Weekly loop | Reply rate by segment, top/bottom first lines, 20–50 simultaneous tests, control inboxes to tell "me vs the platform" | Eric N., SyncGTM, r/Coldemailing | Sheets/Supabase |
**The orchestrator war (this is the story for treg.to):**
- **n8n/Make** (Saraev ×2, Clarence Nap, three of the Reddit "my system" posts) — visual, Sheets as the database, polling loops for async scrapers.
- **Claude Code + skills** (Lead Gen Jay, @fivosaresti "we took every UI out of our outbound stack", @itsalexvacca "21 MCP connections run our GTM from one terminal", Eric N. "2M lines in 45 days") — the fastest-growing camp. Lead Gen Jay explicitly built a "Clay killer" = Perplexity + Claude + own DB. Growth Unhinged: mature teams are moving **MCP → API/CLI** because MCP is 10–32x more expensive in tokens.
- **Claude Cowork + vendor MCPs** (Automate with Marc, the $10K LinkedIn video) — Apollo/Clay/HubSpot/Lemlist connectors, no code.
- **All-in-one** (Apollo, AI SDRs) — Reddit is openly hostile: "AI SDR is a bullshit category, $45M later still can't sell"; "over half of buyers churn inside 90 days".
**What Clay's moat actually is, in the operators' own words:** Eric Nowoslawski keeps Clay for "150+ pre-negotiated data providers — one datapoint from HG Insights without an HG contract" and "automatic API queuing/rate-limit handling across 50 tables". r/gtmengineering: "Clay is great because of their data sources all coming together in one place… what data sources do you use?" That is, word for word, the catalog's pitch — one token, many providers, per-call, no contracts — and Clay's price ("exorbitant", "50–70% cheaper" is the winning alt pitch) is the wedge.
**Numbers that recur** (vendor benchmarks, quote with attribution): cold-email reply 5.1% (2024) → 3.43% (2026, Woodpecker); signal-triggered 4–8% vs generic 1–3%; the 5% who personalize every email get 17–18%; LinkedIn reply 10.3% vs email 5.1% (Expandi); Gmail spam-complaint cap 0.3% and it's **cumulative**, "one bad AI blast poisons a domain you warmed for months"; B2B data decays ~2%/month; Apollo rows: ~66% have LinkedIn URL, ~77% have email, ~25% of sites unscrapable, so plan 10K rows → ~2K deliverable.
**Copy-paste assets the sources already show (we can do these better with real calls):**
- SyncGTM's `CLAUDE.md` ICP block and 3-line email framework (hook/problem/CTA, ≤85 words).
- Explorium's `SCORING_WEIGHTS` dict (funding_90d 30, sales_hiring_spike 25, eng_hiring_spike 20, intent 20, technographic 15, leadership_change 10; threshold 40).
- Saraev's icebreaker prompt ("Spartan, paraphrase never quote, shorten company names, never 'love your website'") and his few-shot layout.
- Clarence Nap's hiring-signal template ("Saw you've got some SDR roles open…").
- Koka's two-sentence signal email ("Saw your comment on the pipeline-velocity post this week. Are you solving that now, or researching how others handle it?").
- SalesRobot's five LinkedIn scripts (connect / follow-up / job change / funding / post engagement).
- Eric N.'s Friday domain-health cron and "guess 6 permutations → verify → keep the valid one" email finder.
- Explorium's `0 2 * * * claude -p "…"` cron pattern and reply-classification JSON schema.
---
## 2. What Reddit is actually asking (ranked)
1. **"Is cold email / outbound still working in 2026?"** — 6+ threads; r/LeadGeneration's top thread (282 comments). Answer people upvote: yes, for Google-Maps-scraped local businesses without a website, 100–200 personalized emails → 3–5 replies; "anyone can blast 10k, few can send 500 hyper-relevant ones".
2. **"How do I make AI emails not sound like AI?"** — 5 threads. Best answer (r/SaaS, 363 pts): feed 50+ hand-written examples, "copy style not content", < 100 words, pre-filter generic LinkedIn posts, A/B the prompt (30% swing). "Identical sentence rhythm across 1,800 emails — nobody replies to tell you, they just stop replying."
3. **"Do AI SDRs actually book meetings?"** — 5 threads; deeply skeptical. One honest founder funnel: 1,842 contacts → 11.6% reply → 52 booked → 31 held. "The tool is 30%. The list filter and the first line are the other 70%."
4. **"Which tool do I start with — enrichment, scoring or sequencing?"** — "Everyone wants to automate a process they have no idea how to do without AI." Answer: source → enrich → qualify → CRM check → draft → human review → send → track; do 20 by hand first.
5. **"Is Clay worth it / cheaper Clay alternative?"** — 3 threads, 40–63 comments each; purely price-driven ("the pricing disturbs me the most"; GTM lead at a $200M-ARR company: "way overpriced").
6. **"Which data source is accurate — Apollo, Clay, Sales Nav, ZoomInfo?"** — "Apollo is honestly trash, under 10% valid phone numbers"; "ZoomInfo and Apollo lack up-to-date email verification and proper buying signals… I was learning to build middleware myself with Claude and n8n."
7. **"Where do I store leads between n8n runs?"** — dedicated thread; both big n8n posts use Google Sheets as the source of truth.
8. **"How do I handle replies — classify or keep human?"** — 3 threads.
9. **"Which sender accepts CSV with custom variables?"** / **"How do I verify emails inside the workflow?"** — 2 each.
10. **"Can you share the JSON?"** — asked ~7× in one thread; the "DM me" non-answer is resented. **Actually shipping the workflow is the whole differentiator.**
11. **"How many LinkedIn accounts / per-seat cost?"** — HeyReach $590/mo/sender vs Unipile €5/account.
12. **"How do I tell whether a reply-rate drop is me or Google/Microsoft?"** — the control-inbox answer.
13. "How do I find clients for my n8n/automation agency?" (924-pt r/AI_Agents thread) — the freelancer audience is huge and broke.
Pain points with the loudest quotes: saturation ("reply rates 3–5% → 1–2% because everyone runs the same stack sending 'hi {firstName} I noticed {scraped_intent_signal}'"), domain burn, tool sprawl ("so many tools… confused which to start with"), Clay cost, data rot, and Reddit's own vendor pollution ("it is his vibecoded SaaS and he's namedropping it here").
---
## 3. Where treg.to fits — the gaps nobody explains how to fetch
The web playbooks all say "go get X data" and stop. Checked against the catalog today:
| The playbook says | Nobody says how | treg.to has it (cheapest working option, no key needed) |
|---|---|---|
| "Posted SDR/BDR roles in last 90 days" | ✗ | `apify.linkedin.search.jobs` $0.001 · `leadmagic.x.jobs-search-v3` $0.025 · `predictleads.companies.job_openings` $0.04 |
| "Raised Series B/C in last 18 months" | ✗ | `aviato.companies.funding_rounds` $0.01 · `predictleads.companies.financing_events` $0.04 · `predictleads.financing.discover` (who-just-raised feed) |
| "Doesn't yet use Salesforce" | ✗ | `tomba.companies.tech_stack` $0.0089 · `predictleads.technologies.users` (reverse lookup) $0.04 |
| "Research their last 3 LinkedIn posts" | ✗ | `tikhub.x.linkedin-web-v2-get-user-posts` $0.001 · `scrapecreators.linkedin.user.profile` $0.00188 (1,997 samples, 99.9%) |
| "Who engaged with the competitor's post" | ✗ | `scrapecreators.x.v1-linkedin-post` $0.00188 (likes, comments) — partial |
| "Scrape X + LinkedIn with Apify, Claude scores each lead" (@IAmAaronWill) | — | `scrapecreators.x.user.posts` $0.00188 · `tikhub.x.user.posts` $0.001 |
| "Find the VP Sales at each company" | — | `icypeas.people.search` $0.00038 · `findymail.search.employees` $0.0198 · `leadmagic.x.role-finder` $0.05 |
| Waterfall email finder | — | tomba $0.0089 → icypeas $0.019 → findymail $0.0198 → hunter $0.0245 → leadsforge/leadmagic $0.025 (6 providers, one token) |
| Verify before send | — | icypeas $0.0019 · leadmagic $0.00625 (2,997 samples) · hunter $0.01225 |
| Google Maps "niche + city, no website" | — | `dataforseo.x.business-data-business-listings-search-live` $0.013 |
| Scrape the prospect's website → markdown | ✗ | **Catalog gap.** Nothing Firecrawl/Jina-shaped; `dataforseo.x.on-page-content-parsing` is the nearest. Worth a `catalog_request`, because 3 of 8 YouTube systems have this step. |
| Website visitor de-anonymization | ✗ | Not in catalog (RB2B/Warmly); leave to own-key tools. |
Caveats to keep the pages honest (CLAUDE.md rules): treg **compares** these providers side by side, it does not waterfall/route automatically — the article's script does the waterfall in 6 lines, which is exactly the point. Findymail/Fiber misses bill at full rate until `_observed_cost_micro` is fixed (see memory) — say "a miss costs nothing" only for providers where it's true.
---
## 4. Articles to write on treg.to (copy, paste, run)
Format rule for every one: the article **is** a runnable file. One `SKILL.md` or one bash/Python script that calls `/call/` with a treg token, real endpoint ids, real prices, a real run's output pasted in, and the tuned prompt inline. This answers Reddit's #10 ("share the JSON") in a way vendors refuse to. Each should end with the exact cost of the run shown.
**Tier 1 — matches the biggest asked questions and the strongest catalog coverage**
0. **"The join problem, solved with three catalog calls"** — the framing from Akshay Pachaar's Seltz-sponsored X Article *Build a Multi-Agent GTM Intelligence System* (Aug 24, 172 likes / 28K views; [blog mirror](https://blog.dailydoseofds.com/p/build-a-multi-agent-gtm-intelligence)): the trigger (new VP, funding closed) and the person's record live in different places, so a snippet-returning search API forces search → fetch → parse per company and per person; a full-record API turns the join into a merge. Three agents: Signal Hunter (news) → People Enricher (career record) → Outreach Strategist (merge, rank, first line). Rebuild it on `predictleads.financing.discover` / `apollo.companies.news` for the trigger, `icypeas.people.search` + `scrapecreators.linkedin.user.profile` for the record, `tikhub` posts for the hook — **with prices**, which Seltz's piece never shows. Keep their honest "when to chain with open web search" section; ours is "when to use your own key".
1. **"The Clay-free waterfall: find and verify a work email across 6 providers for $0.01–0.025"** — the `email.find` cascade (tomba → icypeas → findymail → hunter → leadmagic), verify, stop at first hit. Reddit #5/#6/#9; the "150 providers behind one key" moat, done in a 40-line script. Compare to Clay credits per row. Reuses the shipped `/use-cases/lead-enrichment-for-ai-agents/` proof.
2. **"Hiring-signal outbound in one SKILL.md: companies that posted SDR roles this week → the VP Sales → verified email → templated first line"** — Clarence Nap's $35K/mo system, rebuilt on `apify.linkedin.search.jobs` + `icypeas.people.search` + email waterfall + Saraev's icebreaker prompt. The single most-cited signal across all sources.
3. **"Who just raised: a Monday cron that pulls last week's funding rounds and writes the entry-rule scorecard"** — `predictleads.financing.discover` / `aviato` + Explorium's scoring weights as a real Python dict + `0 2 * * * claude -p`. @itsalexvacca's "write the entry rule first" as the frame.
4. **"Score before you send: the 40-line lead qualifier (and why it 2–3x'd replies while dropping 40% of the list)"** — Lead Gen Jay's qualification prompt + tech-stack check (`tomba.companies.tech_stack`) + LinkedIn profile pull; output a yes/no/reason CSV. Reddit #2/#4.
5. **"LinkedIn post → first line: personalize 500 emails from each prospect's last 3 posts for under a dollar"** — the r/SaaS 363-pt method (`tikhub` posts $0.001 + GPT-4o-mini + "copy style not content" with 25+ examples). Pure copy-paste; the generic-post pre-filter included.
5b. **"Stack the signals: the $0→$2M compliance playbook, as a scoring script"** — from [@pierreeliottlal's post](https://x.com/pierreeliottlal/status/2092548845255209294) (Gojiberry founder, Aug 26: 203 likes, 30K views, **549 bookmarks**). A GDPR/SOC 2 vendor's whole GTM: (1) keywords buyers engage with when compliance becomes urgent, (2) monitor ICP people engaging with that content, (3) filter role/size/geo, (4) enrich, (5) contact while fresh, (6) monitor competitor company pages, founders, salespeople, employees — ICP engagers there are the warmest, (7) monitor Seed/Series A raises because questionnaires follow growth, (8) **stack**: fit → +topic engagement → +follows competitor → +just raised = call now. "Fit tells you who CAN buy, intent tells you who might buy NOW." Ours: the stack as an additive score with real calls behind each term — `predictleads.financing.discover` (7), `scrapecreators.x.v1-linkedin-post` engagers on competitor posts (6, partial), `tikhub` person posts (2, partial), `icypeas.people.search` + waterfall (3–4). Honest gap: keyword-level LinkedIn engagement monitoring (step 2) isn't in the catalog — file it; Gojiberry's moat is exactly that feed.
6. **"Local-business outbound that still works in 2026: Google Maps → no website → owner email → 100 a day"** — the r/LeadGeneration top answer, on `dataforseo` business listings + email find. Cheap, beginner, huge Reddit audience.
7. **"The 2026 outbound stack, mapped: what each tool does, which ones are one API call, and what it costs per lead"** — the nine-layer breakdown (@itsalexvacca's frame) with a price-per-1,000-leads table computed from the catalog. The comparison page Reddit #4/#6 keep asking for; honest about what treg doesn't do (send, warm, de-anon).
8. **"n8n vs Claude Code for outbound: the same hiring-signal pipeline built both ways"** — the orchestrator war, one HTTP node vs one skill, MCP vs CLI token cost. Growth Unhinged's "MCP → CLI" data point.
9. **"Where to store leads between runs: SQLite + a dedupe key, in 20 lines"** — Reddit #7; answers the "never re-contact the same lead" problem every system hits.
10. **"Reply classifier: six labels, one prompt, webhook to your sequencer"** — Explorium's schema, as a runnable script. Reddit #8.
**Tier 3 — LinkedIn and X specifically**
11. **"The comment-first LinkedIn play: pull the engagers on three competitor posts, score them, connect to 20 a day"** — `scrapecreators.x.v1-linkedin-post` + profile pull + SalesRobot's scripts + the 100/week caps stated plainly. Honest that treg reads LinkedIn; sending stays in HeyReach/Unipile/your hands.
12. **"Score X followers as leads: scrape a competitor's audience's posts, let Claude rank them, DM the top 50"** — @IAmAaronWill's viral inbound/outbound split on `scrapecreators.x.user.posts`.
13. **"Control inboxes: a 12-line weekly check that tells you whether Google moved or you did"** — the r/Coldemailing insight nobody has written up; pulls per-domain reply rates from Instantly/Smartlead as own-key tools.
**Don't write:** "best AI SDR tools 2026" listicles (the audience distrusts the category and it's vendor-saturated), anything promising automatic routing/failover, "how to X with ChatGPT" (dead per the pSEO teardown), and PDF-style "ultimate guides" — Lead Gen Jay's audience calls the perceived value of those "trash".
**Distribution loop:** each article's script doubles as (a) a `SKILL.md` installable via `install.sh`, (b) the answer to the matching Reddit thread (post the actual JSON/script — the one thing OPs there never do), (c) an X post in the @fivosaresti "here's the cheat sheet" format, which is what performs in this niche.
---
## 5. Where articles of that quality live (and what to copy from each)
Searched X Articles, the web and GitHub for the hands-on, code-included, honest-caveat shape of the Seltz piece. Four publisher shapes exist; none is written from the "many providers, one token, per-call price" angle.
**A. Sponsored newsletter walkthroughs (the Seltz shape).** Daily Dose of DS (Avi Chawla + Akshay Pachaar, 100K+ readers) runs a *[Hands-on] … explained with code* series where a vendor sponsors one build: Seltz (GTM), Mistral OCR, "Audio RAG with 200x cheaper vector DB", "Semantic code navigation cuts agent tokens 36%", "Query billion-row Postgres". Format: problem framing → 3-agent architecture → CrewAI/MCP code → Streamlit demo → Lightning Studio repo → "when to use something else" → "thanks to X for sponsoring". Akshay's follow-up tweet ("I just built my own multi-agent GTM research assistant — it finds the reason to reach out before it writes a single message", 87 likes) is the distribution post. **Action:** price the slot; the brief writes itself from article #0.
**B. Vendor engineering blogs with real code.**
- Firecrawl — *How to Build an AI SDR that Researches Companies in Real Time* (Jun 2026): Python, four stages (search → schema extract → write → batch to 50 concurrent), Pydantic schema, cites Belkins/Woodpecker with links and flags vendor-sourced stats as "directional". Also *Bulk Sales Lead Extractor in Python*, *Complete Guide to Data Enrichment*, n8n templates. This is the bar for code quality.
- Explorium — *How to build an Outbound Agent with Claude Code* (May 2026): CLAUDE.md, researcher + writer agents, five-stage pipeline, 8-point production audit (HMAC, DLQ, ICP gating), 30/60/90 roadmap, G2 quotes against Apollo/Clay ("credits per row vary 100% from stated"). Heavier on positioning; its "$18K SDR → $200 agent" framing is the one being shared.
- FoxReach — *How to Build an AI SDR: 2026 Build Guide*: the 8-stage closed loop table (source → enrich → research → draft → send → triage → hand off → feedback); "you own the brain, rent the hands"; sending backend as typed tools with JSON schemas. Best architecture prose of the set.
- The Signal Club's Eric Nowoslawski profile, SyncGTM, Salesforge (already in §1) — operator-voice, fewer code blocks.
**C. Open-source GTM skill packs for Claude Code (the fastest-moving shape).**
| Repo | ★ | What it is | Why it matters |
|---|---|---|---|
| `gtmagents/gtm-agents` | 393 | Claude Code plugin marketplace of GTM agents; "generate 100 leads in 5 minutes" use-case docs | The distribution model — a `/plugin marketplace add`; treg's `skill.md` could ship as one |
| `oneshot-agent/oneshot-gtm` | 308 | GTM agent for technical founders, pay-per-result, signed receipts, CLI + local dashboard | Pay-per-result is our billing story told by someone else |
| `Othmane-Khadri/YALC-the-GTM-operating-system` | 288 | "The open-source Clay alternative": MIT, CLI-first, runs in Claude Code, enrichment and workflows in markdown you own, "you pay providers direct" | **Directly adjacent.** YALC 1.0 needs a key per provider; treg is one token for all of them. A "run YALC on treg" article or PR is the highest-leverage integration on this list |
| `AIDevGTM/gtm-cofounder` | 248 | #1 Product of the Day; strategy-side skills (positioning, first users, pricing) | Not data; skip |
| `LeadMagic/gtm-skills` | 45 | 206 SKILL.md skills incl. `outbound-stack`, `prospecting-stack`, `rb2b-outbound-triggers`, `cold-email-strategy`, with QA scripts | LeadMagic is a catalog provider (`leadmagic.people.email.find`, 3,130 samples). Their skills call LeadMagic directly; a fork that calls `/call/` gets a waterfall for free |
**D. The X Article format itself.** Long-form X Articles with a code walkthrough and a "here's the cheat sheet" hook are what perform in this niche this month: @fivosaresti (Claude Code outbound cheat sheet), @itsalexvacca (nine-layer GTM stack, 21 MCPs), @harsehaj ("how I built an SEO/AEO blog engine" as a Browserbase intern, 1,199 likes), @_avichawla ("Cut agent tokens 2.7x", 165K views). The pattern: one specific build, numbers, a diagram, the repo link, an honest limits section.
**What none of them do, which is our opening:** show the price of every call, compare providers for the same step side by side, and let the reader run it without signing up for four vendors. Every article above ends with "get an API key from X, Y and Z".
---
## 6. What to do with it — one build, five outputs
The principle: **build one recipe for real, get a receipt, then reuse that single run everywhere.** The receipt (the terminal showing three `/call/`s, their prices, the merged output) is the asset nobody else in §5 can produce, because they don't have per-call prices or six providers behind one token.
### The build (week 1): the join-problem recipe, run for real
- One `SKILL.md` + one script: Signal Hunter (`predictleads.financing.discover` or `apify.linkedin.search.jobs`) → People Enricher (`icypeas.people.search` → `scrapecreators.linkedin.user.profile` → email waterfall → verify) → Strategist (merge, rank, one-signal first line with Saraev's prompt).
- Run it on 20 real companies. Save: the exact commands, every call's price, total cost, the ranked output, what missed. That run is the receipt.
- Fix or caveat before publishing: the Findymail/Fiber miss-billing (say "a miss costs nothing" only where true); file `catalog_request` for website→markdown scraping so the recipe can research a homepage.
### Output 1 — X (Jason's account first, @treg second)
| Post | Format | Model it copies |
|---|---|---|
| **The join problem, with a receipt** | X Article + hook tweet: "Cold outreach fails on timing, not wording. The trigger and the person live in different records. Here's the join in 3 calls, $0.04 per lead, receipt attached." Terminal screenshot of the run, diagram of the three agents, repo link, honest "when to use your own key" | Akshay's Seltz piece; @fivosaresti's cheat-sheet close ("bookmark + send to your team") |
| **The price-receipt series** (weekly) | One image: one outbound step, all providers side by side — price, measured success %, median ms, samples. "Email verification: 5 providers, $0.0019 to $0.0123, which one your agent should pick." No other account can post this table | @itsalexvacca's nine-layer stack, made numeric |
| **Value replies with the script** | Reply to the viral outbound posts (@IAmAaronWill's inbound/outbound split, @itsalexvacca's entry rule, @aryanXmahajan's AI SDR) with the 10-line version that does their step, prices inline. The paused "X value replies" loop is the vehicle | The one thing Reddit OPs won't do: share the actual code |
| **"Clay's moat in 40 lines"** | Quote Eric N.'s "150 pre-negotiated providers" line, show the waterfall script, the cost per row vs Clay credits | The Clay-alternative threads (40–63 comments each) |
| **YALC-on-treg** | Joint announce with the YALC maintainer once the PR lands: "the open-source Clay alternative now runs on one token" | Open-source cross-post; both audiences |
Rules from the teardown: first tweet carries the number and the receipt, not the pitch; never claim routing; "genuinely" is fine, em-dashes are the tell.
**The format that gets bookmarked** (Pierre's post, 549 bookmarks on 203 likes): a named outcome in line one ("$0 to $2M ARR with one play"), a concrete niche so it feels real (GDPR/SOC 2 → US companies), eight numbered steps, one thought per line, no image, the product named once in the last three lines, "try it free" as a self-reply. Bookmarks, not likes, are the metric for playbook posts — write for the save.
### Output 2 — treg.to articles: a `/recipes/` shelf
- **Page type:** `/recipes/<slug>`, server-rendered from the catalog like `/use-cases/` (`agent_pages.py` `_USE_CASES` pattern, so a new recipe lands in the sitemap for free). Prices and success rates pulled live from the catalog, so the page never goes stale.
- **Fixed anatomy** (copy Firecrawl's rigor, Akshay's framing, FoxReach's loop table): (1) the Reddit question, quoted; (2) the join — which records live where; (3) the calls, with a provider table for each step; (4) the receipt — a real run, total cost; (5) the prompt, inline; (6) "when to use your own key / when to use open web search"; (7) install: `treg skill install <slug>` or the `SKILL.md` download.
- **Order of publication** = §4 Tier 1: join problem → email waterfall → hiring signal → who-just-raised cron → lead qualifier → LinkedIn posts → first line. One a week; the daily "treg.to SEO pages" loop keeps shipping catalog pages underneath.
- **Agent × recipe pages** already have the machinery (`/for/claude-code`, `/for/cursor` agent pages): each recipe gets an install block per agent, which is Pipedream's "agent × platform" win without the empty matrix.
- Each recipe also lands in `src/treg/web/skill.md` / `llms.txt` as a one-line "recipes" index — agent-facing files are the front door.
### Output 3 — distribution beyond our own channels
- **Sponsor the Daily Dose of DS slot.** The brief is article #0; ask for the same anatomy (3 agents, CrewAI or Claude Agent SDK, Streamlit, hosted repo) with treg as the retrieval tool. Fits the creator-sponsorship criteria (workflow builders, rank on views).
- **YALC PR:** a treg provider adapter so "you pay providers direct" becomes "one token". Highest-leverage integration found; 288★, MIT, runs in Claude Code.
- **Fork `LeadMagic/gtm-skills` `prospecting-stack`** to call `/call/` — LeadMagic is already a catalog provider, so this is a waterfall upgrade of their own skill; offer it upstream.
- **List treg in `gtmagents/gtm-agents`** as a marketplace plugin (`skill.md` already exists).
- **Answer the five Reddit threads** (r/MarketingAutomation "where to start", r/gtmengineering Clay alternatives ×2, r/n8n "where to store leads", r/SaaS personalization) with the recipe script, not a link — via the paused Reddit karma loop, one value comment per thread.
### Sequence
Week 1 build + receipt → week 2 X Article + recipe page #1 + YALC PR opened → week 3 price-receipt series starts, Reddit answers, DDoDS outreach → weekly thereafter one recipe, one receipt image, replies as they come. Measure with the existing GSC+Ads loop: impressions on `/recipes/`, first-call rate from those pages (the funnel still needs instrumenting — see memory).
---
## 7. Measured: where this fits the SEO strategy (DataForSEO via the catalog, 2026-08-27, ~$0.16)
Full table and linking map: `marketing/rebuild/00-strategy.md`. Drafts: `marketing/rebuild/01–05`.
**Winnable, worth a page:** "clay alternative(s)" 480+480/mo, CPC $36.72, **KD 0** (SERP = listicles, an r/gtmengineering thread, an HN "should I build one" post) · "clay pricing" 1,900 (KD 3) · "clay api" 390 · "gtm engineering / gtm engineer" 3,600 LOW competition, untargeted by anyone in our sources · "gojiberry ai" 1,600 + "trigify" 720 + "teamfluence" 90 — the no-API subscription LinkedIn-intent tools have real brand volume · "ai sdr" 1,900 (−46% YoY, CPC $92) with "ai sdr open source" showing a bare GitHub repo at #1 · "linkedin scraper" 1,000 / "linkedin profile scraper" 210 / "linkedin post scraper" 110.
**Dead:** "rebuild clay", "build your own clay", "clay clone", "signal based outbound" (10), "waterfall enrichment api" (10), "open source clay alternative" (10), and every "{x} api" phrasing except "email verification api" (210). Same lesson as the memory note: buyers type "scraper", "alternative", "pricing", "vs".
**The shelf:** `/rebuild/` — Rebuild Clay (clay alternative + pricing + api + waterfall) · Rebuild Gojiberry/Trigify (LinkedIn intent, with the keyword-monitoring gap stated) · Build your own AI SDR (the loop; sending stays in your sequencer) · The GTM engineering stack, priced (the 3,600 term) · The join problem (repurposed; doubles as the X Article and the DDoDS brief). Every page is a recipe with a run receipt; every existing use-case page links to the rebuild that extends it, and every rebuild's provider table links back to the catalog comparison pages.
---
## Raw material
Scratchpad: `x_opencli.txt`, `x_threads.txt`, `x_users.txt`, `reddit.txt`, `reddit_threads.txt`, `yt/*.txt` (8 transcripts), `c_p1..8.md` (web pages). Agent Reach v1.5.0 is current.
-88
View File
@@ -1,88 +0,0 @@
# Independent review: treg.to programmatic SEO plan
Overall verdict: the intent model is mostly right, but the proposed launch inventory is too large and the spoke index gate is backwards. The defensible version is a small number of complete, first-party comparison clusters, expanded only after query match and activation are visible—not 250+ URLs produced because the catalog permits them.
## (a) Structure
**Verdict: keep the entity hierarchy; cut the initial indexed surface by more than half.** Agent, platform/pricing, use case, category, and provider are valid entities, and rejecting endpoint-level pages is correct. The problem is treating every valid entity as an indexable search page.
- **Agent hubs:** index ChatGPT, Claude.ai, Claude Code, and Cursor first. They have either a distinct native install path or measured adjacent demand. Put OpenClaw, Hermes, Codex, opencode, pi, and Gemini CLI in one crawlable `/apps` compatibility directory; render individual setup pages as `noindex` until they have distinct setup content or query evidence. Ten near-identical pages whose main instruction is the same `set up treg — /llms.txt` line are not ten search intents.
- **Category hubs:** keep five, but fix the URL model first. The shipped hubs use long slugs such as `/use-cases/seo-data-for-ai-agents`, while the new spokes point to parents such as `/use-cases/seo`; those parents do not exist. Pick one canonical taxonomy and redirect or nest consistently.
- **Pricing pages:** do not launch 47 because a platform has two providers. Start with the five measured platforms, then perhaps `google-serp`, TikTok, people, companies, and backlinks only when the editorial dataset is complete. For every other platform, the existing `/catalog/<slug>` is enough. Also retitle catalog shelves away from “API pricing” intent wherever a separate pricing page exists, or the two surfaces will cannibalize each other.
- **Use-case spokes:** ship the first 24 as a reviewed pilot, not 100–150 automatically. Keep the remaining mapped jobs renderable and `noindex` until they pass a stronger gate.
- **MCP pages:** three provisional pages are a reasonable experiment only if they remain install-focused and materially distinct from pricing. If the content converges, merge them into E and redirect; an exact phrase alone is not enough to justify a second platform page.
- **Provider pages:** they are useful activation documentation, but 47 need not be indexed at launch. Index providers with a verified integration, real setup/limits content, and a runnable call; keep the rest out until complete.
- **Add:** server-rendered parent directories for `/apps`, `/pricing`, `/integrations`, and `/use-cases`, plus an explicit canonical policy across `/catalog/<platform>`, `/pricing/<platform>-api`, and `/mcp/<platform>`. A sitemap is not a substitute for navigational crawl paths.
At the high end, the plan also understates its own footprint: 10 + 5 + 47 + 150 + 3 + 47 + 81 existing catalog shelves is **343** indexable entity pages before core pages, not “250–300.”
## (b) Spoke index gate
**Verdict: not the right replacement. Usage should be one signal, not the index decision, and one call is far too weak.** A test call, an internal operator, or one malformed request can make a page indexable; product usage also does not establish search demand or provider comparability. The argument that a `noindex` page eventually stops passing signals does not justify exposing low-demand pages—hubs can link directly to the valuable pages.
Use a compound, stable gate:
1. at least two providers that are genuinely substitutable for the stated job, not merely assigned the same capability;
2. at least one recently verified endpoint plus complete price, input, test request, provenance, and decision content;
3. either external demand evidence (keyword/GSC impressions, a cited support/community question, or an inbound link) **or** repeated successful use across more than one customer/client—not one observed call;
4. human approval into a deploy-time index manifest.
Do not recompute indexability directly from a rolling 30-day window. That creates index/noindex churn when a quiet job ages out. Use hysteresis: usage can nominate a page for review, but an indexed page remains stable unless it becomes inaccurate or materially empty.
## (c) Thin/scaled-content risk
**Verdict: 250–300 pages is defensible in principle, but this implementation is not yet defensible at that scale.** Google does not penalize a page count; it discounts pages that repeat the same answer, have no independent demand, or exist mainly to transmit internal links. Here, agent tabs, install blocks, generated ledes, provider tables, and platform/job links will repeat across three or four URL families, while the most differentiating field—public observed performance—is still unapproved and statistically confounded.
Pages become defensible when each one has a distinct query job, an answer-first section, source-linked first-party facts, a reproducible normalization method, a runnable example, input/output differences that change the buyer's choice, and a human-reviewed recommendation. Require a page-level completeness score and sample rendered pages for factual duplication before adding them to the index. Stable canonicals, real directory links, honest freshness dates, and actual activation CTAs matter more than FAQ schema or the raw number of internal links.
## (d) Pricing/comparison pages
**Verdict: E is the strongest proposed page type, but only if it becomes a decision tool rather than a catalog table with prose.** Its advantage over a vendor page is cross-provider normalization; its advantage over Blotato is first-party execution evidence. Neither advantage exists merely because treg has listed or once verified every provider.
What is missing:
- a published normalization methodology: batch size, pagination, result caps, minimum commitments, prepaid credits, overages, retries, and failed-call billing;
- comparability beyond price: accepted inputs, returned fields, freshness, geographic coverage, rate limits, legal/access constraints, and output completeness;
- source provenance per price/constraint, not only one page-level “verified” date, plus a change log;
- an interactive standard-task calculator or several task sizes, because one hand-kept constant can make the cheapest vendor look cheapest by construction;
- transparent sample counts and a controlled benchmark before saying “best value” from success/latency. Current production observations exclude many caller errors, have a five-decided-call floor, and are explicitly not a fair benchmark;
- disclosure of treg's own economic relationship and the exact hosted/BYOK charging difference;
- a maintenance owner/SLA for the hand-written provider-by-platform judgments. Forty-seven platforms multiplied by several providers is a research program, not a template field.
Until those exist, publish only the few platforms for which the answer is genuinely better and more current than the vendors' own pages.
## (e) Build order and gates
**Verdict: reorder around complete intent clusters and measurement, not page types.** The proposed waterfall launches isolated hubs, then isolated spokes, then their strongest comparison pages. That weakens both user journeys and the experiment.
Recommended order:
1. settle canonical/category URLs, add organic landing-page attribution through first call and week-two reuse, and define the content/index rubric;
2. ship one complete LinkedIn cluster: ChatGPT/Claude entry pages, the MCP install page, the pricing page, three to five strongest use cases, and only the relevant complete provider pages;
3. submit the live cluster to MCP/directories immediately, not after four internal build steps;
4. ship one non-social comparison cluster as a control, then expand the winning page type.
Day 14 is appropriate for crawl/index diagnostics. Day 30 is appropriate for checking whether intended query families produce qualified impressions, but **top-30 position is not a sound binary verdict** on a new, low-traffic domain and 140–480/month queries. Use index coverage, intended-query share, impression trajectory versus the existing shelf, CTA/install starts, and first calls. Pause expansion on zero qualified impressions across the test set; do not delete type B solely at day 30. Position and click verdicts need roughly 60–90 days with external links; repeated-call value remains the day-60/120 business test.
The repo already records `Org.first_call_at`; what is missing is durable organic landing/page attribution and reuse attribution, not the timestamp itself.
## (f) Feasibility and repo contradictions
**Verdict: feasible, but the spec understates several structural changes and contains several concrete contradictions.**
- `/mcp` is already the mounted MCP transport and `robots.txt` deliberately says `Disallow: /mcp`. Explicit FastAPI GET routes registered before the final mount could technically win, but the proposed SEO pages would still be blocked and would share a security-sensitive protocol namespace. Prefer `/mcp-servers/<platform>`; otherwise add narrow `Allow` rules and tests without weakening the transport block.
- `_page()` has no robots-policy argument, always loads `catalog.css`, and hardcodes its nav/footer and social metadata. It cannot currently produce `noindex,follow` spokes or stamp `pseo.css`. It also links “Start free” to `/app`, whereas the new rows require context-preserving install/connect actions.
- The sitemap currently needs only catalog/file mtimes and no database session. The proposed gate requires live `CallRecord` aggregates across hundreds of endpoint IDs. `endpoint_stats.observed()` explicitly expects a small sibling set, so calling it across the catalog on every sitemap render would violate its design assumption, add DB dependence to a crawler endpoint, and make sitemap membership unstable. Materialize/cache a reviewed index manifest or build a dedicated aggregate.
- The current `tests/test_seo.py` does not literally walk every catalog URL; it samples five shelves and walks all other listed URLs. Adding 200 non-catalog URLs would make the test render all of them unless its classification changes.
- The current five `_USE_CASES` are flat routes with ad-stable long slugs; the proposed nested category routes do not match them. This needs redirects and one shared route map before spokes ship.
- The onboarding agent list is embedded as two Vue computed arrays in the no-build `index.html`, not a shared JSON source. Sharing it with Python is feasible, but requires a fetch/generated asset or server injection, not merely moving a constant.
- Hosted claims create a self-hosting problem. Every `_page()` canonical is based on `public_url`, but the spec demands literal “treg.to,” ChatGPT's public plugin, hosted keys, and a $1 grant on every deployment. Those statements are false on a self-hosted registry. Gate pSEO routes/content to the hosted deployment or derive host-appropriate copy; do not globally hardcode treg.to.
- Adding routes in the API is consistent with “the API is the brain,” but putting hundreds of lines of editorial copy and normalization logic into the already large `api.py` is not necessary. Keep routing/data authority there while placing declarative, reviewed content in data/templates. Update `docs/context/interface/seo.md` in the same change and do not link unshipped routes from `llms.txt` or `skill.md` early.
## (g) Three biggest risks, ranked
1. **Index bloat plus intent cannibalization.** Catalog, pricing, MCP, provider, and use-case pages can repeat the same provider/job table; the one-call gate then scales overlap faster than evidence. Google may simply ignore most of the cluster, making crawl and measurement noisy even without a formal penalty.
2. **The claimed differentiation is not ready to publish.** Observed success/latency is unapproved, low-sample, and confounded; capability equality does not establish output equivalence; many pricing/official-API constraints are not yet structured in the catalog. Remove that layer and the pages become the same generic scaled content the plan says it avoids.
3. **Authority and evaluation are too weak for the proposed rollout.** Pipedream's thin pages sit on an established domain and ecosystem; six directory submissions and internal links do not reproduce that. With 41 clicks and low-volume terms, day-30 position gates will generate false negatives while page-level acquisition-to-repeat-call attribution remains incomplete.
The plan should proceed, but as a cluster experiment: four agent hubs at most, three MCP tests, roughly five pricing pages, the first 24 reviewed spokes, and only complete provider pages. Scale only the page type that proves both query match and successful repeated calls.
@@ -18,17 +18,8 @@ seo_terms:
- "rank tracking api without a subscription"
- "keyword volume api pay per call"
- "backlink data api per call"
ad_keywords: # bid here; do NOT optimize the page for these
- "semrush api alternative"
- "cheapest serp api"
- "keyword research api"
- "seo api for developers"
- "dataforseo alternative"
capabilities: [google.keywords.volume, google.keywords.ideas, google.serp.organic, web.backlinks.summary]
facts_used: [F-01, F-02, F-03, F-04, F-05, F-06, F-07, F-08, F-09, F-10, F-11, F-13, F-20, F-30, F-31, F-32, F-90, F-92]
hypothesis: "Highest traffic, worst cost per first successful call: SEO buyers already own a tool. Predict this page is the one we stop paying for. RE-POINTED 2026-08-17: telemetry shows keyword research is ~1.3% of real usage while SERP scraping is ~22%, so the second workflow now leads on recurring result monitoring."
verify_after: 2026-08-31
status: proof populated from a real run 2026-08-17 ($0.012) · ready for build · revised against 30-day telemetry 2026-08-17
facts_used: [F-01, F-02, F-03, F-04, F-05, F-06, F-07, F-08, F-09, F-10, F-11, F-13, F-20, F-30, F-31, F-32, F-92]
---
# Page 1: SEO and keyword intelligence
@@ -296,50 +287,4 @@ subscriptions, not the suite you read reports in.
# Ad and creator kit
### Responsive search ad headlines
| Headline | Chars |
|---|---|
| `Real SEO Data for Agents` | 24 |
| `Keyword Volume, Per Call` | 24 |
| `No Semrush Seat Required` | 24 |
### Search ad descriptions
| Description | Chars |
|---|---|
| `Keyword volume, difficulty, results and backlinks through one key. $1.00 free to start.` | 87 |
| `Pay per call, not per seat. Price shown before your agent calls. No provider signup.` | 84 |
### Creator video hooks
1. "I asked Claude Code for 50 keywords with real search volume. Watch what the run cost."
2. "Five providers sell the same keyword data. One charges 180 times what another does."
3. "Your agent can't do SEO because it can't see the data. Two-minute fix."
### X post hook
`Your agent writes confident SEO briefs off keyword data it invented. Here's how to give it the real numbers: volume, difficulty, live SERPs: for a fraction of a cent per call.`
### High-intent keyword phrases
`keyword volume api pay per call` · `serp api for claude code` · `seo data for ai agents` ·
`keyword research api without subscription` · `backlink api per call`
### Negative keywords
`free` · `jobs` · `salary` · `course` · `certification` · `wordpress plugin` · `chrome extension` ·
`agency near me` · `meaning` · `what is seo`
### Demonstration a creator can reproduce
Paste `set up treg — https://treg.to/llms.txt` into Claude Code, then paste the workflow prompt. Film the
terminal end to end, and run `treg balance` before and after so the audience sees the actual cost of the
run rather than a claim about it.
### Measurable hypothesis
Highest traffic of the five, worst cost per first successful call: SEO buyers usually already own a tool,
so the pitch is a saving rather than a new capability. If day-14 CPFC is the worst of the five, stop
paying for this page and move the budget to page 2 or 4. Secondary: Copy Prompt clicks predict first-call
conversion better than account creations do.
---
## Numbers used on this page
`F-01` `F-02` `F-03` `F-04` `F-05` `F-06` `F-07` `F-08` `F-09` `F-10` `F-11` `F-13` `F-20` `F-30` `F-31`
`F-32`: all defined in `_facts.md`, verified 2026-08-17. Re-verify with
`treg catalog get <endpoint_id>` before publishing.
Campaign material is maintained in treg-internal.
+2 -55
View File
@@ -17,17 +17,8 @@ seo_terms:
- "company search api per record"
- "email verification api without subscription"
- "prospecting api for developers"
ad_keywords:
- "apollo api alternative"
- "hunter io api pricing"
- "email finder api"
- "b2b data enrichment api"
- "clearbit alternative api"
capabilities: [people.email.find, people.email.verify, people.enrich, companies.search, companies.enrich]
facts_used: [F-01, F-02, F-03, F-04, F-05, F-06, F-07, F-08, F-09, F-10, F-11, F-13, F-21, F-40, F-41, F-42, F-50, F-80, F-90, F-92, F-43]
hypothesis: "Highest commercial intent of the five. Predict the lowest cost per first successful call."
verify_after: 2026-08-31
status: proof populated from the 50-company workflow run 2026-08-26 ($3.62) · ready for build · revised against 30-day telemetry 2026-08-17
facts_used: [F-01, F-02, F-03, F-04, F-05, F-06, F-07, F-08, F-09, F-10, F-11, F-13, F-21, F-40, F-41, F-42, F-50, F-80, F-92, F-43]
---
# Page 2: Lead enrichment
@@ -256,48 +247,4 @@ Run the complete lead generation workflow that produced the numbers on this page
# Ad and creator kit
### Responsive search ad headlines
| Headline | Chars |
|---|---|
| `Verified Emails, Per Call` | 25 |
| `Find Buyers From One Key` | 24 |
| `No Seat. No Credit Pool.` | 24 |
### Search ad descriptions
| Description | Chars |
|---|---|
| `Find companies, identify buyers, verify emails. One key, pay per lookup. $1.00 free.` | 84 |
| `A found email is $0.0245. A miss costs nothing. No seat, no monthly credit pool.` | 80 |
### Creator video hooks
1. "Eleven providers sell company data. One charges 200 times what another charges for the same row."
2. "I built a verified 50-lead list in one prompt. Here's the actual bill."
3. "Stop paying for leads you never contact. Pay for the lookups that found someone."
### X post hook
`Same job: "find companies matching this profile": priced across 11 providers: free, $0.0019, $0.025, $0.38 a record. Your agent can see that table before it calls. Most sales tools can't.`
### High-intent keyword phrases
`work email finder api` · `company search api pay per record` · `email verification api pricing` ·
`lead enrichment api for developers` · `b2b prospecting api no subscription`
### Negative keywords
`free email finder` · `gmail` · `personal email lookup` · `jobs` · `resume` · `linkedin scraper free` ·
`email marketing software` · `newsletter` · `spam` · `phone number lookup`
### Demonstration a creator can reproduce
Run the workflow prompt on a real ICP, then open the Activity page and show the per-call costs next to the
list. The strongest beat is the miss: show a lookup that found nothing and cost $0.00.
### Measurable hypothesis
Highest commercial intent of the five, so this page should produce the **lowest cost per first successful
call** at day 14. If it does, it is the vertical to concentrate spend on. Watch the second-order signal
too: enrichment buyers are the most likely to connect their own existing key, which lowers revenue per
account while raising retention: worth knowing before scaling.
---
## Numbers used on this page
`F-01` `F-02` `F-03` `F-04` `F-05` `F-06` `F-07` `F-08` `F-09` `F-10` `F-11` `F-13` `F-21` `F-40` `F-41`
`F-42` `F-50` `F-80`: defined in `_facts.md`, verified 2026-08-17.
Campaign material is maintained in treg-internal.
+2 -56
View File
@@ -17,17 +17,8 @@ seo_terms:
- "reddit search api for agents"
- "social listening api without subscription"
- "creator data api per call"
ad_keywords:
- "tiktok data api"
- "reddit api pricing"
- "social listening api"
- "x api alternative"
- "instagram data api"
capabilities: [tiktok.*, reddit.search.posts, youtube.*, x.*, instagram.*]
facts_used: [F-01, F-02, F-03, F-04, F-05, F-06, F-07, F-08, F-09, F-10, F-11, F-13, F-22, F-70, F-71, F-80, F-90, F-92, F-93]
hypothesis: "Widest audience, weakest developer overlap. Predict a high Copy Prompt rate and a low install-completion rate. Telemetry 2026-08-17: X alone is 17.5% of all traffic and its search endpoint is the most-called tool on the platform: if this page converts, an X-specific page is likely the better ad destination."
verify_after: 2026-08-31
status: proof populated from real runs 2026-08-17 ($0.00588) · 4 of 4 platforms · ready for build · revised against 30-day telemetry 2026-08-17
facts_used: [F-01, F-02, F-03, F-04, F-05, F-06, F-07, F-08, F-09, F-10, F-11, F-13, F-22, F-70, F-71, F-80, F-92, F-93]
---
# Page 3: Social and creator trends
@@ -280,49 +271,4 @@ paste the setup line from `/llms.txt` into Grok and it can use the treg.to catal
# Ad and creator kit
### Responsive search ad headlines
| Headline | Chars |
|---|---|
| `Social Data, One API Key` | 24 |
| `TikTok + Reddit + X Data` | 24 |
| `No Platform API Approval` | 24 |
### Search ad descriptions
| Description | Chars |
|---|---|
| `Posts, creators and comments across platforms. One key, calls from $0.001. $1.00 free.` | 86 |
| `No developer account, no app review, no invite. Your agent reads real posts today.` | 82 |
### Creator video hooks
1. "TikTok's API is invite-only. Here's how my agent read TikTok anyway, for a tenth of a cent."
2. "I asked my agent what the internet said about my niche this week. It read four platforms."
3. "X charges $200 a month for API access. My whole research run cost less than a dollar."
### X post hook
`Getting API access to TikTok, Instagram and LinkedIn as a small team ranges from "app review" to "no." Here's how I gave my agent read access to all of them this afternoon.`
### High-intent keyword phrases
`tiktok data api pay per call` · `reddit search api for agents` · `social listening api pricing` ·
`instagram data api without app review` · `x api alternative for developers`
### Negative keywords
`free followers` · `bot` · `auto liker` · `download video` · `scheduler` · `hashtag generator` ·
`buy views` · `login` · `deleted posts` · `private account`
### Demonstration a creator can reproduce
Ask the agent one question about the creator's own niche, live, and show the posts coming back with real
view counts. Then show `treg balance` and let the number speak: the gap between "$200/mo X API" and this
run is the whole video.
### Measurable hypothesis
Widest top-of-funnel and the weakest developer overlap of the five: predict a **high Copy Prompt rate and
a low Copy-Prompt-to-first-call rate**, because a social strategist is less likely to have an agent
installed than an SEO or a developer. If that gap shows up, the fix is a page-level change: a hosted
"run it without installing anything" path: not more budget.
---
## Numbers used on this page
`F-01` `F-02` `F-03` `F-04` `F-05` `F-06` `F-07` `F-08` `F-09` `F-10` `F-11` `F-13` `F-22` `F-70` `F-71`
`F-80`: defined in `_facts.md`, verified 2026-08-17.
Campaign material is maintained in treg-internal.
+2 -56
View File
@@ -17,17 +17,8 @@ seo_terms:
- "google ads transparency api"
- "ad creative research for agents"
- "tiktok ad library api"
ad_keywords:
- "ad spy tool"
- "meta ad library api"
- "competitor ad research"
- "facebook ads scraper api"
- "google ads transparency center api"
capabilities: [meta-ads.library.search, google.ads.transparency, tiktok-ads.library.search, linkedin.ads.search]
facts_used: [F-01, F-02, F-03, F-04, F-05, F-06, F-07, F-08, F-09, F-10, F-11, F-13, F-23, F-60, F-80, F-91]
hypothesis: "Narrowest audience, sharpest pain. Predict the highest Copy-Prompt-to-first-call rate of the five, on the lowest volume. NARROWNESS NOW CONFIRMED by telemetry: the Meta ad library is 303 calls / 1.2% of all traffic, the smallest job of the five. Spend last, and only if the Copy-Prompt-to-first-call rate justifies the CPC."
verify_after: 2026-08-31
status: proof populated from real runs 2026-08-17 ($0.00752) · 4 of 4 platforms · ready for build · revised against 30-day telemetry 2026-08-17
facts_used: [F-01, F-02, F-03, F-04, F-05, F-06, F-07, F-08, F-09, F-10, F-11, F-13, F-23, F-60, F-80]
---
# Page 4: Competitor advertising
@@ -285,49 +276,4 @@ search by advertiser domain and get back creatives, spend estimates and targetin
# Ad and creator kit
### Responsive search ad headlines
| Headline | Chars |
|---|---|
| `See Competitor Ads Live` | 23 |
| `Ad Library Data Per Call` | 24 |
| `No Ad Spy Subscription` | 22 |
### Search ad descriptions
| Description | Chars |
|---|---|
| `Pull competitors' live ads across four platforms. One key, from $0.00188 a call.` | 80 |
| `Group ads by offer, hook and format in one agent run. $1.00 free, no subscription.` | 82 |
### Creator video hooks
1. "I pulled every ad five competitors are running, across four platforms, for under a dollar."
2. "Ad spy tools cost $99 a month. Here's the same research as one prompt."
3. "The ad that's been running 94 days is the brief. Here's how to find it in a minute."
### X post hook
`Ad spy tools charge monthly for research you do four times a year. Here's the same sweep: Meta, Google, TikTok, LinkedIn ad libraries: as one agent prompt, priced per call.`
### High-intent keyword phrases
`meta ad library api` · `google ads transparency api` · `competitor ad research api` ·
`tiktok ad library api access` · `ad creative research for agents`
### Negative keywords
`free ad spy` · `jobs` · `course` · `how to advertise` · `ad blocker` · `report an ad` · `ad revenue` ·
`google ads certification` · `ad manager login` · `create an ad`
### Demonstration a creator can reproduce
Pick five well-known competitors in a category the audience knows, run the prompt, and show the grouped
output next to the cost. Land on the longest-running ad: "this one has been live 94 days, that's the
one that's working": then show the balance.
### Measurable hypothesis
Narrowest audience of the five and the sharpest pain, so predict the **highest Copy-Prompt-to-first-call
rate on the lowest traffic**. If that holds, this vertical is worth spending on even at a high CPC, and
the constraint is reach rather than conversion: which points at creator distribution rather than more
search budget.
---
## Numbers used on this page
`F-01` `F-02` `F-03` `F-04` `F-05` `F-06` `F-07` `F-08` `F-09` `F-10` `F-11` `F-13` `F-23` `F-60` `F-80`
Defined in `_facts.md`, verified 2026-08-17.
Campaign material is maintained in treg-internal.
+1 -55
View File
@@ -17,17 +17,8 @@ seo_terms:
- "funding data api without subscription"
- "company funding data api"
- "firmographic api per call"
ad_keywords:
- "crunchbase api alternative"
- "company data api"
- "funding data api"
- "firmographic data api"
- "buying intent data api"
capabilities: [companies.search, companies.enrich, people.enrich]
facts_used: [F-01, F-02, F-03, F-04, F-05, F-06, F-07, F-08, F-09, F-10, F-11, F-13, F-21, F-42, F-50, F-51, F-80, F-92]
hypothesis: "Sits on the catalog's strongest data: 11 providers, 200x spread. Predict the highest week-2 repeat-call rate of the five."
verify_after: 2026-08-31
status: proof populated from real runs 2026-08-17 ($0.10188) · funding proven, activity signals blocked by a platform bug · revised against 30-day telemetry 2026-08-17
---
# Page 5: Company and buying-signal intelligence
@@ -275,49 +266,4 @@ models, no.
# Ad and creator kit
### Responsive search ad headlines
| Headline | Chars |
|---|---|
| `Company Data, Per Record` | 24 |
| `11 Providers, One Key` | 21 |
| `Funding + Hiring Signals` | 24 |
### Search ad descriptions
| Description | Chars |
|---|---|
| `Funding, hiring, headcount and leadership through one key. From free to $0.38 a record.` | 87 |
| `Compare 11 company data providers before you buy one. $1.00 free, no subscription.` | 82 |
### Creator video hooks
1. "Eleven providers sell company data. Three of them are free. Here's the price table nobody publishes."
2. "Before you pay $99 a month for company data, test whether it covers your market for eight cents."
3. "I asked my agent which AI companies raised or hired this quarter, and who to talk to."
### X post hook
`"Search companies by size and funding" is one job with 11 providers behind it: free, $0.0019, $0.026, $0.38 a record. Same job, 200x spread. Most people buy the first one they hear of.`
### High-intent keyword phrases
`company data api pay per record` · `funding data api` · `firmographic api for developers` ·
`crunchbase api alternative pricing`
### Negative keywords
`free company lookup` · `companies house` · `register a company` · `jobs` · `stock` · `ticker` ·
`annual report` · `credit score` · `whois` · `business plan`
### Demonstration a creator can reproduce
Run `treg catalog get apollo.companies.search` on camera and let the sibling table render: eleven
providers, prices, success rates, sample sizes. That single screen is the most persuasive thing treg.to
has, and no competitor can film it.
### Measurable hypothesis
This vertical sits on the catalog's strongest and best-measured data, and company research is recurring
work rather than a one-off. Predict the **highest week-2 repeat-call rate** of the five. Repeat calls
matter more than first calls here: a page that produces one call and silence is worse than a page with
half the signups and a second call in week two.
---
## Numbers used on this page
`F-01` `F-02` `F-03` `F-04` `F-05` `F-06` `F-07` `F-08` `F-09` `F-10` `F-11` `F-13` `F-21` `F-42` `F-50`
`F-51` `F-80`: defined in `_facts.md`, verified 2026-08-17.
Campaign material is maintained in treg-internal.
+16 -159
View File
@@ -1,164 +1,21 @@
# treg.to outcome landing pages
# Outcome landing-page sources
Five vertical landing pages that serve two jobs at once: **Google Ads destinations** (the primary job —
they exist to find out which vertical is worth spending on) and **organic `/use-cases/` pages** (the
secondary job).
The numbered Markdown files, `_shared.md` and `_facts.md` are the source for the published
use-case pages. Keep public copy and its verifiable price sources here. Campaign hypotheses,
paid targeting, execution records and review plans live in
[treg-internal](https://github.com/superdesigndev/treg-internal/tree/main/docs/marketing).
```
marketing/landing/
README.md you are here — the rules, and how to change things safely
_facts.md every number used on any page, with source + date. ONE edit point.
_shared.md blocks that are identical across all five pages
_measurement.md the funnel, the per-page hypotheses, what must be live before spend
01-seo-keyword-intelligence.md
02-lead-enrichment.md
03-social-creator-trends.md
04-competitor-advertising.md
05-company-buying-signals.md
Run from this directory:
```sh
python3 build.py --check
python3 build.py
python3 build_html.py
```
---
`build.py` expands shared blocks into ignored `dist/` Markdown. `build_html.py` renders the
public HTML in `src/treg/web/`. Edit the source, never the generated pages. `build_preview.py`
and `build_review.py` produce previews using the same renderers.
## How this is built to survive SEO changes
The pages are **data + template**, not five hand-written documents. Four separations do the work:
**1. Numbers live in one file.** Every figure on every page comes from `_facts.md`, which records the
value, where it was verified, and when. Each page ends with a *Numbers used on this page* table naming
its fact keys. Catalog prices move; re-verifying is then one script and one file, and `grep` tells you
which pages are stale. **No page may introduce a number that is not in `_facts.md`.**
**2. Shared copy lives in one file.** The CTAs, the trust line, the free-credit statement and six of the
seven objection answers are identical across all five pages and live in `_shared.md`. A product change —
the router shipping, the grant amount changing, a new agent being supported — is one edit that
propagates. Pages only carry the vertical-specific override.
**3. Search terms and ad keywords are separate lists.** Front-matter carries `seo_terms` and
`ad_keywords` as different fields, because they answer to different constraints. Google Ads happily
takes `semrush api alternative`; organic must not target it (see the rules below). An SEO strategy
change edits `seo_terms`. An ads-policy or CPC change edits `ad_keywords`. Neither touches the copy.
**4. Front-matter is machine-readable.** Slug, title, meta, terms, capability ids, hypothesis and the
`verify_after` date are structured. These files can be compiled into HTML, Next.js or Sanity later
without anyone rewriting a sentence.
### Building the hand-off copy
The numbered source files carry `S-XXX` references instead of the shared copy, so a page reads as a
skeleton until it is built. To produce standalone pages a designer or CMS can take:
```bash
python3 build.py # → dist/*.md, shared blocks expanded, editor annotations stripped
python3 build.py --check # verify only; non-zero exit on an unresolved ref or an empty proof field
```
`dist/` is generated and overwritten on every run — **never edit it**. Edit the numbered source files or
`_shared.md`, then rebuild. `--check` is the pre-publish gate: it fails if any `S-` ref is unresolved or
any `[ TO BE POPULATED ]` proof field is still empty.
The build also strips `F-NN` fact keys, which are provenance for editors and not reader-facing.
### Building the live pages
```bash
python3 build.py && python3 build_html.py # → src/treg/web/usecase-*.html
```
Served at `/use-cases/<slug>` off the `_USE_CASES` map in `src/treg/api.py`, with `usecase.css` as the
shared skin — the same split the legal pages use. The skin's tokens are lifted verbatim from
`landing.html` so an ad click lands on something recognisably the same product.
**The generated HTML is never hand-edited** — `build_html.py` overwrites it. Two safety properties worth
knowing before you touch that script:
- It **cuts the document at the ad-kit heading** and refuses to build if that heading is missing. Bid
keywords, negative keywords and our conversion hypotheses cannot reach a public page by accident.
- An unknown slug **404s** rather than falling through to the SPA, so a typo in a live ad is visible.
Requires `pip install markdown` — a build-time dependency only. It is never imported by the shipped
package, so it does not touch the light-CLI install weight.
### When something changes, edit here
| What changed | Edit |
|---|---|
| A catalog price, a provider count, the free grant | `_facts.md` only |
| An objection answer, a CTA, the trust line | `_shared.md` only |
| Which terms we optimize for | `seo_terms` in front-matter |
| Which terms we bid on | `ad_keywords` in front-matter |
| The product itself (e.g. a router ships) | `_shared.md`, then grep pages for the claim |
| A page's verdict is due | `_measurement.md` + that page's `verify_after` |
---
## The rules these pages obey
From `wiki/topics/treg.to SEO.md` and the `/seo-growth` + `/seo-playbook` method. Each cost something
to learn; do not quietly drop one.
**Always write `treg.to`, never bare `treg`, in every title, meta description, heading and backlink
anchor.** `treg` is regulatory T cells — an immunology term that won a Nobel Prize, with Wikipedia, NIH
and Nature owning the SERP. The bare word is not winnable and never will be. The name must be spoken and
written as a URL.
**Job, not persona, in the URL.** Nobody searches "tool platform for media buyers." The slug namespace is
`/use-cases/<job>/`, kept clear of `/tools/<capability>/` and `/compare/<capability>/`, which are reserved
for the catalog pages in the site plan. These five pages will not have to move when those ship.
**Never target `<vendor> mcp` or `<vendor> alternative` organically.** The vendor is the primary source
for its own name and always wins it — six of the top ten for `semrush mcp` are Semrush's own domains. And
the alternative family is in decline across the board (`semrush alternative` -73%, `clearbit alternative`
-48%). These terms are fine to *bid* on. They are a waste of an organic page.
**No "best X" or "X vs Y" listicle shape.** Highest competition, lowest CTR, and the shape can exclude
you from AI recommendations — 69% of self-promotional listicle citations ended with the citing brand
left out of the recommendation. These pages are workflow pages that name real prices, not rankings.
**Do crown a cheapest answer where one exists, with caveats.** This reverses an earlier draft of the site
plan. treg.to does **not** route or fail over — `llms.txt` says so explicitly — so "picking is the problem
and we remove it" contradicts the product. The honest frame is: *choose well, then switch freely without
opening a new account*, with price and measured success visible before the call.
**The CTA test is a hard gate.** What does the reader DO after this page? Here: copy the prompt, run the
call. If a future edit leaves the honest answer as "close the tab," the edit is wrong.
**No page per endpoint.** 2,600+ tools as 2,600+ thin pages is the most obvious idea available and the
one most likely to earn a site-wide penalty. Five pages carrying real measured data is the opposite shape.
**Uniqueness is the measured-comparison table.** Each page carries a provider comparison with real cost,
success rate, sample size and median latency pulled from the live catalog. That table exists nowhere else
on the internet and cannot be regenerated from a prompt — reproducing it requires holding 42 provider
accounts. It is the reason five structurally similar pages are not a template farm.
---
## Before any of this earns a verdict
`_measurement.md` has the detail. The short version: the wiki's standing position is **no paid spend for
treg.to yet**, because install → auth → first call → week-two reuse is not instrumented, and spending
against an uninstrumented funnel buys a false negative on the only question worth asking.
The user's goal here — *find out which vertical deserves the effort* — is exactly what paid traffic is
good at, so the answer is not "don't run ads." It is **instrument first-call attribution before or
alongside the first dollar**, because otherwise the experiment returns account signups, and account
signups are not the conversion these pages exist to produce.
---
## Status
Copy is written and every number is verified against treg.to and the live catalog as of **2026-08-17**.
Scale is stated as **2,600+ tools across 40+ providers** (`F-01`) — rounded down deliberately, and using
"tools" rather than "endpoints" because that is the word in `CLAUDE.md` and the one a visitor understands.
**Proof sections are populated from a real run** on 2026-08-17 — 14 calls, $0.0489 total. The full audit
is in `_run-log.md`, including the five calls that failed and cost nothing.
**Not yet done, and blocking publication:**
- **p5 must not claim hiring or activity signals.** Funding is proven; the signals endpoint is blocked by a
platform bug — see `_platform-bugs-2026-08-17.md`, item 1.
- `/catalog/platforms` reports fewer tools than the other two surfaces (see the note under `F-01`). Worth
resolving before the number goes into a Google Ads asset.
Pages 1–4 are proof-complete and ready to build.
Update `_facts.md` when a published price or claim changes, and update `_shared.md` for copy
shared by all five pages. The public build does not require the private checkout.
-242
View File
@@ -1,242 +0,0 @@
# Demo shooting script — five screen recordings
One per landing page. Each is a **single unbroken terminal take**, 45–70 seconds, no editing beyond
top-and-tail. Every command here was run for real on 2026-08-17, so the outputs below are what you should
actually see — if a take doesn't match, something changed and the page copy needs re-checking too.
Total cost to shoot all five, including one rehearsal pass: **well under $0.50**. Balance was $4.78 on the
`superdesign` team at time of writing.
---
## Before you hit record
**Use the CLI, not an agent's MCP connection.** The MCP grant on this machine is pinned to the
`superdesign-7` team ($0.02 left), not `superdesign` ($4.78). Calls through an agent will bill the wrong
balance and may 402 mid-take. Confirm with:
```bash
treg org ls # expect: * superdesign (active)
treg balance # expect: ~$4.78
```
**Terminal setup**
- Window ~110 columns × 30 rows. Wider than that and the catalog tables wrap; narrower and they truncate.
- Font 15–16pt. The persuasive content is a *table of small numbers* — if it isn't legible on a phone,
the demo does nothing.
- Light theme if you have one, to sit with the page. Dark is fine, the page's prompt block is dark too.
- Clear scrollback (`clear`) immediately before each take.
**Two hard stops**
1. **Never show the token.** Don't run `treg login`, `env`, `cat ~/.treg/*`, or anything that prints
`trg_live_…` on screen. `treg balance` and `treg call` are safe.
2. **Demo 2 returns a real person's real work email.** Do not publish that frame. The script below uses
your own domain so the address is yours. If you'd rather show a recognisable company, blur the address
in post — do not skip this.
**`jq` is required.** Every command is piped through it. `brew install jq` if `jq --version` fails.
**Rehearse first.** Run the whole block below once before recording. It costs about $0.15 and it is the
difference between a clean take and discovering on camera that a provider changed its shape.
```bash
treg balance
treg catalog get serpstat.google.keywords.volume # free
treg catalog get apollo.companies.search # free
treg catalog search "ad library" # free
# then one paid call from each demo below
```
Every jq pipeline in this file was tested against the real 2026-08-17 responses, including the rows where
a field comes back null. If one errors, the provider changed its response shape — which also means the
matching page needs re-checking, not just the demo.
**If a call fails on camera, keep rolling.** A 4xx costs nothing and treg says so. Recovering live — reading
the error, switching provider — is a better demo than a clean run. That is literally the product.
---
## Demo 1 — SEO · for `/use-cases/seo-data-for-ai-agents/`
**The one beat:** five providers sell the same keyword data at a 180× spread, and the cheapest is also the
best-measured.
**Length:** ~60s.
| # | Type this | What lands on screen | Hold |
|---|---|---|---|
| 1 | `treg catalog search "keyword search volume"` | The endpoint list with a COST column | 3s |
| 2 | `treg catalog get serpstat.google.keywords.volume` | **The money frame.** The sibling table: serpstat $0.0005/kw · 100% (77) · 1.2s, seranking $0.00179 · 92% (20), dataforseo $0.09/call · 99% (165) · 3.9s | **6s — this is the shot** |
| 3 | The call below | 13 keywords with real volume, difficulty and intent | 5s |
| 4 | `treg balance` | The charge: $0.010 | 4s |
```bash
treg call serpstat.google.keywords.volume --method POST --data '{"id":"1","method":"SerpstatKeywordProcedure.getKeywordsInfo","params":{"keywords":["mcp server","serp api","agent skills","reddit api","tiktok api","web scraping api","rank tracking api","backlink api","keyword research api","email finder api"],"se":"g_us","withIntents":true}}' \
| jq -r '.result.data[] | [.keyword, .region_queries_count, .difficulty, (.intents[0] // "-")] | @tsv' \
| sort -t$'\t' -k2 -rn | column -t -s $'\t'
```
The `-s $'\t'` matters: without it `column` splits on spaces too and `mcp server` breaks across two
columns on screen. All four pipe stages are load-bearing — copy the line whole.
Expect `mcp server 60500 8 informational` at the top, `serp api 12100 17`, `agent skills 8100 5`.
Cost $0.005–0.010 depending on how many return data.
**If you narrate, one line:** *"Five providers sell this. The cheapest is a hundred and eighty times cheaper
than the most expensive, and it also has the best measured success rate. My agent can see that before it
calls."*
---
## Demo 2 — Enrichment · for `/use-cases/lead-enrichment-for-ai-agents/`
**The one beat:** discovery is free, and a lookup that finds nothing costs nothing. You only pay for answers.
**Length:** ~55s.
⚠️ **Privacy:** step 3 returns a real work email. Use your own domain, or blur it.
| # | Type this | What lands on screen | Hold |
|---|---|---|---|
| 1 | Discovery call below | 9 funded companies **and `cost_usd: 0`** | 5s |
| 2 | `treg catalog get hunter.people.email.find` | COST line: *1 credit, charged only when an email is found; a miss is free* | 5s |
| 3 | Email call below | A found + verified address, `"status": "valid"` | 4s |
| 4 | Deliberate miss below | `"email": null` — and the cost line reads **$0.00** | **6s — this is the shot** |
| 5 | `treg balance` | One charge, not two | 4s |
```bash
# 1 — free discovery
treg call hunter.x.discover-companies --method POST \
--data '{"query":"AI infrastructure companies that recently raised funding, 20-200 employees","limit":10}' \
| jq '{companies: [.data[].organization], charged: 0}'
# 3 — a real find (use YOUR domain and name)
treg call hunter.people.email.find --query domain=YOURDOMAIN.com --query full_name="Your Name" \
| jq '{email: .data.email, score: .data.score, verified: .data.verification.status}'
# 4 — the miss. This is the beat. Nothing is charged.
treg call hunter.people.email.find --query domain=stripe.com --query full_name="Nonexistent Personname" \
| jq '{email: .data.email}'
```
**Narration:** *"Finding the companies was free. Finding an email that doesn't exist was also free. I pay two
and a half cents when it actually finds someone."*
---
## Demo 3 — Social · for `/use-cases/social-trend-research-for-ai-agents/`
**The one beat:** four platforms — including ones whose official APIs you cannot buy — in one run, under a cent.
**Length:** ~70s (it's four calls; keep each tight).
| # | Type this | What lands on screen | Hold |
|---|---|---|---|
| 1 | TikTok call | 10 videos with play counts | 4s |
| 2 | YouTube call | Videos with view counts, one published hours ago | 4s |
| 3 | X call | A live search timeline | 3s |
| 4 | Reddit call | Posts with scores | 3s |
| 5 | `treg balance` | **Four platforms, ~$0.006 total** | **6s — this is the shot** |
```bash
treg call tikhub.tiktok.search.videos --query keyword="ai agents" --query count=10 \
| jq -r '.data.search_item_list[]?.aweme_info | "\(.statistics.play_count)\t\(.desc[0:60])"' | head -5
treg call tikhub.youtube.search.videos --query keyword="ai agents" --query language_code=en --query country_code=us \
| jq -r '.data.videos[] | "\(.view_count)\t\(.published_time)\t\(.title[0:50])"' | head -5
treg call tikhub.x.twitter-web-fetch-search-timeline --query keyword="ai agents" \
| jq '{ok: .code, at: .time}'
treg call scrapecreators.reddit.search.posts --query query="ai agents" --query sort=top --query timeframe=week \
| jq -r '.posts[] | "\(.score)\t\(.subreddit)\t\(.title[0:50])"' | head -5
```
**Honesty note, and I'd keep it in:** the Reddit results are noisy — site-wide search returns off-topic hits.
If you want the take clean, add `--query subreddit=LocalLLaMA`. If you want it credible, leave it and say
*"that one's a bad result — ranking is the provider's, and I'd pin a different one for real work."*
**Narration:** *"TikTok's API is invite-only. X wants two hundred a month. That was four platforms for
six tenths of a cent."*
---
## Demo 4 — Ads · for `/use-cases/competitor-ad-research-for-ai-agents/`
**The one beat:** the Google Ads Transparency call reveals the same advertiser split across three registered
entities — search one name and you see a third of the activity. Nobody expects this.
**Length:** ~60s.
| # | Type this | What lands on screen | Hold |
|---|---|---|---|
| 1 | Meta call | Ad count + live creative with body copy | 4s |
| 2 | Google call | **Semrush INC ~20,000 · Semrush Inc ~9,000 · Semrush Inc. ~500** | **7s — this is the shot** |
| 3 | LinkedIn call | 2,107 ads, full post copy | 4s |
| 4 | TikTok call | Running TikTok ads with dates | 3s |
| 5 | `treg balance` | Four ad libraries, ~$0.0075 | 4s |
```bash
treg call scrapecreators.x.v1-facebook-adlibrary-search-ads --query query="seo tool" --query country=US --query status=ACTIVE --query trim=true \
| jq '{matching_ads: .searchResultsCount, sample: [.searchResults[0].snapshot | {page_name, cta_text, body: .body.text[0:80]}]}'
treg call scrapecreators.x.v1-google-adlibrary-advertisers-search --query query=semrush \
| jq '.advertisers'
treg call scrapecreators.x.v1-linkedin-ads-search --query keyword="seo software" \
| jq '{total: .totalAds, sample: [.ads[0] | {poster, promotedBy}]}'
treg call scrapecreators.x.v1-tiktok-ad-library-search --query query="Semrush" \
| jq '{source, ads: [.ads[0] | {name, first_shown_date}]}'
```
**Narration for the money frame:** *"Same company, three registered advertiser entities. If you searched one
name you'd see a third of what they're actually running."*
---
## Demo 5 — Company data · for `/use-cases/company-research-for-ai-agents/`
**The one beat:** eleven providers answer one job, from free to $0.38 a record. A 200× spread on the same
question.
**Length:** ~55s.
| # | Type this | What lands on screen | Hold |
|---|---|---|---|
| 1 | `treg catalog get apollo.companies.search` | **The money frame.** Eleven siblings with COST / WORKS / SPEED — akta free, thecompaniesapi $0.0019, apollo $0.026, pdl $0.38 | **8s — hold longest of any shot** |
| 2 | Free search below | Real companies, `cost_usd: 0` | 4s |
| 3 | Funding call below | Stripe: $9.8B raised, last round, named investors — $0.10 | 5s |
| 4 | The miss below | *"Company funding data not found"*, `credits_consumed: 0` | 5s |
```bash
treg call hunter.x.discover-companies --method POST \
--data '{"query":"AI infrastructure companies that recently raised funding, 20-200 employees","limit":10}' \
| jq '{found: [.data[].organization] | length, charged: 0}'
treg call leadmagic.x.company-funding --method POST --data '{"company_domain":"stripe.com"}' \
| jq '{company: .basicInfo.companyName, raised: .financialInfo.formattedFunding, last: .financialInfo.lastFundingRound.round}'
# the honest beat — coverage is thin on small startups, and the miss is free
treg call leadmagic.x.company-funding --method POST --data '{"company_domain":"daloopa.com"}'
```
**Narration:** *"Eleven providers answer that one question. Three of them are free, one charges thirty-eight
cents a record. Same job. Most people just buy the first one they've heard of."*
---
## What to do with the five files
- **On the pages:** place below the copy-prompt block, never above it. The conversion is the first call, and a
satisfying video above the fold is a reason to close the tab. The pages already emit `lp_copy_prompt`, so
ship the demo to two of the five first and compare — don't assume it helps.
- **Stamp every one** "recorded 17 Aug 2026". Catalog prices are re-checked over time and a demo hardcodes a
moment, unlike everything else on these pages which routes through `_facts.md`.
- **Reuse:** demo 4's three-entity reveal and demo 5's eleven-provider table are the two that work as
standalone social clips. Demo 2's free-miss is the best 15-second cut.
## Where the take can go wrong
| Symptom | Cause | Do this |
|---|---|---|
| 402 mid-take | Billing the `superdesign-7` team | `treg org use superdesign`, check `treg balance` |
| Terminal floods with JSON | Dropped the `jq` pipe | Re-run with the pipe as written |
| `405` on a TikTok ads call | You used `tikhub.x.tiktok-ads-search-ads` — it's broken | Use `scrapecreators.x.v1-tiktok-ad-library-search` as scripted |
| `no endpoint '…' in the catalog` | Bad id | `treg catalog search "<the job>"` and read the real id |
| Reddit results look irrelevant | Provider ranking, not your query | Add `--query subreddit=…`, or leave it and say so |
+3 -35
View File
@@ -159,41 +159,9 @@ E-commerce 81 · Reviews & Apps 55 · Market data 40 · Community 16 · Develope
---
## Production telemetry — 30 days to 2026-08-17 (`F-90`)
## Public endpoint observations
Swept from the live registry's public endpoint view, which attaches cross-tenant `observed` stats. Source:
the "What Agents Call treg For" artifact. **24,921 catalog calls across every team.**
**Publishable vs internal — read this before using anything here.**
- ✅ **Per-endpoint `calls served`, `ok` rate and `p50`** are fine to publish. `catalog_get` already shows
these cross-tenant numbers to any user, so they are public information, and they are the strongest
reliability evidence we have.
- ❌ **The aggregates are internal**: total platform volume (24,921), the 82%-of-catalog-dark figure, the
provider revenue split, the never-used counts. Those describe our business, not a caller's experience.
Keep them in this file and out of the pages.
### Where the traffic actually goes (`F-91` — internal, for targeting only)
| Job | Calls | Share |
|---|---|---|
| Google SERP (maps + organic + news + trends) | 7,559 | 30.3% |
| X / Twitter | 4,351 | 17.5% |
| People enrichment | 3,657 | 14.7% |
| TikTok | 1,849 | 7.4% |
| Company data | 1,164 | 4.7% |
| LinkedIn | 1,077 | 4.3% |
| Instagram | 1,060 | 4.3% |
| Web scraping | 961 | 3.9% |
| Reddit | 886 | 3.6% |
| AI answer engines | 376 | 1.5% |
| **Meta ad library** | **303** | **1.2%** |
| YouTube | 162 | 0.7% |
Two findings that change page targeting:
- **Keyword research is not what people do.** No keyword endpoint appears in the top 20. Serpstat totals
334 calls (1.3%). The real "SEO" job is **scraping result pages** — Maps alone is 2,646.
- **Local/Maps is the biggest uncovered job**, and no page in this cluster addresses it.
These historical per-endpoint figures were exposed by the public catalog. Refresh before reuse.
### Measured endpoint stats — use these, they beat the old sample sizes (`F-92`)
@@ -237,7 +205,7 @@ Two findings that change page targeting:
### The caveat that limits all of the above
The stats pool every org deliberately: **there is no per-tenant breakdown, so 24,921 calls could be forty
The stats pool every org deliberately: **there is no per-tenant breakdown, so the observed calls could be forty
teams or four.** Our own `superdesign` org contributes under 40 of them, so the demand is genuinely
external — but its concentration is unknown. Treat this as directional for targeting, not as proof of a
market. The team count needs `TREG_ADMIN_TOKEN` and one `/admin/*` call.
+7 -112
View File
@@ -1,114 +1,9 @@
# Measurement — how these five pages answer the actual question
# Landing-page measurement contract
The question behind this whole exercise is **which vertical deserves the effort**. Five pages plus paid
traffic is a good way to answer it. It only works if the thing being counted is the right thing.
`lp_copy_prompt` records a leading interaction; a successful tool call records product use.
The conversion implementation and first-call semantics are documented in
[ads-conversions](../../docs/context/architecture/ads-conversions.md).
---
## The conversion is the first successful call, not the signup
An account is free and costs a visitor nothing to create, so signups measure curiosity. The funnel that
decides whether a vertical is real is:
```
page view → Copy Prompt → account created → agent installed → FIRST SUCCESSFUL CALL → a second call in week 2
```
**None of the steps after "account created" are currently instrumented.** This is the standing blocker in
`wiki/topics/treg.to SEO.md` §8–9, and it is the reason the wiki's position has been *no paid spend yet*:
spend against an uninstrumented funnel returns a verdict on the wrong metric. A vertical whose visitors
sign up and never call looks identical to a vertical whose visitors sign up and call daily.
**This does not mean don't run ads. It means instrument first-call attribution before or alongside the
first dollar.** Minimum viable version, in rough order of cost:
1. A `?utm_source/medium/campaign/content` scheme where `utm_content` carries the page id (`p1`…`p5`),
persisted to the account record at signup.
2. A `first_call_at` timestamp on the team, and the endpoint id of that first call.
3. A weekly join: page → signups → first calls → teams with a call in days 8–14.
Until (2) exists, every number below is a leading indicator, not a verdict.
---
## Two clocks, and the pages are on the slow one
The day-7 gate that makes cheap keyword bets affordable is the **wrong clock for these pages**. It is
built for emerging-term content bets; an evergreen utility or commercial page earns nothing for months and
then compounds. Pointing the day-7 gate at this cluster would kill it just before it started working.
| Channel | Clock | Verdict on |
|---|---|---|
| **Google Ads** | day 7 and day 14 | cost per first successful call, by page |
| **Organic** | day 14/30 (indexation, qualified impressions), then day 60/120 | assisted signups and first calls |
Score the **daily series**, not a trailing average — a 7-day mean printed "scale, confirmed" on another
property while the daily numbers fell for seven consecutive days. And end every window **3 days back**;
Search Console lags 2–3 days and ending on yesterday silently ends every window on preliminary data.
---
## Per-page hypotheses
Each page states one measurable hypothesis in its front-matter. Collected here so they can be scored
together.
| Page | Hypothesis | Scored on |
|---|---|---|
| 01 SEO | Highest volume, lowest intent — SEO people already own a tool. Predict the **most clicks and the worst cost per first call** of the five. | CPFC vs. the other four at day 14 |
| 02 Lead enrichment | Highest commercial intent. Predict the **lowest cost per first successful call**. | CPFC, day 14 |
| 03 Social trends | Widest audience, weakest developer overlap. Predict high Copy Prompt rate and low install-completion. | Copy Prompt → account, then account → first call |
| 04 Competitor ads | Narrowest audience, sharpest pain. Predict the **highest Copy-Prompt-to-first-call rate**, on low volume. | Copy Prompt → first call |
| 05 Company signals | Closest to the catalog's strongest data (11 providers, 200× spread). Predict the **highest week-2 repeat-call rate**. | teams calling again in days 8–14 |
### What the 30-day telemetry already settled (2026-08-17)
Production usage answers part of this before a dollar is spent. It measures **existing** users, not the
ones these pages are meant to acquire, so it constrains rather than decides — but it is real demand and it
moves two of the five.
- **p4's narrowness is confirmed.** The Meta ad library is 303 calls, **1.2% of all traffic** — the
smallest job of the five by a wide margin. The "sharp pain" half of the hypothesis is still open. Fund
it last, and only if Copy-Prompt-to-first-call clears the others by enough to justify the CPC.
- **p1 was aimed at the wrong slice and has been re-pointed.** Keyword research is ~1.3% of usage and no
keyword endpoint appears in the top 20; **scraping result pages is ~22%**. The page now leads its second
workflow with recurring SERP monitoring. Watch whether the two prompts split the Copy-Prompt event — if
the SERP one wins decisively, the hero should follow it.
- **p3's premise strengthened.** X alone is 17.5% of traffic and its search endpoint is the single
most-called tool on the platform. The page was written as a four-platform sweep; if the data holds, an
X-specific page is the better ad destination and this one becomes the organic hub.
**The limit on all three.** The telemetry cannot say how many teams produced it — 24,921 calls could be
forty teams or four. Do not treat a large share as a large market until the team count is known
(`TREG_ADMIN_TOKEN`, one `/admin/*` call). A 17.5% share driven by one integration is a different fact
from the same share across thirty customers.
Two of these are deliberately predictions of *failure*. A test where every arm is expected to win teaches
nothing, and the cheapest outcome here is finding out early which two verticals to stop paying for.
**The cross-page hypothesis:** Copy Prompt clicks predict first-call conversion better than account
creations do. If that holds, Copy Prompt becomes the optimization target for the ad accounts and the
primary on-page event, which is a cheaper signal to collect than the full funnel.
---
## What each page must emit as events
Same names on all five, so the pages are comparable:
| Event | Fires on |
|---|---|
| `lp_view` | page load, with `page_id` |
| `lp_copy_prompt` | any Copy Prompt affordance, with `page_id` **and `prompt`** — the id of the block copied (`#prompt`, `#prompt-2`). p1 carries two workflows and which one people take is the question |
| `lp_cta_primary` | Run This Workflow Free |
| `lp_cta_secondary` | See the Example |
| `lp_scroll_proof` | proof section enters the viewport (does the evidence get read?) |
| `lp_objection_open` | an objection expands, with which one — **this is the highest-value diagnostic on the page**; the objection people open most is the one the hero failed to answer |
---
## Verdict dates
Set on ship. Each page's front-matter carries `verify_after`. A page with a passed `verify_after` and no
recorded verdict is an open bet nobody scored, which is the failure mode that made the scorecard loop a
separate role from the engine loop in the first place.
Hosted campaign hypotheses, bidding decisions and historical verification records live in the
[private marketing records](https://github.com/superdesigndev/treg-internal/tree/main/docs/marketing).
This public file describes the metric referenced by the shipped page instrumentation only.
@@ -1,111 +0,0 @@
# Message to send to the treg.to developer
Copy from the line below. Findings are from ~20 real calls on 2026-08-17 across two teams
(`superdesign` and `superdesign-7`) while building landing-page proof sections. Everything here was
reproduced at least twice unless noted.
---
Hey — I ran about 20 real calls through treg.to today (SEO, enrichment, social, ads, company data) and hit
six things worth fixing. Roughly in order of how much they cost me.
**1. `catalog_search` over MCP returns an endpoint id that doesn't exist.**
Searching `"hiring headcount"` via the MCP `catalog_search` tool returns:
```json
{"endpoint_id": "lusha.companies-signals", "usd_per_call": 0.1248, "no_key_needed": true}
```
But that id doesn't resolve:
```
$ treg catalog get lusha.companies-signals
no endpoint 'lusha.companies-signals' in the catalog
```
The real ids are `lusha.x.company-signal-types`, `lusha.x.company-signal-filters`,
`lusha.x.companies-website-visits`. This breaks the documented search → get → call flow at the first step,
and it does it *with a confident price attached*, so an agent will plan around a call it can never make.
This is the one I'd fix first. It blocked a whole workflow for me.
**2. `tikhub.x.tiktok-ads-search-ads` has the wrong method recorded, and can't be called at all.**
```
POST → treg says: "tikhub.x.tiktok-ads-search-ads is GET — add --method GET"
GET → provider returns 405 Method Not Allowed
```
So treg enforces GET and the upstream rejects GET. Catalog metadata looks wrong. Its stats row also reads
`— (7)` for WORKS with `LAST OK: today`, which I can't interpret — 7 samples, no success rate, but it
answered today? If those 7 are all failures, the row should say so, because it currently reads as "fine,
just new."
`scrapecreators.x.v1-tiktok-ad-library-search` works perfectly for the same job at $0.00188.
**3. Boolean query params get serialized wrong over MCP.**
Calling `thecompaniesapi.companies.search` with `query: {"simplified": true}` returns:
```json
{"details": [{"message": "The value must be a boolean", "rule": "boolean", "field": "simplified"}]}
```
Looks like a Python `True` reaching the wire as `"True"` instead of `"true"`. Worth fixing generally, but
it stings on this endpoint specifically: `simplified=true` is the **free** mode. The bug silently pushes
callers onto the paid path for a query they could have previewed for nothing.
**4. `catalog_search` ranking doesn't break ties on price or measured reliability.**
`catalog_search "ad library"` with `limit=15` returned seven tikhub tiktok-ads endpoints — including the
broken one in #2 — and **omitted** `scrapecreators.x.v1-tiktok-ad-library-search`, which is $0.00188 with
100% success over 16 measured calls. Everything scored 6, so the cut was effectively arbitrary.
The default MCP limit is 8, so in practice an agent asking for ad-library tools gets shown unmeasured and
broken options and not the best-measured one. You already collect WORKS/SPEED/COST — using them as the
tiebreaker inside an equal relevance score would fix this and would make the "your agent picks on evidence"
story actually true at the search step, not just at `catalog_get`.
**5. The MCP OAuth grant is pinned to a team that `treg org ls` doesn't list, with no way to see or change it.**
MCP `balance` reported team `superdesign-7`. My CLI shows only:
```
* superdesign superdesign admin (active)
ai-jason AI Jason admin
```
I spent ~$0.05 out of `superdesign-7` before anyone noticed, because from inside the agent there is no
signal that it's the wrong team — `balance` returns a slug I had no way to recognise as unexpected.
Two asks: (a) make `balance` / `my_tools` return something a human can sanity-check (team display name +
which identity the grant belongs to), and (b) give the agent or the user a way to switch the MCP grant's
team without re-doing OAuth. Right now the consent-screen choice is invisible and permanent, and the
failure mode is silent spend on the wrong balance.
**6. Endpoint and provider counts disagree across three surfaces.**
- treg.to hero: **2,617 endpoints / 42 providers**
- `llms.txt`: **~2,600 endpoints / ~48 providers**
- `GET /catalog/platforms`, summed: **2,363 endpoints / 47 providers / 80 platforms**
Not a functional bug, but I'm about to put a number in Google Ads copy and it has to be defensible on the
page it points at. Whichever is right, the other two should be computed from it. `/catalog/platforms` being
the low one is the awkward part, since that's the surface anyone auditing would hit.
---
**Two provider-side things, not treg bugs, but maybe worth a catalog note:**
- `leadmagic.x.company-funding` found nothing for `runpod.io` or `daloopa.com` (both free misses — the
free-on-miss pricing is great, by the way) but returned a full history for `stripe.com`. Coverage seems
thin on small recently-funded startups, which is the main use case people will bring to it. A coverage
hint on the endpoint would save people the discovery.
- `scrapecreators.reddit.search.posts` on `"ai agents"` returned an unrelated r/lol post as a top result.
Site-wide relevance is weak; subreddit-scoped search is fine.
**What worked really well, since it's all complaints above:** failed calls genuinely cost nothing — I had
five failures including a treg-side refusal that never reached the provider, and all were $0.00. Every
`cost_usd` matched what `catalog_get` quoted beforehand, exactly. And `catalog_get`'s sibling table with
COST/WORKS/SPEED/sample size is the best thing in the product — it's the reason I could pick providers at
all. Item 4 is really just "please use that data one step earlier."
-82
View File
@@ -1,82 +0,0 @@
# Proof run — 2026-08-17
Every proof section in the five pages comes from these runs. **Total spend across both: $0.1499.**
> ⚠️ **Batch 1 billed the wrong team.** The MCP grant is pinned to `superdesign-7`, not the `superdesign`
> team the CLI treats as active. $0.0489 came out of a balance nobody was watching, and there was no signal
> from inside the agent that this was happening. Reported as bug 5 in `_platform-bugs-2026-08-17.md`.
> Batch 2 was run through the CLI and correctly billed `superdesign` ($4.8821 → $4.7811).
## Batch 1 — team `superdesign-7`, via MCP
**Balance before $0.073572 → after $0.024672. Spent: $0.0489.**
| # | Page | Endpoint | Cost | Outcome |
|---|---|---|---|---|
| 1 | p2, p5 | `hunter.x.discover-companies` | $0 | 9 funded AI-infra companies, 20–200 employees |
| 2 | p5 | `thecompaniesapi.companies.search` | $0 | **400** — boolean sent as string. Not billed |
| 3 | p1 | `serpstat.google.keywords.volume` | $0.010 | 20 submitted, 13 returned with volume + difficulty + intent |
| 4 | p3 | `scrapecreators.reddit.search.posts` | $0.00188 | 29 posts. **Poor relevance** — see p3 |
| 5 | p4 | `scrapecreators.x.v1-facebook-adlibrary-search-ads` | $0.00188 | 914 matches, 29 ads with full creative |
| 6 | p2 | `hunter.people.email.find` | $0.0245 | Found + verified `valid`, confidence 80 |
| 7 | p1 | `dataforseo.google.serp.organic` | $0.002 | Top 7 for `serp api`, stamped 02:06:40 UTC |
| 8 | p4 | `scrapecreators.x.v1-linkedin-ads-search` | $0 | **400** — used `query` instead of `keyword`. Not billed |
| 9 | p4 | `scrapecreators.x.v1-google-adlibrary-advertisers-search` | $0.00188 | Semrush across 3 advertiser entities, ~29,500 ads |
| 10 | p4 | `scrapecreators.x.v1-linkedin-ads-search` | $0.00188 | 2,107 ads, 24 with full copy |
| 11 | p5 | `scrapecreators.x.v1-linkedin-company` | $0 | **400 from treg.to itself** — required param missing, refused before reaching the provider |
| 12 | p3 | `tikhub.tiktok.search.videos` | $0.001 | 10 videos with play/like/comment/share |
| 13 | p5 | `scrapecreators.x.v1-linkedin-company` | $0.00188 | **Wrong entity returned** — see p5 |
| 14 | p4 | `tikhub.x.tiktok-ads-search-ads` | $0 | **405** on the documented shape. Not billed, not retried |
| 15 | p3 | `tikhub.youtube.search.videos` | $0.002 | 19 videos + 25 Shorts with view counts |
**14 calls attempted, 9 billed, 5 failures — none of which cost anything.** That is the single most
persuasive number in this table and it was not designed, it just happened.
## Batch 2 — team `superdesign`, via CLI (gap-closing)
**Balance before $4.8821 → after $4.7811. Spent: $0.1010.**
| # | Page | Endpoint | Cost | Outcome |
|---|---|---|---|---|
| 16 | p4 | `scrapecreators.x.v1-tiktok-ad-library-search` | $0.00188 | Semrush's TikTok ads — video URLs, shown dates, audience bands. **Closes p4 to 4 of 4** |
| 17 | p5 | `leadmagic.x.company-funding` (runpod.io) | $0 | No funding data. Free miss |
| 18 | p5 | `leadmagic.x.company-funding` (daloopa.com) | $0 | No funding data. Free miss |
| 19 | p5 | `leadmagic.x.company-funding` (stripe.com) | $0.10 | Full history — $9.8B raised, revenue, last round, named investors |
| 20 | p3 | `tikhub.x.twitter-web-fetch-search-timeline` | $0.001 | X search timeline. **Closes p3 to 4 of 4** |
| — | p5 | `lusha.companies-signals` | — | **Endpoint id does not exist.** Blocked; see bug 1 |
Across both batches: **20 calls, 11 billed, 9 failures or misses, $0.00 charged for every one of them.**
---
## What these runs prove, and what they don't
**Proven and safe to publish:**
- Failed calls are free, including a treg.to-side refusal that never reached the provider.
- Prices match the catalog exactly — every `cost_usd` matched what `catalog_get` quoted beforehand.
- The credential never touched this machine; no provider account was created for any of it.
- Real, current data: results timestamped to the minute, a YouTube video published 11 hours earlier.
**Closed in batch 2:** p3 is 4 of 4 platforms. p4 is 4 of 4 ad libraries. p5's funding leg is proven.
**Still not proven — the page must not claim it:**
- **p5, activity/hiring signals.** Blocked by bug 1: the endpoint id `catalog_search` returns for company
signals does not resolve. Funding is proven; "buying signals" broadly is not.
- Sample sizes are demonstrations, not benchmarks: 20 keywords not 50, one email not 50.
---
## Two findings worth keeping
**The wrong-company result (item 13).** Asked for `linkedin.com/company/runpod`, the provider returned a
two-person retail partnership in Sligo, Ireland — correctly, because that is what is at that URL. The AI
infrastructure company is `/company/runpod-io`. A confident wrong answer for $0.00188. This is the
catalog's first selection rule in practice — match the inputs you actually hold, ahead of price — and it
is the clearest argument for why treg.to relays a request rather than rewriting it.
**Reddit relevance (item 4).** A whole-site search for "ai agents" surfaced an unrelated r/lol post as a
top result. Ranking belongs to the provider. Three others serve Reddit search, one at $0.001, and
subreddit-scoped search is a separate endpoint. Cheap to fix, but it is a real limitation.
Both are on the pages. Neither is flattering, and both make the pages more credible than a clean run would
have.
-311
View File
@@ -1,311 +0,0 @@
# treg.to programmatic pages — build spec
Companion to `pseo-catalog-plan.md` (the why). This is the what: every page type, its URL, what is
on it, where each block's data comes from, how it links, and when it is indexed. Written 2026-08-21.
Rules that apply to every page below:
- **Server-rendered through `_page()` in `api.py`** (title, description, canonical, og/twitter,
JSON-LD all owned by the shell). No SPA, no `#prerender` trick — GPTBot/ClaudeBot/PerplexityBot
do not run JavaScript, and agent retrieval is half the point.
- **Every number and name comes from `catalog_store.load()` at request time.** No page may carry a
count, price or provider name that is not in the catalog. Counts that appear in copy ("81
platforms", "2,800 endpoints") are computed, never typed.
- **`treg.to` in every title, H1 and anchor.** Never bare `treg`.
- **Observed stats** (success rate, p50) appear only with the one-line caveat "measured on treg.to
traffic; inputs and sample sizes differ by provider — not a controlled benchmark", and only if
Jason approves per-vendor publication. Prices appear always.
- **Routing and sitemap from one map**, as `_USE_CASES` / `_SITEMAP_PAGES` do today: a page cannot
be routed and forgotten by the sitemap. `tests/test_seo.py` walks every sitemap URL for a 200.
- **No trailing slashes; canonical = bare path.** `?v=<mtime>` on any new stylesheet.
- **Every row is a CTA.** The visitor's agent is remembered in `localStorage` (first visit: picker);
"Use in ChatGPT / Claude / Claude Code / Cursor" buttons on every row open the install path for
that agent, never `/app`.
- **Sitewide footer** on all of these: the 12 most-called platforms (from `observed` call counts)
and the four agent pages. That is where authority pools.
---
## Page type A — agent hub · `/apps/<agent>` · 10 pages
Revised 2026-08-21: the onboarding (`welcomeAgents` / `welcomeMoreAgents` in `index.html`) already
ships a setup path for **OpenClaw, Hermes Agent, Claude.ai, Claude Code, Codex, opencode, pi,
Cursor, Gemini CLI**, and ChatGPT has the Plugins listing. One page each. The list must move to one
shared source (a small JSON the SPA and the server pages both read) so a page and the dropdown can
never disagree.
The install is mostly **agent-agnostic** — the onboarding's "set up treg — https://treg.to/skill.md …"
line pasted into any agent's chat (`buildAgentPrompt`). What differs per agent, and is the
per-page content: *where* you paste it (screenshot), and the native path where one exists —
ChatGPT: Plugins → Install; Claude.ai: Connectors → `https://treg.to/mcp/` (OAuth); Claude Code /
Cursor / opencode: `treg mcp install`. Hand-written above the fold, generated below.
| Element | Content | Source |
|---|---|---|
| Title | `treg.to for ChatGPT — 2,800 tools your ChatGPT can call, no API keys` | count computed |
| Meta | `Install treg.to from ChatGPT's Plugins directory and ChatGPT can call {N} APIs across {P} platforms — SEO, LinkedIn, Reddit, people search — priced per call, no provider signup.` | computed |
| H1 | `What do you want ChatGPT to do?` | fixed per agent |
| Install block | per agent: **ChatGPT** — "Plugins → search *treg* → Install", with Jason's screenshot; **Claude** — "Settings → Connectors → Add → `https://treg.to/mcp/`" (OAuth); **Claude Code / Cursor** — `curl -fsSL https://treg.to/install.sh \| sh` then `treg mcp install`. A button that starts the flow where a deep link exists. | `llms.txt` §MCP, `skill.md` |
| Five prompts | five pasteable prompts that each complete a real job end to end, e.g. "Find the work email of the VP Marketing at stripe.com and tell me what it cost" / "Pull the top 20 Google results for 'serp api' with each page's backlink count" / "Give me the last 30 days of Reddit posts mentioning 'mcp server' with upvotes". Each prompt names the capability it will hit and its price. | hand-written once, prices computed |
| Sections by category | **Sales & people** · **SEO & search** · **Social: LinkedIn, Instagram, TikTok, X, Reddit, YouTube** · **Companies & market** · **Ads** · **Web & scraping** — each section = rows of use cases (type D) with price range and provider count; the section heading links to its category hub (type C) | catalog categories → `_USE_CASES` |
| Platform grid | all platforms with ≥2 providers, linking to `/mcp/<platform>` (type B), with endpoint count and lowest price | catalog |
| How billing works | $1.00 free per new team, per-call metering, own keys never metered | `_facts.md` keys, not typed |
| JSON-LD | `SoftwareApplication` (name treg.to, `applicationCategory` DeveloperApplication, `offers` $0 with the free grant — matches visible text) + `BreadcrumbList` + `FAQPage` (4 Qs that appear verbatim in an FAQ section: is it free, do I need API keys, which agents, what does a call cost) | — |
| Links out | 4 agent siblings, 5 category hubs, ~25 platform hubs, ~30 use-case rows | — |
| Index | yes, day one | — |
Add `windsurf`, `antigravity`, `perplexity` etc. only when the onboarding dropdown gains them —
the page list is derived from that list, never maintained separately.
## Page type B — `/mcp/<platform>` · 3 pages, provisional
Revised 2026-08-21 after Jason asked "why B?". With E (pricing/comparison) and D (use cases) in
place, B is redundant as a hub — all three list the same providers and jobs. Its only distinct job
is carrying the exact phrase `<platform> mcp server`, which is worth a page only where people type
it. Measured: `linkedin mcp server` 480 · `reddit mcp server` 210 · `youtube mcp server` 140; the
SERPs are GitHub repos and small directories — the most winnable terms in the study. No other
platform showed volume.
So: **three pages**, built like Pipedream's winning `mcp.pipedream.com/app/<app>` pages — short,
exact-match title, install steps for every agent, then links down to `/pricing/<platform>-api` and
the spokes. If none has moved by day 30, drop B and put the MCP install block on E instead. The
platform-level hub role belongs to E, which has the editorial content to deserve it. The section
table below describes the full version; the three pages ship the short one.
| Element | Content | Source |
|---|---|---|
| Title | `LinkedIn MCP server — 8 providers, 43 endpoints, from $0.002/call \| treg.to` | catalog |
| Meta | `Give ChatGPT, Claude, Claude Code or Cursor a LinkedIn MCP server with {N} endpoints from {providers}: profiles, company pages, posts, jobs. Priced per call, compared side by side, no provider signup.` | catalog |
| H1 | `LinkedIn MCP server for ChatGPT, Claude and Cursor` | — |
| Lede | one generated paragraph: what the agent can now do on this platform, in jobs not endpoints ("look up a member's profile, a company page, a post and its comments, search jobs") — composed from the capability descriptions | `capability.description` |
| Install tabs | the same four install blocks as type A, collapsed; the `claude-code`/`cursor` tab includes `treg catalog search "linkedin profile"` as the first command | — |
| **Jobs on this platform** | one row per capability: plain-words name, provider count, price range, verified count, link to its use-case page (type D). Single-provider capabilities are listed but link to `/catalog/<slug>#<cap>` instead of a spoke | catalog |
| **Providers compared** | table: provider · endpoints on this platform · cheapest price · verified · (observed ok-rate / p50 if approved) · "own key?" — the comparison is the product | catalog + `observed` |
| One runnable call | `treg call <cheapest verified endpoint> …` with its `test_request`, and the MCP equivalent ("ask your agent: …") | `test_request` |
| Who owns the key | the honest paragraph: treg.to's key, metered per call; or the team's own key, never metered; treg compares, **does not route** | `CLAUDE.md` rule |
| JSON-LD | `ItemList` of capabilities (as `/catalog/<slug>` does now) + `BreadcrumbList` + `FAQPage` (3 Qs visible on page: what does a LinkedIn profile lookup cost, does it need a LinkedIn login, which agents) | — |
| Links | up: `/apps/<agent>` ×4, `/catalog/<slug>`; down: every use case on this platform; across: `/integrations/<provider>` for each provider | — |
| Index | yes | — |
## Page type C — category hub · `/use-cases/<category>` · 5 pages (exist)
The five landing pages (`seo-data-for-ai-agents`, `lead-enrichment-for-ai-agents`,
`social-trend-research-for-ai-agents`, `competitor-ad-research-for-ai-agents`,
`company-research-for-ai-agents`) stay as the ad destinations they are. Two additions, appended
below the existing copy so `build_html.py` keeps owning the top:
- **a generated "every job in this category" list** — rows linking to type D pages, with price
ranges; this is what makes them hubs;
- the four agent install buttons.
Their ad-kit cut-off rule stays; the generated block is injected by the route, not by the builder.
## Page type D — use case (the spoke) · `/use-cases/<category>/<job>` · ~201 generated, ~100–150 indexed
One per capability with ≥2 providers. Slug = the job in words, not the dotted id:
`people.email.find` → `/use-cases/lead-enrichment/find-work-email`. Slugs are a hand-kept map
(`capability id → slug, category, job sentence`) committed next to the catalog; a capability
without a slug renders nothing, so a typo cannot mint a page.
| Element | Content | Source |
|---|---|---|
| Title | `Find someone's work email from a name and company — 9 providers, from $0.009/success \| treg.to` | slug map + catalog |
| Meta | `{job sentence}. Compare {providers} on price, verified status and measured success, then call the one you pick from ChatGPT, Claude, Claude Code or Cursor — no provider account.` | — |
| H1 | the job sentence | slug map |
| Agent tabs | ChatGPT / Claude / Claude Code / Cursor — each tab shows *the prompt to type* for this job in that client, and the install link if not yet installed. One URL; the tab is client-side state | — |
| **Providers for this job** | table: provider · endpoint id · price (unit: /call, /result, /success) · verified date · (observed) · input it needs (email? LinkedIn URL? name + domain?) — the "pick the one whose inputs match what you HAVE" guidance from `catalog get` | catalog |
| Parameters | union of the inputs, marked required/optional per provider | `input` |
| One runnable call | cheapest verified endpoint's `test_request` as `treg call …` plus the MCP phrasing | `test_request` |
| Example response | first 25 lines of the `example_file`, if present | `example_file` |
| Same job, other platforms | e.g. "Find a person by email" → its siblings in `people.*` | capability family |
| Related jobs on these platforms | links to type B hubs and sibling spokes | — |
| JSON-LD | `BreadcrumbList` + `ItemList` of the provider endpoints (`name`, `url` = `/catalog/endpoints/<id>` JSON is *not* a page — point to the spoke's own `#<provider>` anchor) | — |
| Links | up: category hub, platform hub(s), agent hubs; across: siblings | — |
| **Index rule** | `index` when (≥2 providers) **and** (≥1 verified endpoint) **and** (observed calls in the last 30 days ≥ 1) — else `noindex,follow` and not in the sitemap. Recomputed on each sitemap render, so a job that starts getting used starts getting indexed | `observed` |
First batch (all ≥5 providers, mostly verified, already used in production):
| Slug | Capability | Providers |
|---|---|---|
| `/use-cases/lead-enrichment/find-work-email` | `people.email.find` | 9 |
| `/use-cases/lead-enrichment/enrich-a-person` | `people.enrich` | 13 |
| `/use-cases/lead-enrichment/search-people-by-title-and-company` | `people.search` | 14 |
| `/use-cases/lead-enrichment/verify-an-email` | `people.email.verify` | 5 |
| `/use-cases/lead-enrichment/find-a-phone-number` | `people.phone.find` | 5 |
| `/use-cases/company-research/enrich-a-company` | `companies.enrich` | 19 |
| `/use-cases/company-research/search-companies` | `companies.search` | 17 |
| `/use-cases/seo/google-search-results` | `google.serp.organic` | 5 |
| `/use-cases/seo/keyword-search-volume` | `google.keywords.volume` | 5 |
| `/use-cases/seo/keyword-ideas` | `google.keywords.ideas` | 6 |
| `/use-cases/seo/keywords-a-domain-ranks-for` | `google.domain.ranked_keywords` | 5 |
| `/use-cases/seo/backlink-profile` | `web.backlinks.summary` | 6 |
| `/use-cases/seo/list-backlinks` | `web.backlinks.list` | 6 |
| `/use-cases/seo/referring-domains` | `web.linking_domains.list` | 6 |
| `/use-cases/social/linkedin-profile` | `linkedin.user.profile` | 6 |
| `/use-cases/social/linkedin-company-page` | `linkedin.company.profile` | 4 |
| `/use-cases/social/instagram-profile` | `instagram.user.profile` | 6 |
| `/use-cases/social/instagram-post-comments` | `instagram.post.comments` | 5 |
| `/use-cases/social/tiktok-profile` | `tiktok.user.profile` | 5 |
| `/use-cases/social/tiktok-video` | `tiktok.video.detail` | 4 |
| `/use-cases/social/x-profile` | `x.user.profile` | 5 |
| `/use-cases/social/youtube-video` | `youtube.video.detail` | 5 |
| `/use-cases/social/youtube-search` | `youtube.search.videos` | 5 |
| `/use-cases/social/youtube-comments` | `youtube.video.comments` | 5 |
(`account.usage`, 21 providers, is excluded — it is plumbing, not a job anyone searches.)
Not built, on purpose: a page per endpoint (2,791); "find influencers" (no capability yet);
anything whose only provider is one vendor (their own site owns that query).
## Page type E — API pricing & comparison · `/pricing/<platform>-api` · 47 pages
Jason's call (2026-08-21): a real comparison page, not a retitled shelf. `/catalog/<slug>` stays the
app's comparison UI and gets a link to this page; this page is server-rendered, editorial, and is
the one page only treg.to can write — we have run every provider.
Demand (measured): `twitter api pricing` 720 · `reddit api pricing` 480 at $0 CPC · `linkedin api
pricing` (the shelf already gets impressions for it at pos 86) · `youtube api key` 1,900 ·
`google trends api` 1,600 · `instagram scraper` 1,000 · `linkedin scraper` 1,000. Blotato's
`/blog/<platform>-api-pricing` posts are the precedent in category.
| Element | Content | Source |
|---|---|---|
| Title | `LinkedIn API pricing (2026) — 8 providers compared per call, plus the official API \| treg.to` | catalog; year computed |
| Meta | `What a LinkedIn profile, company page or post lookup actually costs across {providers}, normalised to per-result, with what each one is good at and which inputs it needs. Verified {latest verified date}.` | catalog |
| H1 | `LinkedIn API pricing: 8 providers compared` | — |
| **The answer first** | three computed sentences: cheapest per result for the most-used job; the official API's real cost (free, but needs an approved app / own account — e.g. LinkedIn official endpoints are own-key only); the best-value pick given measured success (if approved) | catalog |
| **Price table, normalised** | provider × job (capability) grid; every cell shows the list unit (`$0.0008/result`, `$0.025/success`, `$0.09/call`) **and** the normalised cost for a standard task ("100 profiles" / "1,000 keywords"). Unit normalisation is the editorial value — per-call with 1,000 keywords in the body is not comparable to per-result until someone does the arithmetic | `cost` + a per-capability "standard task" constant in the slug map |
| **Which one for what** | one row per provider, generated from facts: inputs accepted (URL / email / name + company), which jobs it covers on this platform, verified date, rate limits, whether it is the official API, and — if approved — observed success and p50 with the caveat. Then one hand-written sentence per provider per platform, kept in the slug map, reviewed when the catalog changes ("TikHub is the only one returning post reactions; Bright Data is slowest but the only one that takes a Sales Navigator URL"). The hand-written sentence is the part Ray's clone test cares about | catalog + hand-kept notes |
| **The official API** | its real terms: free tier, approval process, what it will not give you (LinkedIn: nothing about other members), and how it runs on treg with the team's own key, unmetered | catalog `tier`/`scope` + hand-kept |
| **Hidden costs** | what the list price omits: minimums, monthly plans, failed-call billing (per-success vs per-call), rate limits — each as a fact per provider from the catalog `status_note`/limits | catalog |
| One call each way | the cheapest verified endpoint and the official one, as `treg call …` | `test_request` |
| FAQ (visible + `FAQPage`) | "How much does the LinkedIn API cost?" · "Is there a free LinkedIn API?" · "What is the cheapest way to get LinkedIn profile data?" · "Do I need a LinkedIn developer account?" — answered from the numbers on the page | — |
| JSON-LD | `FAQPage` + `BreadcrumbList` + `Table`-free `ItemList` of providers (`Offer` only where a single price is unambiguous — otherwise no `Offer`, because schema must match visible text) | — |
| Links | up: `/mcp/<platform>`, `/catalog/<slug>`, agent hubs; down: every spoke on this platform; across: `/integrations/<provider>` | — |
| Freshness | "Prices verified {date}" from the newest `verified` on the page; the sitemap `lastmod` follows the catalog mtime. Never date-bumped | catalog |
| Index | yes, all 47 platforms with ≥2 providers; single-provider platforms get no page (the vendor owns that query) | — |
Honesty constraints (wiki §8): name the cheapest **with caveats by input, quality, latency and
volume**; observed stats are "measured on treg.to traffic, not a controlled benchmark"; treg
compares and does not route — "choose well, then switch without a new account" is the CTA. A
controlled benchmark with published methodology is a separate later page (`/benchmarks/…`), not
this one.
First five: `linkedin-api`, `reddit-api`, `twitter-api` (platform `x`), `youtube-api`,
`instagram-api` — the measured terms. Then `google-serp-api` (`serp api` 12,100/mo is brand-owned
by SerpApi, but `cheapest serp api` 390/mo is beatable per the wiki), `tiktok-api`, `people-data-api`,
`company-data-api`, `backlink-api`.
## Page type F — provider · `/integrations/<provider>` · ~47 pages
Per wiki §8. Activation pages; their job is "does treg.to work with DataForSEO, and how", not rank.
Title `DataForSEO on treg.to — 61 endpoints, call them with no DataForSEO account`; sections:
what's covered (capabilities by platform), prices as treg charges them, the own-key path
(`treg connections connect --provider dataforseo`, never metered), limits from the catalog, one
runnable call, link to the vendor's own docs. `index` yes — we are not competing for the vendor's
name, we are answering "does it work with".
---
## Build order
| Step | Pages | Work | Verdict checked |
|---|---|---|---|
| 1 | `/apps/chatgpt`, `/apps/claude`, then the other 8 from the shared agent list | new route `apps_page()`, shared agent JSON, install blocks, five prompts, screenshots; sitemap rows; tests | indexed in 14 days; `chatgpt`/`claude` + "treg.to" in GSC |
| 2 | `/mcp/linkedin`, `/mcp/reddit`, `/mcp/youtube` | new route `mcp_page()`, short form; shared providers/jobs block | day 30: impressions for `<platform> mcp server`; if none, drop B |
| 3 | first 24 spokes (type D) | route `use_case_job_page()`, slug map, index-rule function, sitemap conditional | day 30: which spokes get impressions at all |
| 4 | `/pricing/linkedin-api`, `/pricing/reddit-api`, `/pricing/twitter-api` (E); append generated lists to the 5 category hubs (C) | route `pricing_page()`, unit-normalisation constants, hand-written provider sentences for 3 platforms | day 30: impressions for `<platform> api pricing`; `/catalog/linkedin` (linked from it) moves from pos 74 |
| 5 | submit to mcpservers.org, PulseMCP, Glama, Smithery, Docker MCP, Anthropic directory; point each listing and each catalog YAML at its page | outside the repo | referring domains in GSC |
| 6 | remaining spokes and pricing pages, `/integrations/<provider>` | only if steps 3–4 show movement | — |
Prerequisites before step 1 ships: Jason's call on publishing observed stats per vendor; top up the
team balance for the measurement passes; `first_call_at` attribution so step 5 and the day-60 verdict
can be read on first calls rather than signups.
## Files touched
- `src/treg/api.py` — routes `apps_page`, `mcp_page`, `use_case_job_page`, `integration_page`; a
shared `_providers_block(platform|capability)` used by B, D, E; index-rule helper; `_SITEMAP_PAGES`
spreading all four maps.
- `src/treg/catalog/usecase_slugs.yaml` (new) — `capability → slug, category, sentence`.
- `src/treg/web/pseo.css` (new) — one skin for A/B/D/F, tokens lifted from `landing.html`.
- `src/treg/web/media/install/chatgpt-plugins.png` etc. — the screenshots.
- `tests/test_seo.py` — every sitemap URL 200s; every FAQ question appears in the body; no page
carries a number absent from the catalog; noindex spokes are absent from the sitemap.
- `docs/context/interface/seo.md` — updated in the same commit.
- `src/treg/web/llms.txt`, `skill.md` — link the agent pages.
---
## v3 — decisions after independent review (Codex, 2026-08-21; full text in `_review-codex-seo-plan.md`)
The entity hierarchy stands. The launch inventory, the index gate, the URL namespace and the
verdict metric change. Footprint before review: 10 + 5 + 47 + ~150 + 3 + ~47, on top of 81 indexed
shelves = **343** indexable entity pages. Footprint after: **~45 new indexed pages, one complete
cluster first.**
| Decision | Was | Now | Why |
|---|---|---|---|
| Agent hubs (A) | 10 indexed | **4 indexed** (ChatGPT, Claude.ai, Claude Code, Cursor — each has a distinct native install path or measured adjacent demand) + a crawlable `/apps` directory listing all ten + 6 `noindex` setup pages | ten pages whose main instruction is the same setup line are not ten search intents |
| MCP pages (B) | `/mcp/<platform>` ×3 | **`/mcp-servers/<platform>`** ×3 | `/mcp` is the mounted MCP transport and `robots.txt` Disallows it; never share the protocol namespace |
| Category hubs (C) | spokes nest under `/use-cases/seo` etc. | **one shared route map**; the five ad slugs (`/use-cases/seo-data-for-ai-agents`) stay canonical, spokes nest under them; no parent URL that does not exist | the parents the spec pointed at were never routed |
| Spokes (D) | ~150 indexed automatically; gate = ≥2 providers + verified + ≥1 observed call in 30d, recomputed per sitemap render | **first 24, human-reviewed**; gate = substitutable providers + verified + complete price/input/test/provenance + (external demand evidence **or** repeated successful use across >1 customer) + **approval into a deploy-time index manifest**; usage *nominates*, never flips index state; hysteresis, no rolling-window churn | one call is a test call; a sitemap must not need live `CallRecord` aggregates (`endpoint_stats.observed()` is designed for small sibling sets) |
| Pricing (E) | 47 | **5** (linkedin, reddit, twitter, youtube, instagram); the rest stay on `/catalog/<slug>` | the hand-written provider judgments are a research program, not a template field |
| E content | price grid + sentences | add: **published normalisation methodology** (batch size, pagination, caps, minimums, prepaid/overage, retries, failed-call billing), **per-fact provenance + change log**, **three task sizes** not one constant, comparability beyond price (inputs, returned fields, freshness, geo, limits, access constraints), disclosure of hosted-vs-own-key charging, a named maintenance owner | without these E is a catalog table with prose, not a decision tool |
| Catalog shelves | untouched | where a pricing page exists, **retitle the shelf away from pricing intent** and link to it; explicit canonical policy across `/catalog/<p>`, `/pricing/<p>-api`, `/mcp-servers/<p>` | cannibalisation |
| Provider pages (F) | 47 indexed | index only those with a verified integration, real setup/limits content and a runnable call | activation docs, not a rollout |
| Parent directories | sitemap only | server-rendered `/apps`, `/pricing`, `/integrations`, `/use-cases` index pages | a sitemap is not a crawl path |
| `_page()` | as is | gains a `robots` argument (`noindex,follow`), a stylesheet argument, and context-preserving install CTAs instead of "Start free → /app" | the shell cannot currently render a noindex spoke |
| Self-hosting | "treg.to", the ChatGPT plugin, hosted keys and the $1 grant hardcoded | **pSEO routes gated to the hosted deployment** (or host-derived copy) | every one of those statements is false on a self-hosted registry |
| Where copy lives | `api.py` | routing and data authority in `api.py`; editorial copy, slug map, provider sentences and the index manifest as declarative data files | `api.py` is already 10k+ lines |
| Tests | "walks every sitemap URL" | `test_seo.py` samples 5 shelves today; new families need their own classification (walk all reviewed spokes, sample nothing) | the spec overstated the existing test |
| `llms.txt` / `skill.md` | link the agent pages | link only after the routes ship | do not document what is not built |
### Build order, v3 — one complete cluster, then a control
1. **Foundations:** canonical/category URL map + redirects; `_page()` robots/stylesheet args; the
index manifest format; **organic landing-page → first call → week-two reuse attribution**
(`first_call_at` exists; the page attribution does not).
2. **The LinkedIn cluster, complete:** `/apps/chatgpt` + `/apps/claude` (+ `/apps` directory),
`/mcp-servers/linkedin`, `/pricing/linkedin-api` with the full methodology, the 3–5 strongest
LinkedIn spokes (profile, company page, post + comments, jobs search), and only the complete
provider pages for its 8 providers.
3. **Submit the live cluster to directories the same week** (mcpservers.org, PulseMCP, Glama,
Smithery, Docker MCP, Anthropic) — not after four internal steps.
4. **One non-social control cluster** — people/email (`/pricing/people-data-api`, the
lead-enrichment spokes) — so the verdict is not confounded by LinkedIn alone.
5. Expand **the page type that proves both query match and repeated calls**. Not the others.
### Verdicts, v3
- **Day 14:** crawl and index coverage.
- **Day 30:** qualified impressions for the intended query families, impression trajectory vs the
existing shelf, install/connect starts, first calls. **Not position.** Pause expansion on zero
qualified impressions across the test set; do not delete B at day 30.
- **Day 60–90:** position and clicks, with external links in place.
- **Day 60/120:** repeated successful calls by landing page — the business verdict.
## v4 — reader-first simplification (Jason, 2026-08-21)
Two questions settled:
**Agent pages list every use case, categorized.** That is the page's value: "I use ChatGPT — what
can I now do?" answered with the whole list (Sales & people · SEO & search · LinkedIn / Instagram /
TikTok / X / Reddit / YouTube · Companies · Ads · Web), each row a job with its price and the prompt
to paste into *that* client. The four pages share the list; they differ in install and in how a row
is run there. A long, accurate, actionable list is not thin content.
**Provider pages (F) are dropped.** Provider detail lives as an expandable row on every pricing page
the provider appears on — inputs accepted, returned fields, limits, verified date, own-key path, docs
link. "Does treg.to work with DataForSEO" is answered by the catalog's provider view; the vendor's
name SERP stays the vendor's. One page type fewer, nothing a reader loses.
The plan in reader terms — three questions, three page types, plus one experiment:
| Reader asks | Page |
|---|---|
| I use ChatGPT / Claude / Claude Code / Cursor — what can I do now? | `/apps/<agent>` ×4 |
| What does LinkedIn / Reddit / X / YouTube / Instagram data cost, which provider, why? | `/pricing/<platform>-api` ×5 |
| How do I do this one job? | `/use-cases/<category>/<job>` ×24 reviewed |
| (test) `<platform> mcp server` | `/mcp-servers/<platform>` ×3 |
-251
View File
@@ -1,251 +0,0 @@
# treg.to programmatic pages — the idea behind Pipedream, measured, and treg's version
Written 2026-08-21. Sources: DataForSEO ranked-keyword pull for pipedream.com (4,000 US keywords,
four calls), live SERPs for treg's target terms (ScrapeCreators, own key), blotato.com's sitemap,
Jason's wiki (`wiki/topics/treg.to SEO.md` §8 is the operative position, `Blotato growth
teardown.md`, `SEO.md`, `Distribution.md`), treg.to Search Console 2026-07-21→08-18, and the
catalog itself (`catalog_store.load()`).
---
## 1. What Pipedream's pages actually earn (US, DataForSEO ETV, Aug 2026)
| URL pattern | est. visits/mo | share | keywords ranked | in top 10 |
|---|---|---|---|---|
| `/` (brand: "pipedream", "pipedream meaning", "pipe dream") | 15,700 | 67% | 146 | 21 |
| `mcp.pipedream.com/app/<app>` | 3,980 (≈580 without one outlier*) | 17% | 345 | **84** |
| `/apps/<a>/integrations/<b>` | 850 | 3.6% | **1,619** | 49 |
| `/apps/<a>` | 630 | 2.7% | 589 | 53 |
| `/community` | 610 | 2.6% | 596 | 103 |
| `/blog` | 580 | 2.5% | 68 | 31 |
| `/apps/<a>/(triggers\|actions)/<x>` | 370 | 1.6% | 398 | 48 |
| `/vs/<competitor>` | 15 | 0.1% | 2 | 1 |
\* "e conomic" (a Danish accounting brand, 301k/mo, position 10) alone is 3,400 of it.
What this says, and it is not what the page *looks* like it says:
1. **"Lots of traffic" is mostly brand.** 68% of Pipedream's US organic is people typing its name.
Non-brand organic is ~4,000 visits/month for a company with 3,000 apps and a million users.
2. **The app×app pages are a breadth machine, not a volume machine.** 2,600 ranked keywords across
the three `/apps` patterns, ~1,850 visits. Under one visit per keyword per month. The value is the
long tail *and the internal-link graph that gets the app hubs crawled*, not any page's traffic.
3. **The newest, thinnest surface earns the most non-brand traffic.** `mcp.pipedream.com/app/linear`
is a title, an H1 ("Linear MCP Server"), two install steps, no H2s, no internal links, no schema —
and it holds top-5 for "quickbooks mcp", "plaid mcp server", "pipedrive mcp", "sendgrid mcp",
"typeform mcp", 84 top-10 positions in all. That is the first law of the seo-growth playbook
in one table: **a low-effort page wins while the term is young and the SERP is thin**. The
integrations pages had years and authority and rank 11–50.
4. **Comparison pages are nothing.** `/vs/zapier` = 15 visits. The footer column in the screenshot
is there for crawl paths and positioning, not traffic.
5. **What each row on their page is, is a CTA.** The product surface is the SEO surface — that is
the part worth copying, and it is a product decision, not a content one.
## 1b. The mechanism behind Pipedream's pages (full-site crawl, Aug 2026)
Counts come from their open-source registry (`PipedreamHQ/pipedream/components/`: 3,397 apps, 4,626
triggers, 11,681 actions), because **`/sitemap.xml` lists only 11 hub URLs** — none of the
programmatic pages are in any sitemap. The query-intent ladder:
| Level | URL | Title = the query | Count |
|---|---|---|---|
| app | `/apps/notion` | "Notion API Integrations" | ~3,000 |
| pair | `/apps/notion/integrations/slack-v2` | "Integrate the Notion API with the Slack API" | any ordered pair, generated on request (≈9M possible) |
| component | `/apps/notion/triggers/new-page` | component `name` verbatim | ~16,300 |
| workflow | `…/create-a-page-with-notion-api-on-new-gist-from-github-api-int_…` | "{action} with {B} API on {trigger} from {A} API" | every trigger×action (≈54M possible) |
| hand-written | `/vs/zapier` ×5, `/templates` ×40 | — | already stale ("2,200+ apps" vs "3,000+" on generated pages) |
The eight principles, and treg's counterpart to each:
1. **The data is the content; the page is a view.** Every page is a function of the registry
(name, description, version, README, OAuth config). Contributors write code; pages appear.
*treg:* the catalog YAML is that registry. `/catalog/<slug>` already renders from it; the new
pages must too — no page may carry a number or a name the catalog doesn't.
2. **Titles are literal queries, composed from entity names.** Each level targets a different query
shape. *treg:* `<platform> api pricing` (shelf) · `<platform> mcp server` (mcp) · `<agent>` +
"connectors"/"mcp" long tail (agent) · the job in plain words (capability, noindex until proven).
3. **Unbounded tail, concentrated head.** Pairs exist for any combination, but every page links the
same usage-ranked top-24 apps and the footer's 8 money apps. *treg:* we have the usage data —
top 10 endpoints = 49% of 25k monthly calls (Google SERP 30%, X 17%, people enrichment 15%).
Put the top-used platforms in every page's footer; that is where authority should pool.
4. **Two-hop crawl path to every entity** via an unpaginated A–Z directory. *treg:* `/catalog`
already lists all 81 shelves; keep it unpaginated and server-rendered.
5. **Every page is a funnel.** "TRY IT" / "USE THIS ACTION" deep-links into the builder with the
component preselected. *treg:* every row opens connect/install for the visitor's agent; the
`/apps/<agent>` page starts the OAuth or Plugins install from a button.
6. **Thin-page risk is offset by unique machine data** — full source, version, key, doc link per
component; changes with every PR. *treg:* price per call, params, `test_request`, verified date,
observed success/latency. More unique data per page than Pipedream has, not less.
7. **The open-source registry is the content pipeline and the backlink engine.** Every npm package
has `"homepage": "https://pipedream.com/apps/<app>"`; every page says "View on GitHub"; 5.7k forks.
*treg:* the repo is public (423 stars). Each catalog YAML and each MCP-directory / ChatGPT-plugin
listing should point at its page; the directories that *are* the SERP for `<platform> mcp server`
double as the inbound links.
8. **Structure beats polish.** No canonicals, raw markdown in meta descriptions, self-pairs,
JS-only "Load more", a Vue SPA that only Googlebot renders — and it still works. What does not
transfer: AI crawlers don't render JS, so treg's pages must be server-rendered (wiki §8 agrees).
The one lesson *not* to take: Pipedream's pair and workflow pages are indexed far past any demand
("create sitemap with WebScraper.IO on new transaction from YNAB"). They can afford it on their
authority; a 2026 domain with 41 clicks cannot. Generate the graph, index the measured nodes.
## 2. The precedent in treg's own category: blotato.com
The wiki's teardown already covers why it works (561 referring domains beating Buffer's 99k on the
queries that matter; `/tools/*` out-earning 162 blog posts; 81 third-party n8n templates as the
real wedge). The sitemap, pulled today, is the structure Jason sketched, already shipped:
```
/ai-agent/<agent> 16: chatgpt, claude, claude-code, codex, cursor, gemini, windsurf, perplexity, …
/mcp/<platform> 9: instagram, linkedin, tiktok, x, youtube, threads, facebook, bluesky, pinterest
/tools/<free-tool> 12: social-media-api-cost-calculator, best-time-to-post, …
/blog/how-to-post-to-<platform>-with-claude (one per platform — the "how to" lives in blog)
/blog/<platform>-api-pricing (one per platform — the buyer's own words)
/blotato-alternatives (one hub, 18-tool table, "Verified August 2026")
```
Three levels, not four. The job ("post") is fixed by the product, so the how-to is `platform ×
agent`, never `agent × category × job`. The pricing posts are the *same* platforms again under the
other phrasing buyers use.
## 3. What treg.to's own data adds
- **Search Console (28d):** 41 clicks, 35 brand. 24 `/catalog/<slug>` shelves get impressions at
0 clicks, already matched to `linkedin api pricing` (pos 86), `serpapi`, `/catalog/linkedin` pos 74.
Indexed, aimed right, too thin. Cold start (Playbook A) on every cluster.
- **Demand measured 2026-08-21 (US/mo):** `claude connectors` 2,900 · `claude mcp servers` 1,600 ·
`firecrawl mcp` 1,300 · `chatgpt connectors` 1,000 · `chatgpt mcp` 1,000 · `claude code mcp
servers` 880 · `cursor mcp servers` 720 · `linkedin mcp server` 480 · `apify mcp` 480 · `chatgpt
for seo` 480 · `ahrefs mcp` / `semrush mcp` 390 · `dataforseo mcp` 210 · `reddit mcp server` 210 ·
`youtube mcp server` 140 · `how to find instagram influencers` 170.
**Dead (0–20/mo, 30+ phrasings tried):** every "how to <job> with chatgpt/claude" — sdr, find
email, lead list, schedule instagram, instagram analytics, rank tracking, serp analysis, keyword
research. People don't search how to do a job *in* ChatGPT; they open ChatGPT. They search for the
**connector by name**, or the **job by its own name**.
- **Live SERPs:** `linkedin mcp server` / `reddit mcp server` → GitHub repos, mcpservers.org,
PulseMCP, Docker Hub, Apify. Thin, winnable, and those directories are themselves places treg
should be listed. `dataforseo mcp` → 6 of 8 are dataforseo.com (vendor owns its name; wiki §3
already says don't fight it). `claude connectors` / `chatgpt connectors` → Anthropic/OpenAI docs
plus big listicles (Composio, Forte Labs). The hub term is not winnable head-on; the long tail
under it is.
- **Catalog shape:** 81 platforms, 47 of them with ≥2 providers (linkedin 8, google 8, youtube 7,
instagram 6, tiktok 5, x 5, reddit 4). 1,034 capabilities, 201 with ≥2 providers, 72 with ≥3.
- **Product facts that are now true:** treg is a public plugin in ChatGPT's directory (search
"treg" → Install, verified by Jason 2026-08-20); OAuth MCP connector for Claude; `treg mcp
install` for Claude Code / Cursor / opencode.
## 4. What the wiki already decided (do not relitigate)
- `treg.to` in every public title/heading/anchor, never bare `treg` (immunology SERP).
- **No programmatic page per endpoint** — "the single most obvious idea here and the one most
likely to earn a site-wide penalty" (`treg.to SEO.md`, What not to do).
- No "best X" / "X alternatives" listicles; no blog cadence.
- §8 operative URL unit: `/integrations/<provider>/` first, `/use-cases/<job>/` second; capability
URLs **noindex** until demand, comparability and differentiated content are all proven.
- Vendor-name SERPs (`semrush mcp`) are lost, but an integration page with install steps, runnable
calls, pricing and limits "serves activation and agent retrieval, which is not the job the SERP
was being asked to do" — judge it on first calls, not rank.
- Score evergreen pages on day-14/30 indexation and impressions, then day-60/120 on assisted
signups; never the day-7 gate. The primary growth unit is **successful repeated calls**; the
install → auth → first call → week-two funnel is still uninstrumented.
- Any claim of a number comes from the catalog at build time; observed success/latency stats carry
the "not a fair benchmark" caveat; printing reliability numbers for named vendors is Jason's call.
- Pages must be server-rendered: GPTBot/ClaudeBot/PerplexityBot do not run the Vue SPA.
## 5. The structure
The principle, stated once: **treg's entity graph is agent × platform × provider, and a page exists
for a node only where someone types that node's name.** Nobody types the job with an agent prefix;
everybody types the platform with "mcp", "api pricing" or "scraper", and the agent with
"connectors" / "mcp servers". So:
```
/apps/<agent> 4 pages chatgpt · claude · claude-code · cursor
/mcp/<platform> ~25 pages every platform with ≥2 providers AND a measured "<platform> mcp" term
/integrations/<provider> ~47 pages per wiki §8 — activation pages, rank is not their job
/catalog/<slug> 81 pages exists; becomes the "<platform> api pricing" page
/use-cases/<job> ~201 the spoke layer: capability as a job sentence, providers compared, agent tabs; indexed when ≥2 providers AND observed calls
/tools/<free-tool> a few api-cost-calculator style, per Blotato's /tools/* result
```
### `/apps/<agent>` — "treg.to in ChatGPT / Claude / Claude Code / Cursor"
Targets `chatgpt connectors`-family long tail ("chatgpt connectors for seo", "claude mcp server for
linkedin") rather than the head term. Content that is genuinely per-agent: the install path for that
client (ChatGPT: Plugins → search treg → Install, with the screenshot; Claude: OAuth connect; Claude
Code/Cursor: one command), five pasteable prompts that each do a real job end to end, and the list
of platforms as rows linking to `/mcp/<platform>`. The page **starts** the connect flow from a
button — the Pipedream lesson that the product surface is the SEO surface. Four pages, hand-written
above the fold, templated below it.
### `/mcp/<platform>` — "LinkedIn MCP server — 8 providers, from $0.002/call, works in ChatGPT, Claude, Cursor"
The leaf that matches the measured demand and the thin SERPs. Per page: what the agent can now do on
that platform (the capability list in plain words — "find influencers" appears *here*, as a row,
linked to the job search in the product), the provider comparison (price, verified, observed
success/latency with caveat), install tabs per agent, one runnable call. Title carries the
platform's *own* name — `linkedin mcp server`, not "treg.to linkedin". Build only for platforms with
a measured term; the remaining shelves stay on `/catalog/<slug>`.
### `/catalog/<slug>` — becomes the "api pricing" page
Already indexed, already matched to `linkedin api pricing`. Retitle to the buyer's phrasing
("LinkedIn API pricing — 8 providers compared, per-call"), move the prerender from a list to a
comparison table, add the `treg call` sample. No new URLs; this is the metadata-only fix the wiki
calls the fastest ROI on an existing site.
### `/use-cases/<job>` — the spoke layer (revised 2026-08-21 after Jason's push-back)
Pipedream's zero-traffic pages are not waste: they are spokes. Each one carries the hub's name in
its breadcrumb, H1 and parent link (anchor text and entity coverage for `/apps/<app>`, which holds
53 top-10 positions), each is an entry point for an external link (a GitHub issue or forum answer
links to the specific action page, not the hub), and their long tail sums (1,619 keywords → 850
visits). Their best-ranking leaf is the action page whose H1 is the job in plain words
("find social media accounts by email" → the Proxycurl action page).
treg's equivalent is the **capability worded as a job**: 1,034 capabilities, 201 with ≥2 providers.
"Find someone's work email from a LinkedIn URL — 3 providers, from $0.009/success." The provider
comparison is the unique content an endpoint page would never have. Per-agent install is **tabs on
the page**, one URL — never `/apps/<agent>/<job>` ×4.
Jason's categories (sales, instagram, seo…) are **section headings on the hubs**, not a URL level —
as Pipedream uses "Popular triggers / Popular actions" rather than `/apps/notion/triggers/` as a page.
The how-to is the use-case page; the agent is a tab on it.
**Index gate — usage, not search demand.** Wiki §8 says capability URLs stay noindex until search
demand is proven; a noindex page eventually stops passing link signals, so it is not a spoke. Change
the test: a use case with **≥2 providers and real observed calls** (the `observed` block; 485
endpoints had calls last month) is indexed. The rest render for agents and the internal graph and
stay out of the index until they earn it. Expected indexed set: ~100–150 use-case pages, each with
something a competitor cannot regenerate — not 2,791 that don't. Generate the whole graph, index
the used nodes, let the hubs collect the anchor text.
The one measured how-to the catalog cannot serve today: `how to find instagram influencers`
(170/mo) — influencer discovery by niche is not in the catalog; that page waits on the addition.
### What is deliberately absent
Per-endpoint pages (2,791 — banned by the wiki and by the penalty data). Provider×provider or
agent×category×job trees. `/vs/` and "alternatives" (15 visits/mo for Pipedream; declining terms;
excludes the citing brand from AI recommendations). Hub terms `claude connectors` / `chatgpt
connectors` as page titles — owned by Anthropic/OpenAI.
## 6. Order and gates
1. **`/apps/chatgpt` + `/apps/claude`** (product-led; the plugin listing is new and real) and
**`/mcp/linkedin`** (480/mo, SERP of GitHub repos, shelf already has impressions). Three pages.
2. Retitle the 81 `/catalog/<slug>` pages to the pricing phrasing — one template edit.
3. Submit treg to the directories that *are* the SERP: mcpservers.org, PulseMCP, Glama, Smithery,
Docker MCP hub, Anthropic's connector directory. Blotato's lesson: the win is being listed where
buyers already look, and those listings are also the backlinks the pages need.
4. Day 14/30: indexation and impressions per page; did `/mcp/linkedin` enter the top 30 for
`linkedin mcp server`, did `/catalog/linkedin` move from 74. If yes, roll `/mcp/<platform>` to
the other ~24 measured platforms in one release. If no, more pages will not fix it.
5. Day 60/120: assisted signups and first calls by landing page — which needs `first_call_at`
attribution, the same blocker as `marketing/landing/_measurement.md`.
## 7. Open questions for Jason
- Render: server-side `_page()` for all of these (the wiki's AI-crawler point makes this close to
mandatory), with the comparison UI embedded rather than the SPA.
- Whether observed success/latency may appear on public per-vendor pages with the caveat, or only
prices. Wiki says this is Jason's call.
- Whether the catalog should add influencer discovery (the one how-to with measured demand that treg
cannot honestly serve today).
- `treg` team balance is negative (−$0.11) after today's DataForSEO pulls; top up before the next
measurement pass.
-133
View File
@@ -1,133 +0,0 @@
# Shipping the rest: agent pages, use-case pages, workflows
Written 2026-08-21, after `/agents/chatgpt` and `/use-cases/data-enrichment-sales/find-professional-emails`
shipped. The templates exist and are tested; what remains is content, and content is the slow part.
Plan, spec and review: `pseo-catalog-plan.md`, `pseo-build-spec.md`, `_review-codex-seo-plan.md`.
## What a page costs now
| Page | Machine work | Hand-written work |
|---|---|---|
| Agent page | none, the template renders it | 1 dict entry: title, definition, 4 install steps, 4 FAQ answers, 1 screenshot. **~45 min** |
| Use-case page | none | 1 dict entry: sentence, title, lede, prompt, 3 "why it works" lines, 3 "what differs" notes, "what is X", 4 FAQ, related. **~60–90 min**, because the notes require actually reading the providers' docs |
| Workflow page | new template (below) | the workflow itself, run once end to end |
So the constraint is writing and verifying, not code. Everything below is sequenced around that.
## Wave 1 — finish the cluster that is already half-built (this week)
The Codex review's core point: ship one complete cluster, submit it, then expand. The cluster is
**data enrichment**, because it has the most providers, the clearest buyer, and its first page exists.
1. `/agents/claude` — Claude.ai's Connectors flow, screenshot of the connector dialog. Same prompts.
2. `/agents/claude-code` — `treg mcp install`, screenshot of the terminal after install.
3. `/agents/cursor` — same command, Cursor's MCP settings screenshot.
4. Four more enrichment use cases, in demand order:
- `verify-an-email-before-you-send` (5 providers) — pairs with the finder; the obvious next click
- `enrich-a-person-from-an-email-or-linkedin-url` (13)
- `find-people-by-role-company-or-location` (15)
- `enrich-a-company-from-its-domain` (19)
5. **The workflow page** for the cluster (see below).
6. Submit to the directories: mcpservers.org, PulseMCP, Glama, Smithery, Docker MCP hub, Anthropic's
connector directory, and the ChatGPT plugin listing's own description linking `/agents/chatgpt`.
**Gate at day 14:** are all seven pages indexed, and does GSC show impressions for
`email finder api`, `linkedin email finder`, `chatgpt plugin`, `claude connectors`? If nothing is
indexed, stop and fix crawling before writing more.
## Wave 2 — the two clusters with measured search demand (weeks 2–4)
Only if wave 1 indexes. Each is one pricing/comparison page plus its three or four use cases.
- **YouTube & video.** Transcripts were the single most-asked job in the X/Reddit research (16
posts) and people repeatedly fail to self-host it. Pages: `get-a-video-s-transcript`,
`a-channel-s-profile-and-lifetime-stats`, `search-videos-and-channels-by-keyword`,
`a-video-s-comments`.
- **Connect your own accounts.** The biggest demand cluster overall (~32 posts) and every row is
free, which is the strongest hook we have. Pages: `search-console-queries`,
`google-analytics-reports`, `google-ads-search-terms`, `google-business-reviews-and-replies`.
These are single-provider pages, so they follow the **short form**: prompt, why it works, connect
steps, params, one call, FAQ. No comparison section.
### Wave 2 progress
**YouTube & video shipped 2026-08-21** (5 pages, not the 4 planned: `video-details-views-and-stats`
came along because the quota argument is the same argument). Measured US volume behind the titles,
via the free own-key Google Ads endpoint: `youtube transcript` 110,000 · `get youtube transcript`
2,400 · `youtube channel stats` 1,300 · `youtube data api` 1,000 · `youtube transcript api` 880 ·
`youtube video statistics` 590 · `youtube keyword search tool` 480 · `youtube scraper` 390 ·
`youtube comment scraper` 210 · `youtube search api` 170 · `youtube mcp server` 140. Dead, do not
target: `youtube video stats api` (0), `youtube channel data api` (0), `youtube subscriber count
api` (10), `youtube video details api` (10), `youtube metadata api` (10), `youtube channel api` (30).
The pattern from the enrichment pass holds: buyers type the **thing plus a verb** ("get youtube
transcript") or **scraper**, not `<thing> api`, except where the thing is a named product
(`youtube data api`).
Two remain in the cluster for the next run: `trending-videos` and `transcripts-of-x-and-facebook-video-posts`.
**Catalog gap found, needs a human.** `tikhub.x.youtube-web-search-channel` is mapped to capability
`youtube.search.channels`, but it searches *within* one channel by id ("Search within a YouTube
channel by keyword"), which is a different job from finding channels by keyword. It renders as a row
on `/use-cases/youtube-video/search-videos-and-channels-by-keyword`, where the page's FAQ now names
the distinction rather than papering over it. The real cross-channel search is
`tikhub.x.youtube-web-v2-search-channels`. Either remap the endpoint or give within-channel search
its own capability.
## Wave 3 — the rest, ordered by demand, not by catalog size (weeks 5+)
SEO (8 jobs) · Social (9) · Local businesses (4) · Finance (7) · E-commerce (4) · Advertising (3) ·
Market research (3). Write them in the order the measured terms justify; stop adding pages to a
cluster the moment its first pages stop earning impressions.
Remaining agent pages (`opencode`, `codex`, `gemini-cli`, `openclaw`, `hermes`, `pi`) ship as one
batch **noindex** behind an `/agents` directory page, and get indexed individually only when one has
a distinct install path worth its own page. Ten near-identical pages is the thin-content risk Codex
flagged.
## The new thing: workflow pages
`/workflows/<slug>`, the "copy the whole thing" page. A use-case page answers one job; a workflow
page is the sequence a real person runs, with the prompt for each step, what it costs at the end,
and what to do with the output. This is the shape that earns links, because it is genuinely useful
on its own and cannot be regenerated from a vendor's docs.
First one: **`/workflows/find-and-verify-a-lead-list`** (data enrichment).
Step 1 Build the list companies.search "50 Series-A SaaS companies in the US, 50-200 staff"
Step 2 Find the people people.search "the VP of Marketing or Head of Growth at each"
Step 3 Find the emails people.email.find "their work emails, cheapest verified provider"
Step 4 Verify people.email.verify "drop anything not deliverable"
Step 5 Enrich for the copy companies.news "any funding or product news in the last 90 days"
Output a CSV, and the total cost printed by the agent
Every step links to its use-case page, so the workflow is the hub those spokes needed. The page
carries: the one paste-in prompt that runs the whole thing, the step table with per-step prices, a
worked example with real numbers (run it once on 50 rows, publish what it cost), the failure modes
(catch-all domains, no result for tiny companies), and the CSV of the run.
Later workflows, one per cluster: `/workflows/weekly-seo-report` (Search Console + keyword volume +
SERP + backlinks), `/workflows/find-creators-to-sponsor` (search, profile, engagement, contact),
`/workflows/watch-a-competitor` (ads library + posts + news + hiring).
**Cost:** a workflow page needs a real run, so it needs balance. The lead-list run above is roughly
50 company lookups + 50 people searches + 50 email finds + 50 verifies, call it $3–6.
## Standing rules for every page shipped
- Nothing on a page that the catalog does not produce, except the hand-written notes, which name a
provider fact and get re-checked when `verified` changes.
- No em-dashes; the test enforces it.
- Every use-case page: the prompt first, the comparison behind "what the agent sees".
- Per-vendor observed stats are approved (Jason, 2026-08-21) with the "live traffic, not a
controlled benchmark" caveat.
- `.md` mirror ships automatically; `llms.txt` links only `/agents/chatgpt`.
- A page is not done until its screenshot exists. A page with an empty image slot ships without the
slot, never with a placeholder.
## What would make me stop and re-plan
- Day 14: nothing indexed → crawling problem, not a content problem.
- Day 30: indexed but zero qualified impressions across all seven wave-1 pages → the terms are wrong,
and more pages will not fix it. Go back to keyword measurement.
- Day 30: impressions but no install starts → the pages attract readers who do not want a plugin;
rewrite the top of the page, do not add more pages.
-64
View File
@@ -1,64 +0,0 @@
# "Rebuild X" — the outbound cluster, measured
Research: `marketing/_research-ai-outbound-playbook-2026-08-27.md`. Volumes: DataForSEO Google Ads, US, pulled 2026-08-27 through the catalog (`dataforseo.google.keywords.volume`, one $0.09 call for 100 terms; SERPs $0.002 each). KD from `dataforseo.google.keywords.ideas`.
## What the numbers say
| Term | Vol/mo | CPC | Comp | Read |
|---|---|---|---|---|
| gtm engineer / gtm engineering | 3,600 | $9.32 | LOW | Biggest informational term in the set; nobody in our sources targets it |
| crustdata | 2,400 | $31.43 | LOW | Brand; has an API. Comparison mention only |
| ai sdr | 1,900 | $92.30 | MED | −46% YoY. Category distrust on Reddit. Target with "build your own", not a listicle |
| clay pricing | 1,900 | $4.45 | MED | KD 3. The cost-per-row page, not the alternative page |
| ai lead generation (+ tools) | 1,600 + 480 | $54 / $47 | MED / LOW | Head terms; the recipes shelf as a whole targets these |
| gojiberry ai | 1,600 | $3.48 | MED | The no-API, subscription-only LinkedIn-intent tool. "gojiberry" (110K) is the fruit |
| trigify | 720 | $3.54 | MED | Same category, same page |
| clay alternative(s) | 480 + 480 | $36.72 | MED | **KD 0.** SERP = listicles + r/gtmengineering + HN "should I build one" |
| zoominfo / apollo alternative | 480 / 390 | $58 / $54 | MED | Mention inside the Clay page; don't build separate pages yet |
| clay api | 390 | $4.38 | LOW | Developers looking for what Clay doesn't sell |
| intent data / providers | 390 / 210 | $151 / $146 | MED | Bombora's SERP; enterprise buyers. One section, not a page |
| buying signals / intent signals | 260 / 170 | $12 / $71 | LOW / MED | The "stack the signals" page |
| clay vs apollo / zoominfo | 260 / 140 | $45 / $23 | MED | Comparison rows inside the Clay page |
| email verification api | 210 | $20.73 | LOW | Already a catalog page (`Email verification API: {n} verifiers compared`) — link to it |
| linkedin scraper / profile scraper / post scraper | 1,000 / 210 / 110 | $21 / $12 / $13 | MED | Scraper wording wins again. The LinkedIn-intent page carries these |
| waterfall enrichment | 140 | $26 | MED | SERP is Clay, Apollo, ZoomInfo, FullEnrich, Hunter — every vendor explains it, none prices it |
| predictleads / teamfluence / seltz ai | 170 / 90 / 50 | — | — | Mention as providers / comparisons |
**Dead, don't target:** "rebuild clay", "build your own clay", "clay clone", "cheap clay alternative", "signal based outbound" (10), "waterfall enrichment api" (10), "open source clay alternative" (10 — but its SERP is where YALC, OpenClay, Eigent live, so the Clay page should say "open source" once), every "{thing} api" phrasing except email verification. Buyers type "scraper", "alternative", "pricing", "vs".
## The cluster
Five pages, one shelf (`/rebuild/`), one repurposed article, and links from the five live use-case pages.
| # | Page | Primary term | Secondary | File |
|---|---|---|---|---|
| 1 | Rebuild Clay | clay alternative (480, KD 0) | clay pricing, clay api, clay vs apollo, waterfall enrichment, open source clay alternative | `01-rebuild-clay.md` |
| 2 | Rebuild Gojiberry / Trigify (LinkedIn intent) | gojiberry ai (1,600) | trigify, teamfluence, linkedin post scraper, buying signals, intent signals | `02-rebuild-linkedin-intent.md` |
| 3 | Rebuild an AI SDR | ai sdr (1,900) | ai sdr tools, ai sdr open source, build ai sdr | `03-rebuild-ai-sdr.md` |
| 4 | The GTM engineering stack, priced | gtm engineering (3,600, LOW) | gtm engineer, gtm agent, outbound agent | `04-gtm-engineering-stack.md` |
| 5 | The join problem (repurposed) | — (editorial; the X Article + DDoDS shape) | intent signals, hiring signals | `05-join-problem.md` |
Every page is a **recipe** — a runnable script with real endpoint ids, real prices and a run receipt — and every page says plainly what treg does not do (send, warm, route, de-anonymize) and where the catalog has a gap.
## Linking map (existing → new, new → existing)
| Existing page | Link to | Anchor |
|---|---|---|
| `/use-cases/lead-enrichment-for-ai-agents/` (p2) | Rebuild Clay | "the same waterfall, as a Clay replacement" |
| `/use-cases/company-research-for-ai-agents/` (p5) | Rebuild Clay · Join problem | "funding + people in one pass" |
| `/use-cases/social-creator-trends` (p3) | Rebuild LinkedIn intent | "the same post/profile tools, pointed at buyers" |
| `/use-cases/company-buying-signals` (p5) | Rebuild LinkedIn intent · Rebuild AI SDR | "signals → outreach" |
| Catalog pages: Email finder API / Email verification API / People search API / Person & company enrichment API | Rebuild Clay | provider tables cite them |
| Agent pages (`/for/claude-code`, `/for/cursor`, Claude MCP) | every rebuild page | install block per agent |
| `skill.md`, `llms.txt` | `/rebuild/` index | one line: "recipes that rebuild Clay, Gojiberry, an AI SDR on the catalog" |
Reverse links: each rebuild page's provider table links to the matching catalog comparison page; each "what we don't do" section links to the use-case page that covers the adjacent job.
## Honesty rules carried into every draft
- treg **compares** providers; the script does the waterfall. Never "routes", never "fails over".
- Findymail/Fiber misses bill at full rate until `_observed_cost_micro` is fixed — "a miss costs nothing" only for providers where it's measured true (tomba, hunter, leadmagic).
- Catalog gaps stated on the page: website → markdown scraping; keyword-level LinkedIn engagement monitoring; the *likers* of a post (the post endpoint returns commenters with URLs and only a like count); visitor de-anonymization; sending.
- Scripts use the real parameter shapes from `catalog get` (checked 2026-08-27 for tomba, findymail, hunter, leadsforge, leadmagic, icypeas people search, scrapecreators post). Icypeas's email finder/verifier are async and stay out of sync loops. Response paths (`.data.email`, `.contact.email`, `.items[0]`) still need confirming against a live run before publish.
- Vendor benchmarks cited as vendor benchmarks (Woodpecker 3.43%, Expandi 10.3%, Instantly 17–18%).
- No Clay price numbers we haven't verified; use their public credit model and the G2 quote Explorium cites ("stated 11 credits/row, actual 25").
-108
View File
@@ -1,108 +0,0 @@
# Build plan — the outbound cluster on treg.to
Scope: the five drafts in this folder, the X distribution, the integrations, and the measurement loop. Ordered by dependency; each step names its file, its test, and the decision it needs.
## 0. Decisions before code (Jason)
| # | Decision | Default if unanswered |
|---|---|---|
| D1 | **Receipt policy.** (a) aggregate receipt only — counts + cost, no CSV, no per-row; (b) current spec — aggregate + anonymised row-outcome CSV; (c) no receipts — publish as prompt + live-price step table, use-case style | (a). Honest, no person data, still a run behind it |
| D2 | **Page type.** Port into `agent_pages.WORKFLOWS` (gets the hub, `.md` twin, HowTo schema, tests for free) vs a new "recipe" type | WORKFLOWS. Don't add a fourth generator |
| D3 | **Slugs.** `/workflows/rebuild-clay` etc. vs job-phrased slugs (`/workflows/find-and-verify-a-lead-list` style) | Job-phrased slug, "Rebuild Clay" in the H1/title. Slugs are forever; brand names aren't |
| D4 | Sponsor the Daily Dose of DS slot (budget) | Ask for the rate; decide after step 5 ships |
## 1. Branch hygiene (30 min)
- This worktree is 245 commits behind `origin/main`; `/workflows`, `/tools`, `/agents`, `/pricing` exist only on main. **Rebase `seo/pages-2026-08-24` onto main** before touching `agent_pages.py`.
- Commit the untracked research + drafts as docs (`marketing/`), separate from code.
- Run `bash .agents/skills/tools-registry-context/scripts/drift.sh` at the end of every code step; `docs/context/interface/seo.md` is the fragment that moves.
## 2. Catalog prerequisites (1–2 days, mostly waiting)
| Item | Why | Where |
|---|---|---|
| Fix `_observed_cost_micro` for findymail / fiber | drafts say "a miss is free"; the ledger bills the miss | `src/treg/…` per memory note; money code — ledger rules apply |
| `catalog_request`: website → markdown | 3 of 8 systems have the step; the AI SDR and Clay pages state it as a gap until it lands | catalog |
| `catalog_request`: LinkedIn post *likers* and keyword-level engagement search | the Gojiberry page's two stated gaps | catalog |
| Confirm response paths | `.data.email`, `.contact.email`, `.items[0]`, `.comments[].linkedinUrl` — read from `catalog get` examples, not yet exercised | one call each |
None of these block writing; they change what the pages are allowed to claim.
## 3. The runs (½ day each, 5 pages)
For each draft: run the script on a small real input, record the aggregate receipt per D1, keep the CSV local. Order = expected search value:
1. Rebuild Clay — 50 contacts through the waterfall. Receipt: found per rung, verified, total.
2. Build your own AI SDR — one week of SDR postings → VP-level contacts → verified emails. (Reuses 1.)
3. The join problem — 7 days of raises ≥ $5M → revenue leader → profile. (Reuses 1.)
4. Rebuild Gojiberry — three competitor posts → commenters → ICP fit → raise check.
5. GTM stack — no run; the price table is generated from the catalog at build time.
Budget: under $10 total at catalog prices. Every run's receipt line goes into the page's `run` block with a date.
## 4. Port to `WORKFLOWS` (1 day)
- One dict per slug in `agent_pages.WORKFLOWS`: `sentence`, `title`, `lede`, `prompt`, `prompt_why` (4), `steps` as `(name, capability, ask, endpoint, why)`, `once`, `run`.
- Capabilities must exist (`test_every_workflow_step_capability_and_endpoint_exist`); every step links to its use-case page via `USE_CASES` by capability or falls back to the agent-page anchor.
- **Strip every em-dash** — `test_workflow_copy_has_no_em_dashes` will fail the drafts as written. Also the brand rule: treg.to, never bare "treg" in copy.
- No routing claims anywhere; "the script picks the order" sentence stays.
- The GTM-stack page is not a workflow (no run). Ship it as a `/resources` entry or the intro of the `/workflows` hub — decide in D2's spirit: no new page type.
## 5. Wiring (½ day)
| From | To | Mechanism |
|---|---|---|
| `/workflows` hub | 4 new + 1 existing | already spreads from `WORKFLOWS`; sitemap and `.md` twins follow |
| 5 outcome pages (`marketing/landing/0*.md` → `usecase-*.html`) | the workflow that extends each | one "Run the full sequence" link block; rebuild with `marketing/landing/build.py` |
| 38 job pages | the workflows that use their capability | reverse index: for each capability in a workflow's steps, add a "Used in" line on the job page — generated, not hand-written |
| `/tools/<provider>` | workflows that call that provider | same reverse index by endpoint |
| `/agents/*` | `/workflows` | one line in the install block |
| `llms.txt`, `skill.md` | `/workflows` | one line each; these are the front door |
| `docs/context/interface/seo.md` | the new pages | drift.sh will flag it |
Tests: `tests/test_seo.py` walks every sitemap entry for 200; add the four workflow slugs to the served-with-crawler-essentials test.
## 6. Distribution (rolling, starts when page 1 is live)
| Week | X (Jason's account) | Elsewhere |
|---|---|---|
| Page 1 live | The join-problem X Article + hook tweet with the receipt screenshot | Reddit: the r/gtmengineering Clay-alternative threads, script not link (weekly loop, u/jzdesign rules) |
| +1 | Price-receipt image #1: email verification, 3 providers | Open the YALC PR (treg provider adapter); list in `gtmagents/gtm-agents` |
| +2 | "Clay's moat in 40 lines" | Fork `LeadMagic/gtm-skills` `prospecting-stack` to `/call/`; offer upstream |
| +3 | Price-receipt #2: email finders, 6 providers | DDoDS brief sent (D4) |
| weekly after | one receipt image, replies via the @treg_ai loop ("when unsure, skip") | — |
Format rules from the teardown: number and receipt in the first line, product named once at the end, "try free" as a self-reply, write for the bookmark.
## 7. Measurement (from day 1)
- Search Console impressions/clicks on `/workflows/*` and the linked job pages, weekly, via the existing GSC + Ads loop (own-key, free).
- First-call rate from those pages — still uninstrumented (memory: the funnel gap). Minimum: a `?src=workflow-<slug>` on the install CTA and a count in the audit log.
- Review at day 30: keep the shelf if any page has impressions on its primary term; otherwise fold the copy into the job pages and stop adding workflows.
## Sequence, compressed
Decisions → rebase → runs 1–4 (parallel with catalog fixes) → port + tests → wiring + docs → publish one, then one a week → distribution cadence → day-30 review.
## Corrections from the independent Codex review (2026-08-28, task_123bcea8fb2b, 20 findings)
Accepted and applied to the drafts:
- **Not gaps after all.** `scrapecreators.x.v1-linkedin-search-posts` (public posts by keyword, Google-indexed) and `aviato.linkedin.post.reactions` (reactors by post URN) exist; `branddev.brand.ai.query` does named-field extraction from a website. The two `catalog_request`s in §2 are withdrawn; the Gojiberry page and the Grok Bot FAQ now describe the real scope (public posts, paged reactions, structured website extraction, no raw markdown).
- **Free-miss evidence.** Only Hunter and LeadMagic misses are derived by the ledger (`api.py`); Tomba, Findymail and Fiber misses settle at the estimate, and the live workflow's receipt already says so for Tomba. Budget all three at the estimate; say "documented" vs "observed" on the page. The "$0.01–0.03 per verified email" claim is unproven until a receipt; Tomba + Findymail + verify is about $0.035 today.
- **Capability ids in the drafts are wrong** (`linkedin.post`, `jobs.search`, `companies.funding`). Derive every workflow step's capability from the chosen endpoint's catalog entry (`companies.funding_rounds`, `linkedin.search.posts`, `linkedin.post.reactions`, …); `test_every_workflow_step_capability_and_endpoint_exist` enforces it.
- **Parameter shapes in the scripts are guessed** for Apify jobs (POST JSON `jobTitles`, `postedLimit`, a `maxItems` / `maxTotalChargeUsd` cap; rows carry no `company_domain`), Icypeas people search (`leads[]`, `profileUrl`), PredictLeads financing (`type`, `location`, `page`, `limit`; domains under `included`; `effective_date`, `amount_normalized`) and Aviato funding (`website`, `perPage`, `page`; `fundingRounds[].announcedOn`). Every script gets a one-row smoke run before it is quoted.
- **`treg skill install rebuild-clay` does not exist.** Removed from the Clay draft; a packaged skill is a separate deliverable.
- **Bare "treg" and em/en dashes in reader copy** violate CLAUDE.md and `test_no_em_dashes_in_the_hand_written_copy`; strip at port time (the shipped FAQ copy is already clean).
- **Slugs.** The drafts link `/rebuild/...` and two non-existent use-case paths; the live ones are `/use-cases/social-trend-research-for-ai-agents` and `/use-cases/company-research-for-ai-agents`. Freeze the slug map before porting; add an internal-link 200 test (the sitemap walk does not check outbound links).
Accepted, changes the plan:
- **D1(a) is not a shipping option as written.** The `WORKFLOWS` renderer and `tests/test_agent_pages.py` require a dated run, `rows_in`, a row-outcome CSV under `src/treg/workflow_runs/`, a narrative, ≥4 failure modes, exactly 4 FAQ entries and 4 related labels. Aggregate-only receipts mean changing the renderer, spec, docs and tests together as a contract change, or shipping D1(b) with the anonymised CSV. Decision still Jason's; the default flips to D1(b).
- **The renderer has no slot for the drafts' comparison tables, first-line rules or stack layers**; `seo_title`/`meta_description`/`h1` in the frontmatter are ignored. Port by a field-by-field matrix into the fixed sections, or extend the renderer with tested optional sections first.
- **`once` cannot price a multi-provider waterfall or three named source posts.** The cardinality model is one call or `rows_in` calls per endpoint. Either store `calls_by_endpoint` in the run block (docs + tests) or constrain the first pages to workflows the model can express.
- **Order after the crawl gate:** Clay first (or the existing lead-list workflow retitled toward "clay pricing"), a dedicated GTM page second only once it has a page type with its own title and canonical (`/resources` is one static file), Gojiberry third, AI SDR later, the join problem as distribution content.
- **Measurement already exists:** every workflow CTA carries `/app?ref=wf-<slug>` and `sitetrack.js` records pageviews and first-touch UTM/referrer. Persist the `ref` through signup instead of adding a new `src` parameter; gate on indexed → non-brand impressions/position → CTA-to-first-call → clicks, separately.
- **Branch note (§1) was stale** the moment the discovery branch was cut from main; the plan names branches by role now, not by name.
Already done in PR #234 before the review landed: the crawl gate (hub links from the indexed pages, reverse indexes, `/catalog` prerender), the existing workflow used as the pilot and linked from every agent page, and the wiring tests in `tests/test_seo.py`.
+11 -37
View File
@@ -11,15 +11,13 @@ directions: one snapshot ↔ one call. Anything else is counted and left alone
show a team someone else's answer, and "no result on file" is the honest fallback.
Only metered platform 2xx rows that are not already linked are candidates (the archive never holds
anything else). Served hits are skipped: prod recorded in `shadow`, so none exist.
anything else). Served hits are skipped.
python scripts/backfill_call_archive_links.py # dry run: counts only
python scripts/backfill_call_archive_links.py --apply # write the links
python scripts/backfill_call_archive_links.py --window 10 # ± seconds (default 10)
Connects with `TREG_DATABASE_URL` / `--dsn` (a postgres:// DSN). With `--render`, reuses
`scripts/usage_report.py`'s open-allowlist / connect / close-allowlist dance for the prod database
(needs RENDER_API_KEY in the environment or the repo's .env).
Connects with `TREG_DATABASE_URL` / `--dsn` (a postgres:// DSN).
"""
from __future__ import annotations
@@ -27,9 +25,6 @@ import argparse
import asyncio
import os
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
# One statement finds every unambiguous pair. `snap_n` is how many calls a snapshot could be, and
# `call_n` how many snapshots a call could be — a link is written only where both are exactly 1.
@@ -86,40 +81,19 @@ async def main() -> int:
ap.add_argument("--apply", action="store_true", help="write the links (default: dry run)")
ap.add_argument("--window", type=int, default=10, help="± seconds between call and fetch (default 10)")
ap.add_argument("--dsn", default=os.environ.get("TREG_DATABASE_URL", ""), help="postgres DSN")
ap.add_argument("--render", action="store_true", help="open the prod allowlist via the Render API, like usage_report.py")
args = ap.parse_args()
import asyncpg
if args.render:
sys.path.insert(0, str(REPO / "scripts"))
import usage_report as ur # the allowlist dance lives there; do not duplicate it
ip = ur.my_ip()
print(f"opening prod allowlist for {ip}/32 ...", file=sys.stderr)
ur.render_api("PATCH", f"/postgres/{ur.DB_ID}",
{"ipAllowList": [{"cidrBlock": f"{ip}/32", "description": "backfill_call_archive_links.py"}]})
try:
dsn = ur.render_api("GET", f"/postgres/{ur.DB_ID}/connection-info")["externalConnectionString"]
await asyncio.sleep(3)
conn = await asyncpg.connect(dsn, ssl="require", timeout=45)
try:
report = await run(conn, window_s=args.window, apply=args.apply)
finally:
await conn.close()
finally:
ur.render_api("PATCH", f"/postgres/{ur.DB_ID}", {"ipAllowList": []})
print("prod allowlist closed", file=sys.stderr)
else:
if not args.dsn:
print("no DSN: pass --dsn, set TREG_DATABASE_URL, or use --render", file=sys.stderr)
return 2
dsn = args.dsn.replace("postgresql+asyncpg://", "postgresql://")
conn = await asyncpg.connect(dsn, timeout=45)
try:
report = await run(conn, window_s=args.window, apply=args.apply)
finally:
await conn.close()
if not args.dsn:
print("no DSN: pass --dsn, set TREG_DATABASE_URL", file=sys.stderr)
return 2
dsn = args.dsn.replace("postgresql+asyncpg://", "postgresql://")
conn = await asyncpg.connect(dsn, timeout=45)
try:
report = await run(conn, window_s=args.window, apply=args.apply)
finally:
await conn.close()
mode = "APPLIED" if args.apply else "DRY RUN"
print(f"[{mode}] snapshots={report['snapshots']} unlinked_platform_2xx_calls={report['unlinked_calls']} "
+1 -1
View File
@@ -68,7 +68,7 @@ PROVIDERS: dict[str, dict] = {
"url": "https://docs.scrapecreators.com/openapi.json", "units": {"*": "call"}},
# --- prices transcribed from docs / pricing pages → documented ----------------------------
# Just One API's figures are a hand-exported snapshot of the logged-in dashboard's pricing page
# (scripts/data/justoneapi_prices.json) — first-party, but a local file nothing can re-check.
# (operator-supplied justoneapi_prices.json) — first-party, but a local file nothing can re-check.
"justoneapi": {"source": "rate_card_api", "confidence": "documented",
"url": "https://www.justoneapi.com/", "units": {"*": "call"}},
"brightdata": {"source": "docs", "confidence": "documented",
+20 -19
View File
@@ -41,6 +41,17 @@ ROOT = Path(__file__).resolve().parent.parent
CATALOG = ROOT / "src" / "treg" / "catalog"
CACHE = Path(os.environ.get("TREG_INGEST_CACHE") or (Path.home() / ".cache" / "treg-catalog-ingest"))
def pricing_evidence_file(name: str) -> Path:
"""Read operator-supplied evidence without depending on a private checkout."""
directory = os.environ.get("TREG_CATALOG_EVIDENCE_DIR")
if not directory:
raise ValueError("Set TREG_CATALOG_EVIDENCE_DIR to your pricing evidence directory")
path = Path(directory).expanduser() / name
if not path.is_file():
raise FileNotFoundError(f"Missing pricing evidence: {path}")
return path
CJK = re.compile("[ -鿿＀-￯]")
@@ -932,9 +943,8 @@ def ingest_justoneapi(refresh: bool) -> tuple[Path, dict]:
specs = dict(pool.map(load, todo))
skip = core_routes("justoneapi")
# Prices are dashboard-only (no public price API); scripts/data/justoneapi_prices.json is a
# hand-exported snapshot of the dashboard's Pricing page — re-export + rerun to refresh.
prices_file = Path(__file__).parent / "data" / "justoneapi_prices.json"
# Dashboard prices are supplied explicitly by the operator.
prices_file = pricing_evidence_file("justoneapi_prices.json")
joa_prices: dict[str, float] = {}
if prices_file.is_file():
joa_prices = json.loads(prices_file.read_text()).get("prices", {})
@@ -993,7 +1003,7 @@ def ingest_justoneapi(refresh: bool) -> tuple[Path, dict]:
"at /openapi/<platform>/<endpoint>-en.json, enumerated here from the English sitemap.",
"`platform` comes from the spec's own x-platform-id tag, falling back to the docs URL segment.",
"Endpoints whose docs page carries a `-deprecated` slug are kept — the provider still serves",
"them — and are visible as such in the id. Prices come from scripts/data/justoneapi_prices.json,",
"them — and are visible as such in the id. Prices come from the operator-supplied pricing evidence,",
"a hand-exported snapshot of the logged-in dashboard's Pricing page (CNY per successful request);",
"an endpoint without a cost was absent from that export (e.g. not activated for the account).",
]
@@ -1338,22 +1348,13 @@ ANYAPI_OPENAPI = "https://api.getanyapi.com/openapi.json"
# Bumped by hand when the rate card is re-read, so a re-run with no price change is byte-identical.
ANYAPI_CHECKED = "2026-09-11"
# What AnyAPI actually billed, per SKU, over the trailing 60 days: a hand-exported snapshot of the
# vendor's own request ledger (calls, p50, p90, max USD), the same arrangement as
# scripts/data/justoneapi_prices.json. It needs the vendor's production database, so it cannot be
# fetched here; re-export it and re-run to refresh. Only SKUs with at least 5 charged calls in the
# window are in it - the rest fall back to the live rate card. See _anyapi_cost.
ANYAPI_MEASURED_FILE = Path(__file__).parent / "data" / "anyapi_measured_charges.json"
# Measured pricing evidence is supplied by the operator, outside the public checkout.
_ANYAPI_MEASURED: dict[str, dict] | None = None
# CORE-TIER SHELF ROWS WHOSE SOURCES CHARGE DIFFERENT PRICES FOR THE SAME RESULT COUNT.
#
# For these the p90 measures lane spread, not work done, so it is not the price a buyer will pay.
# maps.search, measured per source over the same 60 days: scrapertech $0.00175 flat (312 calls),
# serper $0.00297 flat (743 calls), apify $0.06005 median (485 calls) - and all three average
# 11-12 items per response. The p90 of the blend is $0.07734, which is only ever paid when the
# dearest source serves; the cheapest source serves the same query for $0.00175. Listing $0.07734
# on the price-sorted google.serp.maps shelf misrepresents the endpoint by a factor of 44.
# A blended percentile can overstate the cheapest advertised source for the same result count.
#
# So these rows list the CHEAPEST ADVERTISED price instead - `pricing.from.maxUsd`, the cheapest
# source's price at the input maximum - and keep `source: rate_card_api`, because that is what the
@@ -1377,8 +1378,8 @@ ANYAPI_CORE_ROWS_PRICED_AT_CHEAPEST_SOURCE = {
def _anyapi_measured() -> dict[str, dict]:
global _ANYAPI_MEASURED
if _ANYAPI_MEASURED is None:
_ANYAPI_MEASURED = (json.loads(ANYAPI_MEASURED_FILE.read_text()).get("skus", {})
if ANYAPI_MEASURED_FILE.is_file() else {})
path = pricing_evidence_file("anyapi_measured_charges.json")
_ANYAPI_MEASURED = json.loads(path.read_text()).get("skus", {})
return _ANYAPI_MEASURED
@@ -1389,7 +1390,7 @@ def _anyapi_measured_window() -> tuple[str, int]:
the ledger is re-exported. Reusing one date for both made a re-read silently restate every
measured price as covering days it never saw.
"""
blob = json.loads(ANYAPI_MEASURED_FILE.read_text()) if ANYAPI_MEASURED_FILE.is_file() else {}
blob = json.loads(pricing_evidence_file("anyapi_measured_charges.json").read_text())
return blob.get("as_of", ANYAPI_CHECKED), blob.get("window_days", 60)
@@ -1589,7 +1590,7 @@ def ingest_anyapi(refresh: bool = False):
"anyapi.yaml and the SKUs AnyAPI excludes from this listing (ANYAPI_KEEP_PLATFORMS and",
"ANYAPI_EXCLUDE in scripts/catalog_ingest.py).",
"Prices are MEASURED: cost.value is the p90 of what AnyAPI actually billed for that SKU",
"over the 60 days to the ingest date (scripts/data/anyapi_measured_charges.json), so it is",
"over the 60 days to the ingest date (operator-supplied pricing evidence), so it is",
"what nine calls in ten settle at or below rather than the cheapest source's list price. A",
"SKU with too few charged calls to measure keeps the rate card's cheapest-source price and",
"says so in its note (cost.source: rate_card_api vs observed). One call in ten settles",
File diff suppressed because it is too large Load Diff
-275
View File
@@ -1,275 +0,0 @@
{
"_source": "dashboard.justoneapi.com pricing export justoneapi-pricing-20260728-124405.xlsx (2026-07-28, UTC+8)",
"_currency": "CNY",
"_billing": "per successful request (code 0); errors free",
"_not_activated": [
"/api/jd/get-item-detail/v2"
],
"prices": {
"/api/search/v1": 0.2,
"/api/taobao/get-item-comment/v3": 0.15,
"/api/taobao/get-item-detail/v1": 0.25,
"/api/taobao/get-item-detail/v2": 0.5,
"/api/taobao/get-item-detail/v4": 0.55,
"/api/taobao/get-item-detail/v5": 0.15,
"/api/taobao/get-item-detail/v9": 0.25,
"/api/taobao/get-shop-item-list/v1": 0.15,
"/api/taobao/get-shop-item-list/v2": 0.15,
"/api/taobao/get-shop-item-list/v4": 0.25,
"/api/taobao/get-social-feed/v1": 0.15,
"/api/taobao/search-item-list/v1": 0.15,
"/api/xiaohongshu/get-note-comment/v2": 0.15,
"/api/xiaohongshu/get-note-detail/v1": 0.15,
"/api/xiaohongshu/get-note-detail/v6": 0.15,
"/api/xiaohongshu/get-note-sub-comment/v2": 0.15,
"/api/xiaohongshu/get-topic-note-list/v1": 0.15,
"/api/xiaohongshu/get-user-note-list/v4": 0.15,
"/api/xiaohongshu/get-user/v3": 0.15,
"/api/xiaohongshu/hot-list/v1": 0.15,
"/api/xiaohongshu/hot-search/v1": 0.15,
"/api/xiaohongshu/search-note/v2": 0.15,
"/api/xiaohongshu/search-note/v4": 0.2,
"/api/xiaohongshu/search-recommend/v1": 0.15,
"/api/xiaohongshu/search-user/v2": 0.15,
"/api/xiaohongshu/share-url-transfer/v1": 0.15,
"/api/xiaohongshu-pgy/api/pgy/content_square/search_note_v2/v1": 0.15,
"/api/xiaohongshu-pgy/api/pgy/kol/data/core_data/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/cooperator/blogger/v2/v1": 0.5,
"/api/xiaohongshu-pgy/api/solar/cooperator/user/blogger/userId/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/kol/data/userId/fans_overall_new_history/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/kol/data/userId/fans_profile/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/kol/dataV2/costEffective/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/kol/dataV2/kolContentTags/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/kol/dataV2/kolFeatureTags/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/kol/dataV2/notesDetail/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/kol/dataV3/dataSummary/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/kol/dataV3/fansSummary/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/kol/dataV3/notesRate/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/kol/get_similar_kol/v1": 0.15,
"/api/xiaohongshu-pgy/api/solar/note/noteId/detail/v1": 0.15,
"/api/xiaohongshu-pgy/get-kol-note-list/v1": 0.5,
"/api/douyin/get-user-detail/v3": 0.1,
"/api/douyin/get-user-video-list/v1": 0.1,
"/api/douyin/get-user-video-list/v3": 0.1,
"/api/douyin/get-video-comment/v1": 0.1,
"/api/douyin/get-video-detail/v2": 0.1,
"/api/douyin/get-video-sub-comment/v1": 0.1,
"/api/douyin/hot-search/v1": 0.2,
"/api/douyin/search-user/v2": 0.1,
"/api/douyin/search-video/v4": 0.1,
"/api/douyin/share-url-transfer/v1": 0.1,
"/api/douyin-ec/get-item-comments/v1": 0.1,
"/api/douyin-ec/get-item-detail/v2": 0.1,
"/api/douyin-ec/get-item-sku-info/v1": 0.2,
"/api/douyin-ec/get-shop-item-list/v1": 0.1,
"/api/douyin-ec/search-item-list/v1": 0.15,
"/api/douyin-xingtu/gw/api/aggregator/get_author_commerce_seed_base_info/v1": 0.15,
"/api/douyin-xingtu/gw/api/aggregator/get_author_commerce_spread_info/v1": 0.15,
"/api/douyin-xingtu/gw/api/aggregator/get_author_contract_base_info": 0.15,
"/api/douyin-xingtu/gw/api/aggregator/get_author_homepage_videos/v1": 0.5,
"/api/douyin-xingtu/gw/api/aggregator/get_author_live_statistics/v1": 0.15,
"/api/douyin-xingtu/gw/api/aggregator/get_author_live_watch_distribution/v1": 0.15,
"/api/douyin-xingtu/gw/api/aggregator/get_author_order_experience/v1": 0.15,
"/api/douyin-xingtu/gw/api/aggregator/get_author_side_base_info/v1": 0.15,
"/api/douyin-xingtu/gw/api/aggregator/get_author_tags/v1": 0.15,
"/api/douyin-xingtu/gw/api/author/get_author_base_info/v1": 0.15,
"/api/douyin-xingtu/gw/api/author/get_author_marketing_info/v1": 0.15,
"/api/douyin-xingtu/gw/api/author/get_author_platform_channel_info_v2/v1": 0.15,
"/api/douyin-xingtu/gw/api/author/get_author_show_items_v2/v1": 0.15,
"/api/douyin-xingtu/gw/api/data/get_author_hot_comment_tokens/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/author_audience_distribution/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/author_cp_info/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/author_link_struct/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/author_rec_videos_v2/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/author_touch_distribution/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/author_video_distribution/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/check_author_display/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/get_author_convert_ability/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/get_author_convert_videos_or_products/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/get_author_daily_fans/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/get_author_fans_distribution/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/get_author_link_info/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/get_author_spread_info/v1": 0.5,
"/api/douyin-xingtu/gw/api/data_sp/item_report_detail/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/item_report_th_analysis/v1": 0.15,
"/api/douyin-xingtu/gw/api/data_sp/item_report_trend/v1": 0.15,
"/api/douyin-xingtu/gw/api/gauthor/author_get_business_card_info/v1": 0.15,
"/api/douyin-xingtu/gw/api/gauthor/get_author_content_hot_keywords/v1": 0.15,
"/api/douyin-xingtu/gw/api/gsearch/search_for_author_square/v1": 0.5,
"/api/kuaishou/get-user-detail/v1": 0.1,
"/api/kuaishou/get-user-video-list/v2": 0.1,
"/api/kuaishou/get-video-comment/v1": 0.1,
"/api/kuaishou/get-video-detail/v2": 0.1,
"/api/kuaishou/search-user/v2": 0.1,
"/api/kuaishou/search-video/v1": 0.1,
"/api/kuaishou/search-video/v2": 0.1,
"/api/kuaishou/share-url-transfer/v1": 0.1,
"/api/weixin/convert-article-link/v1": 0.1,
"/api/weixin/get-account-basic-info/v1": 1.0,
"/api/weixin/get-account-history-articles/v1": 0.2,
"/api/weixin/get-account-history-articles/v2": 0.4,
"/api/weixin/get-account-original-count/v1": 1.5,
"/api/weixin/get-account-principal-info/v1": 1.0,
"/api/weixin/get-account-today-articles/v1": 0.2,
"/api/weixin/get-article-comment/v1": 0.2,
"/api/weixin/get-article-detail/v1": 0.15,
"/api/weixin/get-article-detail/v2": 0.15,
"/api/weixin/get-article-detail/v3": 0.2,
"/api/weixin/get-article-detail/v4": 0.15,
"/api/weixin/get-article-detail/v5": 0.15,
"/api/weixin/get-article-metrics/v1": 0.15,
"/api/weixin/get-article-metrics/v2": 0.2,
"/api/weixin/get-article-sub-comment/v1": 0.2,
"/api/weixin/search-account/v1": 0.4,
"/api/weixin/search-account/v2": 1.5,
"/api/weixin/search-article-hot/v1": 0.6,
"/api/weixin/search-article/v1": 0.8,
"/api/weixin/search-article/v2": 0.8,
"/api/weixin/search-miniprogram/v1": 0.8,
"/api/weixin/search-suggestions/v1": 0.8,
"/api/weixin/search-wechat-index/v1": 0.8,
"/api/weixin-channels/convert-export-id/v1": 0.15,
"/api/weixin-channels/get-account-videos/v1": 0.5,
"/api/weixin-channels/get-bound-channel/v1": 1.0,
"/api/weixin-channels/get-video-basic-info/v1": 0.3,
"/api/weixin-channels/get-video-comment/v1": 0.15,
"/api/weixin-channels/get-video-download-url/v1": 0.3,
"/api/weixin-channels/get-video-metrics/v1": 0.15,
"/api/weixin-channels/get-video-sub-comment/v1": 0.15,
"/api/weixin-channels/get-video-title/v1": 0.15,
"/api/weixin-channels/search-account/v1": 1.5,
"/api/weixin-channels/search-account/v2": 1.0,
"/api/weixin-channels/search-account/v3": 0.5,
"/api/weixin-channels/search-video/v1": 0.8,
"/api/weixin-channels/search-video/v2": 1.5,
"/api/qq-huxuan/cgi-bin/advertiser/finder_publisher/detail/v1": 0.3,
"/api/qq-huxuan/cgi-bin/advertiser/finder_publisher/get_finder_video_show/v1": 0.5,
"/api/qq-huxuan/cgi-bin/advertiser/finder_publisher/search/v1": 0.5,
"/api/qq-huxuan/cgi-bin/advertiser/mp_publisher/detail/v1": 0.3,
"/api/qq-huxuan/cgi-bin/advertiser/mp_publisher/get_user_articles/v1": 0.5,
"/api/qq-huxuan/cgi-bin/advertiser/mp_publisher/search/v1": 0.5,
"/api/weibo/get-fans/v1": 0.1,
"/api/weibo/get-followers/v1": 0.1,
"/api/weibo/get-post-comments/v1": 0.1,
"/api/weibo/get-user-detail/v3": 0.1,
"/api/weibo/get-user-post/v1": 0.1,
"/api/weibo/get-user-video-list/v1": 0.1,
"/api/weibo/get-weibo-detail/v1": 0.1,
"/api/weibo/hot-search/v1": 0.1,
"/api/weibo/search-all/v2": 0.1,
"/api/weibo/search-profile/v1": 0.1,
"/api/weibo/tv-component/v1": 0.1,
"/api/bilibili/get-user-detail/v2": 0.1,
"/api/bilibili/get-user-relation-stat/v1": 0.1,
"/api/bilibili/get-user-video-list/v2": 0.1,
"/api/bilibili/get-video-caption/v2": 0.1,
"/api/bilibili/get-video-comment/v2": 0.1,
"/api/bilibili/get-video-danmu/v2": 0.1,
"/api/bilibili/get-video-detail/v2": 0.1,
"/api/bilibili/search-video/v2": 0.1,
"/api/bilibili/share-url-transfer/v1": 0.1,
"/api/jd/get-item-comments/v1": 0.2,
"/api/jd/get-item-comments/v2": 0.15,
"/api/jd/get-item-detail/v1": 0.2,
"/api/jd/get-item-detail/v3": 0.25,
"/api/jd/get-item-price/v1": 0.12,
"/api/jd/get-shop-item-list/v1": 0.2,
"/api/jd/search-item-list/v1": 0.15,
"/api/jd/search-item-list/v2": 0.2,
"/api/xianyu/get-item-detail/v1": 0.2,
"/api/xianyu/search-item-list/v1": 0.2,
"/api/1688/get-item-detail/v1": 0.1,
"/api/1688/search-item-list/v1": 0.1,
"/api/douban/get-movie-comments/v1": 0.1,
"/api/douban/get-movie-review-detail/v1": 0.1,
"/api/douban/get-movie-reviews/v1": 0.1,
"/api/douban/get-recent-hot-movie/v1": 0.1,
"/api/douban/get-recent-hot-tv/v1": 0.1,
"/api/douban/get-subject-detail/v1": 0.1,
"/api/tiktok/get-post-comment/v1": 0.1,
"/api/tiktok/get-post-detail/v1": 0.1,
"/api/tiktok/get-post-sub-comment/v1": 0.1,
"/api/tiktok/get-user-detail/v1": 0.1,
"/api/tiktok/get-user-post/v1": 0.1,
"/api/tiktok/search-post/v1": 0.1,
"/api/tiktok/search-user/v1": 0.1,
"/api/tiktok-shop/get-product-detail/v1": 0.1,
"/api/tiktok-shop/search-products/v1": 0.1,
"/api/youku/get-user-detail/v1": 0.1,
"/api/youku/get-video-detail/v1": 0.1,
"/api/youku/search-video/v1": 0.1,
"/api/instagram/get-comment-replies/v1": 0.1,
"/api/instagram/get-post-comments/v1": 0.1,
"/api/instagram/get-post-detail/v1": 0.1,
"/api/instagram/get-user-detail/v1": 0.1,
"/api/instagram/get-user-detail/v2": 0.1,
"/api/instagram/get-user-posts/v1": 0.1,
"/api/instagram/search-hashtag-posts/v1": 0.1,
"/api/instagram/search-reels/v1": 0.1,
"/api/youtube/get-channel-videos/v1": 0.1,
"/api/youtube/get-video-captions/v1": 0.1,
"/api/youtube/get-video-comment/v1": 0.1,
"/api/youtube/get-video-detail/v1": 0.1,
"/api/youtube/get-video-sub-comment/v1": 0.1,
"/api/youtube/search/v1": 0.1,
"/api/reddit/get-post-comments/v1": 0.1,
"/api/reddit/get-post-detail/v1": 0.1,
"/api/reddit/search/v1": 0.1,
"/api/toutiao/get-article-detail/v1": 0.1,
"/api/toutiao/get-user-detail/v1": 0.1,
"/api/toutiao/get-user-id/v1": 0.1,
"/api/toutiao/search/v2": 0.5,
"/api/zhihu/get-answer-comments/v1": 0.1,
"/api/zhihu/get-answer-list/v1": 0.1,
"/api/zhihu/get-column-article-detail/v1": 0.1,
"/api/zhihu/get-column-article-list/v1": 0.1,
"/api/zhihu/get-comment-replies/v1": 0.1,
"/api/zhihu/get-user-articles/v1": 0.1,
"/api/zhihu/get-user-follow-collections/v1": 0.1,
"/api/zhihu/get-user-follow-columns/v1": 0.1,
"/api/zhihu/get-user-follow-questions/v1": 0.1,
"/api/zhihu/get-user-follow-topics/v1": 0.1,
"/api/zhihu/get-user-followees/v1": 0.1,
"/api/zhihu/get-user-followers/v1": 0.1,
"/api/zhihu/get-user-included-articles/v1": 0.1,
"/api/zhihu/get-user-info/v1": 0.1,
"/api/zhihu/search/v1": 0.2,
"/api/amazon/get-best-sellers/v1": 0.1,
"/api/amazon/get-category-products/v1": 0.1,
"/api/amazon/get-product-detail/v1": 0.1,
"/api/amazon/get-product-top-reviews/v1": 0.1,
"/api/amazon/search-products/v1": 0.1,
"/api/facebook/get-post-comments/v1": 0.2,
"/api/facebook/get-profile-id/v1": 0.2,
"/api/facebook/get-profile-posts/v1": 0.2,
"/api/facebook/search-post/v1": 0.2,
"/api/twitter/get-post-comments/v1": 0.1,
"/api/twitter/get-user-detail/v1": 0.1,
"/api/twitter/get-user-posts/v1": 0.1,
"/api/twitter/search/v1": 0.1,
"/api/linkedin/get-company-employees/v1": 0.2,
"/api/linkedin/get-post-comments/v1": 0.2,
"/api/linkedin/get-post-reactions/v1": 0.2,
"/api/linkedin/get-user-detail/v1": 0.2,
"/api/linkedin/get-user-posts/v1": 0.2,
"/api/linkedin/search-user/v1": 0.2,
"/api/imdb/news-by-category-query/v1": 0.05,
"/api/imdb/streaming-picks-query/v1": 0.05,
"/api/imdb/title-awards-summary-query/v1": 0.05,
"/api/imdb/title-base-query/v1": 0.05,
"/api/imdb/title-box-office-summary/v1": 0.05,
"/api/imdb/title-chart-rankings/v1": 0.05,
"/api/imdb/title-contribution-questions/v1": 0.05,
"/api/imdb/title-countries-of-origin/v1": 0.05,
"/api/imdb/title-critics-review-summary-query/v1": 0.05,
"/api/imdb/title-details-query/v1": 0.05,
"/api/imdb/title-did-you-know-query/v1": 0.05,
"/api/imdb/title-extended-details-query/v1": 0.05,
"/api/imdb/title-more-like-this-query/v1": 0.05,
"/api/imdb/title-plot-query/v1": 0.05,
"/api/imdb/title-redux-overview-query/v1": 0.05,
"/api/imdb/title-release-expectation-query/v1": 0.05,
"/api/imdb/title-top-cast-and-crew/v1": 0.05,
"/api/imdb/title-user-reviews-summary-query/v1": 0.05
}
}
+11 -69
View File
@@ -48,17 +48,10 @@ pass the scan and both credit. That is acceptable for a human-driven ops script
terminal - and the fix if it ever stops being true is a unique column, not a smarter scan. Do not
run two of these at once for the same comp.
WHAT IT DOES TO PRODUCTION
--------------------------
Prod Postgres keeps an EMPTY `ipAllowList`, so it opens a hole for this machine's /32, works, and
closes it in a `finally` — then re-reads the resource to PROVE it closed, exactly as
`usage_report.py` does. If you see "allowlist NOT closed", close it by hand before anything else.
RUN IT FROM `main`, NOT FROM A FEATURE BRANCH
----------------------------------------------
Unlike `usage_report.py` (raw SQL, branch-proof), this one goes through the ORM, so it is bound to
whatever `src/treg/models.py` the checkout has. A branch carrying an unmigrated column makes the
query fail against prod — or worse, half-match. Check out `main` before running it against prod.
DATABASE ACCESS
---------------
Uses the configured TREG_DATABASE_URL through treg's database infrastructure. Run from a checkout
whose schema matches the target database. Hosted operators use their private maintenance runbook.
"""
from __future__ import annotations
@@ -70,10 +63,8 @@ import sys
from decimal import Decimal, InvalidOperation
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
from usage_report import DB_ID, env, my_ip, render_api # noqa: E402
def usd(micro: int | None) -> str:
@@ -94,69 +85,21 @@ def to_micro(amount: str) -> int:
return int(micro)
def prod_dsn() -> str:
"""Render's external connection string, rewritten for SQLAlchemy's asyncpg driver.
`sslmode` is a libpq parameter that asyncpg rejects outright, so it is stripped here and the TLS
requirement is re-expressed as a connect_arg on the engine below.
"""
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
raw = render_api("GET", f"/postgres/{DB_ID}/connection-info")["externalConnectionString"]
parts = urlsplit(raw)
query = [(k, v) for k, v in parse_qsl(parts.query) if k != "sslmode"]
scheme = "postgresql+asyncpg" if parts.scheme in ("postgres", "postgresql") else parts.scheme
return urlunsplit((scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))
async def run(args) -> int:
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
if not os.environ.get("TREG_DATABASE_URL"):
raise SystemExit("Set TREG_DATABASE_URL explicitly before running this maintenance tool")
from sqlmodel import select
from treg.domain import money
from treg.infra.db import dispose_engine, session_maker
from treg.models import CreditBlock, LedgerEntry, Membership, Org, User
ip = my_ip()
print(f"opening prod allowlist for {ip}/32 ...", file=sys.stderr)
render_api("PATCH", f"/postgres/{DB_ID}",
{"ipAllowList": [{"cidrBlock": f"{ip}/32", "description": "manual_grant.py"}]})
try:
engine = create_async_engine(prod_dsn(), connect_args={"ssl": "require"}, future=True)
maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
try:
# Prove the connection BEFORE any money code runs, and ride out the two transient
# failures that mean nothing is wrong: the allowlist PATCH above takes a moment to take
# effect, and Render's Postgres hostname intermittently SERVFAILs. Without this the
# first attempt reliably dies with ConnectionDoesNotExistError (observed 2026-08-27).
# The `finally` below still closes the allowlist if every attempt fails.
for attempt, pause in enumerate((2, 5, 10, 0), start=1):
try:
async with engine.connect():
pass
break
except Exception as exc: # noqa: BLE001 — asyncpg raises several unrelated types
if not pause:
raise
print(f" connect attempt {attempt} failed ({type(exc).__name__}); "
f"retrying in {pause}s", file=sys.stderr)
await asyncio.sleep(pause)
async with maker() as db:
return await _work(db, args, money, User, Membership, Org, CreditBlock,
LedgerEntry, select)
finally:
await engine.dispose()
async with session_maker() as db:
return await _work(db, args, money, User, Membership, Org, CreditBlock,
LedgerEntry, select)
finally:
# Runs on success, on error and on Ctrl-C. The re-read is the point: a PATCH that 200s but
# leaves the list populated would quietly leave prod exposed until somebody noticed.
try:
render_api("PATCH", f"/postgres/{DB_ID}", {"ipAllowList": []})
still = render_api("GET", f"/postgres/{DB_ID}").get("ipAllowList")
if still:
print(f"!! allowlist NOT closed — still {still}. Close it by hand NOW.", file=sys.stderr)
else:
print("prod allowlist closed and verified.", file=sys.stderr)
except Exception as exc: # noqa: BLE001 — never let a bug here leave prod open silently
print(f"!! could not close the allowlist ({exc}). Close it by hand NOW.", file=sys.stderr)
await dispose_engine()
async def _work(db, args, money, User, Membership, Org, CreditBlock, LedgerEntry, select) -> int:
@@ -244,7 +187,6 @@ def main() -> None:
if args.confirm and not all((args.org_id, args.amount_usd, args.ref, args.reason)):
raise SystemExit("--confirm needs --org-id, --amount-usd, --ref and --reason")
env("RENDER_API_KEY") # fail before touching the allowlist, not halfway through
raise SystemExit(asyncio.run(run(args)))
-940
View File
@@ -1,940 +0,0 @@
#!/usr/bin/env python3
"""What every org actually called, what it cost, and what failed — straight from the prod DB.
uv run --frozen python scripts/usage_report.py [--days 1] [--top 25] [--html PATH] [--json]
Run it daily. It prints a summary to stdout and writes a self-contained HTML dashboard.
WHY THIS READS THE DATABASE AND NOT THE ADMIN API
-------------------------------------------------
`/admin/calls` caps at 1,000 rows with no paging — about five hours at current volume — so a daily
job against it would silently miss most of the day. The three `/admin/reconcile/*` reports aggregate
over 30 days server-side but only answer their own three questions, and none of them can group by
endpoint for a provider that doesn't report its own cost. Two columns this report is built on,
`credential_tier` and `refused_by`, are exposed by NO admin route at all: `/orgs/{id}/calls` carries
them but is scoped to the caller's own org. So the database is the only source that can answer
"what is everyone doing", and this script is the sanctioned way to ask.
WHAT IT DOES TO PRODUCTION
--------------------------
The prod Postgres keeps an EMPTY `ipAllowList`, which drops every external connection. This script
opens a hole for this machine's /32, reads, and closes it again in a `finally` — then re-reads the
resource to PROVE it closed. If you see "allowlist NOT closed" in the output, close it by hand
(Render dashboard -> treg-db -> Access Control) before doing anything else. Nothing here writes:
every statement is a SELECT, and the connection is opened read-only.
It deliberately uses RAW SQL rather than the ORM. Importing `treg.models` binds the query to
whatever branch is checked out, and a branch carrying an unmigrated column makes every SELECT fail
against prod with `UndefinedColumnError` — a trap that has cost real time before. Raw SQL against
named columns is immune, so this script runs correctly from any branch.
DEFINITIONS (they are not interchangeable, and conflating them is how "our error rate" gets misread)
----------------------------------------------------------------------------------------------------
refused `refused_by IS NOT NULL` — TREG said no before a byte went upstream: no balance, a daily
cap, a bad token, an unknown endpoint, a malformed request. Costs nothing and is not a
provider failure. `balance` refusals are the paywall, i.e. demand we didn't serve.
failed `status_code >= 400 AND refused_by IS NULL` — the call went upstream and the PROVIDER
answered badly. This is the number that means something is broken.
ok everything else.
spend `SUM(cost_charged_micro)` — what actually hit the org's balance. NOT the estimate:
a released or refunded call still carries an estimate and would over-report spend.
"""
from __future__ import annotations
import argparse
import asyncio
import datetime as dt
import html
import json
import os
import sys
import urllib.request
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
DB_ID = os.environ.get("TREG_PROD_DB_ID", "dpg-d94fp3d7vvec73di5rqg-a")
RENDER_API = "https://api.render.com/v1"
# ---- environment -------------------------------------------------------------------------------
def env(key: str) -> str:
"""Read `key` from the process env, falling back to the repo's .env (same file the deploy uses)."""
if os.environ.get(key):
return os.environ[key]
envfile = REPO / ".env"
if envfile.exists():
for line in envfile.read_text().splitlines():
line = line.strip()
if line.startswith(key + "="):
return line.split("=", 1)[1].strip().strip('"').strip("'")
raise SystemExit(f"{key} is not set (looked in the environment and {envfile})")
def render_api(method: str, path: str, body: dict | None = None):
"""One Render API call. PATCH, never PUT — PUT answers with a non-JSON body on this resource."""
req = urllib.request.Request(
RENDER_API + path,
method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + env("RENDER_API_KEY"),
"Content-Type": "application/json", "Accept": "application/json"},
)
with urllib.request.urlopen(req, timeout=45) as resp:
raw = resp.read()
return json.loads(raw) if raw.strip() else None
def my_ip() -> str:
return urllib.request.urlopen("https://api.ipify.org", timeout=20).read().decode().strip()
# ---- the queries -------------------------------------------------------------------------------
# One CTE defines the window and the three outcome classes once, so every panel below counts the
# same way. `endpoint_id` is NULL for a call to a team's own registered tool; `tool_name` names it,
# so the report coalesces to keep own-tool traffic visible instead of bucketing it all as "unknown".
BASE = """
with c as (
select
coalesce(endpoint_id, tool_name) as ep,
coalesce(provider, split_part(tool_name,'.',1)) as prov,
-- The split that decides which of the two endpoint tables a row lands in. A non-NULL
-- endpoint_id means the call RESOLVED to a catalog endpoint; NULL means it was either a tool
-- the team registered themselves or a shape treg could not resolve at all.
(endpoint_id is not null) as cataloged,
id, method, org_id, user_email, status_code, refused_by, credential_tier, path,
{evidence_cols}
nullif(client,'') as client,
coalesce(cost_charged_micro,0) as spend,
duration_ms, created_at,
(refused_by is not null) as refused,
(status_code >= 400 and refused_by is null) as failed,
(status_code < 400) as ok
from callrecord
where created_at >= $1
)
"""
QUERIES: dict[str, str] = {
"summary": BASE + """
select count(*) calls, count(distinct org_id) orgs, count(distinct ep) endpoints,
count(distinct prov) providers, sum(spend) spend,
sum(case when ok then 1 else 0 end) ok,
sum(case when failed then 1 else 0 end) failed,
sum(case when refused then 1 else 0 end) refused,
percentile_disc(0.5) within group (order by duration_ms) p50_ms,
percentile_disc(0.95) within group (order by duration_ms) p95_ms
from c""",
# {unit} is filled from a two-value whitelist in `collect` — a short window wants the day's
# hourly shape, a long one wants days. Never interpolate anything user-supplied here.
"daily": BASE + """
select date_trunc('{unit}', created_at) d, count(*) calls,
sum(case when ok then 1 else 0 end) ok,
sum(case when failed then 1 else 0 end) failed,
sum(case when refused then 1 else 0 end) refused,
sum(spend) spend, count(distinct org_id) orgs
from c group by 1 order by 1""",
# Every endpoint, with each failure class broken out as its own column so a row says WHERE to
# improve without opening anything: `req` is a request treg rejected, `res` is something it
# could not find or serve, `failed` is the provider answering badly. Three different fixes.
"endpoints": BASE + """
select ep, prov, cataloged, count(*) calls, count(distinct org_id) orgs, sum(spend) spend,
sum(case when ok then 1 else 0 end) ok,
sum(case when failed then 1 else 0 end) failed,
sum(case when refused_by = 'request' then 1 else 0 end) req,
sum(case when refused_by = 'resolution' then 1 else 0 end) res,
sum(case when refused_by = 'balance' then 1 else 0 end) bal,
sum(case when refused_by = 'cap' then 1 else 0 end) cap,
sum(case when refused_by = 'auth' then 1 else 0 end) auth,
sum(case when refused_by = 'policy' then 1 else 0 end) pol,
percentile_disc(0.5) within group (order by duration_ms) p50_ms
from c group by 1,2,3 order by count(*) desc""",
# The per-endpoint drill-down behind each expandable row: one line per (reason, status), with a
# sample path so a 400 can be reproduced. Only non-2xx rows, so it stays small.
# Keyed by (ep, cataloged), NOT by ep alone: the same name can appear in BOTH endpoint tables —
# e.g. a tikhub id that resolved 3,428 times and was also sent 159 times in an unresolvable
# shape. Keying on the name alone would show each row the other one's errors.
# `evidence` and `sent` are the provider's own error message and the caller's request, captured
# for failed PLATFORM calls only (see models.CallRecord.error_response). They are NULL for an
# own-key call by design, and for anything that failed before the capture shipped — so a blank
# column here means "not captured", never "no error".
#
# Both come from ONE row, via the same ORDER BY inside array_agg. They used to be independent
# `max()`es, which silently paired one call's request with a DIFFERENT call's response — a
# plausible, readable, wrong diagnosis, which is worse than showing nothing. Ordering prefers a
# row that actually has evidence, then the newest, and `id` makes it a total order so the two
# aggregates cannot disagree.
#
# `reason` distinguishes three owners, not two: treg refused it, treg could not reach the
# provider (a relay 502 — the provider never answered, and calling that "provider answered" is
# the same false-diagnosis class), or the provider answered badly.
"errdetail": BASE + """
select ep, cataloged,
-- Four cases, not three. Deriving "who failed" from the evidence TEXT is only sound
-- while the text is there: before migration (A35) deploys it is NULL, and after 14 days
-- it is '<expired>'. Both used to fall through to "provider answered", which quietly
-- re-attributed treg's own 502s to the provider — the same false-diagnosis class this
-- report was just fixed for, reappearing whenever evidence is absent. When we cannot
-- tell, say so.
case when refused_by is not null then refused_by
when error_response like 'treg:%' then '(treg never reached it)'
when error_response is null or error_response = '<expired>'
then '(evidence not captured)'
else '(provider answered)' end reason,
status_code, count(*) n, count(distinct org_id) orgs,
left(min(path), 170) sample,
string_agg(distinct method, '/') methods,
{evidence_agg}
from c where status_code >= 400
group by 1,2,3,4 order by 1, 5 desc""",
"providers": BASE + """
select prov, count(*) calls, count(distinct org_id) orgs, sum(spend) spend,
sum(case when failed then 1 else 0 end) failed,
sum(case when refused then 1 else 0 end) refused
from c group by 1 order by count(*) desc""",
"statuses": BASE + """
select status_code, count(*) n, sum(case when refused then 1 else 0 end) refused
from c where status_code >= 400 group by 1 order by 2 desc""",
"refusals": BASE + """
select refused_by, count(*) n, count(distinct org_id) orgs
from c where refused_by is not null group by 1 order by 2 desc""",
"failures": BASE + """
select ep, prov, status_code, count(*) n, count(distinct org_id) orgs
from c where failed group by 1,2,3 order by 4 desc limit 40""",
# credential_tier is the rung of the ladder that served the call (api.py `_marketplace_call`).
# NULL is NOT a fourth rung — it means the call was never a catalog endpoint at all, i.e. a plain
# proxy call to a tool the team registered themselves. Labelling that "(own tool)" next to the
# real "tool" tier is how you end up with two buckets that read identically and mean different
# things, so each row is spelled out here instead.
"tiers": BASE + """
select case credential_tier
when 'platform' then 'platform — treg''s key, metered'
when 'tool' then 'tool — org''s own key, catalog endpoint'
when 'credential' then 'credential — org''s own secret, no tool registered'
else 'not a catalog call — org''s own registered tool'
end tier,
count(*) n, sum(spend) spend, count(distinct org_id) orgs
from c group by 1 order by 2 desc""",
"clients": BASE + """
select coalesce(client,'(unreported)') client, count(*) n, count(distinct org_id) orgs
from c group by 1 order by 2 desc""",
# "The catalog doesn't have X" — filed from the web, the CLI, or by an agent over MCP.
# NOT windowed like everything else, and deliberately: a request is a BACKLOG ITEM, not a time
# series. A `--days 1` run that showed only today's would report an empty queue while a month of
# unanswered asks sat in the table. So every open row is listed, and `in_window` marks the new
# ones. `$1` is referenced only for that flag — collect() passes `since` to every query, and a
# statement that ignores its parameter is a protocol error, not a no-op.
"requests": """
select id, org_id, user_email, capability, query, note, contact, source, status, created_at,
(created_at >= $1) as in_window
from toolrequest
where status = 'open' or created_at >= $1
order by created_at desc limit 300""",
# Catalog searches that matched NOTHING, grouped by query — the demand signal one step before a
# tool request (most agents that miss never file one; the query text is all they leave behind).
# Windowed like the call panels: a miss is a time-series event, not a backlog item — the same
# query missing last month and this month should read as two spikes, not one eternal row.
"search_misses": """
select query, count(*) n, string_agg(distinct source, '/') sources,
min(created_at) first_seen, max(created_at) last_seen
from searchmiss
where created_at >= $1
group by 1 order by count(*) desc, max(created_at) desc limit 200""",
"orgs": BASE + """
select c.org_id, o.slug, count(*) calls, sum(c.spend) spend,
sum(case when c.failed then 1 else 0 end) failed,
sum(case when c.refused then 1 else 0 end) refused,
count(distinct c.ep) endpoints, max(c.created_at) last_seen
from c left join org o on o.id = c.org_id
group by 1,2 order by count(*) desc""",
}
async def collect(since: dt.datetime, unit: str = "day") -> dict:
"""Open prod to this IP, run every query, close prod again. Closing is not optional."""
import asyncpg
if unit not in ("hour", "day"): # the only values that ever reach the SQL string
raise ValueError(unit)
ip = my_ip()
print(f"opening prod allowlist for {ip}/32 ...", file=sys.stderr)
render_api("PATCH", f"/postgres/{DB_ID}",
{"ipAllowList": [{"cidrBlock": f"{ip}/32", "description": "usage_report.py"}]})
try:
dsn = render_api("GET", f"/postgres/{DB_ID}/connection-info")["externalConnectionString"]
# Retry the connect. Two transient failures are common here and neither means anything is
# wrong: the allowlist PATCH takes a moment to take effect, and Render's Postgres hostname
# intermittently SERVFAILs (observed 2026-08-17). An unattended daily run must ride both out
# rather than exit — the `finally` below still closes the allowlist if every attempt fails.
conn = None
for attempt, pause in enumerate((2, 5, 10, 0), start=1):
try:
conn = await asyncpg.connect(dsn, ssl="require", timeout=45,
server_settings={"default_transaction_read_only": "on"})
break
except (OSError, asyncpg.PostgresError) as exc:
if not pause:
raise
print(f" connect attempt {attempt} failed ({type(exc).__name__}: {exc}); "
f"retrying in {pause}s", file=sys.stderr)
await asyncio.sleep(pause)
try:
# The failure-evidence columns arrive with migration (A35). Until that deploys, prod does
# not have them — so probe rather than assume, or this script dies on
# UndefinedColumnError against exactly the database it exists to report on. Degrading to
# "no evidence available" keeps every other panel working through the deploy window.
has_evidence = bool(await conn.fetchval(
"select 1 from information_schema.columns"
" where table_name = 'callrecord' and column_name = 'error_response'"))
if not has_evidence:
print(" note: callrecord has no error_request/error_response yet — evidence panels "
"will be empty until migration (A35) deploys", file=sys.stderr)
ev_cols = ("error_request, error_response," if has_evidence
else "null::text error_request, null::text error_response,")
# `nullif(..., '<expired>')` so an aged-out row reads as "no evidence" rather than as
# evidence whose content is the word `<expired>` — otherwise the drawer prints
# "said <expired>" and the exemplar picker prefers an expired row over a newer real one.
ev_agg = ("""left((array_agg(nullif(error_response, '<expired>')
order by (nullif(error_response, '<expired>') is not null) desc,
id desc))[1], 400) evidence,
left((array_agg(nullif(error_request, '<expired>')
order by (nullif(error_response, '<expired>') is not null) desc,
id desc))[1], 220) sent"""
if has_evidence else "null::text evidence, null::text sent")
# Same probe-don't-assume dance as the evidence columns: `searchmiss` arrives with the
# zero-result search logging, and until that deploys the table does not exist on prod —
# this script must keep working from any branch against exactly that database.
has_misses = bool(await conn.fetchval(
"select 1 from information_schema.tables where table_name = 'searchmiss'"))
if not has_misses:
print(" note: no searchmiss table yet — the empty-search panel will be empty "
"until the search-miss logging deploys", file=sys.stderr)
out = {}
for name, sql in QUERIES.items():
if name == "search_misses" and not has_misses:
out[name] = []
continue
sql = (sql.replace("{unit}", unit)
.replace("{evidence_cols}", ev_cols)
.replace("{evidence_agg}", ev_agg))
rows = [dict(r) for r in await conn.fetch(sql, since)]
out[name] = rows
print(f" {name}: {len(rows)} rows", file=sys.stderr)
return out
finally:
await conn.close()
finally:
# Runs on success, on error, and on Ctrl-C. The verify is the point: a PATCH that 200s but
# leaves the list populated would quietly expose prod until someone noticed.
try:
render_api("PATCH", f"/postgres/{DB_ID}", {"ipAllowList": []})
still = render_api("GET", f"/postgres/{DB_ID}").get("ipAllowList")
if still:
print(f"!! allowlist NOT closed — still {still}. Close it by hand NOW.", file=sys.stderr)
else:
print("prod allowlist closed and verified.", file=sys.stderr)
except Exception as exc: # noqa: BLE001 — never let a reporting bug leave prod open silently
print(f"!! could not close the allowlist ({exc}). Close it by hand NOW.", file=sys.stderr)
# ---- formatting helpers ------------------------------------------------------------------------
def usd(micro) -> str:
return f"${(micro or 0) / 1_000_000:,.2f}"
def pct(n, d) -> str:
return f"{(n / d * 100):.1f}%" if d else "—"
def short(s: str, n: int) -> str:
s = s or "—"
return s if len(s) <= n else s[: n - 1] + "…"
# ---- terminal ------------------------------------------------------------------------------------
def print_summary(data: dict, days: int, top: int) -> None:
s = data["summary"][0]
calls = s["calls"] or 0
print(f"\n\033[1mtreg usage — last {days}d\033[0m ({calls:,} calls · {s['orgs']} orgs · "
f"{s['endpoints']} endpoints · {s['providers']} providers)")
print(f" ok {s['ok']:,} ({pct(s['ok'], calls)}) "
f"provider-failed {s['failed']:,} ({pct(s['failed'], calls)}) "
f"treg-refused {s['refused']:,} ({pct(s['refused'], calls)})")
print(f" spend {usd(s['spend'])} latency p50 {s['p50_ms'] or 0}ms / p95 {s['p95_ms'] or 0}ms")
if data["refusals"]:
print("\n\033[1mwhat treg stopped before sending\033[0m (costs nothing)")
for r in data["refusals"]:
# An `auth` refusal has no org — the token never resolved to one — so 0 here is correct.
where = f"across {r['orgs']} orgs" if r["orgs"] else "no org resolved"
plain, gloss = PLAIN.get(r["refused_by"], (r["refused_by"], ""))
print(f" {r['n']:>6} {plain:<14} {where:<20} {gloss}")
cat = [e for e in data["endpoints"] if e["cataloged"]]
non = [e for e in data["endpoints"] if not e["cataloged"]]
print(f"\n\033[1mtop {top} endpoints\033[0m ({len(cat)} catalog + {len(non)} non-catalog; "
f"the HTML lists every one)")
print(f" {'calls':>7} {'prov':>5} {'req':>5} {'reso':>5} {'spend':>9} {'orgs':>5} endpoint")
for e in data["endpoints"][:top]:
okrate = (e["ok"] or 0) / e["calls"] if e["calls"] else 1
flag = "\033[31m!\033[0m" if e["calls"] >= 20 and okrate < 0.85 else " "
print(f" {e['calls']:>7} {e['failed'] or '':>5} {e['req'] or '':>5} {e['res'] or '':>5} "
f"{usd(e['spend']):>9} {e['orgs']:>5} {flag}{short(e['ep'], 54)}")
fails = data["failures"]
if fails:
print("\n\033[1mtop provider failures\033[0m (the provider answered badly — treg's refusals are excluded)")
for f in fails[:12]:
print(f" {f['n']:>5} {f['status_code']} {short(f['ep'], 62)} ({f['orgs']} orgs)")
reqs = data.get("requests") or []
if reqs:
new = sum(1 for r in reqs if r["in_window"])
# A row with neither an email nor a contact is unanswerable — the request endpoint is
# deliberately anonymous-friendly, so this is the cost of that choice, stated out loud.
anon = sum(1 for r in reqs if not (r["user_email"] or r["contact"]))
print(f"\n\033[1mtool requests\033[0m ({len(reqs)} open · {new} filed in this window · "
f"{anon} with no way to reply)")
for r in reqs[:12]:
who = r["user_email"] or r["contact"] or "anonymous"
print(f" {r['created_at']:%m-%d %H:%M} {r['source']:<4} {short(who, 24):<24} "
f"{short(r['capability'], 58)}")
if len(reqs) > 12:
print(f" … {len(reqs) - 12} more (the HTML lists every one)")
misses = data.get("search_misses") or []
if misses:
hits = sum(m["n"] for m in misses)
print(f"\n\033[1msearches that matched nothing\033[0m ({len(misses)} distinct queries · "
f"{hits} misses in this window)")
for m in misses[:12]:
print(f" {m['n']:>5} {m['sources']:<8} {short(m['query'], 64)}")
if len(misses) > 12:
print(f" … {len(misses) - 12} more (the HTML lists every one)")
print("\n\033[1mtop orgs\033[0m")
for o in data["orgs"][:10]:
print(f" {o['calls']:>6} calls {usd(o['spend']):>9} {(o['slug'] or '?'):<24} "
f"{o['endpoints']} endpoints, {o['failed']} failed, {o['refused']} refused")
# ---- HTML ----------------------------------------------------------------------------------------
CSS = """
:root{
--bg:#eaeeee; --surface:#fff; --surface2:#f4f7f7; --ink:#121a1a; --muted:#5c6d6d;
--line:#d2dcdc; --line-soft:#e3eaea; --accent:#0d6e68; --accent-soft:#0d6e6822;
--ok:#2c7a52; --warn:#a86a14; --crit:#ab332c; --shadow:0 1px 2px #0f26261a;
}
@media (prefers-color-scheme:dark){
:root:not([data-theme="light"]){
--bg:#0d1313; --surface:#141d1d; --surface2:#101818; --ink:#e4eded; --muted:#8fa3a3;
--line:#24312f; --line-soft:#1c2726; --accent:#4ec9be; --accent-soft:#4ec9be22;
--ok:#48a877; --warn:#cf9440; --crit:#d9645c; --shadow:0 1px 2px #0006;
}
}
:root[data-theme="dark"]{
--bg:#0d1313; --surface:#141d1d; --surface2:#101818; --ink:#e4eded; --muted:#8fa3a3;
--line:#24312f; --line-soft:#1c2726; --accent:#4ec9be; --accent-soft:#4ec9be22;
--ok:#48a877; --warn:#cf9440; --crit:#d9645c; --shadow:0 1px 2px #0006;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);
font:14px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
-webkit-font-smoothing:antialiased}
.wrap{max-width:1180px;margin:0 auto;padding:32px 20px 64px;display:flex;flex-direction:column;gap:24px}
.num{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-variant-numeric:tabular-nums}
header{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:12px;
border-bottom:1px solid var(--line);padding-bottom:16px}
h1{margin:0;font-size:20px;font-weight:650;letter-spacing:-.01em}
h2{margin:0 0 12px;font-size:11px;font-weight:650;text-transform:uppercase;letter-spacing:.09em;color:var(--muted)}
.sub{color:var(--muted);font-size:13px}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
.kpi{background:var(--surface);border:1px solid var(--line);border-radius:4px;padding:14px 16px;
box-shadow:var(--shadow);display:flex;flex-direction:column;gap:4px}
.kpi .k{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted)}
.kpi .v{font-size:24px;font-weight:600;letter-spacing:-.02em}
.kpi .d{font-size:12px;color:var(--muted)}
.kpi.ok .v{color:var(--ok)} .kpi.warn .v{color:var(--warn)} .kpi.crit .v{color:var(--crit)}
.card{background:var(--surface);border:1px solid var(--line);border-radius:4px;padding:18px;
box-shadow:var(--shadow);overflow:hidden}
.cols{display:grid;grid-template-columns:1fr 1fr;gap:24px;align-items:start}
@media (max-width:820px){.cols{grid-template-columns:1fr}}
.scroll{overflow-x:auto}
table{border-collapse:collapse;width:100%;font-size:13px}
th{text-align:left;font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:var(--muted);
font-weight:600;padding:0 8px 7px;border-bottom:1px solid var(--line);white-space:nowrap}
td{padding:6px 8px;border-bottom:1px solid var(--line-soft);white-space:nowrap}
tr:last-child td{border-bottom:0}
td.r,th.r{text-align:right}
td.name{white-space:normal;word-break:break-word;min-width:220px}
/* A request filed inside the report window. The left rule carries the meaning; the tint is only a
hint, so this still reads on a monochrome print and in both themes. */
tr.fresh td{background:var(--accent-soft)}
tr.fresh td:first-child{box-shadow:inset 2px 0 0 var(--accent)}
.bar{position:relative;display:block;height:3px;background:var(--accent-soft);border-radius:2px;margin-top:4px}
.bar i{position:absolute;inset:0 auto 0 0;background:var(--accent);border-radius:2px}
.pill{display:inline-block;padding:1px 7px;border-radius:10px;font-size:11px;font-weight:600;
border:1px solid currentColor}
.pill.ok{color:var(--ok)} .pill.warn{color:var(--warn)} .pill.crit{color:var(--crit)}
.dim{color:var(--muted)}
.legend{display:flex;gap:16px;flex-wrap:wrap;font-size:12px;color:var(--muted);margin-top:10px}
.legend b{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px}
footer{color:var(--muted);font-size:12px;border-top:1px solid var(--line);padding-top:14px;line-height:1.7}
code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;
background:var(--surface2);padding:1px 5px;border-radius:3px}
/* --- the two big expandable endpoint tables ------------------------------------------------ */
.toolbar{display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin:0 0 14px}
.toolbar input[type=search]{flex:1 1 220px;min-width:180px;padding:6px 10px;font:inherit;
color:var(--ink);background:var(--surface2);border:1px solid var(--line);border-radius:4px}
.toolbar label{display:flex;gap:6px;align-items:center;font-size:12px;color:var(--muted);
cursor:pointer;user-select:none}
.toolbar .count{font-size:12px;color:var(--muted)}
:is(input,button,tr).exp:focus-visible,tr.exp:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
table.big{min-width:900px}
table.big thead th{position:sticky;top:0;z-index:1;background:var(--surface)}
tr.exp{cursor:pointer}
tr.exp:hover td{background:var(--surface2)}
.caret{display:inline-block;width:13px;color:var(--accent);font-size:10px;
transition:transform .12s ease;transform-origin:center}
tr.exp[aria-expanded="true"] .caret{transform:rotate(90deg)}
@media (prefers-reduced-motion:reduce){.caret{transition:none}}
tr.detail>td{background:var(--surface2);padding:0 8px 10px 26px}
table.inner{width:auto;min-width:520px;font-size:12px;margin-top:2px}
table.inner th{padding-top:8px;border-bottom:1px solid var(--line)}
table.inner td{border-bottom:1px solid var(--line-soft);padding:4px 10px 4px 0}
table.inner code{font-size:11px;background:transparent;padding:0}
/* The captured evidence: what the provider said, and what the caller sent. Wraps, because these are
sentences — a message clipped to a column width is the problem this feature exists to fix. */
tr.evrow td{padding:0 10px 8px 0;border-bottom:1px solid var(--line-soft)}
.ev{display:flex;gap:8px;align-items:baseline;margin-top:2px;font-size:12px}
.ev b{flex:0 0 auto;font-size:10px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)}
.ev code{white-space:pre-wrap;word-break:break-word;color:var(--ink);font-size:11.5px}
"""
# Row expansion + filtering. Vanilla, inline, no network: the page must work from a file:// URL.
JS = """
for (const t of document.querySelectorAll('table.big')) {
t.addEventListener('click', e => {
const tr = e.target.closest('tr.exp'); if (!tr || !t.contains(tr)) return;
const d = tr.nextElementSibling;
if (!d || !d.classList.contains('detail')) return;
const open = d.hidden; d.hidden = !open; tr.setAttribute('aria-expanded', open);
});
t.addEventListener('keydown', e => {
if (e.key !== 'Enter' && e.key !== ' ') return;
const tr = e.target.closest('tr.exp'); if (!tr) return;
e.preventDefault(); tr.click();
});
}
for (const box of document.querySelectorAll('[data-filters]')) {
const table = document.querySelector('#' + box.dataset.filters);
const q = box.querySelector('input[type=search]');
const badOnly = box.querySelector('input[type=checkbox]');
const out = box.querySelector('.count');
const apply = () => {
const needle = q.value.trim().toLowerCase();
let shown = 0, total = 0;
for (const tr of table.tBodies[0].rows) {
if (tr.classList.contains('detail')) continue;
total++;
const hit = (!needle || tr.dataset.ep.toLowerCase().includes(needle))
&& (!badOnly.checked || tr.dataset.bad === '1');
tr.hidden = !hit; if (hit) shown++;
const d = tr.nextElementSibling;
if (d && d.classList.contains('detail') && !hit) { d.hidden = true; tr.removeAttribute('aria-expanded'); }
}
out.textContent = shown === total ? total + ' rows' : shown + ' of ' + total + ' rows';
};
q.addEventListener('input', apply); badOnly.addEventListener('change', apply); apply();
}
"""
def svg_stacked(daily: list[dict], unit: str = "day") -> str:
"""Volume as ok / provider-failed / treg-refused. Hand-built bars — no library, no CDN."""
if not daily:
return '<p class="dim">No calls in this window.</p>'
fmt = "%Hh" if unit == "hour" else "%d"
W, H, PAD = 1100, 190, 26
peak = max(d["calls"] for d in daily) or 1
n = len(daily)
slot = (W - PAD * 2) / n
bw = max(3.0, min(slot * 0.66, 46))
parts = []
for i, d in enumerate(daily):
x = PAD + slot * i + (slot - bw) / 2
y = H - PAD
for key, var in (("refused", "var(--warn)"), ("failed", "var(--crit)"), ("ok", "var(--ok)")):
v = d[key] or 0
if not v:
continue
h = (v / peak) * (H - PAD * 2)
y -= h
parts.append(f'<rect x="{x:.1f}" y="{y:.1f}" width="{bw:.1f}" height="{h:.1f}" fill="{var}"/>')
every = max(1, n // 14) # thin the axis labels so hourly buckets don't collide
if i % every == 0:
parts.append(
f'<text x="{x + bw / 2:.1f}" y="{H - PAD + 14}" text-anchor="middle" font-size="10" '
f'fill="var(--muted)">{d["d"].strftime(fmt)}</text>')
parts.append(f'<title>{d["d"]}: {d["calls"]:,} calls, {d["failed"]} failed, {d["refused"]} refused</title>')
grid = "".join(
f'<line x1="{PAD}" x2="{W - PAD}" y1="{H - PAD - f * (H - PAD * 2):.1f}" '
f'y2="{H - PAD - f * (H - PAD * 2):.1f}" stroke="var(--line-soft)"/>' for f in (0.5, 1.0))
return (f'<svg viewBox="0 0 {W} {H}" width="100%" height="{H}" role="img" '
f'aria-label="Daily call volume">{grid}{"".join(parts)}'
f'<line x1="{PAD}" x2="{W - PAD}" y1="{H - PAD}" y2="{H - PAD}" stroke="var(--line)"/></svg>'
'<div class="legend">'
'<span><b style="background:var(--ok)"></b>ok</span>'
'<span><b style="background:var(--crit)"></b>provider failed</span>'
'<span><b style="background:var(--warn)"></b>treg refused</span></div>')
# The database stores terse gate names. Nobody should have to learn them to read this report, so
# every surface prints plain English and keeps the raw value alongside for grepping the DB.
PLAIN = {
"request": ("bad request", "wrong method, or a missing/invalid parameter"),
"resolution": ("not found", "no such endpoint, or treg has no key to call it with"),
"balance": ("out of credit", "the org's prepaid balance could not cover it"),
# NOT "the org hit its own spending cap": every 429 maps to `cap`, and that covers a member
# call-count cap, a tag call or spend cap, the platform ceiling, a trial allowance and a demo-IP
# limit. Naming one of them was a confident wrong answer for the other five.
"cap": ("hit a limit", "some cap or quota — member, tag, org, platform or trial"),
"auth": ("bad token", "the caller's token was missing, wrong or expired"),
"policy": ("blocked", "an ACL, deny rule or suspension refused it"),
"(treg never reached it)": ("treg could not reach the provider",
"timeout, reset, failed injection or SSRF refusal — no answer came"),
}
def _detail_rows(detail: list[dict], cols: int) -> str:
"""The drill-down revealed when a row is expanded.
The first column answers WHO produced the status, which is the whole point of the drawer: the
identical 400 can be the provider rejecting a relayed call or treg refusing to relay it at all,
and those have different owners. An earlier version put both in one column headed "Refused by",
which made every provider error read as "refused by: the provider answered" — a contradiction.
"""
rows = []
for d in detail:
upstream = d["reason"] == "(provider answered)"
who = ('<span class="pill crit">provider</span>' if upstream
else '<span class="pill warn">treg</span>')
plain, gloss = PLAIN.get(d["reason"], (d["reason"], ""))
why = ('<span class="dim">the provider rejected it</span>' if upstream
else f'<b>{html.escape(plain)}</b> <span class="dim">— {html.escape(gloss)}</span>')
# The method is already on every row and costs nothing to show. It IS the diagnosis for a
# whole failure class: 47 of apollo.people.enrich's failures were a GET at a POST endpoint,
# and without this column the drawer only ever said "bad request".
meth = html.escape(d.get("methods") or "")
rows.append(
f'<tr><td>{who}</td><td>{why}</td><td class="num">{meth} {d["status_code"]}</td>'
f'<td class="r num">{d["n"]:,}</td><td class="r num">{d["orgs"]}</td>'
f'<td class="name dim"><code>{html.escape(d["sample"] or "")}</code></td></tr>')
# The captured evidence, when there is any. Its own full-width row rather than another
# column: a provider's message is a sentence, and squeezing it into a table cell is how it
# ends up truncated to uselessness — which was the whole problem this feature solves.
said, sent = d.get("evidence"), d.get("sent")
if said or sent:
bits = []
if said:
bits.append(f'<div class="ev"><b>said</b> <code>{html.escape(said)}</code></div>')
if sent:
bits.append(f'<div class="ev"><b>sent</b> <code>{html.escape(sent)}</code></div>')
rows.append(f'<tr class="evrow"><td></td><td colspan="5">{"".join(bits)}</td></tr>')
elif not d.get("cataloged"):
# Say WHY there is nothing, rather than letting a blank read as "no detail exists".
# Evidence is captured for platform calls only, so a team's own registered tool — which
# is the single largest failure group, google-ads at 283 — never has any here.
rows.append('<tr class="evrow"><td></td><td colspan="5"><div class="ev dim">'
'not captured — own registered tool, so the request and the provider\'s '
'answer are the team\'s to inspect, not treg\'s</div></td></tr>')
return (f'<tr class="detail" hidden><td colspan="{cols}">'
f'<div class="scroll"><table class="inner"><thead><tr>'
f'<th>Stopped by</th><th>Reason</th><th>Status</th>'
f'<th class="r">Calls</th><th class="r">Orgs</th>'
f'<th>Example request <span class="dim">— a full URL went upstream; '
f'a bare id never left treg</span></th></tr></thead>'
f'<tbody>{"".join(rows)}</tbody></table></div></td></tr>')
def endpoint_table(eps: list[dict], details: dict[tuple[str, bool], list[dict]], *, first_col: str,
table_id: str) -> str:
"""Every endpoint, one row each, expandable into its error breakdown. No truncation."""
if not eps:
return '<p class="dim">Nothing in this bucket.</p>'
peak = max(e["calls"] for e in eps) or 1
head = (f'<th>{first_col}</th><th class="r">Calls</th><th class="r">Orgs</th><th class="r">OK</th>'
'<th class="r">Provider<br/>said no</th><th class="r">Bad<br/>request</th>'
'<th class="r">Not<br/>found</th><th class="r">Out of credit<br/>/ capped</th>'
'<th class="r">Spend</th><th class="r">p50</th>')
cols = 10
out = []
for e in eps:
calls, ep = e["calls"], e["ep"] or "—"
other = (e["bal"] or 0) + (e["cap"] or 0) + (e["auth"] or 0) + (e["pol"] or 0)
bad = (e["failed"] or 0) + (e["req"] or 0) + (e["res"] or 0) + other
okrate = (e["ok"] or 0) / calls if calls else 0
cls = "crit" if okrate < 0.6 else "warn" if okrate < 0.95 else "ok"
det = details.get((ep, e["cataloged"]))
# Only a row that HAS failures is expandable — a clickable row with an empty drawer is a lie.
out.append(
f'<tr class="{"exp" if det else ""}" data-ep="{html.escape(ep)}" '
f'data-bad="{1 if bad else 0}" tabindex="{0 if det else -1}">'
f'<td class="name">{"<span class=\'caret\'>▸</span>" if det else "<span class=\'caret dim\'>·</span>"}'
f'{html.escape(ep)}'
f'<span class="bar"><i style="width:{calls / peak * 100:.1f}%"></i></span></td>'
f'<td class="r num">{calls:,}</td><td class="r num dim">{e["orgs"]}</td>'
f'<td class="r"><span class="pill {cls}">{okrate * 100:.0f}%</span></td>'
f'<td class="r num">{e["failed"] or ""}</td><td class="r num">{e["req"] or ""}</td>'
f'<td class="r num">{e["res"] or ""}</td><td class="r num dim">{other or ""}</td>'
f'<td class="r num">{usd(e["spend"])}</td>'
f'<td class="r num dim">{e["p50_ms"] or 0}ms</td></tr>')
if det:
out.append(_detail_rows(det, cols))
return (f'<div class="scroll"><table class="big" id="{table_id}"><thead><tr>{head}</tr></thead>'
f'<tbody>{"".join(out)}</tbody></table></div>')
def build_html(data: dict, days: int, top: int, since: dt.datetime, unit: str = "day") -> str:
s = data["summary"][0]
calls = s["calls"] or 0
fail_rate = (s["failed"] or 0) / calls if calls else 0
kpi_cls = "crit" if fail_rate > 0.10 else "warn" if fail_rate > 0.05 else "ok"
bal = next((r["n"] for r in data["refusals"] if r["refused_by"] == "balance"), 0)
kpis = [
("Calls", f"{calls:,}", f"{s['orgs']} orgs · {s['endpoints']} endpoints", ""),
("Spend", usd(s["spend"]), f"{usd((s['spend'] or 0) / max(days, 1))}/day", ""),
("Provider failures", f"{pct(s['failed'], calls)}", f"{s['failed']:,} calls", kpi_cls),
("Refused by treg", f"{pct(s['refused'], calls)}", f"{s['refused']:,} calls", "warn" if s["refused"] else ""),
("Hit the paywall", f"{bal:,}", "balance refusals", "warn" if bal else ""),
("Latency p95", f"{s['p95_ms'] or 0}ms", f"p50 {s['p50_ms'] or 0}ms", ""),
]
kpi_html = "".join(
f'<div class="kpi {c}"><span class="k">{k}</span><span class="v num">{v}</span>'
f'<span class="d num">{d}</span></div>' for k, v, d, c in kpis)
def table(head: str, body: str) -> str:
return f'<div class="scroll"><table><thead><tr>{head}</tr></thead><tbody>{body}</tbody></table></div>'
refusals = table(
'<th>Reason</th><th class="r">Calls</th><th class="r">Orgs</th>',
"".join(f'<tr><td><code>{html.escape(r["refused_by"])}</code></td>'
f'<td class="r num">{r["n"]:,}</td><td class="r num">{r["orgs"]}</td></tr>'
for r in data["refusals"]) or '<tr><td colspan="3" class="dim">Nothing refused.</td></tr>')
failures = table(
'<th>Endpoint</th><th class="r">Status</th><th class="r">Calls</th>',
"".join(f'<tr><td class="name">{html.escape(f["ep"] or "—")}</td>'
f'<td class="r num">{f["status_code"]}</td><td class="r num">{f["n"]:,}</td></tr>'
for f in data["failures"][:14]) or '<tr><td colspan="3" class="dim">No provider failures.</td></tr>')
providers = table(
'<th>Provider</th><th class="r">Calls</th><th class="r">Orgs</th><th class="r">Failed</th>'
'<th class="r">Refused</th><th class="r">Spend</th>',
"".join(f'<tr><td>{html.escape(p["prov"] or "—")}</td><td class="r num">{p["calls"]:,}</td>'
f'<td class="r num">{p["orgs"]}</td><td class="r num">{p["failed"] or ""}</td>'
f'<td class="r num">{p["refused"] or ""}</td><td class="r num">{usd(p["spend"])}</td></tr>'
for p in data["providers"][:20]))
orgs = table(
'<th>Org</th><th class="r">Calls</th><th class="r">Endpoints</th><th class="r">Failed</th>'
'<th class="r">Refused</th><th class="r">Spend</th>',
"".join(f'<tr><td>{html.escape(o["slug"] or ("org " + str(o["org_id"])))}</td>'
f'<td class="r num">{o["calls"]:,}</td><td class="r num">{o["endpoints"]}</td>'
f'<td class="r num">{o["failed"] or ""}</td><td class="r num">{o["refused"] or ""}</td>'
f'<td class="r num">{usd(o["spend"])}</td></tr>'
for o in data["orgs"][:15]))
tiers = table(
'<th>Credential</th><th class="r">Calls</th><th class="r">Orgs</th><th class="r">Spend</th>',
"".join(f'<tr><td><code>{html.escape(t["tier"])}</code></td><td class="r num">{t["n"]:,}</td>'
f'<td class="r num">{t["orgs"]}</td><td class="r num">{usd(t["spend"])}</td></tr>'
for t in data["tiers"]))
clients = table(
'<th>Agent</th><th class="r">Calls</th><th class="r">Orgs</th>',
"".join(f'<tr><td>{html.escape(c["client"])}</td><td class="r num">{c["n"]:,}</td>'
f'<td class="r num">{c["orgs"]}</td></tr>' for c in data["clients"][:10]))
reqs = data.get("requests") or []
req_new = sum(1 for r in reqs if r["in_window"])
req_new_label = f" · {req_new} new" if req_new else ""
req_anon = sum(1 for r in reqs if not (r["user_email"] or r["contact"]))
def request_row(r: dict) -> str:
who = r["user_email"] or r["contact"]
who_cell = html.escape(who) if who else '<span class="dim">anonymous</span>'
detail = r["note"] or r["query"]
detail_html = f'<div class="dim">{html.escape(short(detail, 260))}</div>' if detail else ""
return (f'<tr class="{"fresh" if r["in_window"] else ""}">'
f'<td class="num">{r["created_at"]:%Y-%m-%d %H:%M}</td>'
f'<td><code>{html.escape(r["source"] or "?")}</code></td>'
f'<td>{who_cell}</td>'
f'<td class="name">{html.escape(r["capability"] or "—")}{detail_html}</td></tr>')
requests_html = table(
'<th>Filed</th><th>Source</th><th>Who</th><th>Asked for</th>',
"".join(request_row(r) for r in reqs)
or '<tr><td colspan="4" class="dim">No open requests.</td></tr>')
misses = data.get("search_misses") or []
miss_total = sum(m["n"] for m in misses)
misses_html = table(
'<th class="r">Misses</th><th>Source</th><th>Query</th><th class="r">Last seen</th>',
"".join(f'<tr><td class="r num">{m["n"]:,}</td><td><code>{html.escape(m["sources"])}</code></td>'
f'<td class="name">{html.escape(m["query"])}</td>'
f'<td class="r num dim">{m["last_seen"]:%Y-%m-%d %H:%M}</td></tr>'
for m in misses)
or '<tr><td colspan="4" class="dim">No empty searches in this window '
'(or the search-miss logging has not deployed yet).</td></tr>')
details: dict[tuple[str, bool], list[dict]] = {}
for d in data["errdetail"]:
details.setdefault((d["ep"], d["cataloged"]), []).append(d)
cataloged = [e for e in data["endpoints"] if e["cataloged"]]
other = [e for e in data["endpoints"] if not e["cataloged"]]
endpoints = endpoint_table(cataloged, details, first_col="Endpoint", table_id="t-cat")
noncatalog = endpoint_table(other, details, table_id="t-non",
first_col="Tool name / what the caller sent")
def toolbar(target: str, n: int) -> str:
return (f'<div class="toolbar" data-filters="{target}">'
f'<input type="search" placeholder="Filter {n} rows by name…" aria-label="Filter rows"/>'
f'<label><input type="checkbox"/> only rows with errors</label>'
f'<span class="count"></span></div>')
generated = dt.datetime.now().strftime("%Y-%m-%d %H:%M")
# The DOCTYPE is load-bearing, not boilerplate: without it Chrome renders in QUIRKS MODE, where
# <table> does not inherit `color` from its ancestors. Every table cell then falls back to the
# light-theme ink and the whole report is invisible dark-on-dark for a dark-mode reader. Verified
# in a browser — the bug is silent in light mode, so it survives any amount of source review.
return f"""<!doctype html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>treg Call Ledger</title>
<style>{CSS}</style>
</head><body>
<div class="wrap">
<header>
<div><h1>treg call ledger</h1>
<div class="sub num">{since:%Y-%m-%d %H:%M} → now · {days} day{"s" if days != 1 else ""}</div></div>
<div class="sub num">generated {generated}</div>
</header>
<div class="kpis">{kpi_html}</div>
<div class="card"><h2>Volume by {unit}</h2>{svg_stacked(data["daily"], unit)}</div>
<div class="card"><h2>Tool requests — all {len(reqs)} open{req_new_label}</h2>
<p class="sub" style="margin:-4px 0 12px">"The catalog doesn't have X", filed from the web page,
the CLI, or by an agent mid-search over MCP. <b>New rows in this window are highlighted.</b>
Every row here stays until someone flips its <code>status</code> by hand — this is a queue, not a
feed, so it is listed in full regardless of the report window.<br/>
<b>Read them against the catalog before building anything.</b> A request for something treg
already serves is a <i>discovery</i> failure wearing a coverage failure's clothes, and shipping
the endpoint again would not have helped the person who asked.<br/>
{req_anon} of {len(reqs)} carry neither an email nor a contact — filing is deliberately
anonymous-friendly, so those asks cannot be answered even when we build the thing.</p>
{requests_html}</div>
<div class="card"><h2>Searches that matched nothing — {len(misses)} distinct
<span class="dim">· {miss_total} misses</span></h2>
<p class="sub" style="margin:-4px 0 12px">Every catalog search in this window that returned
zero results, from the API/CLI route and from agents over MCP. This is the demand signal one
step <b>before</b> the tool-request queue above — most agents that miss never file, so the
query text is all they leave behind.<br/>
<b>Read each against the catalog before adding anything:</b> a miss for something treg already
serves is a naming/discovery failure — fix the endpoint's words, not the coverage.</p>
{misses_html}</div>
<div class="card"><h2>Catalog endpoints — all {len(cataloged)}</h2>
<p class="sub" style="margin:-4px 0 12px">Calls that resolved to a catalog endpoint. The three
failure columns are separated because each has a different owner:<br/>
<b>Provider said no</b> — treg passed the call on and the provider rejected it. Their end.<br/>
<b>Bad request</b> — wrong HTTP method, or a missing/invalid parameter. treg stopped it; nothing
was sent and nothing was charged. <span class="dim">(stored as <code>request</code>)</span><br/>
<b>Not found</b> — no such endpoint, or it exists but treg has no key to call it with. Also
stopped before sending. <span class="dim">(stored as <code>resolution</code>)</span><br/>
<b>Out of credit / capped</b> — the org ran out of balance or hit its own cap. Not a bug; this is
demand you did not serve.<br/>
Click any row with errors for the status codes and an example request.</p>
{toolbar("t-cat", len(cataloged))}{endpoints}</div>
<div class="card"><h2>Not a catalog endpoint — all {len(other)}</h2>
<p class="sub" style="margin:-4px 0 12px">Everything absent from the table above, because no
<code>endpoint_id</code> was attached. Two populations live here: <b>tools a team registered
itself</b> (real hosts — google-ads, search-console, render, vercel), and <b>calls treg could not
make sense of</b> — a bare catalog id sent where <code>/call/</code> wants the full upstream URL,
or <code>https:</code> written with one slash. A high <b>Not found</b> count is the second kind.</p>
{toolbar("t-non", len(other))}{noncatalog}</div>
<div class="cols">
<div class="card"><h2>Why treg said no</h2>{refusals}
<p class="sub" style="margin:12px 0 0">These never reached a provider and cost nothing.
<code>balance</code> is demand that arrived and was turned away.</p></div>
<div class="card"><h2>Provider failures</h2>{failures}</div>
</div>
<div class="card"><h2>Providers</h2>{providers}</div>
<div class="cols">
<div class="card"><h2>Whose credential paid</h2>{tiers}</div>
<div class="card"><h2>Which agent called</h2>{clients}</div>
</div>
<div class="card"><h2>Busiest orgs</h2>{orgs}</div>
<footer>
<b>Stopped by treg</b> = refused before anything went upstream (bad request, not found, out of
credit, capped, bad token, blocked) — costs nothing and is not a provider fault.
<b>Provider said no</b> = the call went upstream and came back 4xx/5xx.
<b>spend</b> = <code>cost_charged_micro</code>, what actually hit the org's balance — not the
reserved estimate.<br>
Generated by <code>scripts/usage_report.py</code> from the production database.
</footer>
</div>
<script>{JS}</script>
</body></html>"""
# ---- entry point ---------------------------------------------------------------------------------
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--days", type=int, default=1, help="window size in days (default 1)")
ap.add_argument("--top", type=int, default=25, help="endpoints to list (default 25)")
ap.add_argument("--html", metavar="PATH",
help="write the dashboard here (default reports/usage-<date>.html; '-' to skip)")
ap.add_argument("--json", action="store_true", help="dump the raw aggregates to stdout instead")
args = ap.parse_args()
days = max(1, args.days)
# Naive UTC — the convention every timestamp column in this app stores (see reconcile.window_start).
since = dt.datetime.now(dt.UTC).replace(tzinfo=None) - dt.timedelta(days=days)
unit = "hour" if days <= 2 else "day" # a one-day report wants the day's shape, not one bar
data = asyncio.run(collect(since, unit))
if args.json:
print(json.dumps(data, indent=2, default=str))
return
print_summary(data, days, args.top)
if args.html == "-":
return
# The window goes in the FILENAME. Without it a --days 1 and a --days 7 run on the same date
# write to the same path, and the second silently replaces the first — two very different
# reports, one name, no way to tell them apart afterwards.
out = Path(args.html) if args.html else REPO / "reports" / f"usage-{dt.date.today()}-{days}d.html"
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(build_html(data, days, args.top, since, unit), encoding="utf-8")
print(f"\nwrote {out}")
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1388,7 +1388,7 @@ async def _note_capacity_recovery(mk: MarketplaceCall) -> None:
async def _record_first_call(org_id: int) -> None:
"""Set Org.first_call_at once — the metric that decides whether a marketing channel is real (see
marketing/landing/_measurement.md). A CONDITIONAL UPDATE, not read-then-write: concurrent first
docs/context/architecture/ads-conversions.md). A CONDITIONAL UPDATE, not read-then-write: concurrent first
calls would both see NULL and both fire. Set for EVERY org (it is a product metric in its own
right); adsconv.queue() itself no-ops for orgs with no ad_gclid, so the conversion side stays
ad-attributed-only.
+27
View File
@@ -241,3 +241,30 @@ async def test_admin_credit_org_missing_ref_or_reason_is_400(c):
})
assert r.status_code == 400
assert "reason" in r.json()["detail"]
async def test_manual_grant_uses_configured_database_without_cloud_credentials(c, monkeypatch):
"""The standalone tool still grants once after removing hosted connection helpers."""
import importlib.util
from pathlib import Path
from types import SimpleNamespace
path = Path(__file__).parents[1] / "scripts/manual_grant.py"
spec = importlib.util.spec_from_file_location("manual_grant_test", path)
script = importlib.util.module_from_spec(spec)
spec.loader.exec_module(script)
monkeypatch.delenv("RENDER_API_KEY", raising=False)
_, org, *_ = await _seed(c)
args = SimpleNamespace(email="a@x.dev", org_id=org["org_id"], amount_usd="1.25",
ref="maintenance-test", reason="test", by="test", confirm=False)
assert await script.run(args) == 0
args.confirm = True
assert await script.run(args) == 0
assert await script.run(args) == 1
async with session_maker() as db:
entries = (await db.execute(select(LedgerEntry).where(
LedgerEntry.org_id == org["org_id"], LedgerEntry.kind == "grant",
))).scalars().all()
credits = [entry for entry in entries if entry.meta.get("ref") == "maintenance-test"]
assert len(credits) == 1
assert credits[0].amount_micro == 1_250_000
+37
View File
@@ -0,0 +1,37 @@
"""Pricing imports must never silently lose their private evidence."""
import importlib.util
import json
from pathlib import Path
import pytest
@pytest.fixture
def ingest():
path = Path(__file__).parents[1] / "scripts/catalog_ingest.py"
spec = importlib.util.spec_from_file_location("catalog_evidence_ingest", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_missing_evidence_directory_fails(ingest, monkeypatch):
monkeypatch.delenv("TREG_CATALOG_EVIDENCE_DIR", raising=False)
with pytest.raises(ValueError, match="TREG_CATALOG_EVIDENCE_DIR"):
ingest._anyapi_measured()
def test_missing_snapshot_fails(ingest, monkeypatch, tmp_path):
monkeypatch.setenv("TREG_CATALOG_EVIDENCE_DIR", str(tmp_path))
with pytest.raises(FileNotFoundError, match="Missing pricing evidence"):
ingest._anyapi_measured()
def test_supplied_snapshot_preserves_prices_and_window(ingest, monkeypatch, tmp_path):
monkeypatch.setenv("TREG_CATALOG_EVIDENCE_DIR", str(tmp_path))
prices = {"example.search": {"calls": 8, "p90_usd": 0.04}}
(tmp_path / "anyapi_measured_charges.json").write_text(json.dumps({
"skus": prices, "as_of": "2025-01-01", "window_days": 30,
}))
assert ingest._anyapi_measured() == prices
assert ingest._anyapi_measured_window() == ("2025-01-01", 30)