thoughtleaders-cli 0.6.1__tar.gz → 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/.claude-plugin/plugin.json +1 -1
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/PKG-INFO +7 -4
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/README.md +6 -3
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/commands/tl.md +2 -2
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/docs/architecture.md +4 -4
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/pyproject.toml +1 -1
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/skills/tl/SKILL.md +5 -6
- thoughtleaders_cli-0.6.3/skills/tl/references/business-glossary.md +243 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/skills/tl/references/postgres-schema.md +42 -5
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/__init__.py +1 -1
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/channels.py +1 -45
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/uv.lock +1 -1
- thoughtleaders_cli-0.6.1/skills/tl/references/business-glossary.md +0 -159
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/.claude-plugin/marketplace.json +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/.github/workflows/python-publish.yml +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/.gitignore +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/AGENTS.md +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/CLAUDE.md +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/LICENSE +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/agents/tl-analyst.md +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/commands/tl-balance.md +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/commands/tl-reports.md +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/commands/tl-sponsorships.md +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/hooks/hooks.json +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/hooks/scripts/post-usage.sh +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/hooks/scripts/pre-check.sh +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/skills/tl/references/elasticsearch-schema.md +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/skills/tl/references/firebolt-schema.md +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/_completions.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/auth/__init__.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/auth/commands.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/auth/login.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/auth/pkce.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/auth/token_store.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/client/__init__.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/client/errors.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/client/http.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/__init__.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/ask.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/balance.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/brands.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/changelog.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/comments.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/db.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/deals.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/describe.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/doctor.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/matches.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/proposals.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/recommender.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/reports.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/schema.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/setup.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/snapshots.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/sponsorships.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/uploads.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/commands/whoami.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/config.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/filters.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/hints.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/main.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/output/__init__.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/output/formatter.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/src/tl_cli/self_update.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/tests/__init__.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/tests/test_auth.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/tests/test_filters.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/tests/test_output.py +0 -0
- {thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/tests/test_sponsorships.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: thoughtleaders-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence
|
|
5
5
|
Project-URL: Homepage, https://thoughtleaders.io
|
|
6
6
|
Project-URL: Repository, https://github.com/ThoughtLeaders-io/thoughtleaders-cli
|
|
@@ -83,14 +83,17 @@ tl uploads show 1174310:0BehkmVa7ak
|
|
|
83
83
|
|
|
84
84
|
# Search channels via raw SQL — `tl db pg` against thoughtleaders_channel
|
|
85
85
|
# (run `tl schema pg` once to confirm the live column set).
|
|
86
|
+
# NOTE: For topic / category discovery, prefer the vector recommender over
|
|
87
|
+
# `content_category` equality — `tl recommender top-channels "<tag>"`
|
|
88
|
+
# returns channels ranked by how strongly they load on the topic, not just
|
|
89
|
+
# rows where the single category code matches exactly.
|
|
86
90
|
tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel
|
|
87
91
|
WHERE content_category = <COOKING_CODE> AND total_views >= 100000
|
|
88
92
|
ORDER BY total_views DESC LIMIT 50 OFFSET 0"
|
|
89
93
|
tl db pg "SELECT id, channel_name FROM thoughtleaders_channel
|
|
90
94
|
WHERE is_tl_channel = TRUE LIMIT 200 OFFSET 0" # all TPP channels (~169)
|
|
91
|
-
# MSN status
|
|
92
|
-
#
|
|
93
|
-
# structured filter is the right tool: `tl channels list msn:yes|no`.
|
|
95
|
+
# MSN status: filter on `media_selling_network_join_date IS [NOT] NULL`
|
|
96
|
+
# in the same raw SQL query (column is scrubbed from advertiser sandboxes).
|
|
94
97
|
|
|
95
98
|
# Show channel detail — accepts numeric ID or channel name.
|
|
96
99
|
# Names that match more than one active channel print a candidate list
|
|
@@ -56,14 +56,17 @@ tl uploads show 1174310:0BehkmVa7ak
|
|
|
56
56
|
|
|
57
57
|
# Search channels via raw SQL — `tl db pg` against thoughtleaders_channel
|
|
58
58
|
# (run `tl schema pg` once to confirm the live column set).
|
|
59
|
+
# NOTE: For topic / category discovery, prefer the vector recommender over
|
|
60
|
+
# `content_category` equality — `tl recommender top-channels "<tag>"`
|
|
61
|
+
# returns channels ranked by how strongly they load on the topic, not just
|
|
62
|
+
# rows where the single category code matches exactly.
|
|
59
63
|
tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel
|
|
60
64
|
WHERE content_category = <COOKING_CODE> AND total_views >= 100000
|
|
61
65
|
ORDER BY total_views DESC LIMIT 50 OFFSET 0"
|
|
62
66
|
tl db pg "SELECT id, channel_name FROM thoughtleaders_channel
|
|
63
67
|
WHERE is_tl_channel = TRUE LIMIT 200 OFFSET 0" # all TPP channels (~169)
|
|
64
|
-
# MSN status
|
|
65
|
-
#
|
|
66
|
-
# structured filter is the right tool: `tl channels list msn:yes|no`.
|
|
68
|
+
# MSN status: filter on `media_selling_network_join_date IS [NOT] NULL`
|
|
69
|
+
# in the same raw SQL query (column is scrubbed from advertiser sandboxes).
|
|
67
70
|
|
|
68
71
|
# Show channel detail — accepts numeric ID or channel name.
|
|
69
72
|
# Names that match more than one active channel print a candidate list
|
|
@@ -18,8 +18,8 @@ The user wants to query ThoughtLeaders data. Translate their request into the ri
|
|
|
18
18
|
## Examples
|
|
19
19
|
|
|
20
20
|
- "/tl sold sponsorships for Nike in Q1" → `tl sponsorships list status:sold brand:"Nike" purchase-date-start:2026-01-01 purchase-date-end:2026-03-31`
|
|
21
|
-
- "/tl cooking channels over 100k subs" → `tl
|
|
22
|
-
- "/tl mobile-first US cooking channels" → `tl
|
|
21
|
+
- "/tl cooking channels over 100k subs" → `tl recommender top-channels "cooking" --limit 50` (then post-filter by `subscribers >= 100000` on the resulting IDs)
|
|
22
|
+
- "/tl mobile-first US cooking channels" → `tl recommender top-channels "cooking" --limit 100` (then narrow by `demographic_device_primary = 'mobile'` / `demographic_usa_share >= 50` with raw SQL on the resulting IDs)
|
|
23
23
|
- "/tl Nike's sponsorship activity" → `tl brands show Nike`
|
|
24
24
|
- "/tl run my Q1 report" → `tl reports --json` then `tl reports run <id>`
|
|
25
25
|
- "/tl check my balance" → `tl balance`
|
|
@@ -48,7 +48,6 @@ All data commands use explicit subcommands: `list`, `show`, `create`/`add`. Runn
|
|
|
48
48
|
| `tl proposals create --channel <id> --brand <id>` | Create a proposal (free) |
|
|
49
49
|
| `tl uploads list [filters...]` | List video uploads (ES) |
|
|
50
50
|
| `tl uploads show <id> [<id>...]` | Show upload detail(s) by ID |
|
|
51
|
-
| `tl channels list [filters...]` | Search channels. Responses carry boolean `msn` (Media Selling Network) and `tpp` (TL-managed) fields; filterable via `msn:` / `tpp:` tri-state (`yes` / `no` / `both`, default `both`). |
|
|
52
51
|
| `tl channels show <id-or-name>` | Channel detail, including active adspots with price/cost/CPM |
|
|
53
52
|
| `tl channels history <id-or-name>` | Sponsorship history (videos with detected sponsors) |
|
|
54
53
|
| `tl channels similar <id-or-name>` | Vector-similarity recommender. 50 credits; Intelligence plan. Tri-state `msn:` (default `yes`) and `tpp:` (default `both`) filters. Ambiguous names return 400 + candidates list. Hidden `look-alike` alias. |
|
|
@@ -75,8 +74,10 @@ Filters are passed as `key:value` pairs after `list`:
|
|
|
75
74
|
tl sponsorships list status:sold brand:"Nike" purchase-date:2026-01
|
|
76
75
|
tl sponsorships list status:pending send-date:2026-03
|
|
77
76
|
tl uploads list channel:12345 type:longform since:2026-03
|
|
78
|
-
# Channel discovery is raw SQL —
|
|
79
|
-
#
|
|
77
|
+
# Channel discovery is raw SQL — default to:
|
|
78
|
+
# (For topic/category lookups, `tl recommender top-channels "<tag>"` is a
|
|
79
|
+
# better starting point than `content_category` equality — it ranks by
|
|
80
|
+
# how strongly each channel loads on the topic, not exact-match.)
|
|
80
81
|
tl db pg "SELECT id, channel_name, total_views FROM thoughtleaders_channel
|
|
81
82
|
WHERE content_category = <COOKING_CODE> AND language = 'en'
|
|
82
83
|
AND total_views >= 1000000
|
|
@@ -387,7 +388,6 @@ Most CLI endpoints can reuse existing views/utilities rather than being built fr
|
|
|
387
388
|
| **`POST /api/cli/v1/sponsorships`** | `api/create-bulk-proposal` (`CreateBulkProposalView`) + MCP `save_proposals_for_email` in `mcp/proposals.py` | Adapt for single-proposal creation from CLI params |
|
|
388
389
|
| **`GET /api/cli/v1/uploads`** | `api/articles` (`ArticlesView`) — full ES article search with configurable columns, filters, aggregation. Already handles channel format, brand filters, content type. | Translate CLI filters → ArticlesView params. Restrict to VIDEO format. |
|
|
389
390
|
| **`GET /api/cli/v1/uploads/<id>`** | `api/articles/<id>` (`SingleArticleView`) | Add CLI envelope |
|
|
390
|
-
| **`GET /api/cli/v1/channels`** | `api/v2/external-youtube-thoughtleaders` (`ExternalYoutubeThoughtleadersView`) — rich ES-powered channel search with configurable columns, aggregation. Also `api/v1/channels/dropdown` for simple name search. | Translate CLI filters → existing view params |
|
|
391
391
|
| **`GET /api/cli/v1/channels/<id>`** | `api/v1/channels/<id>` (`ChannelAPIViewSet.retrieve`) — returns channel detail with loyalty metrics | Add CLI envelope + breadcrumbs linking to snapshots |
|
|
392
392
|
| **`GET /api/cli/v1/brands/<query>`** | `api/v1/brands` (`BrandsViewSet`) for brand lookup + `api/articles` with `sponsored_brand_mentions` filter for intelligence data. Also `api/brand/<id>/matcher` (`BrandChannelMatcherAPI`) for channel discovery. | Combine brand lookup + ES brand mention query |
|
|
393
393
|
| **`GET /api/cli/v1/snapshots/channel/<id>`** | `api/v2/channel-history` (`ChannelHistoryView`) — already queries Firebolt `channel_metrics` with pagination, uses `get_firebolt_query_results()` utility. Also `api/v2/external-channel-total-views` for time-series with granularity. | Direct reuse — just translate params + add CLI envelope |
|
|
@@ -56,7 +56,7 @@ The centre of the data model is **Sponsorships** — business relationships betw
|
|
|
56
56
|
- **Proposals** — matches that have been proposed to both sides to consider
|
|
57
57
|
- **Deals** — contractually agreed-upon sponsorships (sold), either in production or published
|
|
58
58
|
|
|
59
|
-
Sponsorships are sometimes called "Ads" or "Ad campaigns".
|
|
59
|
+
Sponsorships are sometimes called "Ads" or "Ad campaigns". **"AdLink"** is another name for the same thing — it's the term the database uses (`thoughtleaders_adlink`) and shows up across internal code, schema docs, and AM Slack threads. Treat "sponsorship" and "adlink" as interchangeable; the user-facing word is "sponsorship," the engineering/DB word is "adlink."
|
|
60
60
|
|
|
61
61
|
The CLI has shortcut commands for each type: `tl matches`, `tl proposals`, `tl deals`. These filter `tl sponsorships` by status.
|
|
62
62
|
|
|
@@ -130,8 +130,7 @@ tl proposals show <id> # Proposal detail (2 credits)
|
|
|
130
130
|
tl proposals create --channel <id> --brand <id> # Create proposal (free)
|
|
131
131
|
tl uploads list [filters...] # Video uploads from ES — list curve, mult 1.0
|
|
132
132
|
tl uploads show <id> # Upload detail (2 credits)
|
|
133
|
-
tl channels
|
|
134
|
-
tl channels show <id-or-name> # Channel detail (2 credits; accepts numeric ID or name)
|
|
133
|
+
tl channels show <id-or-name> # Channel detail (2 credits; accepts numeric ID or name) — for channel search use raw SQL on thoughtleaders_channel
|
|
135
134
|
tl channels history <id-or-name> # Sponsorship history (5 credits/result, linear)
|
|
136
135
|
tl channels similar <id-or-name> # Vector-similarity recommender (50 credits flat; Intelligence plan)
|
|
137
136
|
tl brands show <id-or-name> # Brand detail (1 credit)
|
|
@@ -155,7 +154,7 @@ tl comments add <adlink-id> "msg" # Add comment (free)
|
|
|
155
154
|
|
|
156
155
|
**"List curve"** above means non-linear pricing: `cost = 1 + mult × 0.126 × n^1.2`. The flat 1-credit setup applies to every list call; the `mult` reflects per-resource complexity. `tl db {pg,fb,es}` shares the same curve at mult=1.4. Concrete totals:
|
|
157
156
|
|
|
158
|
-
| Rows | mult=1.0 (
|
|
157
|
+
| Rows | mult=1.0 (comments, uploads, sponsorships) | mult=1.2 (snapshots) | mult=1.3 (reports) | mult=1.4 (db.pg / db.fb / db.es) |
|
|
159
158
|
|---:|---:|---:|---:|---:|
|
|
160
159
|
| 1 | 1 | 1 | 1 | 1 |
|
|
161
160
|
| 10 | 3 | 3 | 4 | 4 |
|
|
@@ -360,7 +359,7 @@ tl db pg "SELECT id, channel_name, demographic_device_primary, total_views
|
|
|
360
359
|
|
|
361
360
|
For per-country share beyond the recommender's "USA share" tag, use the `demographic_geo` jsonb in raw SQL: `(demographic_geo->>'gb')::int >= 25`. Same pattern with `demographic_device->>'mobile'` for non-primary device shares.
|
|
362
361
|
|
|
363
|
-
**MSN status (`media_selling_network_join_date`) is scrubbed from the advertiser sandbox view.** Raw SQL can't filter on it from an advertiser context
|
|
362
|
+
**MSN status (`media_selling_network_join_date`) is scrubbed from the advertiser sandbox view.** Raw SQL can't filter on it from an advertiser context. For MSN-only / non-MSN lookups, run the same raw SQL with `media_selling_network_join_date IS [NOT] NULL` from a context that has access to it (full-access role), or rely on the recommender's MSN-aware filters: `tl recommender top-channels "<tag>" msn:yes|no|all`.
|
|
364
363
|
|
|
365
364
|
### Output flags
|
|
366
365
|
- `--json` — structured JSON (use this for parsing)
|
|
@@ -398,7 +397,7 @@ Every query costs credits. Before running expensive queries:
|
|
|
398
397
|
Users only see data their plan allows:
|
|
399
398
|
- **Media buyers** see deals where their org is the brand. They see `price` but never `cost`.
|
|
400
399
|
- **Media sellers** see deals where their org is the publisher. They see `cost` but never `price`.
|
|
401
|
-
- **Intelligence plan** required for `tl brands`, full `tl
|
|
400
|
+
- **Intelligence plan** required for `tl brands`, the full `tl recommender` surface, and full `tl uploads list`.
|
|
402
401
|
- **Paid plan** required for `tl snapshots`.
|
|
403
402
|
|
|
404
403
|
## Important: Status Labels
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# ThoughtLeaders Business Glossary
|
|
2
|
+
|
|
3
|
+
Maps business terms to database concepts.
|
|
4
|
+
|
|
5
|
+
## Revenue & Deal Lifecycle
|
|
6
|
+
|
|
7
|
+
| Business Term | DB Concept | Notes |
|
|
8
|
+
|--------------|------------|-------|
|
|
9
|
+
| **Revenue / Sold ad** | `adlink.publish_status = 3` (SOLD) | Only status=3 counts as real revenue |
|
|
10
|
+
| **Gross ads / Gross revenue** | `SUM(adlink.price)` where sold | Total advertiser spend |
|
|
11
|
+
| **Net revenue / TL profit** | `price - cost` per adlink | What TL earns as a company |
|
|
12
|
+
| **Cost** | `adlink.cost` | What the channel earns |
|
|
13
|
+
| **Price** | `adlink.price` | What the advertiser pays |
|
|
14
|
+
| **Closed-lost** | `publish_status IN (4, 5, 9)` | All three rejection statuses |
|
|
15
|
+
| **Open opportunity** | `publish_status IN (0, 2, 6, 7, 8)` | Pipeline — not revenue, not lost |
|
|
16
|
+
| **Proposal Approved** | `publish_status = 6` | AM approved to show to brand — NOT brand approval. Internal gate only. |
|
|
17
|
+
| **Pending** | `publish_status = 2` | Brand has agreed — this is the real high-intent signal |
|
|
18
|
+
| **Weighted pipeline** | `SUM(weighted_price)` for open opps | Pre-calculated on save |
|
|
19
|
+
| **Ad is live** | `publish_date IS NOT NULL` | Until publish_date is set, ad is not on YouTube |
|
|
20
|
+
| **Cancellation risk** | Sold but `publish_date IS NULL` | Sold deals without publish_date can still be canceled |
|
|
21
|
+
|
|
22
|
+
## Performance Grade (`adlink.performance_grade`)
|
|
23
|
+
|
|
24
|
+
| Value | Label | Description |
|
|
25
|
+
|-------|-------|-------------|
|
|
26
|
+
| 0 | Pending | Not yet graded (treat as NULL) |
|
|
27
|
+
| 1 | Loser | Underperforming ad — do not renew |
|
|
28
|
+
| 2 | Neutral | Mixed results — test one more time, ideally at a better rate. Second test determines if channel becomes Loser or Winner |
|
|
29
|
+
| 3 | Winner | High-performing ad — should always be renewed |
|
|
30
|
+
|
|
31
|
+
**Renewal logic:** All Winners should be renewed. Neutrals get one more test (ideally at a lower CPM), then reclassified as Winner or Loser.
|
|
32
|
+
|
|
33
|
+
## View Guarantees
|
|
34
|
+
|
|
35
|
+
| Field | Description |
|
|
36
|
+
|-------|-------------|
|
|
37
|
+
| `adlink.impressions_guarantee` | The number of views guaranteed for the ad (bigint). 0 or NULL = no guarantee. |
|
|
38
|
+
| `adlink.view_guarantee_hit_date` | Timestamp when the guarantee was met. NULL = not yet hit or no guarantee. |
|
|
39
|
+
| `adlink.projected_views_at_purchase_date` | Projected views at time of purchase (used for CPM estimation). |
|
|
40
|
+
|
|
41
|
+
## Entities
|
|
42
|
+
|
|
43
|
+
| Business Term | DB Table | Notes |
|
|
44
|
+
|--------------|----------|-------|
|
|
45
|
+
| **Deal / Sponsorship** | `thoughtleaders_adlink` | One brand ↔ channel placement |
|
|
46
|
+
| **Brand** | `thoughtleaders_brand` | Advertiser entity (the buying-side brand) |
|
|
47
|
+
| **Brand profile** | `thoughtleaders_profile` | Advertiser entity / account |
|
|
48
|
+
| **Organization** | `thoughtleaders_organization` | Parent entity for profiles |
|
|
49
|
+
| **Channel** | `thoughtleaders_channel` | YouTube channel |
|
|
50
|
+
| **Ad Spot (Catalogue item)** | `thoughtleaders_adspot` | TL's catalogue of buyable placements. Price/cost on adspot are *list prices* only — each adlink (instance) can have completely different price/cost |
|
|
51
|
+
| **Campaign** | `dashboard_campaign` | Groups multiple deals |
|
|
52
|
+
|
|
53
|
+
## Ad Spots & Channels
|
|
54
|
+
|
|
55
|
+
- A channel can have **multiple ad spots** because different people sell the same channel (talent manager, direct, multiple agencies)
|
|
56
|
+
- Ad spots are the **catalogue** — adlinks are **instances** of catalogue items
|
|
57
|
+
- Price/cost on adspot = list/catalog values; price/cost on adlink = actual deal values
|
|
58
|
+
- **Only one active adspot with integration=mention per channel at any time** (MSN rule)
|
|
59
|
+
|
|
60
|
+
## Ownership & Accountability
|
|
61
|
+
|
|
62
|
+
| Field | Model | Meaning |
|
|
63
|
+
|-------|-------|---------|
|
|
64
|
+
| `owner_sales_id` | `adlink` | **Most important.** Person responsible for closing the deal and for the revenue. Final accountability. |
|
|
65
|
+
| `owner_advertiser_id` | `adlink` | Brand-side owner for this specific deal |
|
|
66
|
+
| `owner_publisher_id` | `adlink` | Channel-side owner for this specific deal |
|
|
67
|
+
| `owner_advertiser_id` | `profile` | **Account owner.** Who owns the brand relationship overall. Often same person as owner_sales on adlinks, but not always. |
|
|
68
|
+
| `owner_publisher_id` | `profile` | Channel relationship owner on the profile level |
|
|
69
|
+
| `owner_sales_id` | `profile` | Sales owner at profile level |
|
|
70
|
+
|
|
71
|
+
**Key insight:** Ownership exists on both `profile` (account-level) and `adlink` (deal-level). For revenue attribution, always use `adlink.owner_sales_id`.
|
|
72
|
+
|
|
73
|
+
## MSN (Media Selling Network)
|
|
74
|
+
|
|
75
|
+
- Channels where TL has **≥80% confidence** they can buy an ad tomorrow
|
|
76
|
+
- Key data: **who is the contact** to buy the ad from
|
|
77
|
+
- `thoughtleaders_channel.media_selling_network_join_date` = when channel joined MSN
|
|
78
|
+
- `thoughtleaders_channel.is_tl_channel` = TPP/VIP channel (subset of MSN)
|
|
79
|
+
- **Rule:** Only one active adspot with `integration=mention` per channel at any time
|
|
80
|
+
- MSN quality depends on having current, accurate contact info
|
|
81
|
+
|
|
82
|
+
## Channels & Audience
|
|
83
|
+
|
|
84
|
+
Vocabulary that AMs use about channels, mapped to the actual DB encoding. Most of these are silent-wrong-name traps where the team term doesn't match the column name.
|
|
85
|
+
|
|
86
|
+
| Business Term | DB Concept | Notes |
|
|
87
|
+
|---------------|------------|-------|
|
|
88
|
+
| **Subscribers** | `thoughtleaders_channel.reach` (bigint) | ⚠️ There is no `subscribers` column. The DB column is `reach`. |
|
|
89
|
+
| **MSN member** | `media_selling_network_join_date IS NOT NULL` | Whole MSN pool. NOT `is_tl_channel = true` — that's the VIP subset only. |
|
|
90
|
+
| **TPP / VIP channel** | `is_tl_channel = true` | The small VIP subset of MSN (~144 channels at 100k+ reach). Don't use as a general "MSN" proxy — silently drops ~98% of MSN. |
|
|
91
|
+
| **Active channel** | `is_active = true AND last_published >= CURRENT_DATE - INTERVAL '120 days'` | Standard filter for "channel is live and posting." Always include `is_active = true` in channel queries. |
|
|
92
|
+
| **Country / Geo of a deal** | `thoughtleaders_channel.country` (ISO 3166-1 alpha-2) | `thoughtleaders_adlink` has NO geo column. Geo for sponsorships almost always means the channel's country. |
|
|
93
|
+
| **Language of a channel** | `thoughtleaders_channel.language` (short ISO 639 code) | ⚠️ Short ISO 639 codes — NOT BCP-47. Mostly 2-letter ISO 639-1 (`en`, `pt`, `hi`) for major languages; occasionally 3-letter ISO 639-2/3 (`arc`, `arz`, `ase`, `ceb`) for languages without a 2-letter code. Filtering with BCP-47 (`en-US`/`pt-BR`) returns zero. Don't assume `LENGTH(language) = 2`. |
|
|
94
|
+
| **Brand on a deal** | `adlink → creator_profile_id → profile_brands.profile_id → profile_brands.brand_id → brand` | 3-table chain. There is NO direct `brand_id` on adlink. See [postgres-schema.md](postgres-schema.md). |
|
|
95
|
+
| **Channel on a deal** | `adlink.ad_spot_id → adspot.channel_id → channel` | NO direct `channel_id` on adlink. |
|
|
96
|
+
| **Brand-virgin / VPN-virgin (etc.)** | Channel has no `adlink` row joined to any of the target brand_ids | Used in candidate sourcing ("never sponsored by any VPN brand"). Caveat: only catches TL-brokered deals; channels that ran the brand directly (no TL involvement) appear "virgin" but aren't — cross-check ES `sponsored_brand_mentions` before final outreach. |
|
|
97
|
+
| **Channel quality score** *(internal-only)* | `sponsorship_score` on the indexed channel doc + `thoughtleaders_channel.sponsorship_score` (PG) | TL-internal composite score combining engagement, fulfillment, and historical sponsorship performance. **Use it internally to rank/tiebreak candidates, but do NOT quote the raw decimal in AM-facing or external output** — the score isn't documented to AMs and the absolute value isn't meaningful without context. In AM-facing prose, translate to qualitative language: "top-quartile fit," "strongest quality score in the candidate set," "high sponsorship-quality signal." |
|
|
98
|
+
|
|
99
|
+
## Projected Views (PV) — three related but distinct fields
|
|
100
|
+
|
|
101
|
+
AMs use "PV" loosely. There are three different DB fields, each meaning something different:
|
|
102
|
+
|
|
103
|
+
| AM Term | DB Field | What it actually is |
|
|
104
|
+
|---------|----------|---------------------|
|
|
105
|
+
| **PV (channel baseline)** | `thoughtleaders_channel.impression` | Channel-level "typical views per video" used as CPM denominator. ⚠️ Coverage and freshness vary; cross-check Firebolt longform median for hero-tier deals. |
|
|
106
|
+
| **PV (deal-specific)** | `thoughtleaders_adlink.projected_views_at_purchase_date` | Snapshot of projected views at the moment the deal was sold. Use this for historical CPM analysis. |
|
|
107
|
+
| **VG (View Guarantee)** | `thoughtleaders_adlink.impressions_guarantee` | The contractual minimum views the brand is guaranteed. 0/NULL = no guarantee. NOT the same as PV — VG is a contractual floor, PV is an estimate. |
|
|
108
|
+
|
|
109
|
+
When an AM says "what's the PV on this channel?" — they almost always mean `channel.impression`. When they say "what was the PV on this deal?" — they mean `adlink.projected_views_at_purchase_date`. When they say "did we hit the VG?" — they mean `adlink.view_guarantee_hit_date IS NOT NULL`.
|
|
110
|
+
|
|
111
|
+
## Channel Sponsorship Signals
|
|
112
|
+
|
|
113
|
+
Two derived metrics on the indexed channel doc that AMs use to qualify a channel before pitching. Both are pre-aggregated in the search index, computed against historical sponsored-content patterns.
|
|
114
|
+
|
|
115
|
+
| AM Term | Underlying field | Definition | What an AM does with it |
|
|
116
|
+
|---------|------------------|------------|--------------------------|
|
|
117
|
+
| **Fulfillment rate** | `fulfillment_rate` (channel doc, scaled_float) | The share of a channel's content that is sponsored — `sponsored / all` content over the measurement window, expressed as a fraction. Higher = the channel reliably delivers paid integrations. | Quality signal: a high fulfillment rate means past brands have actually run on this channel, not just been pitched. AMs use it to filter out "looks promising but never closes" channels. |
|
|
118
|
+
| **Renewal rate** | `renewal_rate` (channel doc, scaled_float) | The rate at which a brand-channel sponsorship relationship repeats over time, computed from clusters of sponsorship deals between a single subject (channel or brand) and its linked entities, with date-distribution heuristics (default 365-day max interval). | Loyalty signal: a high renewal rate means brands keep coming back to this channel. AMs use it to identify "sticky" channels worth premium positioning, and to flag low-renewal channels as one-shots. |
|
|
119
|
+
|
|
120
|
+
Both metrics live on the channel side of the indexed video docs (the `channel.*` nested object). Channel pages in TL's product surface these as quality scores; in AM-facing reports, you can quote them as percentages (`0.45 → "45% renewal rate"`).
|
|
121
|
+
|
|
122
|
+
## Industry Terms vs TL Vocabulary
|
|
123
|
+
|
|
124
|
+
Some MarTech-industry terms are **NOT used at TL.** When you encounter them in a question, prompt, or message — translate to TL-native vocabulary before querying or answering. If you see one of these on the left, swap to the right.
|
|
125
|
+
|
|
126
|
+
| Industry term (don't use) | TL-native equivalent | Notes |
|
|
127
|
+
|---------------------------|----------------------|-------|
|
|
128
|
+
| **Flight** (a single ad-run instance) | **deal** / **sponsorship** / **adlink** | One row in `thoughtleaders_adlink`. "Every flight" → "every deal." |
|
|
129
|
+
| **Flight** (a campaign time window) | **window** / **campaign window** | "60-day flight" → "60-day window." Or just specify dates. |
|
|
130
|
+
| **Flight cadence** | **deal cadence** / **renewal cadence** | How often deals run on a channel. |
|
|
131
|
+
| **Flight-over-flight** | **deal-over-deal** / **renewal-over-renewal** | Comparing successive deals on the same channel. |
|
|
132
|
+
| **Post-flight** | **post-publish** / **post-deal** | After the deal/ad has run. |
|
|
133
|
+
|
|
134
|
+
**General rule:** if you reach for an ad-industry term that isn't already in this glossary, check whether TL actually uses it before introducing it.
|
|
135
|
+
|
|
136
|
+
## Infrastructure Terms — Translate Before User-Facing Output
|
|
137
|
+
|
|
138
|
+
AMs and brand-facing readers don't know (or care) what Postgres, Elasticsearch, or Firebolt are. **When surfacing data in any AM-facing or external output, translate infrastructure names and column names to business language.** Internal engineering chats are the only place these raw terms belong.
|
|
139
|
+
|
|
140
|
+
| Internal term (don't use in AM/external output) | AM-friendly translation | What it actually is |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| **PG** / **Postgres** / `thoughtleaders_*` table names / `tl db pg` invocations | **"our deals data"** / **"TL's deals book"** / **"our pipeline"** | The relational DB that holds deals, brands, channels, profiles — the source of truth for what TL has brokered |
|
|
143
|
+
| **ES** / **Elasticsearch** / `tl-platform-*` indices / `tl db es` invocations | **"TL's YouTube content tracking"** / **"our video index"** / **"our scraped video data"** | The search index that holds tracked YouTube videos, brand mentions, transcripts — the wider market view |
|
|
144
|
+
| **Firebolt** / `article_metrics` / `channel_metrics` / `tl db fb` invocations | **"historical metrics"** / **"view-curve data"** | The data warehouse that stores time-series snapshots — used for trend analysis |
|
|
145
|
+
| `sponsored_brand_mentions` (ES field) | **"tracked sponsorships"** / **"logged sponsored mentions"** / **"sponsored videos we tracked"** | Per-video brand-ID tags showing which brands paid for that video |
|
|
146
|
+
| `organic_brand_mentions` (ES field) | **"organic / unpaid mentions"** | Brand mentioned in a video without a paid sponsorship |
|
|
147
|
+
| `publish_status = 3` | **"sold"** | Already in glossary; never write the integer to AMs |
|
|
148
|
+
| `creator_profile_id` chain / 3-table join | **"the brand's record"** | Engineering plumbing for brand lookup; AMs just hear "the brand" |
|
|
149
|
+
| `reach` (PG column) | **"subscribers"** | Already in glossary; AMs say subscribers, SQL says reach |
|
|
150
|
+
| `impression` (PG column) | **"projected views"** / **"PV"** | Already in glossary; flagged for reliability caveats |
|
|
151
|
+
|
|
152
|
+
### Common translations in context
|
|
153
|
+
|
|
154
|
+
When you'd write internally → write this for an AM:
|
|
155
|
+
|
|
156
|
+
| Internal phrasing | AM-friendly phrasing |
|
|
157
|
+
|---|---|
|
|
158
|
+
| "Pulling ES `sponsored_brand_mentions` for the market view" | "Cross-checking against our video tracking data" |
|
|
159
|
+
| "ES has 11,304 sponsored mentions of Robinhood" | "Robinhood ran 11,304 sponsored videos in the last 12 months" |
|
|
160
|
+
| "PG has 139 sold adlinks for brand_id=29332" | "We sold Investing.com 139 sponsorships" |
|
|
161
|
+
| "Firebolt `article_metrics` 180-day longform median" | "Recent typical-video views over the last 6 months" |
|
|
162
|
+
| "Filter on `publish_status=3` AND `purchase_date>=2025-01-01`" | "Sold deals since the start of 2025" |
|
|
163
|
+
|
|
164
|
+
**General rule:** if a sentence mentions `tl db pg|fb|es` invocations, table names, column names, or integer codes, ask yourself — *would the AM Slack me a question using this language?* If no, translate. The infrastructure exists to serve the AM; the AM shouldn't have to know it exists.
|
|
165
|
+
|
|
166
|
+
## Teams & Ownership
|
|
167
|
+
|
|
168
|
+
### Brand-led Revenue (Sales / Account Management)
|
|
169
|
+
|
|
170
|
+
These teams close deals and manage brand relationships. Revenue is attributed via `adlink.owner_sales_id`.
|
|
171
|
+
|
|
172
|
+
**Emma's team:**
|
|
173
|
+
| Person | auth_user.id | Owner field |
|
|
174
|
+
|--------|-------------|-------------|
|
|
175
|
+
| Emma | 11158 | `adlink.owner_sales_id` |
|
|
176
|
+
| Orli | 2042 | `adlink.owner_sales_id` |
|
|
177
|
+
| Eli | 20836 | `adlink.owner_sales_id` |
|
|
178
|
+
| Grace | 20835 | `adlink.owner_sales_id` |
|
|
179
|
+
| Mark | 23979 | `adlink.owner_sales_id` |
|
|
180
|
+
| Abbie | 23978 | `adlink.owner_sales_id` |
|
|
181
|
+
| Ariella | 23977 | `adlink.owner_sales_id` |
|
|
182
|
+
|
|
183
|
+
**Nicole's team:**
|
|
184
|
+
| Person | auth_user.id | Owner field |
|
|
185
|
+
|--------|-------------|-------------|
|
|
186
|
+
| Nicole | 9929 | `adlink.owner_sales_id` |
|
|
187
|
+
| Maika | 5412 | `adlink.owner_sales_id` |
|
|
188
|
+
| Yuval | 14252 | `adlink.owner_sales_id` |
|
|
189
|
+
| Revital | 14251 | `adlink.owner_sales_id` |
|
|
190
|
+
|
|
191
|
+
### Network Growth (SDR / Partnerships) — Pauline's team
|
|
192
|
+
|
|
193
|
+
Responsible for growing the MSN (new channels) and MBN (new brands). SDR outreach on both sides.
|
|
194
|
+
|
|
195
|
+
| Person | auth_user.id | Role | Owner field |
|
|
196
|
+
|--------|-------------|------|-------------|
|
|
197
|
+
| Pauline | 218 | Team lead | `adlink.owner_publisher_id` (channel handovers) |
|
|
198
|
+
| Morgan | 5710 | Channel SDR | — |
|
|
199
|
+
| Jen | 873 | Channel SDR | — |
|
|
200
|
+
| Ruby Jean | 9011 | Channel SDR | — |
|
|
201
|
+
| Molly | 11361 | Channel SDR | — |
|
|
202
|
+
| Pierra | 11323 | Brand SDR | — |
|
|
203
|
+
| Nian | 8795 | Brand SDR | — |
|
|
204
|
+
|
|
205
|
+
### Ad Ops — Jody's team
|
|
206
|
+
|
|
207
|
+
Manages getting sold ads published. `profile.owner_publisher_id` = ad ops manager of an account.
|
|
208
|
+
|
|
209
|
+
| Person | auth_user.id | Owner field |
|
|
210
|
+
|--------|-------------|-------------|
|
|
211
|
+
| Jody | 71 | `profile.owner_publisher_id` (account-level ad ops owner) |
|
|
212
|
+
| Kathleen | 9274 | `profile.owner_publisher_id` |
|
|
213
|
+
| Shane | 18159 | `profile.owner_publisher_id` |
|
|
214
|
+
| Kevin | 5799 | `profile.owner_publisher_id` |
|
|
215
|
+
| Airis | 5804 | `profile.owner_publisher_id` |
|
|
216
|
+
| Lara | 10743 | `profile.owner_publisher_id` |
|
|
217
|
+
| Josh | 11592 | `profile.owner_publisher_id` |
|
|
218
|
+
|
|
219
|
+
### Querying by team
|
|
220
|
+
|
|
221
|
+
```sql
|
|
222
|
+
-- Emma's team pipeline
|
|
223
|
+
SELECT ... FROM thoughtleaders_adlink al
|
|
224
|
+
WHERE al.owner_sales_id IN (11158, 2042, 20836, 20835, 23979, 23978, 23977)
|
|
225
|
+
|
|
226
|
+
-- Nicole's team pipeline
|
|
227
|
+
SELECT ... FROM thoughtleaders_adlink al
|
|
228
|
+
WHERE al.owner_sales_id IN (9929, 5412, 14252, 14251)
|
|
229
|
+
|
|
230
|
+
-- All brand-led revenue (both teams)
|
|
231
|
+
SELECT ... FROM thoughtleaders_adlink al
|
|
232
|
+
WHERE al.owner_sales_id IN (11158, 2042, 20836, 20835, 23979, 23978, 23977, 9929, 5412, 14252, 14251)
|
|
233
|
+
|
|
234
|
+
-- Pauline's network growth team (channel SDRs)
|
|
235
|
+
-- owner_publisher_id on adlink for channel-side work
|
|
236
|
+
SELECT ... FROM thoughtleaders_adlink al
|
|
237
|
+
WHERE al.owner_publisher_id IN (218, 5710, 873, 9011, 11361)
|
|
238
|
+
|
|
239
|
+
-- Jody's ad ops team accounts (profile-level ownership)
|
|
240
|
+
SELECT ... FROM thoughtleaders_profile p
|
|
241
|
+
WHERE p.owner_publisher_id IN (71, 9274, 18159, 5799, 5804, 10743, 11592)
|
|
242
|
+
```
|
|
243
|
+
|
{thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/skills/tl/references/postgres-schema.md
RENAMED
|
@@ -42,7 +42,7 @@ The main deals table. Each row = one sponsorship deal between a brand and a YouT
|
|
|
42
42
|
| `weighted_price_currency` | varchar | Always USD |
|
|
43
43
|
| `cost` | numeric | Cost to TL |
|
|
44
44
|
| `ad_spot_id` | int FK | → `thoughtleaders_adspot.id` |
|
|
45
|
-
| `creator_profile_id` | int FK | → brand/advertiser profile |
|
|
45
|
+
| `creator_profile_id` | int FK | → `thoughtleaders_profile.id` (the brand/advertiser's profile). ⚠️ The table is named `thoughtleaders_profile`, NOT `creator_profile` — the "creator_" prefix lives on the FK column, not the table. |
|
|
46
46
|
| `owner_advertiser_id` | int FK | → `auth_user.id` (brand-side owner) |
|
|
47
47
|
| `owner_publisher_id` | int FK | → `auth_user.id` (channel-side owner) |
|
|
48
48
|
| `owner_sales_id` | int FK | → `auth_user.id` (sales rep) |
|
|
@@ -125,13 +125,50 @@ A channel can have multiple adspots (different sellers: talent manager, direct,
|
|
|
125
125
|
| Column | Type | Description |
|
|
126
126
|
|--------|------|-------------|
|
|
127
127
|
| `id` | int | Primary key |
|
|
128
|
-
| `channel_name` | varchar | Display name |
|
|
129
|
-
| `external_channel_id` | varchar | YouTube channel ID (`UCxxxxxx`). ⚠️ There is NO `youtube_id` column. |
|
|
128
|
+
| `channel_name` | varchar | Display name. ⚠️ The column is `channel_name`, NOT `name`. |
|
|
129
|
+
| `external_channel_id` | varchar | YouTube channel ID (e.g., `UCxxxxxx`). ⚠️ There is NO `youtube_id` column — use this one. |
|
|
130
130
|
| `url` | varchar | Channel URL |
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
131
|
+
| `reach` | bigint | Subscriber count. ⚠️ There is NO `subscribers` column — `reach` is the subscriber count. Many internal docs and outputs use the word "subscribers"; in SQL, always query `reach`. |
|
|
132
|
+
| `media_selling_network_join_date` | date/timestamptz | When channel joined MSN. **MSN membership = this column IS NOT NULL.** |
|
|
133
|
+
| `is_tl_channel` | boolean | True = TPP/VIP channel (the small VIP subset, ~144 channels at 100k+ reach). ⚠️ **`is_tl_channel` is NOT the MSN flag.** Naive `WHERE is_tl_channel = true` as an "MSN filter" silently drops ~98% of the MSN pool (8,652 → 144 at 100k+). For MSN, use `media_selling_network_join_date IS NOT NULL`. |
|
|
134
|
+
| `content_category` | int | Content category code (1–22). See "`content_category` Constants" below for the code → label mapping. ⚠️ **Data-quality notes:** (1) per-row category assignments are often inconsistent with the official label (e.g. cat 15 = Technology, but many top-`reach` channels in cat 15 are clearly Entertainment). (2) Several codes are essentially unused in practice — codes 1, 2, 4, 6, 7, 8, 9, 11, 13 (Backend Development, Design, Frontend Development, Marketing, Mobile Development, Sales, Travel, Photography, Personal Finance) return ~0 active high-reach channels. Most travel creators land under Lifestyle (5), not Travel (9). The label table below is authoritative; the per-row assignment is best-effort. **For topic/category discovery, prefer `tl recommender top-channels "<tag>"` (ranked) over `WHERE content_category = <code>` (equality). |
|
|
135
|
+
| `is_active` | boolean | Whether the channel is active. ⚠️ **Always include `is_active = true` in channel queries** unless explicitly looking for archived rows. |
|
|
136
|
+
| `country` | varchar | Channel's primary country (ISO 3166-1 alpha-2 code, e.g. `US`, `GB`, `BR`). Often the cleanest answer to "geo" questions on sponsorships (since adlink itself has no geo). May be NULL or blank on ~10% of channels. |
|
|
137
|
+
| `language` | varchar | Primary content language. ⚠️ **Short ISO 639 codes — NOT BCP-47.** Mostly 2-letter ISO 639-1 (`en`, `pt`, `hi`) for major languages; occasionally 3-letter ISO 639-2/3 (`arc`, `arz`, `ase`, `ceb`) for languages without a 2-letter code. Filtering with `language = 'en-US'` returns zero rows. **Don't assume `LENGTH(language) = 2`** — that silently drops the 3-letter long-tail. May be NULL on ~10% of channels. |
|
|
138
|
+
| `last_published` | date | Date of the channel's most recent video. Use for "is the channel still active?" filters — e.g. `last_published >= CURRENT_DATE - INTERVAL '120 days'`. |
|
|
139
|
+
| `sponsorship_score` | double precision | TL-internal channel quality score (higher is better). Useful as a tiebreaker when ranking candidate channels. |
|
|
140
|
+
| `description` | text | LLM-generated description of the channel. Sometimes useful as a regex-target for thematic filtering when the integer category is too coarse (e.g. filtering "Technology" cat 15 down to actual tech reviewers via keywords like `tech|gadget|review|software`). |
|
|
133
141
|
| `evergreenness` | float | Cached evergreen score |
|
|
134
142
|
|
|
143
|
+
#### `content_category` Constants
|
|
144
|
+
|
|
145
|
+
Source of truth: `thoughtleaders.taxonomies.ContentCategory` (Django `IntEnum` in the main `thoughtleaders` repo).
|
|
146
|
+
|
|
147
|
+
| Value | Constant | Pretty Label |
|
|
148
|
+
|-------|----------|--------------|
|
|
149
|
+
| 1 | BACKEND_DEVELOPMENT | Backend Development |
|
|
150
|
+
| 2 | DESIGN | Design |
|
|
151
|
+
| 3 | ENTREPRENEURSHIP | Entrepreneurship |
|
|
152
|
+
| 4 | FRONTEND_DEVELOPMENT | Frontend Development |
|
|
153
|
+
| 5 | LIFESTYLE | Lifestyle |
|
|
154
|
+
| 6 | MARKETING | Marketing |
|
|
155
|
+
| 7 | MOBILE_DEVELOPMENT | Mobile Development |
|
|
156
|
+
| 8 | SALES | Sales |
|
|
157
|
+
| 9 | TRAVEL | Travel |
|
|
158
|
+
| 10 | BUSINESS | Business |
|
|
159
|
+
| 11 | PHOTOGRAPHY | Photography |
|
|
160
|
+
| 12 | GENERAL_KNOWLEDGE | General Knowledge |
|
|
161
|
+
| 13 | PERSONAL_FINANCE | Personal Finance |
|
|
162
|
+
| 14 | NEWS_POLITICS | News & Politics |
|
|
163
|
+
| 15 | TECHNOLOGY | Technology |
|
|
164
|
+
| 16 | GAMING | Gaming |
|
|
165
|
+
| 17 | FOOD | Food |
|
|
166
|
+
| 18 | SPORTS | Sports |
|
|
167
|
+
| 19 | HOWTO | How To & Crafts |
|
|
168
|
+
| 20 | ENTERTAINMENT | Entertainment |
|
|
169
|
+
| 21 | HEALTH_FITNESS | Health & Fitness |
|
|
170
|
+
| 22 | MUSIC | Music |
|
|
171
|
+
|
|
135
172
|
### `auth_user` (Django Users)
|
|
136
173
|
|
|
137
174
|
Standard Django user table. Used for owner lookups.
|
|
@@ -11,7 +11,7 @@ from tl_cli.filters import parse_filters
|
|
|
11
11
|
from tl_cli.hints import detail_hint
|
|
12
12
|
from tl_cli.output.formatter import detect_format, output, output_single
|
|
13
13
|
|
|
14
|
-
app = typer.Typer(help="YouTube channels (
|
|
14
|
+
app = typer.Typer(help="YouTube channels (detail, history, and similar-channel recommendations)")
|
|
15
15
|
|
|
16
16
|
# Columns for the `similar` endpoint result table. The server enriches every
|
|
17
17
|
# row so the user can size up each suggestion without follow-up queries.
|
|
@@ -25,50 +25,6 @@ SIMILAR_COLUMN_CONFIG = {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
@app.callback(invoke_without_command=True)
|
|
29
|
-
def channels(ctx: typer.Context) -> None:
|
|
30
|
-
"""YouTube channels — search and detail."""
|
|
31
|
-
if ctx.invoked_subcommand is None:
|
|
32
|
-
ctx.invoke(list_cmd, args=[], json_output=False, csv_output=False, md_output=False, limit=50, offset=0)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@app.command("list")
|
|
36
|
-
def list_cmd(
|
|
37
|
-
args: list[str] = typer.Argument(None, help="Filters (key:value pairs). Run 'tl describe show channels' for available filters."),
|
|
38
|
-
json_output: bool = typer.Option(False, "--json", help="JSON output"),
|
|
39
|
-
csv_output: bool = typer.Option(False, "--csv", help="CSV output"),
|
|
40
|
-
md_output: bool = typer.Option(False, "--md", help="Markdown output"),
|
|
41
|
-
toon_output: bool = typer.Option(False, "--toon", help="TOON output (token-efficient for LLMs)"),
|
|
42
|
-
limit: int = typer.Option(50, "--limit", "-l", help="Max results"),
|
|
43
|
-
offset: int = typer.Option(0, "--offset", help="Pagination offset"),
|
|
44
|
-
) -> None:
|
|
45
|
-
"""Search channels with optional filters.
|
|
46
|
-
|
|
47
|
-
Examples:
|
|
48
|
-
tl channels list # List channels
|
|
49
|
-
tl channels list category:cooking min-subs:100k # Search with filters
|
|
50
|
-
"""
|
|
51
|
-
fmt = detect_format(json_output, csv_output, md_output, toon_output)
|
|
52
|
-
filters = parse_filters(args or [])
|
|
53
|
-
|
|
54
|
-
client = get_client()
|
|
55
|
-
try:
|
|
56
|
-
params = {**filters, "limit": str(limit), "offset": str(offset)}
|
|
57
|
-
data = client.get("/channels", params=params)
|
|
58
|
-
for r in data.get("results", []):
|
|
59
|
-
r["channel_id"] = r.pop("id", None)
|
|
60
|
-
output(
|
|
61
|
-
data,
|
|
62
|
-
fmt,
|
|
63
|
-
columns=["channel_id", "name", "url", "msn", "tpp", "subscribers", "gender", "countries", "category", "sponsorship_score", "trend"],
|
|
64
|
-
title="Channels",
|
|
65
|
-
)
|
|
66
|
-
except ApiError as e:
|
|
67
|
-
handle_api_error(e)
|
|
68
|
-
finally:
|
|
69
|
-
client.close()
|
|
70
|
-
|
|
71
|
-
|
|
72
28
|
@app.command("show")
|
|
73
29
|
def show_cmd(
|
|
74
30
|
channel_ref: str = typer.Argument(..., help="Channel ID (numeric) or name (partial match, must be unique)"),
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
# ThoughtLeaders Business Glossary
|
|
2
|
-
|
|
3
|
-
Maps business terms to database concepts.
|
|
4
|
-
|
|
5
|
-
## Revenue & Deal Lifecycle
|
|
6
|
-
|
|
7
|
-
| Business Term | DB Concept | Notes |
|
|
8
|
-
|--------------|------------|-------|
|
|
9
|
-
| **Revenue / Sold ad** | `adlink.publish_status = 3` (SOLD) | Only status=3 counts as real revenue |
|
|
10
|
-
| **Gross ads / Gross revenue** | `SUM(adlink.price)` where sold | Total advertiser spend |
|
|
11
|
-
| **Net revenue / TL profit** | `price - cost` per adlink | What TL earns as a company |
|
|
12
|
-
| **Cost** | `adlink.cost` | What the channel earns |
|
|
13
|
-
| **Price** | `adlink.price` | What the advertiser pays |
|
|
14
|
-
| **Closed-lost** | `publish_status IN (4, 5, 9)` | All three rejection statuses |
|
|
15
|
-
| **Open opportunity** | `publish_status IN (0, 2, 6, 7, 8)` | Pipeline — not revenue, not lost |
|
|
16
|
-
| **Proposal Approved** | `publish_status = 6` | AM approved to show to brand — NOT brand approval. Internal gate only. |
|
|
17
|
-
| **Pending** | `publish_status = 2` | Brand has agreed — this is the real high-intent signal |
|
|
18
|
-
| **Weighted pipeline** | `SUM(weighted_price)` for open opps | Pre-calculated on save |
|
|
19
|
-
| **Ad is live** | `publish_date IS NOT NULL` | Until publish_date is set, ad is not on YouTube |
|
|
20
|
-
| **Cancellation risk** | Sold but `publish_date IS NULL` | Sold deals without publish_date can still be canceled |
|
|
21
|
-
|
|
22
|
-
## Performance Grade (`adlink.performance_grade`)
|
|
23
|
-
|
|
24
|
-
| Value | Label | Description |
|
|
25
|
-
|-------|-------|-------------|
|
|
26
|
-
| 0 | Pending | Not yet graded (treat as NULL) |
|
|
27
|
-
| 1 | Loser | Underperforming ad — do not renew |
|
|
28
|
-
| 2 | Neutral | Mixed results — test one more time, ideally at a better rate. Second test determines if channel becomes Loser or Winner |
|
|
29
|
-
| 3 | Winner | High-performing ad — should always be renewed |
|
|
30
|
-
|
|
31
|
-
**Renewal logic:** All Winners should be renewed. Neutrals get one more test (ideally at a lower CPM), then reclassified as Winner or Loser.
|
|
32
|
-
|
|
33
|
-
## View Guarantees
|
|
34
|
-
|
|
35
|
-
| Field | Description |
|
|
36
|
-
|-------|-------------|
|
|
37
|
-
| `adlink.impressions_guarantee` | The number of views guaranteed for the ad (bigint). 0 or NULL = no guarantee. |
|
|
38
|
-
| `adlink.view_guarantee_hit_date` | Timestamp when the guarantee was met. NULL = not yet hit or no guarantee. |
|
|
39
|
-
| `adlink.projected_views_at_purchase_date` | Projected views at time of purchase (used for CPM estimation). |
|
|
40
|
-
|
|
41
|
-
## Entities
|
|
42
|
-
|
|
43
|
-
| Business Term | DB Table | Notes |
|
|
44
|
-
|--------------|----------|-------|
|
|
45
|
-
| **Deal / Sponsorship** | `thoughtleaders_adlink` | One brand ↔ channel placement |
|
|
46
|
-
| **Brand** | `thoughtleaders_brand` | Advertiser entity (the buying-side brand) |
|
|
47
|
-
| **Brand profile** | `thoughtleaders_profile` | Advertiser entity / account |
|
|
48
|
-
| **Organization** | `thoughtleaders_organization` | Parent entity for profiles |
|
|
49
|
-
| **Channel** | `thoughtleaders_channel` | YouTube channel |
|
|
50
|
-
| **Ad Spot (Catalogue item)** | `thoughtleaders_adspot` | TL's catalogue of buyable placements. Price/cost on adspot are *list prices* only — each adlink (instance) can have completely different price/cost |
|
|
51
|
-
| **Campaign** | `dashboard_campaign` | Groups multiple deals |
|
|
52
|
-
|
|
53
|
-
## Ad Spots & Channels
|
|
54
|
-
|
|
55
|
-
- A channel can have **multiple ad spots** because different people sell the same channel (talent manager, direct, multiple agencies)
|
|
56
|
-
- Ad spots are the **catalogue** — adlinks are **instances** of catalogue items
|
|
57
|
-
- Price/cost on adspot = list/catalog values; price/cost on adlink = actual deal values
|
|
58
|
-
- **Only one active adspot with integration=mention per channel at any time** (MSN rule)
|
|
59
|
-
|
|
60
|
-
## Ownership & Accountability
|
|
61
|
-
|
|
62
|
-
| Field | Model | Meaning |
|
|
63
|
-
|-------|-------|---------|
|
|
64
|
-
| `owner_sales_id` | `adlink` | **Most important.** Person responsible for closing the deal and for the revenue. Final accountability. |
|
|
65
|
-
| `owner_advertiser_id` | `adlink` | Brand-side owner for this specific deal |
|
|
66
|
-
| `owner_publisher_id` | `adlink` | Channel-side owner for this specific deal |
|
|
67
|
-
| `owner_advertiser_id` | `profile` | **Account owner.** Who owns the brand relationship overall. Often same person as owner_sales on adlinks, but not always. |
|
|
68
|
-
| `owner_publisher_id` | `profile` | Channel relationship owner on the profile level |
|
|
69
|
-
| `owner_sales_id` | `profile` | Sales owner at profile level |
|
|
70
|
-
|
|
71
|
-
**Key insight:** Ownership exists on both `profile` (account-level) and `adlink` (deal-level). For revenue attribution, always use `adlink.owner_sales_id`.
|
|
72
|
-
|
|
73
|
-
## MSN (Media Selling Network)
|
|
74
|
-
|
|
75
|
-
- Channels where TL has **≥80% confidence** they can buy an ad tomorrow
|
|
76
|
-
- Key data: **who is the contact** to buy the ad from
|
|
77
|
-
- `thoughtleaders_channel.media_selling_network_join_date` = when channel joined MSN
|
|
78
|
-
- `thoughtleaders_channel.is_tl_channel` = TPP/VIP channel (subset of MSN)
|
|
79
|
-
- **Rule:** Only one active adspot with `integration=mention` per channel at any time
|
|
80
|
-
- MSN quality depends on having current, accurate contact info
|
|
81
|
-
|
|
82
|
-
## Teams & Ownership
|
|
83
|
-
|
|
84
|
-
### Brand-led Revenue (Sales / Account Management)
|
|
85
|
-
|
|
86
|
-
These teams close deals and manage brand relationships. Revenue is attributed via `adlink.owner_sales_id`.
|
|
87
|
-
|
|
88
|
-
**Emma's team:**
|
|
89
|
-
| Person | auth_user.id | Owner field |
|
|
90
|
-
|--------|-------------|-------------|
|
|
91
|
-
| Emma | 11158 | `adlink.owner_sales_id` |
|
|
92
|
-
| Orli | 2042 | `adlink.owner_sales_id` |
|
|
93
|
-
| Eli | 20836 | `adlink.owner_sales_id` |
|
|
94
|
-
| Grace | 20835 | `adlink.owner_sales_id` |
|
|
95
|
-
| Mark | 23979 | `adlink.owner_sales_id` |
|
|
96
|
-
| Abbie | 23978 | `adlink.owner_sales_id` |
|
|
97
|
-
| Ariella | 23977 | `adlink.owner_sales_id` |
|
|
98
|
-
|
|
99
|
-
**Nicole's team:**
|
|
100
|
-
| Person | auth_user.id | Owner field |
|
|
101
|
-
|--------|-------------|-------------|
|
|
102
|
-
| Nicole | 9929 | `adlink.owner_sales_id` |
|
|
103
|
-
| Maika | 5412 | `adlink.owner_sales_id` |
|
|
104
|
-
| Yuval | 14252 | `adlink.owner_sales_id` |
|
|
105
|
-
| Revital | 14251 | `adlink.owner_sales_id` |
|
|
106
|
-
|
|
107
|
-
### Network Growth (SDR / Partnerships) — Pauline's team
|
|
108
|
-
|
|
109
|
-
Responsible for growing the MSN (new channels) and MBN (new brands). SDR outreach on both sides.
|
|
110
|
-
|
|
111
|
-
| Person | auth_user.id | Role | Owner field |
|
|
112
|
-
|--------|-------------|------|-------------|
|
|
113
|
-
| Pauline | 218 | Team lead | `adlink.owner_publisher_id` (channel handovers) |
|
|
114
|
-
| Morgan | 5710 | Channel SDR | — |
|
|
115
|
-
| Jen | 873 | Channel SDR | — |
|
|
116
|
-
| Ruby Jean | 9011 | Channel SDR | — |
|
|
117
|
-
| Molly | 11361 | Channel SDR | — |
|
|
118
|
-
| Pierra | 11323 | Brand SDR | — |
|
|
119
|
-
| Nian | 8795 | Brand SDR | — |
|
|
120
|
-
|
|
121
|
-
### Ad Ops — Jody's team
|
|
122
|
-
|
|
123
|
-
Manages getting sold ads published. `profile.owner_publisher_id` = ad ops manager of an account.
|
|
124
|
-
|
|
125
|
-
| Person | auth_user.id | Owner field |
|
|
126
|
-
|--------|-------------|-------------|
|
|
127
|
-
| Jody | 71 | `profile.owner_publisher_id` (account-level ad ops owner) |
|
|
128
|
-
| Kathleen | 9274 | `profile.owner_publisher_id` |
|
|
129
|
-
| Shane | 18159 | `profile.owner_publisher_id` |
|
|
130
|
-
| Kevin | 5799 | `profile.owner_publisher_id` |
|
|
131
|
-
| Airis | 5804 | `profile.owner_publisher_id` |
|
|
132
|
-
| Lara | 10743 | `profile.owner_publisher_id` |
|
|
133
|
-
| Josh | 11592 | `profile.owner_publisher_id` |
|
|
134
|
-
|
|
135
|
-
### Querying by team
|
|
136
|
-
|
|
137
|
-
```sql
|
|
138
|
-
-- Emma's team pipeline
|
|
139
|
-
SELECT ... FROM thoughtleaders_adlink al
|
|
140
|
-
WHERE al.owner_sales_id IN (11158, 2042, 20836, 20835, 23979, 23978, 23977)
|
|
141
|
-
|
|
142
|
-
-- Nicole's team pipeline
|
|
143
|
-
SELECT ... FROM thoughtleaders_adlink al
|
|
144
|
-
WHERE al.owner_sales_id IN (9929, 5412, 14252, 14251)
|
|
145
|
-
|
|
146
|
-
-- All brand-led revenue (both teams)
|
|
147
|
-
SELECT ... FROM thoughtleaders_adlink al
|
|
148
|
-
WHERE al.owner_sales_id IN (11158, 2042, 20836, 20835, 23979, 23978, 23977, 9929, 5412, 14252, 14251)
|
|
149
|
-
|
|
150
|
-
-- Pauline's network growth team (channel SDRs)
|
|
151
|
-
-- owner_publisher_id on adlink for channel-side work
|
|
152
|
-
SELECT ... FROM thoughtleaders_adlink al
|
|
153
|
-
WHERE al.owner_publisher_id IN (218, 5710, 873, 9011, 11361)
|
|
154
|
-
|
|
155
|
-
-- Jody's ad ops team accounts (profile-level ownership)
|
|
156
|
-
SELECT ... FROM thoughtleaders_profile p
|
|
157
|
-
WHERE p.owner_publisher_id IN (71, 9274, 18159, 5799, 5804, 10743, 11592)
|
|
158
|
-
```
|
|
159
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/skills/tl/references/elasticsearch-schema.md
RENAMED
|
File without changes
|
{thoughtleaders_cli-0.6.1 → thoughtleaders_cli-0.6.3}/skills/tl/references/firebolt-schema.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|