thoughtleaders-cli 0.7.15__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/.claude-plugin/plugin.json +1 -1
  2. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/AGENTS.md +1 -1
  3. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/API.md +5 -5
  4. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/PKG-INFO +6 -6
  5. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/README.md +5 -5
  6. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/agents/tl-analyst.md +6 -6
  7. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/pyproject.toml +1 -1
  8. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl/SKILL.md +24 -18
  9. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl/references/business-glossary.md +13 -14
  10. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl/references/firebolt-schema.md +4 -4
  11. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl/references/postgres-schema.md +16 -16
  12. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/references/peer-cohort.md +2 -2
  13. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/references/red-flags.md +1 -1
  14. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/analyze_channel.py +1 -1
  15. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/anomaly_detector.py +2 -2
  16. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/engagement_ratios.py +1 -1
  17. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/peer_cohort.py +4 -4
  18. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/report.py +2 -2
  19. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/resolve_channel.py +2 -2
  20. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/tl_cli.py +5 -5
  21. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-keyword-research/SKILL.md +2 -2
  22. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-keyword-research/scripts/search_channels.py +2 -2
  23. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/SKILL.md +15 -15
  24. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/columns_sponsorships.md +1 -1
  25. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/intelligence_filterset_schema.json +5 -5
  26. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/intelligence_widget_schema.json +10 -10
  27. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/report_glossary.md +5 -5
  28. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/sortable_columns.json +4 -4
  29. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/sponsorship_filterset_schema.json +2 -2
  30. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/sponsorship_widget_schema.json +8 -8
  31. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/widgets.md +12 -12
  32. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-top-partnerships/SKILL.md +6 -6
  33. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-top-partnerships/scripts/top_partnerships.py +16 -16
  34. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/__init__.py +1 -1
  35. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/brands.py +1 -1
  36. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/channels.py +1 -1
  37. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/snapshots.py +1 -1
  38. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/sponsorships.py +5 -5
  39. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/filters.py +1 -1
  40. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests/test_filters.py +1 -1
  41. thoughtleaders_cli-0.8.0/tests/test_sponsorships.py +41 -0
  42. thoughtleaders_cli-0.7.15/tests/test_sponsorships.py +0 -41
  43. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/.claude-plugin/marketplace.json +0 -0
  44. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/.github/dependabot.yml +0 -0
  45. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/.github/workflows/ci.yml +0 -0
  46. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/.github/workflows/cli-integration.yml +0 -0
  47. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/.github/workflows/python-publish.yml +0 -0
  48. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/.gitignore +0 -0
  49. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/CLAUDE.md +0 -0
  50. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/LICENSE +0 -0
  51. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/agents/youtube-comment-classifier.md +0 -0
  52. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/hooks/hooks.json +0 -0
  53. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/hooks/scripts/load-tl-skill.mjs +0 -0
  54. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/hooks/scripts/post-usage.sh +0 -0
  55. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/hooks/scripts/pre-check.sh +0 -0
  56. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl/references/elasticsearch-schema.md +0 -0
  57. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/.gitignore +0 -0
  58. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/SKILL.md +0 -0
  59. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/references/comment-patterns.md +0 -0
  60. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/references/scoring.md +0 -0
  61. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/_io_utf8.py +0 -0
  62. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/comment_analyzer.py +0 -0
  63. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/comment_scraper.py +0 -0
  64. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/score.py +0 -0
  65. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/video_integrity.py +0 -0
  66. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-channel-authenticity/scripts/view_curves.py +0 -0
  67. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-keyword-research/references/context-classifier.md +0 -0
  68. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-keyword-research/scripts/fetch_context.py +0 -0
  69. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-keyword-research/scripts/probe.py +0 -0
  70. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/columns_brands.md +0 -0
  71. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/columns_channels.md +0 -0
  72. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-save-report/references/columns_content.md +0 -0
  73. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-views-guarantee/SKILL.md +0 -0
  74. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/skills/tl-views-guarantee/scripts/vg.py +0 -0
  75. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/_completions.py +0 -0
  76. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/_typer_utils.py +0 -0
  77. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/auth/__init__.py +0 -0
  78. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/auth/commands.py +0 -0
  79. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/auth/login.py +0 -0
  80. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/auth/pkce.py +0 -0
  81. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/auth/token_store.py +0 -0
  82. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/client/__init__.py +0 -0
  83. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/client/errors.py +0 -0
  84. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/client/http.py +0 -0
  85. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/__init__.py +0 -0
  86. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/_comments_common.py +0 -0
  87. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/balance.py +0 -0
  88. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/bulk_import.py +0 -0
  89. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/changelog.py +0 -0
  90. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/credits.py +0 -0
  91. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/db.py +0 -0
  92. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/deals.py +0 -0
  93. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/describe.py +0 -0
  94. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/doctor.py +0 -0
  95. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/matches.py +0 -0
  96. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/proposals.py +0 -0
  97. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/recommender.py +0 -0
  98. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/reports.py +0 -0
  99. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/schema.py +0 -0
  100. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/setup.py +0 -0
  101. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/uploads.py +0 -0
  102. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/commands/whoami.py +0 -0
  103. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/config.py +0 -0
  104. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/hints.py +0 -0
  105. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/main.py +0 -0
  106. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/output/__init__.py +0 -0
  107. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/output/formatter.py +0 -0
  108. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/src/tl_cli/self_update.py +0 -0
  109. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests/__init__.py +0 -0
  110. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests/test_auth.py +0 -0
  111. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests/test_brands_winner_channels.py +0 -0
  112. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests/test_channels_lookalike.py +0 -0
  113. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests/test_describe.py +0 -0
  114. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests/test_http_auth.py +0 -0
  115. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests/test_output.py +0 -0
  116. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests/test_reports.py +0 -0
  117. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests/test_setup.py +0 -0
  118. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests_cli/AGENTS.md +0 -0
  119. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests_cli/conftest.py +0 -0
  120. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests_cli/test_balance.py +0 -0
  121. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests_cli/test_db_es.py +0 -0
  122. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests_cli/test_db_fb.py +0 -0
  123. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests_cli/test_db_pg.py +0 -0
  124. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests_cli/test_schema.py +0 -0
  125. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/tests_cli/test_whoami.py +0 -0
  126. {thoughtleaders_cli-0.7.15 → thoughtleaders_cli-0.8.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tl-cli",
3
- "version": "0.7.15",
3
+ "version": "0.8.0",
4
4
  "description": "ThoughtLeaders CLI — query sponsorship deals, channels, brands, uploads, and intelligence from the terminal",
5
5
  "author": {
6
6
  "name": "ThoughtLeaders",
@@ -52,7 +52,7 @@ This repo is also a Claude Code plugin, and can directly be installed as one.
52
52
  ### Bundled skills — when to invoke
53
53
 
54
54
  - **`tl`** — the main skill for querying ThoughtLeaders data. Default for any sponsorship / channel / brand / upload / report question.
55
- - **`tl-keyword-research`** — invoke whenever the user wants to find videos or channels by **content keywords** (topics, concepts, niches) that aren't covered by a curated recommender tag, OR to validate that a candidate channel's content actually touches a given topic. **Default output: a ranked, context-validated set of channels** (each flagged for sponsorability) discovered via field-weighted ES relevance (`title` > `summary` > `transcript`), then sense-checked so channels that only use the keyword in an unrelated sense are dropped. The keyword-distribution shape (`{operator, keywords:[{keyword,count}]}`) is now an **opt-in mode**, produced only when the user explicitly asks for keyword counts / distribution. **Do not compose keyword sets by hand for `tl db es` content searches — delegate to this skill first.** See `skills/tl/SKILL.md` → *Channel & video discovery* for the four-path decision tree and when to use this vs the recommender / raw SQL.
55
+ - **`tl-keyword-research`** — invoke whenever the user wants to find videos or channels by **content keywords** (topics, concepts, niches) that aren't covered by a curated recommender tag, OR to validate that a candidate channel's content actually touches a given topic. It researches the topic by composing a boolean keyword query (`--any` OR-groups AND'd together, `--not` exclusions) over **≥3 refinement rounds**, returning a **CNF (Conjunctive Normal Form) keyword expression** as the reusable headline artifact alongside the ranked, sponsorability-flagged channels it selects. **Interactive by default** — checkpoints with the user after round 3 unless told to "run autonomously" (or invoked with `autonomous` / `--auto`). The keyword-distribution shape (`{operator, keywords:[{keyword,count}]}`) is an **opt-in mode**, produced only when the user explicitly asks for keyword counts / distribution. **Do not compose keyword sets by hand for `tl db es` content searches — delegate to this skill first.** See `skills/tl/SKILL.md` → *Channel & video discovery* for the four-path decision tree and when to use this vs the recommender / raw SQL.
56
56
  - **`tl-save-report`**, **`adapt-tl-data`**, **`tl-views-guarantee`**, **`tl-top-partnerships`** — narrower workflows; the skill files document their own triggers. `tl-top-partnerships` is brand-user-facing: ranks a brand's sold sponsorships by live eCPM vs the sold-date projection and delivers a two-tab Google Sheet via `gws`.
57
57
 
58
58
  ### Skill content boundaries
@@ -182,16 +182,16 @@ curl -sS "$TL_API_BASE/raw/pg" \
182
182
  -H 'X-TL-Auth: API-KEY' \
183
183
  -H 'Content-Type: application/json' \
184
184
  -d '{
185
- "query": "SELECT id, channel_name, reach FROM thoughtleaders_channel WHERE is_tl_channel = TRUE ORDER BY reach DESC LIMIT 5 OFFSET 0"
185
+ "query": "SELECT id, channel_name, subscribers FROM thoughtleaders_channel WHERE is_tpp = TRUE ORDER BY subscribers DESC LIMIT 5 OFFSET 0"
186
186
  }' | jq
187
187
  ```
188
188
 
189
189
  ```python
190
190
  sql = """
191
- SELECT id, channel_name, reach
191
+ SELECT id, channel_name, subscribers
192
192
  FROM thoughtleaders_channel
193
- WHERE is_tl_channel = TRUE
194
- ORDER BY reach DESC
193
+ WHERE is_tpp = TRUE
194
+ ORDER BY subscribers DESC
195
195
  LIMIT 5 OFFSET 0
196
196
  """
197
197
  print(post('/raw/pg', {'query': sql}))
@@ -200,7 +200,7 @@ print(post('/raw/pg', {'query': sql}))
200
200
  ```json
201
201
  {
202
202
  "results": [
203
- {"id": 12345, "channel_name": "MrBeast", "reach": 320000000},
203
+ {"id": 12345, "channel_name": "MrBeast", "subscribers": 320000000},
204
204
  ...
205
205
  ],
206
206
  "total": 5,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: thoughtleaders-cli
3
- Version: 0.7.15
3
+ Version: 0.8.0
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
@@ -121,7 +121,7 @@ tl whoami
121
121
  # profile → profile_brands → brand chain.
122
122
  tl db pg "SELECT al.id, al.weighted_price, al.purchase_date
123
123
  FROM thoughtleaders_adlink al
124
- JOIN thoughtleaders_profile p ON p.id = al.creator_profile_id
124
+ JOIN thoughtleaders_profile p ON p.id = al.advertiser_profile_id
125
125
  JOIN thoughtleaders_profile_brands pb ON pb.profile_id = p.id
126
126
  JOIN thoughtleaders_brand b ON b.id = pb.brand_id
127
127
  WHERE al.publish_status = 3
@@ -228,7 +228,7 @@ ThoughtLeaders has its internal terminology that's exposed throughout this tool.
228
228
  * **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost and a computed CPM.
229
229
  * **AdLink** — engineering / DB name for the row that backs a sponsorship. Treat as interchangeable with "sponsorship"; the table is `thoughtleaders_adlink`.
230
230
  * **MSN** (Media Selling Network) — the ~12k YouTube channels that have opted in to receive sponsorship offers. A channel is in MSN if `channel.media_selling_network_join_date IS NOT NULL`.
231
- * **TPP** (ThoughtLeaders Partner Program) — TL's closest-partner channels, a strict subset of MSN. A channel is TPP if `channel.is_tl_channel = TRUE`. Prefer TPP channels when booking — fastest response, easiest to close.
231
+ * **TPP** (ThoughtLeaders Partner Program) — TL's closest-partner channels, a strict subset of MSN. A channel is TPP if `channel.is_tpp = TRUE`. Prefer TPP channels when booking — fastest response, easiest to close.
232
232
  * **MBN** (Media Buying Network) — the brand-side counterpart to MSN: profiles that have opted in to receive proposed sponsorships (`profile.media_buying_network_join_date IS NOT NULL`).
233
233
 
234
234
  Sponsorships are the centre of attention in ThoughtLeaders — all other analytics and operations serve to produce or optimise sponsorships. Note that the term "Sponsorship" is wide and encompasses pre-deal stages. The funnel is large at the Sponsorship end and narrowest at the Deal end.
@@ -250,7 +250,7 @@ Talk naturally in Claude Code:
250
250
  ```
251
251
  /tl Which channels did we sponsor in Q1?
252
252
  /tl sold sponsorships for Nike in Q1
253
- /tl show me pending proposals with send dates in April
253
+ /tl show me pending proposals with scheduled dates in April
254
254
  /tl what channels does Nike sponsor?
255
255
  /tl find me Cooking creators in the US with mobile-heavy audiences
256
256
  /tl check my balance
@@ -258,7 +258,7 @@ Talk naturally in Claude Code:
258
258
 
259
259
  Resource-specific slash commands:
260
260
  ```
261
- /tl-sponsorships pending with send dates in April
261
+ /tl-sponsorships pending with scheduled dates in April
262
262
  /tl-reports run my Q1 pipeline
263
263
  /tl-balance
264
264
  ```
@@ -290,7 +290,7 @@ By default, output is a styled table in the terminal and JSON when piped.
290
290
 
291
291
  ```bash
292
292
  tl sponsorships show 12345 --json | jq '.results'
293
- tl db pg "SELECT id, channel_name FROM thoughtleaders_channel WHERE is_tl_channel = TRUE
293
+ tl db pg "SELECT id, channel_name FROM thoughtleaders_channel WHERE is_tpp = TRUE
294
294
  LIMIT 200 OFFSET 0" --csv > tpp.csv
295
295
  tl channels show "MrBeast" --md # markdown table for Slack / docs
296
296
  tl channels show "MrBeast" --toon # token-efficient encoding for LLMs
@@ -93,7 +93,7 @@ tl whoami
93
93
  # profile → profile_brands → brand chain.
94
94
  tl db pg "SELECT al.id, al.weighted_price, al.purchase_date
95
95
  FROM thoughtleaders_adlink al
96
- JOIN thoughtleaders_profile p ON p.id = al.creator_profile_id
96
+ JOIN thoughtleaders_profile p ON p.id = al.advertiser_profile_id
97
97
  JOIN thoughtleaders_profile_brands pb ON pb.profile_id = p.id
98
98
  JOIN thoughtleaders_brand b ON b.id = pb.brand_id
99
99
  WHERE al.publish_status = 3
@@ -200,7 +200,7 @@ ThoughtLeaders has its internal terminology that's exposed throughout this tool.
200
200
  * **Adspots** — types of ads a channel carries (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost and a computed CPM.
201
201
  * **AdLink** — engineering / DB name for the row that backs a sponsorship. Treat as interchangeable with "sponsorship"; the table is `thoughtleaders_adlink`.
202
202
  * **MSN** (Media Selling Network) — the ~12k YouTube channels that have opted in to receive sponsorship offers. A channel is in MSN if `channel.media_selling_network_join_date IS NOT NULL`.
203
- * **TPP** (ThoughtLeaders Partner Program) — TL's closest-partner channels, a strict subset of MSN. A channel is TPP if `channel.is_tl_channel = TRUE`. Prefer TPP channels when booking — fastest response, easiest to close.
203
+ * **TPP** (ThoughtLeaders Partner Program) — TL's closest-partner channels, a strict subset of MSN. A channel is TPP if `channel.is_tpp = TRUE`. Prefer TPP channels when booking — fastest response, easiest to close.
204
204
  * **MBN** (Media Buying Network) — the brand-side counterpart to MSN: profiles that have opted in to receive proposed sponsorships (`profile.media_buying_network_join_date IS NOT NULL`).
205
205
 
206
206
  Sponsorships are the centre of attention in ThoughtLeaders — all other analytics and operations serve to produce or optimise sponsorships. Note that the term "Sponsorship" is wide and encompasses pre-deal stages. The funnel is large at the Sponsorship end and narrowest at the Deal end.
@@ -222,7 +222,7 @@ Talk naturally in Claude Code:
222
222
  ```
223
223
  /tl Which channels did we sponsor in Q1?
224
224
  /tl sold sponsorships for Nike in Q1
225
- /tl show me pending proposals with send dates in April
225
+ /tl show me pending proposals with scheduled dates in April
226
226
  /tl what channels does Nike sponsor?
227
227
  /tl find me Cooking creators in the US with mobile-heavy audiences
228
228
  /tl check my balance
@@ -230,7 +230,7 @@ Talk naturally in Claude Code:
230
230
 
231
231
  Resource-specific slash commands:
232
232
  ```
233
- /tl-sponsorships pending with send dates in April
233
+ /tl-sponsorships pending with scheduled dates in April
234
234
  /tl-reports run my Q1 pipeline
235
235
  /tl-balance
236
236
  ```
@@ -262,7 +262,7 @@ By default, output is a styled table in the terminal and JSON when piped.
262
262
 
263
263
  ```bash
264
264
  tl sponsorships show 12345 --json | jq '.results'
265
- tl db pg "SELECT id, channel_name FROM thoughtleaders_channel WHERE is_tl_channel = TRUE
265
+ tl db pg "SELECT id, channel_name FROM thoughtleaders_channel WHERE is_tpp = TRUE
266
266
  LIMIT 200 OFFSET 0" --csv > tpp.csv
267
267
  tl channels show "MrBeast" --md # markdown table for Slack / docs
268
268
  tl channels show "MrBeast" --toon # token-efficient encoding for LLMs
@@ -38,7 +38,7 @@ If estimated cost > 200 credits, ask the user to confirm before proceeding.
38
38
  ```sql
39
39
  tl db pg "SELECT b.name, SUM(a.weighted_price) AS pipeline, COUNT(*) AS deals
40
40
  FROM thoughtleaders_adlink a
41
- JOIN thoughtleaders_profile p ON a.creator_profile_id = p.id
41
+ JOIN thoughtleaders_profile p ON a.advertiser_profile_id = p.id
42
42
  JOIN thoughtleaders_profile_brands pb ON p.id = pb.profile_id
43
43
  JOIN thoughtleaders_brand b ON pb.brand_id = b.id
44
44
  WHERE a.publish_status = 3
@@ -52,16 +52,16 @@ One query → ranked list. No client-side aggregation, no paginated walk.
52
52
  ### Cross-resource analysis (raw PG)
53
53
  "Show me deal slippage this month"
54
54
  ```sql
55
- tl db pg "SELECT a.id, a.send_date, a.publish_status, b.name AS brand, ch.channel_name
55
+ tl db pg "SELECT a.id, a.scheduled_date, a.publish_status, b.name AS brand, ch.channel_name
56
56
  FROM thoughtleaders_adlink a
57
57
  JOIN thoughtleaders_adspot s ON a.ad_spot_id = s.id
58
58
  JOIN thoughtleaders_channel ch ON s.channel_id = ch.id
59
- JOIN thoughtleaders_profile p ON a.creator_profile_id = p.id
59
+ JOIN thoughtleaders_profile p ON a.advertiser_profile_id = p.id
60
60
  JOIN thoughtleaders_profile_brands pb ON p.id = pb.profile_id
61
61
  JOIN thoughtleaders_brand b ON pb.brand_id = b.id
62
62
  WHERE a.publish_status = 10
63
- AND a.send_date < CURRENT_DATE
64
- ORDER BY a.send_date
63
+ AND a.scheduled_date < CURRENT_DATE
64
+ ORDER BY a.scheduled_date
65
65
  LIMIT 100 OFFSET 0"
66
66
  ```
67
67
  Then suggest `tl sponsorships comment-add <id> "..."` for each.
@@ -83,7 +83,7 @@ Then suggest `tl sponsorships comment-add <id> "..."` for each.
83
83
  "Give me a full picture of channel 12345"
84
84
  1. `tl channels show 12345 --json` → profile, scores, demographics (structured — wraps several joins already)
85
85
  2. `tl snapshots channel 12345 --json` → growth over time (snapshots wrap interpolation logic)
86
- 3. `tl db pg "SELECT id, send_date, publish_status, price FROM thoughtleaders_adlink WHERE ad_spot_id IN (SELECT id FROM thoughtleaders_adspot WHERE channel_id = 12345) ORDER BY send_date DESC LIMIT 100 OFFSET 0"` — deal history with the columns you actually want, no over-fetch.
86
+ 3. `tl db pg "SELECT id, scheduled_date, publish_status, price FROM thoughtleaders_adlink WHERE ad_spot_id IN (SELECT id FROM thoughtleaders_adspot WHERE channel_id = 12345) ORDER BY scheduled_date DESC LIMIT 100 OFFSET 0"` — deal history with the columns you actually want, no over-fetch.
87
87
  4. `tl uploads list channel:12345 --json` → recent content
88
88
 
89
89
  ### Transcript / brand-mention search (raw ES)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "thoughtleaders-cli"
7
- version = "0.7.15"
7
+ version = "0.8.0"
8
8
  description = "ThoughtLeaders CLI — query sponsorship data, channels, brands, and intelligence"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -37,7 +37,7 @@ If doing a database query, follow this recipe:
37
37
  ```bash
38
38
  tl db pg "SELECT al.id, b.name AS brand, al.weighted_price AS price
39
39
  FROM thoughtleaders_adlink al
40
- JOIN thoughtleaders_profile p ON p.id = al.creator_profile_id
40
+ JOIN thoughtleaders_profile p ON p.id = al.advertiser_profile_id
41
41
  JOIN thoughtleaders_profile_brands pb ON pb.profile_id = p.id
42
42
  JOIN thoughtleaders_brand b ON b.id = pb.brand_id
43
43
  WHERE al.publish_status = 3
@@ -87,29 +87,29 @@ Other key concepts:
87
87
  - **Comments** — notes attached to sponsorships, channels, or brands
88
88
  - **Adspots** — types of ads a channel is willing to publish (e.g. mention, dedicated video, product placement). Returned by `tl channels show`; each carries price/cost.
89
89
  - **Profiles** — actors that own sponsorship records on behalf of either side of a deal. A profile is either buyer-side or seller-side:
90
- - *Buyer-side (brand) profiles* — represent a sponsoring brand. Each brand profile has an M2M link to at most one `Brand` record (which are the actual advertiser identities). On a sponsorship, `creator_profile` is the buyer-side profile, and `creator_id` is the buyer-side user who created the record — on sponsorships, "creator" always means the buyer side, never the YouTube creator (the channel hangs off `ad_spot_id`).
90
+ - *Buyer-side (brand) profiles* — represent a sponsoring brand. Each brand profile has an M2M link to at most one `Brand` record (which are the actual advertiser identities). On a sponsorship, `advertiser_profile` is the buyer-side profile, and `advertiser_id` is the buyer-side user who created the record — the advertiser is always the buyer/brand side, never the YouTube creator (the channel hangs off `ad_spot_id`).
91
91
  - *Seller-side (publisher) profiles* — attached to a `Publication`, which in turn owns one or more `Channel` records. A channel's adspots therefore inherit ownership through `channel.publication.profile`.
92
92
  - **How to tell them apart** — three signals on the `thoughtleaders_profile` row, used in this order:
93
93
  1. **`persona`** (canonical) — `1=Brand`, `4=Media Agency`, `3=Talent Manager` are buyer-side; `2=Creator`, `5=Creator Service` are seller-side. May be null on legacy rows.
94
94
  2. **`is_advertiser` / `is_publisher`** booleans — feature flags; either or both can be true for staff-style profiles, but on normal user profiles they reliably mark side.
95
- - Org scoping for sponsorships is profile-mediated: a sponsorship belongs to your org if **either** `creator_profile.organization` (brand side) **or** `ad_spot.channel.publication.profile.organization` (publisher side) matches yours.
95
+ - Org scoping for sponsorships is profile-mediated: a sponsorship belongs to your org if **either** `advertiser_profile.organization` (brand side) **or** `ad_spot.channel.publication.profile.organization` (publisher side) matches yours.
96
96
  - **MSN** (Media Selling Network) — the ~12k YouTube channels that have opted in to receive sponsorship offers. A channels is in the MSN group if the `channel.media_selling_network_join_date` field is not null.
97
97
  - **MBN** (Media Buying Network) — the brand-side counterpart to MSN: brand profiles that have opted in to receive proposed sponsorships. A profile is in the MBN group if the `profile.media_buying_network_join_date` field is not null.
98
- - **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the ~170 channels TL has the closest working relationship with. A channel is in the TPP group if `channel.is_tl_channel` is True. **Prefer TPP channels when booking**: they respond fastest, are the easiest to close, and don't need an outreach round-trip — treat them as immediately bookable. TPP is a strict subset of MSN, so the same booking rules (one active mention adspot, etc.) apply.
98
+ - **TPP** (ThoughtLeaders Partner Program, a.k.a. "TL channels") — the ~170 channels TL has the closest working relationship with. A channel is in the TPP group if `channel.is_tpp` is True. **Prefer TPP channels when booking**: they respond fastest, are the easiest to close, and don't need an outreach round-trip — treat them as immediately bookable. TPP is a strict subset of MSN, so the same booking rules (one active mention adspot, etc.) apply.
99
99
  - **`demographics_updated_at`** (on channels) — If non-null, the channel has demographics screenshots on file. If null, no demographics screenshots have been uploaded. Use this to check whether a channel has demographics data from screenshots.
100
- - **`reach`** (on channels) — subscriber count. ⚠️ Despite the name, this is NOT ad-industry "reach" (unique audience exposed). There is no `subscribers` field — `reach` is it.
101
- - **`impression`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed. ⚠️ NOT actual views and NOT ad-industry "impressions" (ads served).
100
+ - **`subscribers`** (on channels) — the channel's subscriber count.
101
+ - **`projected_views`** (on channels) — projected views per video on that channel. Forward-looking estimate. May be null when not yet computed. ⚠️ NOT actual views and NOT ad-industry "impressions" (ads served).
102
102
  - **`views`** (on sponsorships) — actual view count of the sold and published sponsored video, accessible when `article_id` is set.
103
- - **`impressions_guarantee`** (on sponsorships) — projected/guaranteed impressions for the sponsorship. Numeric.
103
+ - **`views_guarantee`** (on sponsorships) — projected/guaranteed impressions for the sponsorship. Numeric.
104
104
  - **Sponsorship detail fields** (returned by `tl sponsorships show <id> --json`) — the detail payload includes `integration` (raw int), `publish_count`, `common_name`, `outreach_email`, nested `publisher` (`first_name`, `last_name`, `email`), nested `brand_contact` (`first_name`, `last_name`, `email`), and `brand.organization_name`. Use these when generating IOs, contracts, or outreach.
105
105
  - **CPM** has two distinct meanings depending on level — pick the one the user actually wants:
106
- - **Channel CPM** = `(adspot.price / channel.impression) * 1000` — projected price per thousand projected views. Used for pricing decisions **before** a sponsorship is sold. Available for channels with active adspots via `tl channels show <channel_id>`.
107
- - **Sponsorship CPM** = calculated in either of two ways: if `views` is present, then CPM is `(sponsorship.price / sponsorship.views) × 1000`, meaning realized cost per thousand actual views, computed post-publication. If `views` is null, Compute from the sponsorship's `price` and the channel's `impression` fields.
106
+ - **Channel CPM** = `(adspot.price / channel.projected_views) * 1000` — projected price per thousand projected views. Used for pricing decisions **before** a sponsorship is sold. Available for channels with active adspots via `tl channels show <channel_id>`.
107
+ - **Sponsorship CPM** = calculated in either of two ways: if `views` is present, then CPM is `(sponsorship.price / sponsorship.views) × 1000`, meaning realized cost per thousand actual views, computed post-publication. If `views` is null, Compute from the sponsorship's `price` and the channel's `projected_views` fields.
108
108
  - Where possible, calculate the correct CPM in a SQL expression.
109
109
  - **Sponsorship dates** — each sponsorship has four distinct dates, useful for different queries:
110
110
  - **`created_at`** — when the sponsorship record was created in the system
111
111
  - **`purchase_date`** — when the sponsorship was purchased (i.e. when the deal was made); These make up bookings.
112
- - **`send_date`** — the date the video is/was expected to be published (scheduled)
112
+ - **`scheduled_date`** — the date the video is/was expected to be published (scheduled)
113
113
  - **`publish_date`** — the date the video was actually published; These make up live ads.
114
114
  - **Credits** — every data query costs credits; use `tl describe` to see rates. Top up with `tl credits buy --amount-usd N` (free; opens a browser checkout). New accounts get a starter balance on first `tl auth login`; the rate is shown by `tl credits pricing`.
115
115
 
@@ -240,6 +240,7 @@ tl channels update 12345 '{"demographic_geo": {"US": 60, "UK": 12, "CA": 8}}'
240
240
  tl channels update 12345 '{"demographic_male_share": 55, "demographic_usa_share": 70}'
241
241
  tl channels update 12345 '{"outreach_email": "press@creator.com"}'
242
242
  tl channels update 12345 '{"all_emails": {"press@creator.com": {"timestamp": "2026-06-25T12:00:00+00:00", "source": "manual"}}}'
243
+ tl channels update 12345 '{"media_selling_network_join_date": "2026-01-15"}'
243
244
  ```
244
245
 
245
246
  **Channel contact emails.** Besides demographics, `tl channels update` accepts two
@@ -254,6 +255,11 @@ contact fields:
254
255
  `scheduled_rescrape`. Use a descriptive token such as `manual` for hand-added entries.
255
256
  An edit **replaces** the whole `all_emails` object, so include every entry you want to keep.
256
257
 
258
+ **MSN membership.** `tl channels update` also accepts **`media_selling_network_join_date`** —
259
+ the date the channel joined the Media Selling Network, as a `YYYY-MM-DD` string. This date is
260
+ itself the membership flag: set it to add the channel to MSN (it then reads as `is_msn` and
261
+ appears in MSN-filtered queries), or send `null` to remove the channel from the network.
262
+
257
263
  ### Creating and vetting sponsorships
258
264
 
259
265
  This is the end-to-end workflow for proposing a sponsorship, then moving it through the funnel as the two sides respond. Three create commands plus `tl sponsorships update` cover every state transition the CLI exposes.
@@ -404,8 +410,8 @@ tl db fb "SELECT age, view_count, like_count FROM article_metrics
404
410
  WHERE channel_id = 12345 AND id = 'dQw4w9WgXcQ'
405
411
  ORDER BY age"
406
412
 
407
- # Channel reach over time
408
- tl db fb "SELECT scrape_date, total_views, reach FROM channel_metrics
413
+ # Channel subscribers over time
414
+ tl db fb "SELECT scrape_date, total_views, subscribers FROM channel_metrics
409
415
  WHERE id = 12345
410
416
  ORDER BY scrape_date"
411
417
  ```
@@ -418,7 +424,7 @@ See [references/firebolt-schema.md](references/firebolt-schema.md) for accepted-
418
424
  # Example: Top brands by deal count
419
425
  tl db pg "SELECT b.name, COUNT(*) AS deals
420
426
  FROM thoughtleaders_adlink a
421
- JOIN thoughtleaders_profile p ON a.creator_profile_id = p.id
427
+ JOIN thoughtleaders_profile p ON a.advertiser_profile_id = p.id
422
428
  JOIN thoughtleaders_profile_brands pb ON p.id = pb.profile_id
423
429
  JOIN thoughtleaders_brand b ON pb.brand_id = b.id
424
430
  WHERE a.publish_status = 3
@@ -557,20 +563,20 @@ Use `tl recommender top` for category/topic discovery (it's ranked) and `tl chan
557
563
 
558
564
  Then run the actual content search via `tl db es` (`multi_match` on the `title`, `summary`, `transcript` fields) with the surviving high-count keywords. The skill's full procedure (Phase 1 = seed expansion by you; Phase 2 = the script) is in the `tl-keyword-research` skill file.
559
565
 
560
- **Path 4. Pure attribute filter** — user wants channels filtered by metadata like: `is_tl_channel`, `language`, `demographic_device_primary`, country share in `demographic_geo` JSON, aggregations, joins. Use `tl db pg` with a SELECT on `thoughtleaders_channel`. Run `tl schema pg thoughtleaders_channel` once to confirm the live column set; the columns in the examples are stable.
566
+ **Path 4. Pure attribute filter** — user wants channels filtered by metadata like: `is_tpp`, `language`, `demographic_device_primary`, country share in `demographic_geo` JSON, aggregations, joins. Use `tl db pg` with a SELECT on `thoughtleaders_channel`. Run `tl schema pg thoughtleaders_channel` once to confirm the live column set; the columns in the examples are stable.
561
567
 
562
568
  ```bash
563
569
  # All TPP (TL-managed) channels — pure attribute filter, not a category query
564
570
  tl db pg "SELECT id, channel_name, content_category, total_views
565
571
  FROM thoughtleaders_channel
566
- WHERE is_tl_channel = TRUE
572
+ WHERE is_tpp = TRUE
567
573
  ORDER BY total_views DESC
568
574
  LIMIT 200 OFFSET 0"
569
575
 
570
576
  # Mobile-first non-TPP channels — device share, not topic
571
577
  tl db pg "SELECT id, channel_name, demographic_device_primary, total_views
572
578
  FROM thoughtleaders_channel
573
- WHERE is_tl_channel = FALSE
579
+ WHERE is_tpp = FALSE
574
580
  AND demographic_device_primary = 'mobile'
575
581
  ORDER BY total_views DESC
576
582
  LIMIT 100 OFFSET 0"
@@ -632,7 +638,7 @@ Users only see data their plan allows:
632
638
  ```bash
633
639
  tl db pg "SELECT al.id, al.weighted_price, al.purchase_date, b.name AS brand
634
640
  FROM thoughtleaders_adlink al
635
- JOIN thoughtleaders_profile p ON p.id = al.creator_profile_id
641
+ JOIN thoughtleaders_profile p ON p.id = al.advertiser_profile_id
636
642
  JOIN thoughtleaders_profile_brands pb ON pb.profile_id = p.id
637
643
  JOIN thoughtleaders_brand b ON b.id = pb.brand_id
638
644
  WHERE al.publish_status = 3
@@ -734,7 +740,7 @@ tl db es '{
734
740
  # TL-brokered deal count for the brand (PG, not ES — adlinks where the brand is on the creator profile)
735
741
  tl db pg "SELECT COUNT(*) AS tl_brokered
736
742
  FROM thoughtleaders_adlink al
737
- JOIN thoughtleaders_profile p ON p.id = al.creator_profile_id
743
+ JOIN thoughtleaders_profile p ON p.id = al.advertiser_profile_id
738
744
  JOIN thoughtleaders_profile_brands pb ON pb.profile_id = p.id
739
745
  WHERE pb.brand_id = 21416 AND al.article_id IS NOT NULL"
740
746
  ```
@@ -18,7 +18,7 @@ Maps business terms to database concepts.
18
18
  | **Weighted pipeline** | `SUM(weighted_price)` for open opps | `weighted_price` is derived from the brand/channel approval combination on OPEN deals. |
19
19
  | **Ad is live** | `publish_date IS NOT NULL` | Until publish_date is set, ad is not on YouTube |
20
20
  | **Cancellation risk** | Sold but `publish_date IS NULL` | Sold deals without publish_date can still be canceled |
21
- | **Immediately bookable** | `is_tl_channel = true` | TPP channels — TL's closest partners. Fastest to respond, easiest to close, prefer when booking. |
21
+ | **Immediately bookable** | `is_tpp = true` | TPP channels — TL's closest partners. Fastest to respond, easiest to close, prefer when booking. |
22
22
 
23
23
  ## Performance Grade (`adlink.performance_grade`)
24
24
 
@@ -35,7 +35,7 @@ Maps business terms to database concepts.
35
35
 
36
36
  | Field | Description |
37
37
  |-------|-------------|
38
- | `adlink.impressions_guarantee` | The number of views guaranteed for the ad (bigint). 0 or NULL = no guarantee. |
38
+ | `adlink.views_guarantee` | The number of views guaranteed for the ad (bigint). 0 or NULL = no guarantee. |
39
39
  | `adlink.view_guarantee_hit_date` | Timestamp when the guarantee was met. NULL = not yet hit or no guarantee. |
40
40
  | `adlink.projected_views_at_purchase_date` | Projected views at time of purchase (used for CPM estimation). |
41
41
 
@@ -65,7 +65,7 @@ Maps business terms to database concepts.
65
65
  | `owner_sales_id` | `adlink` | **Most important.** Person responsible for closing the deal and for the revenue. Final accountability. |
66
66
  | `owner_advertiser_id` | `adlink` | Brand-side owner for this specific deal |
67
67
  | `owner_publisher_id` | `adlink` | Channel-side owner for this specific deal |
68
- | `creator_id` | `adlink` | The brand-side user account that *created the record*. ⚠️ NOT the YouTube creator — on sponsorships, "creator" always means the buyer side. Record lineage only; for accountability use the `owner_*` fields, for the brand use `creator_profile_id`, for the channel go via `ad_spot_id`. |
68
+ | `advertiser_id` | `adlink` | The brand-side (advertiser/buyer) user account that *created the record*. Record lineage only; for accountability use the `owner_*` fields, for the brand use `advertiser_profile_id`, for the channel go via `ad_spot_id`. |
69
69
  | `owner_advertiser_id` | `profile` | **Account owner.** Who owns the brand relationship overall. Often same person as owner_sales on adlinks, but not always. |
70
70
  | `owner_publisher_id` | `profile` | Channel relationship owner on the profile level |
71
71
  | `owner_sales_id` | `profile` | Sales owner at profile level |
@@ -77,7 +77,7 @@ Maps business terms to database concepts.
77
77
  - Channels where TL has **≥80% confidence** they can buy an ad tomorrow
78
78
  - Key data: **who is the contact** to buy the ad from
79
79
  - `thoughtleaders_channel.media_selling_network_join_date` = when channel joined MSN
80
- - `thoughtleaders_channel.is_tl_channel` = TPP channel — TL's closest partners, a strict subset of MSN. **Prefer when booking**: fastest response, easiest to close. Not a gating flag; not a process barrier.
80
+ - `thoughtleaders_channel.is_tpp` = TPP channel — TL's closest partners, a strict subset of MSN. **Prefer when booking**: fastest response, easiest to close. Not a gating flag; not a process barrier.
81
81
  - **Rule:** Only one active adspot with `integration=mention` per channel at any time
82
82
  - MSN quality depends on having current, accurate contact info
83
83
 
@@ -87,13 +87,13 @@ Vocabulary that AMs use about channels, mapped to the actual DB encoding. Most o
87
87
 
88
88
  | Business Term | DB Concept | Notes |
89
89
  |---------------|------------|-------|
90
- | **Subscribers** | `thoughtleaders_channel.reach` (bigint) | ⚠️ There is no `subscribers` column. The DB column is `reach`. |
91
- | **MSN member** | `media_selling_network_join_date IS NOT NULL` | Whole MSN pool. NOT `is_tl_channel = true` — that's the VIP subset only. |
92
- | **TPP / TL channel** | `is_tl_channel = true` | TL's closest-partner channels (~144 at 100k+ reach). Prefer when booking — fastest to respond, easiest to close. A strict subset of MSN; *don't* use as a general "MSN" proxy — silently drops ~98% of MSN. |
90
+ | **Subscribers** | `thoughtleaders_channel.subscribers` (bigint) | Channel subscriber count. |
91
+ | **MSN member** | `media_selling_network_join_date IS NOT NULL` | Whole MSN pool. NOT `is_tpp = true` — that's the VIP subset only. |
92
+ | **TPP / TL channel** | `is_tpp = true` | TL's closest-partner channels (~144 at 100k+ subscribers). Prefer when booking — fastest to respond, easiest to close. A strict subset of MSN; *don't* use as a general "MSN" proxy — silently drops ~98% of MSN. |
93
93
  | **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. |
94
94
  | **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. |
95
95
  | **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`. |
96
- | **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). |
96
+ | **Brand on a deal** | `adlink → advertiser_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). |
97
97
  | **Channel on a deal** | `adlink.ad_spot_id → adspot.channel_id → channel` | NO direct `channel_id` on adlink. |
98
98
  | **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. |
99
99
  | **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." |
@@ -104,11 +104,11 @@ AMs use "PV" loosely. There are three different DB fields, each meaning somethin
104
104
 
105
105
  | AM Term | DB Field | What it actually is |
106
106
  |---------|----------|---------------------|
107
- | **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. |
107
+ | **PV (channel baseline)** | `thoughtleaders_channel.projected_views` | Channel-level "typical views per video" used as CPM denominator. ⚠️ Coverage and freshness vary; cross-check Firebolt longform median for hero-tier deals. |
108
108
  | **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. |
109
- | **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. |
109
+ | **VG (View Guarantee)** | `thoughtleaders_adlink.views_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. |
110
110
 
111
- 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`.
111
+ When an AM says "what's the PV on this channel?" — they almost always mean `channel.projected_views`. 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`.
112
112
 
113
113
  ## Channel Sponsorship Signals
114
114
 
@@ -147,9 +147,8 @@ AMs and brand-facing readers don't know (or care) what Postgres, Elasticsearch,
147
147
  | `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 |
148
148
  | `organic_brand_mentions` (ES field) | **"organic / unpaid mentions"** | Brand mentioned in a video without a paid sponsorship |
149
149
  | `publish_status = 3` | **"sold"** | Already in glossary; never write the integer to AMs |
150
- | `creator_profile_id` chain / 3-table join | **"the brand's record"** | Engineering plumbing for brand lookup; AMs just hear "the brand" |
151
- | `reach` (PG column) | **"subscribers"** | Already in glossary; AMs say subscribers, SQL says reach |
152
- | `impression` (PG column) | **"projected views"** / **"PV"** | Already in glossary; flagged for reliability caveats |
150
+ | `advertiser_profile_id` chain / 3-table join | **"the brand's record"** | Engineering plumbing for brand lookup; AMs just hear "the brand" |
151
+ | `projected_views` (PG column) | **"projected views"** / **"PV"** | Flagged for reliability caveats |
153
152
 
154
153
  ### Common translations in context
155
154
 
@@ -71,7 +71,7 @@ Tracks YouTube video metrics over time. Each row = one scrape of one video on on
71
71
  |--------|------|-------------|
72
72
  | `id` | INT | TL channel ID |
73
73
  | `total_views` | INT | Channel total views at time of scrape |
74
- | `reach` | INT | Subscriber count at time of scrape |
74
+ | `subscribers` | INT | Subscriber count at time of scrape |
75
75
  | `scrape_date` | DATE | When this data point was captured |
76
76
 
77
77
  **Primary Index: `(id)`**
@@ -124,7 +124,7 @@ tl recommender top-channels "Tech" msn:yes --limit 50 --json \
124
124
  tl db pg "SELECT al.id, al.article_id, s.channel_id
125
125
  FROM thoughtleaders_adlink al
126
126
  JOIN thoughtleaders_adspot s ON s.id = al.ad_spot_id
127
- JOIN thoughtleaders_profile p ON p.id = al.creator_profile_id
127
+ JOIN thoughtleaders_profile p ON p.id = al.advertiser_profile_id
128
128
  JOIN thoughtleaders_profile_brands pb ON pb.profile_id = p.id
129
129
  JOIN thoughtleaders_brand b ON b.id = pb.brand_id
130
130
  WHERE al.publish_status = 3
@@ -182,7 +182,7 @@ tl db fb "SELECT id, age, view_count, like_count, comment_count, duration, publi
182
182
  ### Channel subscriber/view growth over time
183
183
 
184
184
  ```bash
185
- tl db fb "SELECT scrape_date, total_views, reach
185
+ tl db fb "SELECT scrape_date, total_views, subscribers
186
186
  FROM channel_metrics
187
187
  WHERE id = 12345
188
188
  ORDER BY scrape_date"
@@ -191,7 +191,7 @@ tl db fb "SELECT scrape_date, total_views, reach
191
191
  ### Compare multiple channels' growth
192
192
 
193
193
  ```bash
194
- tl db fb "SELECT id, scrape_date, total_views, reach
194
+ tl db fb "SELECT id, scrape_date, total_views, subscribers
195
195
  FROM channel_metrics
196
196
  WHERE id IN (123, 456, 789)
197
197
  ORDER BY id, scrape_date"
@@ -19,9 +19,9 @@ The main sponsorships table. Each row = one sponsorship between a brand and a Yo
19
19
  The profile table is tightly coupled with the brand table for media buyers, so many reports that operate on the brand levels must access the profile data first.
20
20
 
21
21
  > 🚨 **Columns that DO NOT exist on `thoughtleaders_adlink` — common hallucinations:**
22
- > - ❌ `brand_id` — there is NO direct brand FK. Brand is reached via `creator_profile_id → profile → profile_brands → brand`.
23
- > - ❌ `organization_id` — there is NO direct org FK. Org is reached via `creator_profile_id → profile.organization_id → organization`.
24
- > - ❌ `channel_id` — channel is reached via `ad_spot_id → adspot.channel_id → channel`. Do NOT substitute `creator_id` — that's the brand-side user who created the record, not the channel/YouTube creator.
22
+ > - ❌ `brand_id` — there is NO direct brand FK. Brand is reached via `advertiser_profile_id → profile → profile_brands → brand`.
23
+ > - ❌ `organization_id` — there is NO direct org FK. Org is reached via `advertiser_profile_id → profile.organization_id → organization`.
24
+ > - ❌ `channel_id` — channel is reached via `ad_spot_id → adspot.channel_id → channel`. Do NOT substitute `advertiser_id` — that's the brand-side user who created the record, not the channel.
25
25
  > - ❌ `youtube_id` (on channel) — use `external_channel_id`.
26
26
  > - ❌ `msn_join_date` (on channel) — use `media_selling_network_join_date`.
27
27
  > - ❌ `mbn_join_date` (on profile) — use `media_buying_network_join_date`.
@@ -35,12 +35,12 @@ The profile table is tightly coupled with the brand table for media buyers, so m
35
35
  | `updated_at` | timestamptz | Last modification |
36
36
  | `publish_status` | int | Deal status (see constants below) |
37
37
  | `ad_spot_id` | int FK | → `thoughtleaders_adspot.id` |
38
- | `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. |
39
- | `creator_id` | int FK | → `auth_user.id` — the brand-side user account that created the sponsorship record. ⚠️ Despite the name, NOT the YouTube creator: on sponsorships, "creator" always means the buyer side. Record lineage only — for the channel use `ad_spot_id → adspot.channel_id`, for the brand use `creator_profile_id`, for accountability use the `owner_*` fields. |
38
+ | `advertiser_profile_id` | int FK | → `thoughtleaders_profile.id` (the brand/advertiser's profile). ⚠️ The table is named `thoughtleaders_profile`, NOT `advertiser_profile`. |
39
+ | `advertiser_id` | int FK | → `auth_user.id` — the brand-side user account that created the sponsorship record (the advertiser/buyer side). Record lineage only — for the channel use `ad_spot_id → adspot.channel_id`, for the brand use `advertiser_profile_id`, for accountability use the `owner_*` fields. |
40
40
  | `owner_advertiser_id` | int FK | → `auth_user.id` (brand-side owner) |
41
41
  | `owner_publisher_id` | int FK | → `auth_user.id` (channel-side owner) |
42
42
  | `owner_sales_id` | int FK | → `auth_user.id` (sales rep) |
43
- | `send_date` | timestamptz | Scheduled send/publish date |
43
+ | `scheduled_date` | timestamptz | Scheduled send/publish date |
44
44
  | `publish_date` | timestamptz | Actual publish date |
45
45
  | `outreach_date` | timestamptz | When outreach was sent |
46
46
  | `purchase_date` | timestamptz | When deal was purchased/sold |
@@ -166,9 +166,9 @@ A channel can have multiple adspots (different sellers: talent manager, direct,
166
166
  | `url` | varchar | Channel URL (external — usually the YouTube URL). |
167
167
  | `slug` | varchar | TL-platform-specific slug. Used to build the canonical TL channel URL: `https://app.thoughtleaders.io/youtube/<slug>`. Prefer this over `url` when linking to a channel from any user-facing surface (reports, samples, Slack posts). Fall back to an ID-based TL path if `slug` is NULL; never fall back to the external YouTube URL. |
168
168
  | `total_views` | int | Total views for the entire channel |
169
- | `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`. |
169
+ | `subscribers` | bigint | Subscriber count. |
170
170
  | `media_selling_network_join_date` | date/timestamptz | When the channel joined the MSN. **MSN membership = this column IS NOT NULL.** |
171
- | `is_tl_channel` | boolean | True = TPP channel — TL's closest-partner channels (~144 at 100k+ reach), a strict subset of MSN. Prefer when booking: fastest response, easiest to close. ⚠️ **This is not the MSN flag.** For MSN, use `media_selling_network_join_date IS NOT NULL`. |
171
+ | `is_tpp` | boolean | True = TPP channel — TL's closest-partner channels (~144 at 100k+ subscribers), a strict subset of MSN. Prefer when booking: fastest response, easiest to close. ⚠️ **This is not the MSN flag.** For MSN, use `media_selling_network_join_date IS NOT NULL`. |
172
172
  | `content_category` | int | Content category code (1–22), as assigned by YouTube. This assignment is too unreliable, do not use it for discovering channels. **For topic/category discovery, prefer `tl recommender top-channels "<tag>"` |
173
173
  | `is_active` | boolean | Whether the channel is active. ⚠️ **Always include `is_active = true` in channel queries** unless explicitly looking for archived rows. |
174
174
  | `country` | varchar | Channel's primary country (ISO 3166-1 alpha-2 code, e.g. `US`, `GB`, `BR`). This is often the cleanest answer to "geography" questions on sponsorships. May be NULL or blank. |
@@ -193,8 +193,8 @@ A channel can have multiple adspots (different sellers: talent manager, direct,
193
193
 
194
194
  When composing `SELECT ... FROM thoughtleaders_channel ...`, do not improvise column names from semantic intuition — consult the output of `tl schema pg thoughtleaders_channel` or the column table above. The `tl schema` command is authoritative. Failed guesses return *"column '\<name\>' does not exist"* and cost a round-trip. Recurring problems:
195
195
 
196
- - Subscriber count is in the field named `reach`
197
- - Projected views is in the field named `impression`
196
+ - Subscriber count is in the field named `subscribers`
197
+ - Projected views is in the field named `projected_views`
198
198
  - ❌ **Suffix/qualifier variants of date columns** (e.g. an `_max` / `latest_` / `_date` form when the canonical column has neither). Date columns use bare names.
199
199
  - ❌ **Platform-name-prefixed ID forms** (e.g. a platform-name prefix when the canonical column uses a neutral `external_` prefix). See the column table for the actual ID column.
200
200
  - ❌ **Bare-noun forms without the table-prefix** (e.g. `name` instead of `channel_name`). This table prefixes its display fields with `channel_` to avoid SQL keyword collisions and ambiguity in joins.
@@ -237,7 +237,7 @@ thoughtleaders_adlink
237
237
  ├── owner_advertiser_id → auth_user.id
238
238
  ├── owner_publisher_id → auth_user.id
239
239
  ├── owner_sales_id → auth_user.id
240
- └── creator_profile_id → thoughtleaders_profile.id
240
+ └── advertiser_profile_id → thoughtleaders_profile.id
241
241
  ├── organization_id → thoughtleaders_organization.id
242
242
  └── profile_brands.profile_id → brand.id
243
243
 
@@ -270,15 +270,15 @@ JOIN thoughtleaders_channel ch ON s.channel_id = ch.id
270
270
 
271
271
  **Adlink → Brand name:**
272
272
  ```sql
273
- JOIN thoughtleaders_profile p ON a.creator_profile_id = p.id
273
+ JOIN thoughtleaders_profile p ON a.advertiser_profile_id = p.id
274
274
  JOIN thoughtleaders_profile_brands pb ON p.id = pb.profile_id
275
275
  JOIN thoughtleaders_brand b ON pb.brand_id = b.id
276
- -- NEVER: JOIN brand b ON b.id = a.creator_profile_id (different ID spaces, returns wrong data)
276
+ -- NEVER: JOIN brand b ON b.id = a.advertiser_profile_id (different ID spaces, returns wrong data)
277
277
  ```
278
278
 
279
279
  **Adlink → Organization:**
280
280
  ```sql
281
- JOIN thoughtleaders_profile p ON a.creator_profile_id = p.id
281
+ JOIN thoughtleaders_profile p ON a.advertiser_profile_id = p.id
282
282
  JOIN thoughtleaders_organization o ON p.organization_id = o.id
283
283
  ```
284
284
 
@@ -323,7 +323,7 @@ LIMIT 100 OFFSET 0
323
323
 
324
324
  **Sold deals this month:**
325
325
  ```sql
326
- SELECT id, price, purchase_date, ad_spot_id, creator_profile_id
326
+ SELECT id, price, purchase_date, ad_spot_id, advertiser_profile_id
327
327
  FROM thoughtleaders_adlink
328
328
  WHERE publish_status = 3
329
329
  AND purchase_date >= date_trunc('month', CURRENT_DATE)
@@ -346,7 +346,7 @@ SELECT a.id, a.price, a.publish_status, b.name AS brand, ch.channel_name
346
346
  FROM thoughtleaders_adlink a
347
347
  JOIN thoughtleaders_adspot s ON a.ad_spot_id = s.id
348
348
  JOIN thoughtleaders_channel ch ON s.channel_id = ch.id
349
- JOIN thoughtleaders_profile p ON a.creator_profile_id = p.id
349
+ JOIN thoughtleaders_profile p ON a.advertiser_profile_id = p.id
350
350
  JOIN thoughtleaders_profile_brands pb ON p.id = pb.profile_id
351
351
  JOIN thoughtleaders_brand b ON pb.brand_id = b.id
352
352
  WHERE a.id = 12345
@@ -11,7 +11,7 @@ being inflated.
11
11
  1. **Preferred:** `tl channels similar <id> --limit 24` (the recommender).
12
12
  Best niche match.
13
13
  2. **Fallback** (recommender empty): PG cohort —
14
- same `content_category` + `language`, `is_active`, `reach` within ±50%,
14
+ same `content_category` + `language`, `is_active`, `subscribers` within ±50%,
15
15
  `last_published` within 60 days, excluding the subject channel.
16
16
  3. For up to 12 peers, pull each peer's last 10 longforms via `tl db es`,
17
17
  require ≥5,000 aggregate views and ≥3 videos (skip dead peers).
@@ -36,7 +36,7 @@ deleting the cache file or calling `get_baseline(ch, refresh=True)`.
36
36
  If no usable peers at all (rare — niche too small / all peers dead), a generic
37
37
  English-tech floor is used (`like 2%, comment 0.25%`) and `source` is recorded
38
38
  as `fallback-generic` in the metrics so the report consumer knows the
39
- baseline was weak. Prefer widening the reach band over trusting this.
39
+ baseline was weak. Prefer widening the subscribers band over trusting this.
40
40
 
41
41
  ## Caveats
42
42
 
@@ -16,7 +16,7 @@ label only.
16
16
  | `A_per_video_outliers` | ≥⅓ of longforms >1.5σ below the channel's own like:view mean | One real audience produces consistent ratios; promoted videos don't. |
17
17
 
18
18
  Peer baseline: niche-matched (`tl channels similar`, fallback PG cohort:
19
- same content_category+language, active, reach ±50%, published <60d), median
19
+ same content_category+language, active, subscribers ±50%, published <60d), median
20
20
  of each peer's last-10-longform like/comment rates. Cached 30 days.
21
21
 
22
22
  ## Group B — view-curve time-series (`anomaly_detector.py`)
@@ -165,7 +165,7 @@ def main() -> None:
165
165
  {
166
166
  "id": c.get("id"),
167
167
  "name": c.get("channel_name"),
168
- "subscribers": c.get("reach"),
168
+ "subscribers": c.get("subscribers"),
169
169
  }
170
170
  for c in exc.candidates
171
171
  ],