Install
$ agentstack add skill-sekolah76-syadagentic-threads-auto-reply ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →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 RAWrequestsSCRIPTS. 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) — previouslyevery 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 = 1and cron intervalevery 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.idin 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 withdatetime.fromisoformat()and compare todatetime.now(timezone.utc). Also handles relative text (2h,3m,1d,2j). Script:parserelativetime()inthreadsreplyv6.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_TARGETconstant. - For v6 script (
threads_reply_v6.py), checkREPLIES_TARGETnear 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:
# 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):
- Inject IG cookies to
.instagram.comONLY viacontext.add_cookies() - Navigate to
https://www.threads.com/login - Click "Continue with Instagram" (Meta SSO)
- After SSO redirect → verified logged in on threads.com
- Proceed with search + reply
Approach B — Dual cookie injection (new, direct auth):
- Extract BOTH IG and Threads cookies via
browser_cookie3.chrome()(see Cookie Extraction below) - Inject IG cookies to
.instagram.com+ Threads cookies to.threads.com - Navigate directly to
threads.com— already logged in (no SSO needed) - 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:
// 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.pyusesp.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 = falsevia init scriptkeyboard.type()instead ofexecCommand('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:
- Priority 1: Find "ReplyN" (with number) → ALWAYS original post reply
- Priority 2: If NO numbered button found AND only ONE plain "Reply" button exists → that IS the original post reply (0 replies case)
- If MULTIPLE plain "Reply" buttons → first/only one near top of page = original post
Updated regex (handles CommentN, ReplyN, BalasN with optional space):
// 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:
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):
- Load cookies from
~/instagram_cookies.json(flat dict) - Inject to
.instagram.comdomain ONLY viacontext.add_cookies() - Navigate to
https://www.threads.com/login - Click "Continue with Instagram" → Meta SSO redirect
- After SSO, browser has valid Threads session with correct cookies
- 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:
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_nameparameter is NOT supported — always uses default profile- IG dsuserid: 3310347890, Threads dsuserid: 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.mdfor 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
- Source: Sekolah76/syadagentic
- License: MIT
- Homepage: https://github.com/Sekolah76/syadagentic
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.