# Threads Auto Reply

> Threads affiliate auto-reply. v10 stealth: 9router LLM + burst-rest scheduler + sleep window + log-normal delay. See refs/stealth-v10-architecture.md, refs/9router-model-selection.md, refs/moderation-false-positives.md, scripts/threads_human_behavior.py, scripts/threads_content_gen.py. v9 templates fallback. Triggers: 'reply threads', 'komen threads', 'nimbrung'.

- **Type:** Skill
- **Install:** `agentstack add skill-sekolah76-syadagentic-threads-auto-reply`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Sekolah76](https://agentstack.voostack.com/s/sekolah76)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Sekolah76](https://github.com/Sekolah76)
- **Source:** https://github.com/Sekolah76/syadagentic/tree/main/arsenal-skills/skills-lengkap/affiliate/threads-auto-reply
- **Website:** https://github.com/Sekolah76/syadagentic

## Install

```sh
agentstack add skill-sekolah76-syadagentic-threads-auto-reply
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Threads Affiliate Auto-Reply (v7.0)

**⚠️ SHADOWBAN PROTOCOL**: Read `references/shadowban-verification.md` for details on how to intercept silent API failures and verify posts via page reloads.

**⚠️ SCRIPT EXECUTION ENVIRONMENT:**
- Uses Playwright Headless via `~/.hermes/scripts/threads_reply_v6.py`. DO NOT RUN RAW `requests` SCRIPTS. Threads GraphQL is locked down. Handle timeout issues by running job in background (`cronjob action='run'`).
- Originally set to 1 during fragile period (2026-06-05) due to account hard blocks on attempt 2.
- 7-day clean streak achieved (2026-06-09) but SYADAGENTIC decided to keep at 1 reply permanently.
- **Cron interval: `every 150m` (2.5 jam, updated 2026-06-15) — previously `every 2h`.**
- **Reply 1:** Affiliate targeting (existing strategy — fashion, skincare, tech, lifestyle)
- ~~Reply 2-3~~ — **DISABLED permanently.** Re-enable only on explicit SYADAGENTIC approval.
- Script `REPLIES_TARGET = 1` and cron interval `every 2h`.
- **🚨 ACCOUNT SAFETY: When account is at risk of suspension → PAUSE ALL Threads automation immediately (SYADAGENTIC directive 2026-06-12).** This includes: Reply cron, Post cron, Cookie Refresh cron. Resume only when user confirms safe. Pattern: `cronjob(action='pause', job_id='...')` for all 3 jobs.
- **Filter Rules (SYADAGENTIC directive 2026-06-11, updated 2026-06-15):**
  - Skip if post NOT relevant to target category (`is_post_relevant()`, v6.2)
  - Skip if "MUA" or "makeup artist" in post (word boundary regex)
  - Skip if `s.shopee.co.id` in ORIGINAL POST content
  - Skip if shopee link in THREAD/UTAS chain
  - OK if shopee link only in COMMENTS from other users (not the post creator)
  - **Skip posts older than 12 hours** (SYADAGENTIC directive 2026-06-12) — `` tags return ISO datetime (`2026-06-12T01:57:49.000Z`), parse with `datetime.fromisoformat()` and compare to `datetime.now(timezone.utc)`. Also handles relative text (`2h`, `3m`, `1d`, `2j`). Script: `parse_relative_time()` in `threads_reply_v6.py`.
- **Shopee check targets ORIGINAL POST only**, not comments section

**⚠️ CONFIG SYNC: Script's `REPLIES_TARGET` MUST match safety limit!** (Verified 2026-06-05 21:34)
- Skill doc says "1 reply/cron" but script default was 2 → 2nd attempt predictably hard blocked.
- When reducing safety limit, update BOTH: (1) this skill doc AND (2) script's `REPLIES_TARGET` constant.
- For v6 script (`threads_reply_v6.py`), check `REPLIES_TARGET` near top of file.
- For cron prompt customization: include explicit "only attempt 1 reply" instruction.

**Previous limits:** 5x → reduced to 2x (2026-06-04) → increased to 3x with mixed strategy (2026-06-04) → reduced to 2x affiliate-only (2026-06-05, Reply 3 politics caused full escalation).

```
0. KILL CHROME: `pkill -9 -f "Google Chrome" 2>&1 || true; sleep 2` — ALWAYS first! Stale Chrome locks port 9222 → every cron run fails silently.
1. session check: verify IG cookies valid (check instagram.com loads)
2. load links: read affiliate-link-database.md, pick first ❌ UNUSED
3. Run script: `cd ~/.hermes/hermes-agent && venv/bin/python3 ~/.hermes/skills/affiliate/threads-auto-reply/scripts/threads_reply_db_reader.py`
4. Script handles: inject IG cookies→ .instagram.com, Meta SSO, search, reply, API interception, verify
5. If auth fails → cookies expired, report to user
6. If "Sign up to chime in" in dialog → account restricted, ABORT, report to user
7. If API returns "pending" → reload post to verify; "pending" is NOT always invisible (recovery state 2026-06-04 showed 100% visible)
8. If API returns "Media blocked due to integrity" → hard blocked, abort all keywords
9. If success → update affiliate-link-database.md (mark USED, sync copies)
```

**If ANY step fails, check Pitfalls section below BEFORE trying alternatives.**

### 🔍 Debugging: Capturing API Responses
When the reply dialog closes but the comment doesn't appear, intercept the API to check:
```python
# Monkey-patch fetch to capture API response before clicking Post
# IMPORTANT: Patch MUST be on the page where Post button is clicked.
# After page.goto(), JS context resets — window._apiLogs is LOST.
page.evaluate("""() => {
    const origFetch = window.fetch;
    window._apiLogs = [];
    window.fetch = async function(...args) {
        const req = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
        const opts = args[1] || {};
        const logEntry = {url: req, method: opts.method || 'GET', body: opts.body};
        const resp = await origFetch.apply(this, args);
        const clone = resp.clone();
        try {
            logEntry.status = resp.status;
            logEntry.responseBody = (await clone.text()).substring(0, 2000);
        } catch(e) {}
        window._apiLogs.push(logEntry);
        return resp;
    };
}""")
# ... click Post, wait 8s ...
# Wrap in try/except — _apiLogs is undefined after page.goto() (new JS context)
try:
    logs = page.evaluate("() => (window._apiLogs || []).filter(l => l.url.includes('configure_text_only_post'))")
except Exception:
    logs = []  # Monkey-patch lost after navigation — expected
```
Key field: `integrity_review_decision` in the response. If `"pending"`, account is flagged — BUT see nuance below: during partial recovery, `"pending"` responses CAN still be visible.

### ⚠️ CRITICAL: Cookie Injection — Updated 2026-06-03

**Two approaches for cookie injection:**

**Approach A — SSO (original, still works):**
1. Inject IG cookies to `.instagram.com` ONLY via `context.add_cookies()`
2. Navigate to `https://www.threads.com/login`
3. Click "Continue with Instagram" (Meta SSO)
4. After SSO redirect → verified logged in on threads.com
5. Proceed with search + reply

**Approach B — Dual cookie injection (new, direct auth):**
1. Extract BOTH IG and Threads cookies via `browser_cookie3.chrome()` (see Cookie Extraction below)
2. Inject IG cookies to `.instagram.com` + Threads cookies to `.threads.com`
3. Navigate directly to `threads.com` — already logged in (no SSO needed)
4. Proceed with search + reply

**⚠️ IG cookies MUST NOT go to `.threads.com`** — IG sessionid has different value than Threads sessionid. Injecting IG cookies to .threads.com causes 404 "Not all who wander are lost".

**⚠️ BUT real Threads cookies CAN go to `.threads.com`** — These are extracted by browser_cookie3 from the actual Chrome Profile 16 login. They contain the correct Threads sessionid (different from IG sessionid).

**Both approaches are STILL subject to bot detection** — even with correct cookies, Playwright headless returns `integrity_review_decision: "pending"`.

## ⚠️ CRITICAL: UI Changed — "Comment" not "Reply" (Verified 2026-05-30)

**Some Threads UI versions show "Comment" instead of "Reply" on post buttons.** The regex must handle BOTH:

```javascript
// Updated regex — handles CommentN, ReplyN, BalasN
if (/^(Comment|Balas|Reply)\s*\d+$/.test(text)) { btn.click(); return text; }
// Same for plain buttons
.filter(b => /^(Comment|Balas|Reply)$/.test(b.textContent.trim()) && b.offsetWidth > 0);
```

**Detection heuristic:** If "no reply button found" on multiple posts, the UI is using "Comment" — update the regex before retrying.

## ⚠️ Pitfall: Multi-Post Iteration (expected failures normal)
When iterating through posts, some replies will fail silently (API returns `"pending"` or dialog closes without posting). This is NORMAL — the script should try multiple posts until one verifies successfully. Pattern:
- 3-5 "FAIL Reply not visible" results → continue to next post
- 1 SUCCESS → stop and update DB
- ALL posts fail → account likely flagged, cooldown 1-2 days

- **⚠️ EARLY ABORT on hard block:** If API returns `{"message":"Media blocked due to integrity.","status":"fail"}` → ABORT immediately. Don't burn through more keywords/posts — ALL will fail. Report the block and exit. See "Account flagging: HARD BLOCK" pitfall below. **🔴 NEW (2026-06-05): This abort applies to ALL subsequent runs for the rest of the day.** Even if the hard block was on Reply 3 (gossip/politics), do NOT run another session hoping affiliate keywords still work — they won't. The account escalates from content-type-specific block to full block within ~2 hours.
- **⚠️ EARLY ABORT on silent failures:** If the FIRST keyword iterates through 6+ posts and ALL fail silently (no button found, no dialog, etc.), this MAY indicate an account-level issue (not keyword saturation). Before burning through more keywords, try ONE plain-text test: craft a comment WITHOUT any affiliate link, post to a random post, and verify. If that ALSO fails → account is likely hard-blocked → abort all keywords. If plain-text succeeds → keyword's posts are the issue (saturated, locked, etc.) → continue to next keyword. Pattern verified 2026-06-04 night: "rekomendasi skincare" 6/6 silent fail, then "korupsi" hit hard block on first try — the silent failures WERE an account signal.

**🟡 Playwright Version PARTIALLY WORKS (Verified 2026-06-12, updated 2026-06-17)** — Playwright headless Chromium can login, search, find posts, and type replies. BUT submit button detection is UNRELIABLE — Threads uses non-standard UI elements for the Post/Kirim button (not ``, likely custom React `` with click handler). v5 test (2026-06-17): typed reply successfully but found 0 submit buttons across multiple selector strategies. **CDP (real Chrome) is more reliable for reply** — button detection works because real Chrome renders the full React component tree correctly. Use Playwright for POST (works perfectly), use CDP for REPLY.

**⚠️ Playwright vs CDP — When to Use Which:**
- **Playwright (preferred for most cases):** Lighter, faster, headless. Works for: search, reply, post. Test: `threads_reply_playwright.py`
- **CDP (real Chrome):** Heavier, launches full Chrome. Use when: need real browser fingerprint, anti-bot detection issues, or debugging. Test: `threads_reply_v6.py`
- **Post script already uses Playwright** — `threads_post_v6.py` uses `p.chromium.launch(headless=True)`. No need to change.
- **Reply script now has both versions** — CDP (v6) and Playwright (v7). Choose based on needs.

**🚨 CRITICAL: Bot Detection — Playwright Replies ALWAYS Invisible (Verified 2026-06-03)** ← OUTDATED, see above
ALL Playwright/Chromium approaches result in `integrity_review_decision: "pending"` — reply accepted by API but INVISIBLE to everyone. Tested approaches that ALL failed:
- headless Chromium (default)
- headed Playwright (non-headless)
- `channel='chrome'` (real Chrome binary)
- stealth args (`--disable-blink-features=AutomationControlled`)
- `navigator.webdriver = false` via init script
- `keyboard.type()` instead of `execCommand('insertText')`
- Fresh cookies from manual Chrome login (browser_cookie3)
- Both IG + Threads cookies to respective domains
SYADAGENTIC can reply manually from real Chrome → confirmed NOT an account flag. Threads detects browser fingerprint at API level. **Auto-reply via Playwright is NOT viable.** Use manual reply with draft content instead, or find alternative automation method.

## ⚠️ CRITICAL RULES
- **ANTI-SPAM LINK OBFUSCATION**: Link **WAJIB** affiliate. Bisa pakai format DIOBFUSCATE (disamarkan): `s . shopee . co . id / XXXX (hapus spasi ya kak)`. Namun jika diminta user, kembalikan ke format link klik asli dan tingkatkan **HUMAN TYPING DELAY**.
- **JANGAN PERNAH** post link asli produk.
- **HUMAN TYPING DELAY**: Wajib tambahkan `time.sleep(random.randint(15, 30))` sebelum klik Kirim.
- **SEARCH KEYWORDS**: Gunakan kata kunci "keluhan/problem" (cth: "muka break out").
- **CRON SCHEDULE**: Gunakan `every 240m` (tiap 4 jam) untuk batch kecil natural.
- **NEVER reuse same product link** — rotate from database
- Check coverage stats: prioritize ❌ UNUSED over ✅ USED

### ⚠️ Reply Button FIX (v4.2 — Updated 2026-05-30)

**UI may show "Comment" OR "Reply" depending on Threads version/locale. Handle BOTH.**

**ORIGINAL POST reply button = button dengan ANGKA** (e.g., "Reply17", "Reply 3", "Balas 26", "Comment15")
**Comment reply button = button TANPA angka** (e.g., "Balas", "Reply", "Comment")

### CRITICAL: 0-Reply Posts
When a post has **0 replies**, there is NO numbered button. The plain "Reply" IS the original post reply button. Only ONE "Reply" button exists.

**Detection logic:**
1. Priority 1: Find "ReplyN" (with number) → ALWAYS original post reply
2. Priority 2: If NO numbered button found AND only ONE plain "Reply" button exists → that IS the original post reply (0 replies case)
3. If MULTIPLE plain "Reply" buttons → first/only one near top of page = original post

**Updated regex (handles CommentN, ReplyN, BalasN with optional space):**
```javascript
// Numbered button: /^(Comment|Reply|Balas)\s*\d+$/
for (const btn of document.querySelectorAll('div[role="button"], span[role="button"]')) {
    const text = btn.textContent.trim();
    if (/^(Comment|Balas|Reply)\s*\d+$/.test(text)) {
        btn.click(); return text; // Original post reply
    }
}
// Fallback: 0-reply posts — check if only one Reply/Comment button
const replyBtns = [...document.querySelectorAll('div[role="button"], span[role="button"]')]
    .filter(b => /^(Comment|Balas|Reply)$/.test(b.textContent.trim()) && b.offsetWidth > 0);
if (replyBtns.length === 1) { replyBtns[0].click(); return 'plain-only'; }
```

### ⚠️ CRITICAL: Submit Button text varies by locale
After clicking reply and opening the dialog, the submit button text depends on UI locale:
- **English UI:** "Post"
- **Indonesian UI:** "Kirim" (verified working 2026-06-04)
- **Old docs said "Kirim" is wrong** — that was incorrect. Both are valid.

**CORRECT submit code — try both:**
```javascript
const dialog = document.querySelector('[role="dialog"]');
for (const btn of dialog.querySelectorAll('[role="button"]')) {
    const text = btn.textContent.trim();
    if (text === 'Post' || text === 'Kirim') {
        btn.click(); return text;
    }
}
```

**Domain note:** `threads.com` and `threads.net` are interchangeable — both resolve to the same site. Scripts can use either domain; no config needed.

## ⚠️ Cookie Injection — Updated 2026-06-03

**CRITICAL: NEVER inject IG cookies to `.threads.com` domain.** This was the root cause of repeated failures. IG cookie values (especially sessionid) are DIFFERENT from Threads cookie values for the same keys.

**Correct flow (both post and reply scripts):**
1. Load cookies from `~/instagram_cookies.json` (flat dict)
2. Inject to `.instagram.com` domain ONLY via `context.add_cookies()`
3. Navigate to `https://www.threads.com/login`
4. Click "Continue with Instagram" → Meta SSO redirect
5. After SSO, browser has valid Threads session with correct cookies
6. Proceed with search/reply/post

## ⚠️ Cookie Extraction — browser_cookie3 (Verified 2026-06-03)

**Manual AES-CBC decryption is BROKEN** — produces garbled values with `v10` prefix artifacts. Use `browser_cookie3` directly:

```python
import browser_cookie3, json, os

# Extract IG cookies
cj_ig = browser_cookie3.chrome(domain_name='.instagram.com')
ig = {c.name: c.value for c in cj_ig}

# Extract Threads cookies (DIFFERENT sessionid!)
cj_th = browser_cookie3.chrome(domain_name='.threads.com')
threads = {c.name: c.value for c in cj_th}

# Save
with open(os.path.expanduser("~/instagram_cookies.json"), 'w') as f:
    json.dump(ig, f, indent=2)
with open(os.path.expanduser("~/threads_cookies.json"), 'w') as f:
    json.dump(threads, f, indent=2)
```

**Key facts:**
- `browser_cookie3.chrome()` uses the DEFAULT Chrome profile (Profile 16 for this user)
- `profile_name` parameter is NOT supported — always uses default profile
- IG ds_user_id: 3310347890, Threads ds_user_id: 38122991886 (DIFFERENT!)
- IG: 10 cookies, Threads: 8 cookies
- Chrome MUST be closed before extraction (SQLite lock)
- `uv pip install browser_cookie3 pycryptodome lz4` (NOT system Python 3.9 which has broken lz4)

## ⚠️ Critical Distinctions
- See `references/anti-spam-rules.md` for mandatory safety delays, obfuscated link formats, and keyword guidelines.
- *

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Sekolah76](https://github.com/Sekolah76)
- **Source:** [Sekolah76/syadagentic](https://github.com/Sekolah76/syadagentic)
- **License:** MIT
- **Homepage:** https://github.com/Sekolah76/syadagentic

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** yes
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-sekolah76-syadagentic-threads-auto-reply
- Seller: https://agentstack.voostack.com/s/sekolah76
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
