---
name: merlin-social
description: Use when the user wants Discord notifications, Slack posts, email marketing (Klaviyo audit, campaign creation, cold outbound, flows, deliverability, RFM segmentation), SMS marketing (Postscript/Attentive/Klaviyo SMS, TCPA compliance, A2P 10DLC, flows, campaign cadence), Reddit organic prospecting/drafting/posting, Threads posts, competitor ad intelligence via Meta Ad Library, or any non-paid social/community channel. Covers the 6 essential DTC email flows with revenue-mix benchmarks, post-Apple-MPP engagement benchmarks (click rate as real signal), RFM segmentation, deliverability basics (SPF/DKIM/DMARC, Google/Yahoo 2024 requirements), subject + preheader rules, SMS compliance (TCPA quiet hours, 10DLC, STOP keywords) + essential SMS flows + campaign cadence, first-touch/linear/time-decay attribution models, Reddit's 7-layer compliance preflight, and the weekly competitor ad scan with hook extraction.
owner: ryan
bytes_justification: 27KB — owned channels (email + SMS + Reddit organic + competitor intel) share attribution models, compliance requirements, and deliverability/cadence reasoning. Splitting email and SMS into separate skills would duplicate the RFM segmentation, attribution, and benchmark tables; SMS flows mirror email flows by design so they belong side-by-side. The Klaviyo template-bulk-upload + flows-bulk-import routing block lives here (not in merlin-content) because it's about pushing prepared HTMLs and email automations into the backend, not authoring creative — pairing them with email flows + RFM benchmarks keeps the agent on a single mental model. Postscript automation API (added 2026-04-29) shipped the bulk-import-flow surface with full token-swap + TCPA gate documentation here; v1.20.7 (2026-04-29 evening) added the symmetric Klaviyo Flows API with CAN-SPAM gate. Hard-capped at 30KB before splitting.
---

# Owned & Earned Channels

## Discord (`mcp__merlin__discord`)

| Action | Key params |
|---|---|
| `setup` | (changes channel) |
| `post` | `slackMessage` (reused field name) |

**Connect:** `platform_login({platform: "discord"})` — opens Discord's bot authorization, user picks server, bot auto-discovers text channels.

**Auto-posts:** When Discord is connected, Merlin posts automatically when ads are published, paused, scaled, or new creatives generated. No manual trigger needed for these.

## Email Marketing (`mcp__merlin__email`)

| Action | Key params |
|---|---|
| `audit` | `brand` — existing flows, lists, campaigns, missing essentials, recommendations |
| `revenue` | `brand` — attribution-based revenue per flow |

If Klaviyo isn't connected, tell the user to click the Klaviyo tile or paste an API key.

### Attribution models — always state which one

| Model | Use for |
|---|---|
| **First-touch** | "Which channel brought them in" — 100% credit to the first email that touched the buyer |
| **Linear** | Nurture-heavy journeys where every touch mattered — equal credit across every touch |
| **Time-decay** | **Default for per-flow ROI** — half-life 7 days, recent touches weighted higher |

Never mix models in the same report. "Is welcome series working?" → time-decay. "Is email worth investing in?" → first-touch.

### Essential DTC email flows (+ revenue mix benchmarks)

Healthy flow revenue mix (% of total email revenue, Klaviyo aggregated DTC data):

| # | Flow | Structure | Share of flow rev |
|---|---|---|---|
| 1 | **Welcome Series** | 3 emails / 5 days: welcome + brand story → bestsellers → social proof + first-purchase offer | 5–10% |
| 2 | **Abandoned Cart** | 3 emails: reminder (1hr) → social proof (24hr) → urgency/discount (48hr) | 15–25% ← largest flow |
| 3 | **Browse Abandonment** | 2 emails: "Still looking?" (4hr) → related products (24hr) | 3–5% |
| 4 | **Post-Purchase** | 3 emails: thank you → how to use → review request (14d) | 5–10% |
| 5 | **Win-back** | 3 emails: "We miss you" (60d) → bestsellers (75d) → final discount (90d) | 3–5% |
| 6 | **Sunset** | 2 emails: "Still interested?" (90d no opens) → final chance (120d) | <1% — purpose is deliverability, not revenue |

Campaigns (one-to-many sends) should contribute 40–55% of email revenue; flows 45–55%. Over-reliance on campaigns means automated money is being left on the table.

### Engagement benchmarks (post-Apple MPP — click rate is the real signal)

**Warm list:** 25–35% open / 2–3% click / <0.3% unsubscribe. Since Apple Mail Privacy Protection, opens auto-inflate — **use click rate as the real engagement signal.**

**Cold outbound (B2B):** 50%+ open / 3%+ reply / 1%+ positive reply. Diagnostic order: opens → subject + sender + preheader; replies → body + CTA; positive replies → ICP + offer.

**Diagnostic order (warm list):** opens → envelope (subject + sender + preheader); clicks → body + CTA; unsubs → list hygiene or send frequency.

### List segmentation — RFM (Recency, Frequency, Monetary)

Segment before every send. Blasting every campaign to the full list is the fastest way to tank deliverability.

- **Engaged-30** — opened OR clicked in last 30 days → primary campaign segment
- **Engaged-90** — engaged in last 90 days → secondary segment, lower frequency
- **VIP** — top 10% lifetime spend → exclusive previews, early access, higher frequency OK
- **At-risk** — engaged 91–180 days ago → move to win-back flow
- **Dormant / Sunset** — no engagement 180+ days → suppress from campaigns, sunset flow only
- **Recent purchasers (90 days)** — suppress from new-customer acquisition offers (avoid promo resentment)

### Send cadence

- **Newly subscribed (0–14 days):** welcome flow only; no campaigns.
- **Engaged list:** 2–4 campaigns per week; drop to 1 during poor engagement windows.
- **Re-engagement:** 1 campaign every 2 weeks until they engage or hit sunset threshold.
- **Never send twice in 24h** without a compelling reason.

### Deliverability (non-negotiable basics)

- **Authentication:** SPF, DKIM, and **DMARC with `p=quarantine` minimum** (Google/Yahoo require DMARC since Feb 2024 for bulk senders). Verify via MXToolbox / Google Postmaster Tools monthly.
- **Dedicated sending subdomain:** send from `mail.brand.com`, not `brand.com` — protects the root if reputation dips.
- **Warm-up new IPs/domains:** first 30 days, send only to Engaged-30, ramp volume 25% daily.
- **List hygiene:** remove hard bounces immediately; remove 3× soft bounces; never buy lists.
- **Complaint rate target:** <0.1%. At 0.3% Gmail throttles; at 0.5% you're flagged.
- **Google Postmaster Tools:** register and monitor weekly — Gmail is ~45% of US inbox.

### Subject line + preheader (free real estate)

- **Subject line:** 30–50 chars (mobile-first). Question / specific number / curiosity gap beats clever pun. A/B test at least one element per send if list >10k.
- **Preheader:** 40–100 chars; never auto-pulled from body. Complements the subject, doesn't repeat it.
- **Sender name:** first-name-from-brand ("Ryan at Merlin") beats corporate ("Merlin Team") on warm lists. Keep consistent.

### Email template rules

- 600px wide (Klaviyo standard). Table-based HTML with inline styles.
- Use the real logo PNG (`logo/logo.png`), never AI-generated text.
- Use real product photos from Shopify CDN, never AI-generated product shots.
- Brand colors are exact hex codes from `brand.md` → Brand Colors section.
- **Dark-mode preview** — test in both light and dark Gmail; transparent-background logos fail on dark backgrounds.
- **Plain-text alternative** — every HTML email needs a plain-text MIME part; missing one drops inbox placement.

## SMS Marketing

SMS drives 10–20% of revenue for DTC brands with SMS live (Postscript / Attentive / Klaviyo SMS published data). Zero SMS is a real revenue gap. Connect via Klaviyo SMS (if Klaviyo is the ESP) or recommend Postscript / Attentive as specialty platforms.

### Compliance (TCPA US + CTIA carriers) — blocking requirements

- **Express written consent** before the first message. Collect via: checkout checkbox (separate from email opt-in, NEVER pre-checked), SMS keyword opt-in (`TEXT SHOP TO 12345`), pop-up with TCPA disclosure.
- **Disclosure text at opt-in** must include: brand name, "consent not required for purchase," message frequency ("4 msgs/mo"), "Msg & data rates may apply," "Reply STOP to unsubscribe / HELP for help," link to terms + privacy.
- **STOP / UNSUBSCRIBE / CANCEL / END / QUIT / OPT OUT** must all work — handled automatically by Postscript/Attentive/Klaviyo. Honor within 24h.
- **Quiet hours:** send only 8am–9pm in the recipient's local timezone. Schedules must be TZ-aware.
- **A2P 10DLC registration** required since 2023 for US SMS. Unregistered brands get throttled or blocked. Handled by the SMS platform but must be completed during setup.
- **Toll-free numbers** require verification and have stricter content rules; short codes (5–6 digits) have highest throughput, highest cost.

### Essential SMS flows (mirror email, not duplicate)

| # | Flow | Structure | Share of SMS flow rev |
|---|---|---|---|
| 1 | **Welcome** | 2 msgs / 3 days: welcome + offer → reminder if unused | 10–15% |
| 2 | **Abandoned Cart** | 2 msgs: 30min reminder → 24hr urgency | 30–40% ← largest |
| 3 | **Browse Abandonment** | 1 msg: 2hr "Still looking?" | 5–10% |
| 4 | **Post-Purchase** | 2 msgs: shipping update → review request 14d | 5–10% |
| 5 | **Back-in-stock** | 1 msg on restock | 5–10% |
| 6 | **Win-back** | 1 msg at 60d + 1 at 90d | 3–5% |

### SMS campaigns

- **Frequency:** 4–8 campaigns per month max. SMS is high-intimacy; over-sending nukes the list.
- **Length:** stay under 160 chars (1 segment) when possible — each segment costs. Hook, first name, 1 link, 1 CTA.
- **MMS** (with image) lifts CTR 25–50% but costs 3–4× per segment. Reserve for launches and hero campaigns.
- **Link handling:** always use the platform's branded short-link (`short.brandname.co`) — bit.ly is flagged as carrier spam.
- **Send timing:** 10am–12pm and 3pm–6pm local TZ perform best for DTC. Avoid Mondays (inbox pile-up) and Friday evenings.
- **Personalization:** first name + product name + order detail. Generic SMS ("Shop now!") underperforms personalized 2–3×.

### SMS-specific metrics

- **CTR** healthy: 5–15% (vs email's 1–3%) — SMS intent is much higher.
- **Unsubscribe per send:** <2% healthy; >3% = message is off (segment, offer, or frequency).
- **Revenue per recipient:** $0.50–$2.00 per send for DTC.
- **Click-to-conversion** should beat email — if it doesn't, the landing page isn't mobile-ready (see `merlin-analytics`).

### Postscript SMS — full automation API (`mcp__merlin__postscript`)

Postscript exposes Automations (their flow analog) as a fully-scriptable API — unlike Klaviyo Flows, which are dashboard-only. This lets Merlin import a complete SMS flow setup from a JSON manifest in one tool call.

| Action | Purpose | Key params |
|---|---|---|
| `status` | Connection + shop metadata | — |
| `subscribers` | List subscribers | `limit` |
| `campaigns` | List campaigns w/ stats | `limit` |
| `keywords` | List opt-in keywords | — |
| `automations` | List configured flows | — |
| `automation-get` | Full body of one flow | `automationId` |
| `automation-create` | Create one flow inline | `automationFlow`, `activate` |
| `automation-update` | Patch flow metadata | `automationId`, `patchBody` |
| `automation-delete` | Delete one flow | `automationId` |
| `automation-activate` / `deactivate` | Toggle status | `automationId` |
| `automation-steps` | List steps in a flow | `automationId` |
| `automation-step-create` | Add a step | `automationId`, `automationStep` |
| `automation-step-update` | Edit a step | `automationId`, `stepId`, `patchBody` |
| `automation-step-delete` | Remove a step | `automationId`, `stepId` |
| `bulk-import-flow` | Manifest → flows | `manifestPath`, `brand`, `activate?`, `forceReimport?` |

#### `bulk-import-flow` — the morning SMS-setup verb

Manifest path MUST live under `assets/brands/<brand>/` (the binary refuses arbitrary filesystem reads). Manifest shape:

```json
{
  "flows": [
    {
      "name": "POG / Welcome Series",
      "trigger": {"type": "subscriber_added_to_list", "list_id": "..."},
      "steps": [
        {"type": "delay", "duration_seconds": 0},
        {"type": "send_message", "body": "Welcome to POG! Use POG10 for 10% off: {{COUPON_URL}} — {{UNSUB_REPLY}}"},
        {"type": "delay", "duration_seconds": 86400},
        {"type": "send_message", "body": "Hi {{FIRST_NAME}}, your POG10 code expires tonight."}
      ]
    }
  ]
}
```

**Token swap (auto-rendered before send):**

| Generic | Postscript-native |
|---|---|
| `{{FIRST_NAME}}` | `{{first_name}}` |
| `{{LAST_NAME}}` | `{{last_name}}` |
| `{{COUPON_CODE}}` | `{{coupon_code}}` |
| `{{COUPON_URL}}` | `{{coupon_url}}` |
| `{{SHOP_URL}}` | `{{shop_url}}` |
| `{{UNSUB_REPLY}}` | `Reply STOP to unsubscribe` (literal string) |

**TCPA gate (refuses, never auto-fixes):** every flow runs through `CheckFlowTCPA` BEFORE any HTTP call. Failures HALT the import for that flow:

1. **First send_message must contain STOP opt-out language** (literal "Reply STOP" / "Text STOP" / "STOP to unsubscribe", or use `{{UNSUB_REPLY}}` which renders to the same).
2. **Trigger must be on the opt-in allowlist** (`subscriber_added_to_list`, `keyword`, `checkout`, `checkout_completed`, `product_purchased`, `cart_abandoned`, `browse_abandoned`, `back_in_stock`). Manual-segment triggers are refused — they carry no consent guarantee.
3. **Brand must have `postscriptTenDLCID` configured** (US 10DLC campaign id).
4. **Flow must have at least one `send_message` step.**

Failures surface in the report with the specific rule + message — Ryan needs to see exactly what blocked. Auto-stripping STOP language and shipping anyway is the literal worst outcome ($500–$1,500/msg statutory damages).

#### Routing hints (postscript-specific)

- "upload my SMS flows" / "import these Postscript automations" / "bulk import flows" → `postscript({action: "bulk-import-flow", manifestPath: "assets/brands/<brand>/sms/flows.json", brand: "<brand>"})`. Always preview the manifest count + first flow's name before running. Confirm with the user.
- "create a welcome SMS flow" / "make a cart-abandonment automation" → `postscript({action: "automation-create", automationFlow: {...}, activate: false})`. Default `activate: false` — let the user review in the Postscript dashboard before going live.
- "pause my POG SMS welcome" / "deactivate the back-in-stock flow" → `postscript({action: "automation-deactivate", automationId: "..."})`.
- After a `bulk-import-flow` succeeds, list the per-flow `dashboard_url` values from the report so the user can spot-check each new automation in Postscript's UI.

The bulk-import report is structured: `{imported, blocked, failed, total, results: [{name, automation_id, status, issues, dashboard_url, ...}]}`. Summarize as: *"Imported 7 of 8 flows. Blocked: 'Browse Abandonment' (TCPA: first message missing STOP opt-out). Created flows are drafts — review in Postscript dashboard, then activate or pass `activate: true` on next import."*

<!-- Updated 2026-05-10 (v1.22.0 RSI fixes B001/B002/B004/D004/D005/E003) -->
## Klaviyo (`mcp__merlin__klaviyo`)

`performance` · `lists` · `campaigns` · `templates-list` · `template-get` · `template-create` · `template-update` · `template-delete` · `templates-bulk-upload` · `flow-performance` · `flow-message-performance` · `metric-aggregate`

### Flow performance routing (numbers, not playbook)

When the user wants email/SMS flow analytics — "how are my flows doing", "what's my recovered revenue", "which subject line is winning", "how many checkouts last week" — route to the dedicated reporting actions instead of paraphrasing from `performance`:

| Question style | Tool call |
|---|---|
| "how are my flows doing" / "recovered revenue" / "is abandoned cart working" | `klaviyo({action: "flow-performance", brand})` |
| "which subject line is winning" / "best message in welcome series" | `klaviyo({action: "flow-message-performance", brand})` |
| "how many checkouts last week" / "aggregate add-to-cart count" | `klaviyo({action: "metric-aggregate", brand})` |

Cross-link: `merlin-analytics` owns the same routing hints from the analytics-question side (flow ROI as a perf metric). This skill owns the *playbook* (which flows to build, RFM segmentation, deliverability); `merlin-analytics` owns the *numbers*. Either path lands on the same Klaviyo actions.


**Review solicitation pattern** (daily scheduled task): find fulfilled orders 5–7 days old (via `shopify-orders`), draft a Klaviyo campaign per order (max 3/day to avoid spam): product photo + "How are you liking your {product}?" + review link. Publish as draft — user or `merlin-optimize` approves.

### Template bulk upload

When the user says any of: *"upload my email templates"*, *"import these HTMLs to Klaviyo"*, *"bulk upload my welcome flow"*, *"push these emails to Klaviyo"*, *"I have N email HTMLs to upload"* → call `klaviyo({action: "templates-bulk-upload", brand: "<brand>", dir: "<path>", nameTemplate: "<brand> / <flow> / {basename}", applyTokens: true})`.

**Always confirm the directory and the count** before running. The directory MUST be inside `assets/brands/<brand>/` — paths outside are rejected by the binary. `nameTemplate` substitutes `{basename}` with each file's stem; if omitted, the bare basename is used. `applyTokens` defaults to `true` and translates generic placeholders (`{{UNSUB_URL}}` → `{{ unsubscribe }}`, `{{ FIRST_NAME }}` → `{{ first_name|default:'there' }}`, `{{ EMAIL }}` → `{{ person.email }}`, `{{COMPANY_NAME}}` / `{{COMPANY_ADDRESS}}` from `brand.md`); set `applyTokens: false` if the HTML was already authored against Klaviyo's Django syntax.

The response is a structured envelope `{total, succeeded, failed, perFile: [{filename, name, templateId | error}]}`. Report exact counts to the user — *"uploaded 49 of 51, failed: welcome-3.html (422), abandoned-7.html (422)"* — never paraphrase. Do NOT confabulate template IDs that the binary did not return.

**Klaviyo Flows ARE programmatic** (corrected in v1.20.7 — the prior v1.20.1 release notes + this skill incorrectly claimed Flows were UI-only). After bulk-upload of templates, you can wire the full flow topology via the Flows API. v1.20.8 fixed a body-shape bug introduced in v1.20.7: the create payload was inferred from prose in Klaviyo's docs and got the trigger enum, action structure, and field names wrong; the corrected version conforms to the OpenAPI spec at revision `2026-04-15`.

The bulk-import path is `klaviyo({action: "flows-bulk-import", brand, manifestPath, forceReimport?})`. Manifest lives at `assets/brands/<brand>/email/flows.json` and references uploaded templates by their `template_id` from a prior `templates-bulk-upload` pass. Manifest shape:

```json
{
  "manifest_version": "1.0",
  "brand": "pog",
  "flows": [
    {
      "name": "POG / Welcome Series",
      "status": "draft",
      "trigger": {"type": "list_added", "list_id": "L_MAIN"},
      "steps": [
        {"type": "delay", "duration_seconds": 0},
        {"type": "send_email", "subject": "Welcome to POG", "from_name": "POG", "from_email": "hi@trypog.co", "template_id": "T_WELCOME_1"},
        {"type": "delay", "duration_seconds": 86400},
        {"type": "send_email", "subject": "Day 1: how it works", "from_name": "POG", "from_email": "hi@trypog.co", "template_id": "T_WELCOME_2"}
      ]
    },
    {
      "name": "POG / Cart Recovery",
      "trigger": {"type": "ecommerce_started_checkout", "metric_id": "01H8...PLACED_ORDER"},
      "steps": [...]
    }
  ]
}
```

Step types: `delay {duration_seconds}`, `send_email {subject, preheader, from_name, from_email, template_id, body?}`, `wait_until {time_of_day, timezone}`, `branch {condition}`. Single-flow create is `klaviyo({action: "flow-create", flowBody: {...}})`; status flip is `klaviyo({action: "flow-update-status", flowId, status: "draft"|"manual"|"live"})`.

### Trigger types — manifest enum vs Klaviyo enum

The manifest exposes friendly names; the binary translates to Klaviyo's actual API enum (`list` / `segment` / `metric` / `date`) before POSTing. **Metric-backed triggers REQUIRE a `metric_id`** (Klaviyo identifies metrics by ULID, not name).

| Manifest `trigger.type` | Klaviyo enum | Required field | Notes |
|---|---|---|---|
| `list_added` / `added_to_list` | `list` | `list_id` | Subscriber added to the named list |
| `segment_added` | `segment` | `list_id` (segment ULID) | Field name is `list_id` for both list and segment triggers |
| `ecommerce_placed_order` | `metric` | `metric_id` | Klaviyo's "Placed Order" metric — fetch the ID via `klaviyo({action: "performance"})` |
| `ecommerce_started_checkout` | `metric` | `metric_id` | "Started Checkout" metric ID |
| `viewed_product` | `metric` | `metric_id` | "Viewed Product" metric ID |
| `custom_event` | `metric` | `metric_id` | Any custom-tracked event — find the ID in Klaviyo Metrics |
| `back_in_stock` | `metric` | `metric_id` | Routes via the back-in-stock metric event (the dedicated `low-inventory` trigger has different required fields and isn't what most users mean) |
| `profile_subscribed_marketing` | `metric` | `metric_id` | "Subscribed to List" / "Marketing Consent Updated" metric |
| `birthday` | `date` | (none — defaults supplied) | Uses `ProfilePropertyDateTrigger` with `date_profile_property: "Birthday"` and 9:00 trigger time |

Workflow for any metric-triggered flow (cart abandonment, post-purchase, browse abandonment, etc.):

1. Run `klaviyo({action: "performance", brand})` to get the metric list with IDs.
2. Find the relevant metric (e.g. `Placed Order` → `01HABC...`).
3. Put the ID in the manifest's `trigger.metric_id` field for that flow.
4. Run `flows-bulk-import`.

If you skip step 3 the binary refuses the build with a clear error: *"klaviyo: metric-backed trigger 'ecommerce_placed_order' requires metric_id (Klaviyo identifies metrics by ULID, not name…)"* — don't try to invent a metric ID; always pull it from `performance`.

**CAN-SPAM gate** (the email equivalent of Postscript's TCPA gate): every flow must have an unsubscribe token in inline bodies (`{{ unsubscribe }}` or `{% unsubscribe %}`), a physical mailing address (inline OR resolvable from `brand.md` `Address:`), and `trigger.type` on the documented-consent allowlist (`list_added`, `segment_added`, `profile_subscribed_marketing`, `ecommerce_placed_order`, `ecommerce_started_checkout`, `viewed_product`, `custom_event`). Failures are surfaced verbatim — never auto-fix; refuse the import and tell the user which rule fired on which step.

The dependency chain: `templates-bulk-upload` first → grab `template_id`s from the response → put them in the flows manifest → `flows-bulk-import`. Klaviyo's per-minute cap is plan-tier-global, so flows + templates share the same 6s sequential pacing.

## Reddit Organic

Organic-growth pipeline for finding pain-point threads, clustering them, drafting quality-gated replies, and (optionally) posting under heavy compliance preflight. Same OAuth + `redditAccessToken` as Reddit Ads.

| Action | Purpose | Key params |
|---|---|---|
| `reddit-prospect-scan` | Search Reddit for relevant threads | `brand`, `keywords`, `subreddits` (comma list or blank = sitewide), `scanLimit` |
| `reddit-prospect-draft` | Cluster pain points + draft quality-gated replies | `brand`, `keywords`, `subreddits`, `draftLimit`, `draftDryRun` |
| `reddit-prospect-post` | Submit ONE approved reply | `brand`, `threadId`, `subreddit`, `draftBody`, `approved: true` |
| `reddit-shadowban-check` | Authed /me vs unauth /user probe | `brand` |

### Quality gate (`reddit-prospect-draft`)

Every draft passes through:
1. **Structural gate** — 40–300 words, no banned self-promo phrases, no shouting, no URLs unless sub allows, must reference at least one thread-content token.
2. **Optional AI gate** — Gemini scores authenticity/helpfulness/thread-fit/compliance/overall; fails closed on network error.

Drafts below threshold are dropped, not surfaced.

### Compliance preflight (`reddit-prospect-post`, auto mode)

7 layers before `/api/comment`:
1. Account ≥30 days old
2. Combined karma ≥100
3. Max 5 posts / 24h
4. ≥120 min between posts to same subreddit
5. ≥300s between any two posts
6. Body not posted in last 72h (SHA-256 of normalized body)
7. No cached shadowban

Any block writes a `reddit_posts_<ts>.json` envelope with a friendly reason.

### `redditPostMode` config

| Value | Behavior |
|---|---|
| `"auto"` (default) | Full preflight → `/api/comment`. Gated on account age/karma/shadowban. |
| `"draft-only"` | Skip preflight + write. Reply saved to `results/reddit_draft_<ts>.txt` for manual paste. Zero shadowban risk, zero API writes. The legitimate path for fresh accounts / warm-up / shadowban recovery. |

When `auto` blocks on account-age / karma / shadowban, Merlin **saves the draft to disk anyway** as a fallback and surfaces `suggestion` hinting to switch to `draft-only`. When `auto` blocks on cadence or dedup (real spam signals), the draft is NOT saved — user waits or rewrites.

### Approval model

Every `reddit-prospect-post` surfaces an Electron approval card with sub + reply preview (200 chars). `approved: true` in the cmd envelope is defense-in-depth — the binary refuses to proceed without it.

## Threads (`mcp__merlin__threads`)

Connect via `platform_login({platform: "threads"})`. Post via threads action. Rate-limited separately from Meta Ads.

## Competitor Intelligence

### Discovery (onboarding + weekly digest)

**Step 1 — Infer from brand.** Read `brand.md` + product catalog → niche → WebSearch:
- `"<niche> brand" site:shopify.com`
- `"<category>" -[brand name]`
- Related brands on Instagram/TikTok in same niche

Find 5–8 competitors. For each: name, URL, product overlap, price range (cheaper / same / premium).

**Step 2 — Save to `assets/brands/<brand>/competitors.md`:**

```markdown
# Competitors — <Brand Name>
Discovered: YYYY-MM-DD

## Direct (same niche + price)
- **<Brand>** — <url> — <category>, $X-$Y

## Adjacent (overlapping audience)
- **<Brand>** — <url> — <category>, $X-$Y

## Aspirational (where the brand could grow toward)
- **<Brand>** — <url> — <category>, $X-$Y
```

**Step 3 — Weekly Ad Scan** (if `metaAccessToken` configured):

```json
{"action": "competitor-scan", "blogBody": "Madhappy,Pangaia,Teddy Fresh", "imageCount": 5}
```

Queries Meta Ad Library (UK/EU transparency — most US DTC brands run there too). Returns: `ad_creative_bodies`, `ad_creative_link_titles`, CTA captions, snapshot URL, publisher platforms.

Then:
1. Read each ad's copy → extract hooks, CTAs, offers.
2. WebFetch snapshot URLs to describe the visual creative.
3. Compare to recent ads.
4. Log insights to `memory.md` under `## Competitor Signals`.

**No Meta token** → fall back to WebSearch for competitor news.

**Limit:** Ad Library returns only ads that ran in UK/EU. Purely domestic US brands won't appear. Rate limit: 200 calls/hour.

### What to look for

- **Hook patterns**: "POV:", "Wait till you see...", "This changed everything"
- **Format trends**: video vs static, UGC vs polished, length
- **Script style**: conversational or scripted (read transcriptions)
- **Offer patterns**: free shipping, % off, BOGO, bundles
- **Running duration**: ads running 30+ days are proven winners — study these closely
- **New products**: anything we haven't seen before

### How this feeds back

- Heavy competitor video testimonials → try talking-head mode (route to `merlin-content`)
- Competitors running sales → consider value-focused angle instead of discounting
- Trending hook style → adapt for our brand voice
- Long-running competitor ads → reference their structure in our scripts
- Save winning patterns to `memory.md ## Competitor Signals`

## Slack

### Slack File Upload — 3-step (the ONLY method that works)

1. `GET https://slack.com/api/files.getUploadURLExternal?filename=X&length=Y` (query params, NOT JSON body)
2. `POST` raw file bytes to returned `upload_url`
3. `POST https://slack.com/api/files.completeUploadExternal` with JSON: `{files: [{id, title}], channel_id, initial_comment}`

`files.upload` and `files.uploadV2` are deprecated and will fail.

### Bot scopes

`channels:read`, `channels:join`, `files:read`, `files:write`, `chat:write`. `files:write` alone will upload but silently fail to share to channels.

### Posting rules

Post to both Slack + Discord if both configured. Activity notifications (ad published, killed, scaled) are automatic.

## Routing hints

- "connect discord" / "set up discord" / "discord channel" → `platform_login({platform: "discord"})`
- "email flows" / "klaviyo" / "welcome series" / "abandoned cart" → `email({action: "audit"})`
- "upload my email templates" / "import these HTMLs to klaviyo" / "bulk upload emails" → `klaviyo({action: "templates-bulk-upload", brand, dir, nameTemplate})` (confirm dir + count first)
- "wire my klaviyo flows" / "import my email automations" / "set up the welcome series in klaviyo" / "build the abandoned cart flow" → `klaviyo({action: "flows-bulk-import", brand, manifestPath: "assets/brands/<brand>/email/flows.json"})` (run AFTER `templates-bulk-upload` so templates exist; CAN-SPAM gate refuses non-compliant flows verbatim)
- "scan competitors" / "what are competitors running" → `competitor-scan` + Ad Library process
- "reply on reddit" / "post to reddit organically" → Reddit organic pipeline
- "post to slack" / "share to team" → Slack 3-step upload or `chat.postMessage`
