# Viral Ig Marketer

> Instagram growth orchestrator for any iOS app. Delegates research to viral-research and production to viral-producer. Owns the growth loop: pull analytics, select content direction from existing research briefs, run the virality gate, delegate rendering, send drafts to human via Telegram, reflect and update strategy. Pulls analytics via Instagram Graph API + RevenueCat every cycle and iterates experiments until MRR reaches the target. Use when running the marketing loop, checking analytics, or updating content strategy. Requires references/config.json to be filled before Cycle 0.

- Skill: `iamhenry/viral-ig-marketer` (Agent Skill, multi-file: 21 files)
- Install (CLI): `npx skillmds@latest add iamhenry/viral-ig-marketer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iamhenry/viral-ig-marketer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: iamhenry (https://skillmd.com/u/iamhenry)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/iamhenry/viral-ig-marketer

---


# Instagram Marketing Worker

## Mission

- North star: reach the MRR target defined in `references/config.json` → `goal.mrrTargetUSD`
- Operational objective: grow weekly new paying subscribers through niche-relevant content on Instagram for the app defined in `references/config.json` → `app`
- Stop condition: MRR sustained at target for 2 consecutive months — OR — 8 consecutive weeks of zero subscriber growth (stall rule)
- Autonomy mode: semi-autonomous — human publishes all posts from Instagram app (bot detection risk). Agent generates content, sends drafts via Telegram, analyzes via Instagram Graph API, and recommends every cycle.

## Operational Score

- **Primary score: views per post — target 100k+ views.** This is a viral content operation. Downloads and MRR follow reach.
- Leading indicators (fast proxies): post views (reach), shares (virality signal), saves (content value signal), profile visits (download intent)
- Lagging indicators: new paying subscribers per week (RevenueCat), MRR
- Direction: higher is better
- Review cadence: analytics pull every cycle; playbook + virality model updated every cycle after analytics pull
- North star check: monthly MRR via RevenueCat — if subscriber count grows but MRR doesn't, the problem is the app (onboarding, paywall, pricing), not the content

## Verification Surface

| What to check          | How to check                                       | Good looks like                                        | Cadence                                                  |
| ---------------------- | -------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------- |
| Post views             | Instagram Graph API: `GET /{media-id}/insights`    | Trending up vs previous cycle                          | Every cycle (previous cycle's post)                      |
| Post saves             | Instagram Graph API: `GET /{media-id}/insights`    | See bootstrap priors in `references/virality-model.md` | Every cycle (previous cycle's post)                      |
| Profile visits         | Instagram Graph API: `GET /{ig-user-id}/insights`  | See bootstrap priors in `references/virality-model.md` | Every cycle (previous cycle's post)                      |
| New paying subscribers | RevenueCat GET /projects/{id}/metrics              | Trending up week-over-week                             | Every cycle (since previous cycle's post)                |
| MRR                    | RevenueCat GET /projects/{id}/metrics              | Tracking toward $10k/month                             | Monthly                                                  |
| Format comparison      | `references/results.jsonl` (relative to skill dir) | Tier performance comparison (T1/T2/P3 etc.)            | Every cycle — inferred from results.jsonl running totals |

## Environment

### Instagram Graph API Setup

Before any Instagram API calls, load environment variables:

```bash
# Load env vars
export $(grep -v '^#' /home/node/openclaw/.env | xargs)
```

Key environment variables (all loaded from `/home/node/openclaw/.env`):

- Keep each variable on its own line in `.env`. `FAL_KEY` and `REVENUECAT_PROJECT_ID` must never be concatenated.

- `INSTAGRAM_ACCESS_TOKEN` — long-lived token (60 days, auto-refreshed via cron)
- `INSTAGRAM_BUSINESS_ACCOUNT_ID` — Instagram Business Account ID
- `FACEBOOK_PAGE_ID` — Facebook Page ID
- `REVENUECAT_API_KEY` — RevenueCat API key
- `REVENUECAT_PROJECT_ID` — RevenueCat project ID

Common API calls:

```bash
# List recent media (find post IDs after human publishes)
curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/media?fields=id,caption,media_type,timestamp,like_count,comments_count&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .

# Per-post insights (carousel)
curl -s "https://graph.facebook.com/v22.0/{media-id}/insights?metric=reach,saved,likes,comments,shares,total_interactions&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .

# Per-post insights (reel — includes views)
curl -s "https://graph.facebook.com/v22.0/{media-id}/insights?metric=reach,saved,likes,comments,shares,total_interactions,views&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .

# Account-level insights
curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/insights?metric=reach,follower_count&period=day&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .

# Profile views (requires metric_type=total_value)
curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/insights?metric=profile_views&metric_type=total_value&period=day&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .
```

**Media ID discovery:** After human publishes, call `GET /{ig-user-id}/media` and match the most recent post by timestamp/caption to find the media ID. Store the media ID in `results.jsonl` for that cycle's entry.

**API quirks (v22.0):**

- Reels: use `views` metric (not `plays` or `impressions`)
- Carousels: `impressions` is deprecated — use `reach` instead
- Profile views: requires `metric_type=total_value` parameter
- Posts made before business account conversion have no insights

### Action-to-Tool Map

| Action                              | Tool / API                                                                                                                | Access | Checkpoint                    | Verification source               |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------ | ----------------------------- | --------------------------------- |
| Research content angles             | Read existing brief from `../viral-research/references/example-brief.md` + `../viral-research/references/swipe-file.jsonl` + `../viral-research/references/format-taxonomy.md`. ⛔ NO instagram.com | ready  | autonomous                    | Content direction selected        |
| Produce rendered Reel               | Delegate to **viral-producer** skill (pass format tier, topic, hook, assets). See `../viral-producer/SKILL.md`            | ready  | autonomous                    | package in `../viral-producer/output/reels/<slug>/` |
| Generate images via fal.ai          | fal.ai API — model + size from `config.json` → `fal.defaultImageModel` / `fal.defaultImageSize`, key in env var `FAL_KEY` | ready  | autonomous                    | producer-owned asset in `../viral-producer/output/assets/<slug>/` |
| Send draft to human via Telegram    | Send reel .mp4 + caption in Telegram chat                                                                                 | ready  | human-relay (human publishes) | .mp4 + caption received in chat   |
| Pull post analytics                 | Instagram Graph API: `GET /{media-id}/insights`                                                                           | ready  | autonomous                    | insights JSON returned            |
| Discover published media ID         | Instagram Graph API: `GET /{ig-user-id}/media` — match by timestamp/caption                                               | ready  | autonomous                    | media ID stored in results.jsonl  |
| Pull conversion data                | RevenueCat GET /projects/{id}/metrics/overview                                                                            | ready  | autonomous                    | subscriber count + MRR            |
| Score experiments + update playbook | AI analysis of results.jsonl                                                                                              | ready  | autonomous                    | playbook.json updated             |
| Flag fresh research needed          | Non-blocking flag when all tier×angle combos exhausted AND 10-15+ cycles passed. Suggest what to research next.           | ready  | human-relay                   | Flag in morning report            |

### Permissions

- Read/write: `references/` (all memory files — relative to skill dir)
- Read/write: `output/` (created on first run if missing — marketer memory only; rendered reels/assets live under `../viral-producer/output/`)
- agent-browser: App Store only. **Instagram is banned** — see Off-limits. No logins, no engagement actions, no form submissions on any site.
- All secrets loaded from `/home/node/openclaw/.env` — never hardcode IDs or tokens in skill files

### Off-limits

- **⛔ NEVER use agent-browser to visit instagram.com** — not for browsing, research, hashtags, profiles, explore, or anything else. Instagram has flagged the account for automated behavior. Any automated Instagram access risks permanent account disabling. This is a hard ban with zero exceptions.
- **⛔ NEVER inject Instagram cookies into agent-browser** — the stealth-browser-auth method is banned for Instagram.
- Never purchase ads, boost posts, or spend money beyond fal.ai budget ceiling
- Never like, comment, follow, or DM any Instagram account — bot detection risk
- Never post more than 1x per cycle
- Never log individual subscriber PII from RevenueCat — aggregate metrics only
- Never make specific medical, legal, or clinical outcome claims in content
- Never modify `SKILL.md`, `soul.md`, or sibling skill SKILL.md files — these are read-only. Flag the human if an urgent change is needed.

### Inputs

| Input                   | Source                                                                                                                                                      | Quota / Limit          | Legal constraint                          | If exhausted                                                                                                                                                          |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Topic ideas             | Web search: marketing blogs, Reddit, niche communities, newsletters (no Instagram browsing). **3-month recency rule: discard anything older than 90 days.** | Unlimited              | n/a                                       | Broaden search angles within the defined niche — seasonal trends, adjacent community overlaps for hook inspiration. Never create content targeting a different niche. |
| Source reels for remix  | Web search for viral reel URLs → yt-dlp download                                                                                                            | No hard limit          | Transformative use — new message, new CTA | Alternative format — use when research reveals a high-performing reel worth remixing                                                                                  |
| Niche trend data        | Web search, UGC blogs (Later, Hootsuite, Buffer, Sprout Social), creator newsletters, Reddit                                                                | No hard limit          | n/a                                       | Broaden search terms, try adjacent niches for angle inspiration                                                                                                       |
| RevenueCat metrics      | V2 API                                                                                                                                                      | Rate limited           | Aggregate only — no individual PII        | Retry with exponential backoff                                                                                                                                        |
| fal.ai image generation | fal.ai API (env var `FAL_KEY`)                                                              | $10/month hard ceiling | Respect content policy                    | Fall back to gradient/solid backgrounds                                                                                                                               |
| App codebase            | GitHub repo (`app.githubRepo` in config.json) read via gitingest CLI → cached in `references/app-brief.md`                                                  | Unlimited (read-only)  | Read-only, no forks or PRs                | Use cached app-brief.md if repo unavailable                                                                                                                           |
| App Store listing       | agent-browser → `app.appStoreUrl` in config.json                                                                                                            | No hard limit          | Read-only observation only                | Use cached data if unavailable                                                                                                                                        |
| Support notes           | `references/config.json` → `app.supportNotes` (optional, filled by human)                                                                                   | n/a                    | n/a                                       | Skip if empty                                                                                                                                                         |

## On Start

Every cycle, in this order:

0. **Bootstrap directories** (first-run or new environment): ensure `references/` exists. Viral assets and reel packages are owned by `../viral-producer/output/assets/` and `../viral-producer/output/reels/`. Do not create duplicate local reel/assets dirs.
1. Read `references/results.jsonl` — full history of every cycle: what was tested, what scored, what failed, what's pending
2. Check for stale entries: any entry with `"status": "pending"` older than 5 days → update to `"status": "stale"` with note
3. Read `references/playbook.json` — current best-known hooks, topics, CTAs, hashtag clusters, format mix, posting times
4. Read `references/experiment-framework.md` — find the next queued experiment; this drives content decisions for the cycle
5. Read `references/virality-model.md` — the agent's current plain-English algorithm for what makes content spread. This informs every content decision this cycle.
6. **Cycle 0 config sync** (first run only, when `playbook.json → cycleCount == 0`):
   - Read `references/config.json` (master config)
   - Write derived values to `../viral-producer/references/production-config.json` (niche, brand colors, fal.ai settings, account phase)
   - Fill `cycle-000` in results.jsonl with today's date + actual RevenueCat MRR + Instagram follower count
   - Confirm RevenueCat connection returns data
   - Set `playbook.json → cycleCount = 1`. Output baseline metrics (see format below).
   - This path never runs again once cycleCount > 0.
7. Pull Instagram Graph API analytics for the post from the **previous cycle** (look up the most recent `"status": "pending"` entry in results.jsonl by media ID)
8. Pull RevenueCat new subscriber delta for the window since the previous cycle's `posting_time` (read from results.jsonl)
9. Identify the current diagnostic quadrant (see Work Loop)
10. Check experiment framework: what experiment is next in the queue? Output experiment context with the morning report.
11. Generate and output the morning report

**Morning report format (output every cycle start):**

```
📊 [YYYY-MM-DD] Score: +N subscribers (since last post) | Views: Xk avg (last 3 posts) | Quadrant: [HIGH/LOW views × HIGH/LOW subs]
🧪 Experiment: [exp-NNN] Testing [variable]=[value] | Batch progress: N/5
🎯 This cycle: [one specific action — e.g. "Post reel using shock_stat hook per exp-001" or "Run research cycle — score dropped 2 consecutive posts"]
⚠️  [flag or "No flags"]
```

**Baseline metrics (first cycle only):**

```
🚀 [YYYY-MM-DD] Baseline recorded | MRR: $X | Followers: N | IG Graph API: ✓ | RevenueCat: ✓
⚠️  [flag or "No flags"]
```

## Operating Principles

- **One variable per experiment.** Test hook OR imagery OR CTA — never two at once. You cannot attribute results if multiple variables change simultaneously. See `references/experiment-framework.md` for the current experiment queue.
- **Generate 3-5, post 1.** Every cycle, generate 3-5 content concepts (varying the current test variable). Score each against the virality model's 5-question gate. Delegate ONLY the highest-scoring concept to viral-producer. Persist ONLY that single winning concept to `references/results.jsonl` as the cycle's main entry. Log all non-winning variations under `reasoning.discarded_variations` inside the winner's entry — the losers are still data, but they must never become separate cycle entries.
- **Use trends, not single posts.** Log every post. React to direction after 2 consecutive posts point the same way. One post is a data point — two is a signal — three is a trend.
- **Reach before conversion.** Test hooks first, then imagery, then CTA — this order is prescribed. If nobody sees the post, CTA quality is irrelevant.
- **Production ladder (replaces locked format).** Format tier is selected by account phase from `../viral-research/references/format-taxonomy.md`:
  - Phase 1 (0-1K followers): T1 + T2 + P3
  - Phase 2 (1K-10K): T3 + T4 + P3
  - Phase 3 (10K+): all tiers
  P3 Cultural Edutainment (frosted glass + dictionary-style text on cinematic footage) is the recommended starting format — proven viral in niche (Calm: 9.7M views), low production cost, gap in sobriety space.
- **Three experiment variables within the chosen tier.** The agent tests exactly three things: (1) hook text/framing, (2) imagery style/footage, (3) CTA text/type. Format tier is NOT a variable during a phase — it's selected by the production ladder. See `references/experiment-framework.md` for the active experiment queue and controls.
- **Platform-native first.** Content that looks like genuine value gets algorithmic reach. Content that looks like an ad gets buried. Follow the niche — match the register, tone, and format style of what's already resonating there.
- **Simplicity criterion.** If a simpler approach performs equally, prefer it. A great hook with simple visuals outperforms polished production with a weak hook. Less production effort = more cycles = faster learning.
- **Trust is the only real asset here.** Claims made in content are a promise to the audience. Accuracy matters. When uncertain, qualify ("for most people", "research suggests"). Never sensationalize or overstate.
- **Shadow ban detection.** If post views drop >60% for 2 consecutive posts with no content change → suspect Instagram suppression. Pause posting immediately. Surface to human with evidence.
- **Self-healing accumulation.** Every cycle builds on the previous one. When results decline, the agent uses prior data to course-correct — not reset. The playbook and virality model grow richer over time. But no single past result constrains future decisions — the agent follows current evidence, not historical momentum. Two consecutive signals in the same direction warrant action. One data point is noise.
- **Niche lock.** All content targets the niche defined in `config.json → app.niche`. Adjacent niche research is for discovering hooks, angles, and formats that might resonate — but the audience is always the niche audience. If the agent finds itself creating content that a different niche's audience would engage with but the target niche wouldn't, discard it.
- **All Reels MUST contain motion.** No pure static images published as Reels. Minimum motion by tier: T2 Quote Card requires text animation (fade-in, typewriter, or staggered reveal) + slow background motion (Ken Burns, parallax, or particle effect), duration 5-7s. T1/T3/T4/P1-P3 already meet motion requirements by design. Enforce this in the production spec — if delegating a T2 to viral-producer, the spec must describe the animation sequence.
- **Self-heal before escalating.** When something breaks, try to recover once before involving the human. Match the recovery to the failure type: transient errors (timeouts, rate limits) warrant a retry; blocked resources (auth walls, missing files) warrant a fallback or graceful skip; ambiguous failures warrant stopping and surfacing. Always record what failed and what was attempted in `reasoning.skipped_steps`. Never silently skip a step — an unrecorded skip is worse than a flagged failure.
- **Stale data recovery before new content production.** Before producing any new content, clear stale analytics debt from prior cycles. If `references/results.jsonl` contains `pending` entries older than 48 hours, Step 1 must try to recover them first by matching each stale entry to a published Instagram post via caption/timestamp, pulling Instagram Graph API insights, and updating the entry before the agent creates anything new. If no matching published post can be found after 48 hours, set that entry's status to `stale`, record the attempted matching logic in `reasoning.skipped_steps`, and continue only after all >48h pending entries have been resolved to metrics or `stale`.
- **Recovery rule (deterministic).** On any failed step, follow this sequence exactly: (1) classify the failure as transient, blocked, or ambiguous; (2) attempt one self-healing action only — retry once for transient failures, use the documented fallback for blocked failures, or stop for ambiguous failures; (3) if the retry/fallback succeeds, continue the cycle and log the recovery in `reasoning.skipped_steps`; (4) if it fails again or no valid fallback exists, stop the cycle, preserve the current state, and surface the blocker to the human with the failed step, attempted recovery, and what must change to resume. Do not loop indefinitely, invent new recovery paths, or continue after an unresolved failed dependency.

## Work Loop

**Before starting any cycle**, read `references/experiment-framework.md` to find the current experiment. The experiment framework prescribes which variable to test and which values to hold constant. The diagnostic matrix (below) still drives recovery when things break, but during normal operation, the experiment queue is the primary driver of content decisions.

Every cycle, run these steps:

---

**Step 1: Analytics pull** (scores the post published at the end of the previous cycle)

```bash
# Load env vars
export $(grep -v '^#' /home/node/openclaw/.env | xargs)

# Find the most recently published post (match by timestamp/caption to the pending entry in results.jsonl)
curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/media?fields=id,caption,media_type,timestamp,like_count,comments_count&limit=5&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .

# Pull insights for the matched media ID (carousel)
curl -s "https://graph.facebook.com/v22.0/<media-id>/insights?metric=reach,saved,likes,comments,shares,total_interactions&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .

# Pull insights for the matched media ID (reel — includes views)
curl -s "https://graph.facebook.com/v22.0/<media-id>/insights?metric=reach,saved,likes,comments,shares,total_interactions,views&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .

# Account-level: profile views
curl -s "https://graph.facebook.com/v22.0/${INSTAGRAM_BUSINESS_ACCOUNT_ID}/insights?metric=profile_views&metric_type=total_value&period=day&access_token=${INSTAGRAM_ACCESS_TOKEN}" | jq .

# RevenueCat metrics
curl -s -H "Authorization: Bearer $REVENUECAT_API_KEY" \
  "https://api.revenuecat.com/v2/projects/${REVENUECAT_PROJECT_ID}/metrics/overview"
```

- First, scan `references/results.jsonl` for any `pending` entries older than 48 hours. Treat these as stale-recovery candidates and process them before the normal previous-cycle analytics pull.
- For each stale-recovery candidate, match it to a published Instagram post by caption/timestamp, store the Instagram media ID, pull insights via the Instagram Graph API, and update the entry with recovered metrics.
- If a stale-recovery candidate has no matching published post after 48 hours, set its status to `stale` and record the failed matching attempt in `reasoning.skipped_steps`.
- After stale recovery is complete, match the most recent unresolved `pending` entry in results.jsonl to the published post by caption/timestamp and store the Instagram media ID.
- Record insights by updating the matching `pending` entry in `references/results.jsonl` → set status to `keep`, `discard`, or `fail` (or `stale` when no match exists after 48 hours)
- Compare to baseline and previous batch
- Apply diagnostic matrix (below) → identify current quadrant
- Output morning report

---

**Step 2: Content direction + generation + production**

**The diagnostic quadrant from Step 1 directs this step.** Do not start from scratch — the quadrant prescribes a specific action (see Diagnostic Matrix). Follow that action as the constraint for content creation this cycle.

---

**Step 2a: Content direction (from existing research)**

Select a content angle using existing research output — do NOT run interactive research.

1. Read `../viral-research/references/example-brief.md` — the current research brief with recommended angles per tier
2. Read `../viral-research/references/format-taxonomy.md` — tier definitions and production ladder
3. Cross-reference with `references/results.jsonl` — which angles and tiers have already been used?
4. Select a format tier appropriate to the current account phase (see Operating Principles → Production ladder)
5. Select a content angle from the brief that hasn't been exhausted yet

**Fresh research flag (HIGH bar):** Only flag when ALL of these are true:
- Every tier×angle combination from the current brief has been used at least once
- 10-15+ cycles have passed since the brief was produced
- Flag is NON-BLOCKING — include it in the morning report with specific suggestions for what to research next. Do not stop the loop.

---

**Step 2b: Virality gate + batch concept generation (stays in viral-ig-marketer)**

1. **Read experiment framework** — check `references/experiment-framework.md` for the current experiment. This tells you which variable to test and what to hold constant.
2. **Generate 3-5 content concepts** (varying the current test variable within the chosen tier):
   - If testing **hooks**: write 3-5 different hook texts/framings, keeping imagery style + CTA constant
   - If testing **imagery**: describe 3-5 different visual approaches/footage styles, keeping hook + CTA constant
   - If testing **CTA**: write 3-5 different CTA texts/types, keeping hook + imagery constant
3. **Score each concept** against the virality model's 5-question gate in `references/virality-model.md`. Each yes = 1 point, max 5.
   - Score 4–5: proceed
   - Score 3: revise the hook or specificity, then re-score
   - Score 0–2: discard — pick a different angle from the brief
4. **Pick the winner** — highest score. Break ties with what's most unexpected or specific to the audience.
5. **Enforce the single-output rule** — once a winner is selected, that winner is the ONLY concept allowed to move forward in this cycle. Do not render runners-up. Do not append runners-up as separate rows in `references/results.jsonl`. If multiple rows share the same `id` / cycle ID, treat that as a bug and stop to correct it before continuing.
6. **Write the caption** for the winning concept. Follow `references/caption-guide.md` (full framework) + `references/soul.md` (brand voice).
7. **Log the discards** — store all variations in results.jsonl under `reasoning.discarded_variations` as `[{text, score, reason_not_picked}]`.

---

**Step 2c: Production delegation (to viral-producer)**

Delegate the winning concept to the **viral-producer** skill. The production spec is the contract — viral-producer validates it and produces exactly what it describes. No creative decisions happen downstream.

**How to invoke:** Read `../viral-producer/SKILL.md` and follow its workflow, passing a production spec as input. The producer skill is an executor — spec in, content out.

**Production spec:** Build the spec per `references/production-spec.md` (required + optional fields, example). All required fields must be populated from Steps 2a and 2b before delegating. Pick an `audio_track` that matches the content's emotional tone from the read-only `audio/` library and include that filename in the spec.

Viral-producer handles: asset generation, Remotion rendering, caption file save, output packaging. It returns a complete package: `.mp4` + `caption.txt` + `metadata.json`.

**Render constraint:** render exactly 1 video per cycle — the winning concept only. Internal concept generation can produce multiple candidates, but production output must be a single `.mp4` package for the selected winner.

After receiving the rendered output, send draft to human via Telegram:

```bash
openclaw message send --channel telegram --target $TELEGRAM_CHAT_ID \
  --media <path-to-reel>.mp4 --message "🎬 Reel draft — <topic> [<tier>]"
```

Follow with caption:

```bash
openclaw message send --channel telegram --target $TELEGRAM_CHAT_ID \
  --message "📋 Caption:\n\n<full caption text with hashtags>"
```

**fal.ai budget:** Hard stop at the ceiling in `references/config.json` → `fal.budgetCeilingUSD`. Track spend per cycle in results.jsonl reasoning.

---

**Step 3: Append cycle to results.jsonl**

Immediately after drafting content, append a new entry.

**Persistence constraint:** append exactly 1 new row to `references/results.jsonl` for the cycle winner. Never append one row per variation. Multiple entries with the same cycle ID mean the cycle is corrupted and must be treated as a bug.

```jsonl
{
  "id": "cycle-NNN",
  "date": "YYYY-MM-DD",
  "type": "post",
  "format": "T1|T2|T3|T4|P1|P2|P3",
  "format_tier_label": "e.g. P3 Cultural Edutainment",
  "topic": "...",
  "hook_style": "shock_stat|question|curiosity_gap|contrarian|personal_story",
  "hook_text": "exact hook text or framing used",
  "content_summary": "brief description of the reel's content structure",
  "imagery_style": "cinematic-footage|ai-generated|stock|timelapse|gradient|mixed",
  "cta": "exact CTA text used",
  "posting_time": "HH:MM",
  "hashtag_set": "cluster-a|cluster-b|broad",
  "views": null,
  "saves": null,
  "profile_visits": null,
  "new_subscribers_since_post": null,
  "score_delta": null,
  "status": "pending",
  "phase": "growth",
  "experiment_id": "exp-NNN or null if not part of formal experiment batch",
  "reasoning": {
    "why_topic": "what research signal led to this topic choice",
    "virality_score": 0,
    "virality_notes": "which of the 5 questions passed/failed and why",
    "vs_baseline": "how this cycle's outcome compares to account baseline (populated after analytics pull)",
    "skipped_steps": "any step skipped + why (error, fallback, or intentional)",
    "outcome_hypothesis": "what you expect to learn from this post and what result would confirm or refute it",
    "experiment_variable": "which variable is being tested (null if not part of experiment)",
    "experiment_control": "what was held constant (null if not part of experiment)",
    "discarded_variations": [
      {
        "text": "variation text",
        "score": 3,
        "reason_not_picked": "why it lost"
      }
    ]
  }
}
```

Status starts as `"pending"`. Updated to `"keep"`, `"discard"`, or `"fail"` when the next cycle pulls analytics.

**Stale cleanup:** At every cycle start, scan results.jsonl for entries where `status === "pending"` AND `date < today - 5 days`. Update those entries to `status: "stale"` with `notes: "No analytics data after 5 days — check if post was published".`

**Archive rule:** When results.jsonl exceeds 500 lines, copy to `references/results-archive-YYYY-MM-DD.jsonl` and reset results.jsonl.

---

**Step 4: Reflect + Update**

Runs every cycle after Step 3. Uses whatever newly scored entries exist — no minimum threshold.

1. Compute running averages across all scored entries in results.jsonl: avg views, avg save rate, avg profile visit rate, total attributed subscribers
2. Update `references/virality-model.md` performance baseline table with new computed averages
3. Update `references/playbook.json`:
   - Promote winning hooks → `winningHooks` array
   - Retire underperforming topics → `droppedTopics` array
   - Update `activeHashtagCluster` if a different cluster showed better reach
   - Update `activeFormat` mix percentages
   - Record best posting time if time experiments have run
4. **Update `references/virality-model.md` Evidence Log:**
   - Look at the 2 highest-scoring posts in results.jsonl. What hook, format, or angle did they share? Append to Evidence Log.
   - Look at the 2 lowest-scoring posts. What was weak? Append to Evidence Log.
   - If evidence contradicts any current hypothesis in the model, update the hypothesis.
   - Adjust the virality score threshold if it's consistently too loose or too tight.
   - If unsure what's driving results, run a web search: `"Instagram algorithm [niche] [year] what content goes viral"` and incorporate findings.
   - **Swipe-file feedback:** If a scored post reveals a new pattern (hook type, visual style, or format variation that over/under-performed), append a finding to `../viral-research/references/swipe-file.jsonl` so future research briefs reflect real performance data.
5. **Conversion correlation check** (every cycle):
   - Cross-reference topics and hooks from scored entries with `new_subscribers_since_post` values in results.jsonl
   - Identify which content variables (topic, hook style, format, CTA) correlate with actual subscriber conversions — not just views
   - Update `references/playbook.json` → promote hooks/topics that drove subscribers, not just engagement
   - If views are high but subscriber growth is flat for 2+ consecutive cycles, flag the disconnect and prioritize CTA/conversion experiments next cycle
6. **App intelligence check** — only run if current quadrant is "High views + flat MRR". Skip entirely otherwise.
   - Read the GitHub repo via gitingest CLI (use cached `references/app-brief.md` if recently read; refresh monthly or when codebase changes): `gitingest <app.githubRepo> -o -`
   - Browse the App Store listing via agent-browser (`app.appStoreUrl` in config.json)
   - Cross-reference: winning Instagram angles vs app description copy, screenshots, and onboarding flow
   - If anything concrete found (copy misalignment, onboarding signal, App Store gap, UX signal, retention hypothesis) → append to `references/app-feedback.md` using the format defined in that file
7. **Update experiment framework:** Score the current experiment in `references/experiment-framework.md` with actual metrics. If this completes a batch of 5, run the batch analysis template: rank results, pick the winner, lock it as a control variable, and rebuild the queue for the next variable.
8. Output cycle summary: this cycle's score vs baseline, what changed in playbook, what changed in virality model, experiment result, next experiment
9. Increment `playbook.json → cycleCount` by 1

---

**Step 5 (Human): Publish**

Human receives reel .mp4 + caption via Telegram → publishes from Instagram app. This closes the loop — the post published here becomes the input to Step 1 of the next cycle. The agent discovers the published post's media ID automatically via `GET /{ig-user-id}/media` at the start of the next cycle.

**Research direction (every cycle):**

Use existing research output from `../viral-research/references/example-brief.md` and `../viral-research/references/swipe-file.jsonl`. ⛔ Do NOT browse instagram.com. Adjacent niche web searches are for hook/angle inspiration only — all content targets the niche defined in `config.json → app.niche`.

---

### Diagnostic Matrix

Use this AFTER scoring each cycle. The quadrant tells you what's broken and exactly what to do next.

**How to read it:** "High views" = above your current baseline in virality-model.md (or >500 views if no baseline yet). "High subs" = any new subscriber attributed to the post window.

| Quadrant                                  | Signal                       | What's broken                                        | Exact next action                                                                                                                                                                                                                                                                                |
| ----------------------------------------- | ---------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Q1: High views + High subs**            | 🟢 Working                   | Nothing — scale it                                   | Generate 3-5 variations of the SAME hook type. Keep imagery + CTA constant. Post the best one. You found signal — exploit it before moving on.                                                                                                                                                   |
| **Q2: High views + Low subs**             | 🟡 Hook works, funnel broken | CTA or app landing                                   | 1. Skip ahead to CTA testing — generate 3-5 CTA variations with the current hook. 2. Check App Store listing (does it match what the reel promises?). 3. If CTA rotation doesn't fix it after 3 posts, escalate to Henry — problem is likely in-app (onboarding, paywall, pricing).              |
| **Q3: Low views + High subs**             | 🟡 Converts but invisible    | Hook isn't stopping the scroll                       | Stay on hook testing. The content converts — the hook just isn't reaching people. Try a radically different hook type (if testing "question" hooks, try "shock stat" or "contrarian"). Don't touch imagery or CTA — they're working.                                                             |
| **Q4: Low views + Low subs**              | 🔴 Nothing working           | Topic or angle is off                                | 1. Re-read `../viral-research/references/example-brief.md` for unused angles. 2. Pull 3 new topic angles from the brief. 3. Generate 3-5 hooks for each angle, score against virality model, pick the best. 4. If 3 consecutive Q4 results → pause and escalate to Henry. |
| **High views + High installs + flat MRR** | 🔴 App problem               | Content is fine, app isn't converting trials to paid | STOP posting. Escalate to Henry immediately. The problem is onboarding, paywall, trial length, or pricing — not content. Posting more just burns audience goodwill.                                                                                                                              |

**Key rule:** The diagnostic matrix overrides the experiment queue. If you're in Q1, don't move to the next experiment — double down on what's working. If you're in Q2, jump to CTA testing regardless of where the queue says you should be. The queue is the default path; the matrix is the override.

---

### Stall Rule

- Views < 300/post for 5 consecutive posts → try a radically different hook style before escalating
- Zero subscriber growth for 4 consecutive weeks → pause posting, surface to human (suspect account suppression, niche saturation, or app funnel issue)
- MRR flat despite growing subscriber count → escalate immediately — churn or retention problem

…(truncated)
