Skip to content

Commit 6e9f491

Browse files
ccross2claude
andcommitted
cmo: drop @mrhaven_agent, migrate to per-account credentials
@mrhaven_agent suspended by X for inauthentic behavior (2026-04-03). Account abandoned without appeal. API app was registered under that account and is now revoked. Changes: - Remove mrhaven_agent from all config, scripts, and docs - Add per-account credential routing (ACCOUNT_TOKEN_MAP) in executor - Update daily-post.js to use X_SOVREN_ACCESS_TOKEN/SECRET - Update GitHub Actions workflow to new secret names - Simplify collector and hydrator to bearer-only auth - Fix pre-existing test bug (missing seen parameter) - Document Phase 7 decisions, rationale, trade-offs, and limitations in CMO-AUTOMATION-IMPLEMENTATION-PLAN.md Pipeline now operates on 2 accounts only with scheduled root posts. Automated reply/quote engagement disabled. New X Developer App under @TheCesarCross required before API access resumes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e3444f3 commit 6e9f491

File tree

12 files changed

+165
-80
lines changed

12 files changed

+165
-80
lines changed

.github/workflows/daily-post.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ jobs:
3636

3737
- name: Post next queued item
3838
env:
39-
TWITTER_API_KEY: ${{ secrets.TWITTER_API_KEY }}
40-
TWITTER_API_SECRET: ${{ secrets.TWITTER_API_SECRET }}
41-
TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }}
42-
TWITTER_ACCESS_SECRET: ${{ secrets.TWITTER_ACCESS_SECRET }}
39+
X_API_KEY: ${{ secrets.X_API_KEY }}
40+
X_API_SECRET: ${{ secrets.X_API_SECRET }}
41+
X_SOVREN_ACCESS_TOKEN: ${{ secrets.X_SOVREN_ACCESS_TOKEN }}
42+
X_SOVREN_ACCESS_SECRET: ${{ secrets.X_SOVREN_ACCESS_SECRET }}
4343
run: |
4444
if [ "${{ inputs.dry_run }}" == "true" ]; then
4545
node scripts/daily-post.js --dry-run

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,7 @@ node scripts/daily-post.js --add "text" --category thesis # Append new post
479479

480480
**D4: Credentials via secrets.env, not dotenv**
481481
- **Decision:** Cron sources `~/.claude/secrets.env` directly. No dotenv package for credential loading.
482-
- **Rationale:** Twitter API credentials (`TWITTER_API_KEY`, `TWITTER_API_SECRET`, `TWITTER_ACCESS_TOKEN`, `TWITTER_ACCESS_SECRET`) are already available via direnv in interactive sessions and via secrets.env for cron. Adding dotenv would duplicate existing infrastructure.
482+
- **Rationale:** X API credentials (`X_API_KEY`, `X_API_SECRET`, `X_SOVREN_ACCESS_TOKEN`, `X_SOVREN_ACCESS_SECRET`) are already available via direnv in interactive sessions and via secrets.env for cron. Adding dotenv would duplicate existing infrastructure.
483483

484484
### Voice Rules (enforced by daily-post.js)
485485

@@ -509,7 +509,7 @@ node scripts/daily-post.js --add "text" --category thesis # Append new post
509509
### Dependencies
510510

511511
- `twitter-api-v2` (devDependency) — Twitter API v2 client
512-
- Environment: `TWITTER_API_KEY`, `TWITTER_API_SECRET`, `TWITTER_ACCESS_TOKEN`, `TWITTER_ACCESS_SECRET`
512+
- Environment: `X_API_KEY`, `X_API_SECRET`, `X_SOVREN_ACCESS_TOKEN`, `X_SOVREN_ACCESS_SECRET`
513513

514514
---
515515

ops/cmo-automation/README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ CMO Automation (X/Twitter)
22

33
Purpose
44
- Internal, data-driven replacement for outsourced engagement operations.
5-
- Covers three coordinated accounts:
6-
- @TheCesarCross
7-
- @sovren_software
8-
- @mrhaven_agent
5+
- Covers two coordinated accounts:
6+
- @TheCesarCross (founder)
7+
- @sovren_software (brand)
98

109
What this does today
1110
1) Collect snapshot data from X API (profile + timeline)
@@ -16,9 +15,10 @@ What this does today
1615
6) Build execution report from approved actions (dry-run default)
1716

1817
Current mode
19-
- Quality-gated assisted automation with optional live execution.
20-
- Root posts can execute live.
21-
- Quote-style engagement is subject to X platform eligibility constraints and may fail with 403 when not in-thread/not mentioned.
18+
- Scheduled root posts only. No automated replies or quotes.
19+
- @mrhaven_agent was removed after X suspension for inauthentic behavior (2026-04-03).
20+
- API credentials are per-account (X_FOUNDER_* for @TheCesarCross, X_SOVREN_* for @sovren_software).
21+
- See reports/CMO-AUTOMATION-IMPLEMENTATION-PLAN.md Phase 7 for full decision log.
2222

2323
Structure
2424
- config/cmo_accounts.yaml
@@ -37,8 +37,9 @@ Structure
3737

3838
Quick start
3939
1) Export credentials (or source ~/.claude/secrets.env)
40-
- Required: X_API_KEY, X_API_SECRET, X_BEARER_TOKEN, X_ACCESS_TOKEN, X_ACCESS_TOKEN_SECRET
41-
- Script also maps from TWITTER_* variables.
40+
- App credentials: X_API_KEY, X_API_SECRET, X_BEARER_TOKEN
41+
- Per-account tokens: X_FOUNDER_ACCESS_TOKEN, X_FOUNDER_ACCESS_SECRET (for @TheCesarCross)
42+
- Per-account tokens: X_SOVREN_ACCESS_TOKEN, X_SOVREN_ACCESS_SECRET (for @sovren_software)
4243
2) Run:
4344
- python3 scripts/collect_x_data.py
4445
- python3 scripts/analyze_x_cmo.py

ops/cmo-automation/config/cmo_accounts.yaml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@ accounts:
1313
root_posts_per_day: 1
1414
replies_per_day: 10
1515
quote_posts_per_week: 3
16-
- handle: mrhaven_agent
17-
role: product-agent
18-
voice: guardian-utility
19-
target_mix:
20-
root_posts_per_day: 2
21-
replies_per_day: 6
22-
quote_posts_per_week: 2
23-
2416
analysis:
2517
lookback_days: 21
2618
baseline_label: fiverr-period

ops/cmo-automation/config/operating_policy.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@
2525
"mix_pct": {"root": 45, "reply": 40, "quote": 15},
2626
"daily_reply_cap": 10,
2727
"time_windows_est": ["10:00-11:00", "14:00-15:00", "19:00-20:00"]
28-
},
29-
"mrhaven_agent": {
30-
"role": "product-agent",
31-
"mix_pct": {"root": 60, "reply": 25, "quote": 15},
32-
"daily_reply_cap": 6,
33-
"time_windows_est": ["08:00-09:00", "13:00-14:00", "21:00-22:00"]
3428
}
3529
},
3630
"targeting": {

ops/cmo-automation/reports/CMO-AUTOMATION-IMPLEMENTATION-PLAN.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,89 @@ Status
176176
~/cDesign/sovren-website/ops/cmo-automation
177177
- Upgrade details, decision log, limitations, and remaining work:
178178
reports/CMO-QUALITY-UPGRADE-2026-04-02.md
179+
180+
---
181+
182+
## Phase 7 — Account Restructure and Credential Migration (2026-04-03)
183+
184+
Context
185+
- @mrhaven_agent suspended by X on 2026-04-02 for "inauthentic behaviors."
186+
- Root causes: unverified bot-labeled account (no blue check, no delegated admin, 4 followers),
187+
13 consecutive HTTP 403 failures from quote attempts to conversations the account was not
188+
part of, coordinated API usage across 3 accounts sharing one developer app.
189+
- The X Developer App was registered under @mrhaven_agent. Suspension revoked all API
190+
credentials (HTTP 401 on bearer-token read-only calls confirmed).
191+
- @TheCesarCross and @sovren_software survived due to blue check verification and delegated
192+
admin access — protective factors @mrhaven_agent lacked.
193+
194+
Decisions
195+
196+
D1. Abandon @mrhaven_agent without appeal
197+
- Rationale: account carried bot-labeled baggage, only 4 followers, no organic social graph.
198+
Appealing risks drawing further scrutiny to the coordinated account cluster.
199+
Product updates flow through @sovren_software instead.
200+
- Trade-off: lose the handle and any residual SEO value. X Premium subscription requires
201+
manual cancellation (X does not auto-cancel on suspension).
202+
- Expected benefit: clean break, no residual risk to surviving accounts.
203+
204+
D2. Register new X Developer App under @TheCesarCross
205+
- Rationale: strongest account (verified, 974 followers, established). App owner gets
206+
simplest auth flow for self-posting.
207+
- Trade-off: ties API infrastructure to the founder's personal account. If founder account
208+
is ever restricted, API access is lost again.
209+
- Expected benefit: API app inherits the trust signals of the founder account.
210+
211+
D3. Per-account access tokens with unified app credentials
212+
- Rationale: each account (@TheCesarCross, @sovren_software) gets its own OAuth access
213+
token pair. App-level credentials (API key, API secret, bearer token) are shared.
214+
This enables per-account credential routing in the execution layer.
215+
- Trade-off: more credentials to manage (7 vars vs 5). Scripts need account-to-token mapping.
216+
- Expected benefit: eliminates single-token-for-all-accounts pattern that contributed to
217+
coordinated behavior detection. Each account's posting is independently authenticated.
218+
219+
D4. Scheduled roots only — no automated replies or quotes
220+
- Rationale: the 13 consecutive 403 failures on quote attempts were the primary trigger
221+
signal. Reply/quote automation on accounts without organic conversation participation
222+
is inherently risky under X's authenticity rules.
223+
- Trade-off: reduced engagement volume and reach. No automated discovery of new audiences
224+
through reply threads.
225+
- Expected benefit: eliminates the engagement pattern that caused the suspension. Root-post
226+
scheduling is the safest automation mode under X's rules.
227+
228+
Implemented changes
229+
- secrets.env: removed dead mrhaven_agent credentials, added 7-variable per-account structure
230+
(X_API_KEY, X_API_SECRET, X_BEARER_TOKEN, X_FOUNDER_ACCESS_TOKEN, X_FOUNDER_ACCESS_SECRET,
231+
X_SOVREN_ACCESS_TOKEN, X_SOVREN_ACCESS_SECRET). Values empty pending new app creation.
232+
- collect_x_data.py: removed mrhaven_agent from account list, simplified to bearer-only auth.
233+
- execute_approved_queue.py: added ACCOUNT_TOKEN_MAP for per-account credential routing,
234+
set_account_tokens() called before each action execution.
235+
- hydrate_approved_queue.py: simplified to bearer-only auth, removed mrhaven_agent queries.
236+
- daily-post.js: updated to use X_SOVREN_ACCESS_TOKEN/SECRET for @sovren_software posting.
237+
- daily-post.yml: GitHub Actions secrets updated to new variable names.
238+
- cmo_accounts.yaml: removed mrhaven_agent entry.
239+
- operating_policy.json: removed mrhaven_agent account strategy.
240+
- CMO-ECOSYSTEM-MARKETING-BRIEF.md: removed mrhaven_agent messaging intent.
241+
- CLAUDE.md: updated credential references.
242+
- Tests: fixed pre-existing seen parameter bug, updated fixtures. 6/6 passing.
243+
244+
Drawbacks and known limitations
245+
- API keys are empty until new X Developer App is created manually at developer.x.com.
246+
All X API functionality (daily posts, data collection, CMO pipeline) is offline until then.
247+
- GitHub Actions secrets must also be updated manually in the sovren-software repo settings.
248+
- @sovren_software posting requires OAuth user tokens generated through the new app's auth
249+
flow — this is a separate manual step from creating the app itself.
250+
- The hydration layer still produces templated replies with "Specific to [keyword salad]"
251+
suffixes. This copy quality issue predates the restructure and needs a generator rewrite
252+
before reply automation could safely resume.
253+
- No circuit breaker in execute_approved_queue.py — if reply automation is re-enabled in
254+
the future, the script should abort after N consecutive failures to avoid triggering
255+
platform detection.
256+
- X Premium subscription on @mrhaven_agent must be cancelled manually (via App Store,
257+
Google Play, or X support depending on how it was purchased).
258+
259+
Remaining work to fully complete this phase
260+
1. Create new X Developer App at developer.x.com under @TheCesarCross (Read+Write permissions)
261+
2. Fill 7 credential values in ~/.engram/envrc/secrets/secrets.env
262+
3. Update 4 GitHub Actions secrets in sovren-software repo settings
263+
4. Run direnv reload, then verify: x-cli -j user get TheCesarCross
264+
5. Cancel @mrhaven_agent X Premium subscription

ops/cmo-automation/reports/CMO-ECOSYSTEM-MARKETING-BRIEF.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ This brief aligns all outbound messaging with the mission, architecture, and roa
4343

4444
## Account-level messaging intent
4545
- @sovren_software: thesis, architecture, milestones, category narrative.
46-
- @mrhaven_agent: concrete proof posture, verifiable outcomes, grounded operational commentary.
4746
- @TheCesarCross: founder context, strategic synthesis, conviction with specificity.
4847

4948
## Hard editorial rules for generated copy

ops/cmo-automation/scripts/collect_x_data.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,21 @@
1111
DATA = ROOT / "data"
1212
DATA.mkdir(parents=True, exist_ok=True)
1313

14-
ACCOUNTS = ["TheCesarCross", "sovren_software", "mrhaven_agent"]
14+
ACCOUNTS = ["TheCesarCross", "sovren_software"]
1515
MAX_TIMELINE = int(os.getenv("CMO_TIMELINE_MAX", "20"))
1616

1717

1818
def load_credential_mapping() -> None:
19-
# x-cli expects X_* variables. Existing environment may use TWITTER_*.
20-
mapping = {
21-
"X_API_KEY": os.getenv("X_API_KEY") or os.getenv("TWITTER_API_KEY"),
22-
"X_API_SECRET": os.getenv("X_API_SECRET") or os.getenv("TWITTER_API_SECRET"),
23-
"X_BEARER_TOKEN": os.getenv("X_BEARER_TOKEN") or os.getenv("TWITTER_BEARER_TOKEN"),
24-
"X_ACCESS_TOKEN": os.getenv("X_ACCESS_TOKEN") or os.getenv("TWITTER_ACCESS_TOKEN"),
25-
"X_ACCESS_TOKEN_SECRET": os.getenv("X_ACCESS_TOKEN_SECRET") or os.getenv("TWITTER_ACCESS_SECRET"),
26-
}
27-
missing = [k for k, v in mapping.items() if not v]
28-
if missing:
29-
raise SystemExit(f"Missing required credentials: {', '.join(missing)}")
30-
os.environ.update(mapping)
19+
"""Load X API credentials. Data collection uses bearer token only (read-only)."""
20+
bearer = os.getenv("X_BEARER_TOKEN")
21+
if not bearer:
22+
raise SystemExit("Missing required credential: X_BEARER_TOKEN")
23+
os.environ["X_BEARER_TOKEN"] = bearer
24+
# x-cli also needs app credentials for some endpoints
25+
for key in ("X_API_KEY", "X_API_SECRET"):
26+
val = os.getenv(key)
27+
if val:
28+
os.environ[key] = val
3129

3230

3331
def run_json(cmd: list[str], retries: int = 2):

ops/cmo-automation/scripts/execute_approved_queue.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,32 @@ def load_json(path: Path):
1818
return json.loads(path.read_text())
1919

2020

21+
ACCOUNT_TOKEN_MAP = {
22+
"TheCesarCross": ("X_FOUNDER_ACCESS_TOKEN", "X_FOUNDER_ACCESS_SECRET"),
23+
"sovren_software": ("X_SOVREN_ACCESS_TOKEN", "X_SOVREN_ACCESS_SECRET"),
24+
}
25+
26+
2127
def load_credential_mapping() -> None:
22-
mapping = {
23-
"X_API_KEY": os.getenv("X_API_KEY") or os.getenv("TWITTER_API_KEY"),
24-
"X_API_SECRET": os.getenv("X_API_SECRET") or os.getenv("TWITTER_API_SECRET"),
25-
"X_BEARER_TOKEN": os.getenv("X_BEARER_TOKEN") or os.getenv("TWITTER_BEARER_TOKEN"),
26-
"X_ACCESS_TOKEN": os.getenv("X_ACCESS_TOKEN") or os.getenv("TWITTER_ACCESS_TOKEN"),
27-
"X_ACCESS_TOKEN_SECRET": os.getenv("X_ACCESS_TOKEN_SECRET") or os.getenv("TWITTER_ACCESS_SECRET"),
28-
}
29-
missing = [k for k, v in mapping.items() if not v]
30-
if missing:
31-
raise SystemExit(f"Missing required credentials: {', '.join(missing)}")
32-
os.environ.update(mapping)
28+
"""Load X API app credentials. Per-account tokens are set before each action."""
29+
for key in ("X_API_KEY", "X_API_SECRET", "X_BEARER_TOKEN"):
30+
val = os.getenv(key)
31+
if not val:
32+
raise SystemExit(f"Missing required credential: {key}")
33+
os.environ[key] = val
34+
# Validate per-account tokens exist
35+
for account, (tok, sec) in ACCOUNT_TOKEN_MAP.items():
36+
if not os.getenv(tok) or not os.getenv(sec):
37+
raise SystemExit(f"Missing access tokens for @{account}: {tok}, {sec}")
38+
39+
40+
def set_account_tokens(account: str) -> None:
41+
"""Set X_ACCESS_TOKEN/SECRET for the given account before x-cli execution."""
42+
token_keys = ACCOUNT_TOKEN_MAP.get(account)
43+
if not token_keys:
44+
raise RuntimeError(f"No credentials configured for @{account}")
45+
os.environ["X_ACCESS_TOKEN"] = os.getenv(token_keys[0], "")
46+
os.environ["X_ACCESS_TOKEN_SECRET"] = os.getenv(token_keys[1], "")
3347

3448

3549
def flatten_approved_actions(review: dict) -> list[dict]:
@@ -91,6 +105,7 @@ def build_execution_plan(review: dict, live: bool = False) -> dict:
91105
"source": action,
92106
}
93107
if live and ready:
108+
set_account_tokens(action.get("account", ""))
94109
row.update(run_live(action))
95110
elif live:
96111
row.update({"status": "blocked"})

ops/cmo-automation/scripts/hydrate_approved_queue.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,15 @@ def load_json(path: Path):
4848

4949

5050
def load_credential_mapping() -> None:
51-
mapping = {
52-
"X_API_KEY": os.getenv("X_API_KEY") or os.getenv("TWITTER_API_KEY"),
53-
"X_API_SECRET": os.getenv("X_API_SECRET") or os.getenv("TWITTER_API_SECRET"),
54-
"X_BEARER_TOKEN": os.getenv("X_BEARER_TOKEN") or os.getenv("TWITTER_BEARER_TOKEN"),
55-
"X_ACCESS_TOKEN": os.getenv("X_ACCESS_TOKEN") or os.getenv("TWITTER_ACCESS_TOKEN"),
56-
"X_ACCESS_TOKEN_SECRET": os.getenv("X_ACCESS_TOKEN_SECRET") or os.getenv("TWITTER_ACCESS_SECRET"),
57-
}
58-
missing = [k for k, v in mapping.items() if not v]
59-
if missing:
60-
raise SystemExit(f"Missing required credentials: {', '.join(missing)}")
61-
os.environ.update(mapping)
51+
"""Load X API credentials. Hydration uses bearer token for tweet lookups."""
52+
bearer = os.getenv("X_BEARER_TOKEN")
53+
if not bearer:
54+
raise SystemExit("Missing required credential: X_BEARER_TOKEN")
55+
os.environ["X_BEARER_TOKEN"] = bearer
56+
for key in ("X_API_KEY", "X_API_SECRET"):
57+
val = os.getenv(key)
58+
if val:
59+
os.environ[key] = val
6260

6361

6462
def run_json(cmd: list[str], retries: int = 2):
@@ -227,7 +225,6 @@ def default_resolver(target_user: str | None, account: str) -> dict | None:
227225
account_queries = {
228226
"TheCesarCross": "AI founders OR agentic workflow OR product distribution",
229227
"sovren_software": "AI automation OR workflow systems OR developer tools",
230-
"mrhaven_agent": "agent memory OR orchestration OR evals",
231228
}
232229

233230
if target_user:

0 commit comments

Comments
 (0)