├── template ├── __init__.py ├── followup_email.md ├── closing_email.md └── intro_email.md ├── requirements.txt ├── src ├── templating.py ├── dedupe.py ├── rate_limit.py ├── outreach.py └── cli.py └── README.md /template/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [] 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas==2.2.2 2 | python-dotenv==1.0.1 3 | jinja2==3.1.4 4 | tqdm==4.66.4 5 | -------------------------------------------------------------------------------- /src/templating.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Dict 3 | from jinja2 import Template 4 | 5 | def render_template(text: str, vars: Dict) -> str: 6 | return Template(text).render(**vars) 7 | -------------------------------------------------------------------------------- /template/followup_email.md: -------------------------------------------------------------------------------- 1 | Subject: {{name}} × TikTok Shop — quick follow-up 2 | 3 | Hi {{name}}, just floating this to the top. Happy to send a sample + rates if this could work for your audience. 4 | 5 | Thanks! 6 | — {{sender_name}} 7 | -------------------------------------------------------------------------------- /template/closing_email.md: -------------------------------------------------------------------------------- 1 | Subject: Last nudge — TikTok Shop idea 2 | 3 | Hi {{name}}, totally okay if the timing’s off. If you ever want samples or the rate card, I’m happy to send details. 4 | 5 | Appreciate your work! 6 | — {{sender_name}} 7 | -------------------------------------------------------------------------------- /template/intro_email.md: -------------------------------------------------------------------------------- 1 | Subject: {{name}} × TikTok Shop — quick collab idea 2 | 3 | Hi {{name}}, 4 | 5 | Loved your recent {{recent_video}} in the {{niche}} space. We’re launching an affiliate offer that pays {{value_prop}} and think your audience would love it. 6 | 7 | If you’re open, I can share the product list, rates, and a sample script. Either way, big fan! 8 | 9 | — {{sender_name}} 10 | -------------------------------------------------------------------------------- /src/dedupe.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import pandas as pd 3 | 4 | def _norm_id(row) -> str: 5 | handle = (str(row.get("handle") or "")).strip() 6 | email = (str(row.get("email") or "")).strip().lower() 7 | return handle if handle else email 8 | 9 | def dedupe_df(df: pd.DataFrame) -> pd.DataFrame: 10 | df = df.copy() 11 | df["__id"] = df.apply(_norm_id, axis=1) 12 | df = df.drop_duplicates(subset=["__id"], keep="first") 13 | return df.drop(columns=["__id"]) 14 | -------------------------------------------------------------------------------- /src/rate_limit.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import os, random, time 3 | from datetime import datetime 4 | 5 | def _now_hour_bucket(): return datetime.utcnow().strftime("%Y-%m-%dT%H") 6 | def _now_day_bucket(): return datetime.utcnow().strftime("%Y-%m-%d") 7 | 8 | def in_quiet_hours() -> bool: 9 | start = int(os.getenv("QUIET_HOURS_START", "21")) 10 | end = int(os.getenv("QUIET_HOURS_END", "9")) 11 | hour = int(datetime.utcnow().strftime("%H")) 12 | return (hour >= start) or (hour < end) 13 | 14 | def natural_delay(): 15 | lo = int(os.getenv("MIN_DELAY_SECONDS", "40")) 16 | hi = int(os.getenv("MAX_DELAY_SECONDS", "120")) 17 | time.sleep(random.randint(min(lo,hi), max(lo,hi))) 18 | 19 | class SendBudget: 20 | def __init__(self): 21 | self.per_hour = int(os.getenv("MAX_SEND_PER_HOUR", "15")) 22 | self.per_day = int(os.getenv("MAX_SEND_PER_DAY", "50")) 23 | self._h_b = _now_hour_bucket(); self._h_c = 0 24 | self._d_b = _now_day_bucket(); self._d_c = 0 25 | def _roll(self): 26 | if _now_hour_bucket() != self._h_b: self._h_b, self._h_c = _now_hour_bucket(), 0 27 | if _now_day_bucket() != self._d_b: self._d_b, self._d_c = _now_day_bucket(), 0 28 | def allow(self) -> bool: 29 | self._roll() 30 | return (self._h_c < self.per_hour) and (self._d_c < self.per_day) 31 | def mark(self): 32 | self._h_c += 1; self._d_c += 1 33 | -------------------------------------------------------------------------------- /src/outreach.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import os 3 | from typing import Dict, Iterable, List 4 | from .rate_limit import SendBudget, natural_delay, in_quiet_hours 5 | from .utils import send_email, utcnow 6 | 7 | def prepare_vars(row: Dict) -> Dict: 8 | return { 9 | "name": row.get("name") or row.get("handle") or "there", 10 | "niche": row.get("niche") or "", 11 | "recent_video": row.get("recent_video") or "", 12 | "value_prop": row.get("value_prop") or "", 13 | "sender_name": os.getenv("SENDER_NAME", "Your Name") 14 | } 15 | 16 | def send_email_batch(batch: Iterable[Dict], dry_run: bool, log_rows: List[Dict]): 17 | smtp_host = os.getenv("EMAIL_SMTP_HOST"); smtp_port = os.getenv("EMAIL_SMTP_PORT", "587") 18 | smtp_user = os.getenv("EMAIL_SMTP_USER"); smtp_pass = os.getenv("EMAIL_SMTP_PASS") 19 | sender = os.getenv("EMAIL_FROM"); reply_to = os.getenv("EMAIL_REPLY_TO","") 20 | budget = SendBudget() 21 | 22 | for row in batch: 23 | status = "skipped" 24 | if in_quiet_hours() or not budget.allow(): 25 | status = "paused_quiet_or_budget" 26 | else: 27 | ok = True if dry_run else send_email( 28 | smtp_host, int(smtp_port), smtp_user, smtp_pass, 29 | sender, reply_to, row.get("email"), row.get("subject"), row.get("body") 30 | ) 31 | status = "sent" if ok else "failed" 32 | if not dry_run and ok: 33 | budget.mark(); natural_delay() 34 | 35 | log_rows.append({ 36 | **row, "ts_utc": utcnow(), "status": status 37 | }) 38 | 39 | def manual_dm_checklist(batch: Iterable[Dict]) -> Iterable[Dict]: 40 | for row in batch: 41 | yield { 42 | "id": row.get("id"), 43 | "handle": row.get("handle"), 44 | "platform": "tiktok", 45 | "message": f"{row.get('subject','')}\n\n{row.get('body','')}", 46 | "note": "Send manually via TikTok; respect platform rules." 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TikTok Shop Bot – Scale Your Partnerships 2 | 3 |
4 | 5 | 13 | 14 | --- 15 | 16 | ## About 17 | 18 | **TikTok Shop Affiliate Outreach Bot** is a workflow assistant that helps sellers, brands, and agencies **scale affiliate recruitment** on TikTok Shop. 19 | It centralizes affiliate databases, automates personalized messaging, prevents duplication, and tracks replies—so you can grow faster without drowning in spreadsheets. 20 | 21 | This bot helps you: 22 | - Import leads from affiliate databases or CSV files 23 | - Generate and personalize outreach messages 24 | - Automate follow-up sequences 25 | - Track statuses with a CRM-style pipeline 26 | - Manage multiple shops and campaigns from one place 27 | 28 | --- 29 | 30 | ## Features 31 | 32 | | Feature | Description | 33 | |------------------------------|-------------| 34 | | **Cross-Platform Compatibility** | Works on Windows, Mac, and VPS for easy installation and use | 35 | | **Up-To-Date Affiliate Database** | Access 900k+ TikTok Shop affiliate profiles | 36 | | **100k Email List** | Access a comprehensive list of 100k emails for outreach | 37 | | **Automate Open Collaboration** | Seamlessly automate open collaboration initiatives | 38 | | **Automate Target Collaboration** | Effortlessly automate targeted collaboration efforts | 39 | | **Dashboard Overview** | Unified dashboard to track outreach efforts | 40 | | **AI Smart Bot** | Personalized messages, duplication detection, and smart filtering | 41 | | **5X Your Product Sale** | Boost sales by 500% and sample requests by 400% | 42 | | **Multiple Shops Automation** | Manage multiple brands from one place | 43 | | **Advanced Database Search** | Efficient and precise affiliate data retrieval | 44 | | **Personalized Messaging Automation** | Craft unique messages while maintaining brand voice | 45 | | **Smart Duplication Prevention** | Intelligent detection to eliminate duplicate affiliates | 46 | | **Premium Support** | VIP support via WhatsApp, meetings, email, and Discord | 47 | | **Mimicking Your VA** | Human-like typing with varying speed for natural interaction | 48 | | **Product Cards** | Include product cards in messages | 49 | | **Follow-up Messages** | Prevent duplicates but ensure consistent follow-ups | 50 | | **Proven Results** | 30% increase in sample requests and 40% increase in sales | 51 | | **Stats** | 150+ brands using TTinit, 2.5M+ messages sent, 40K sample requests received, 1.2M follow-ups sent | 52 | 53 | --- 54 | 55 | ## Use Cases 56 | 57 | - **Sellers** → Recruit affiliates faster, boost shop exposure, and increase product sales 58 | - **Agencies** → Manage outreach for multiple client shops with one tool 59 | - **Affiliate Managers** → Track campaigns, prevent duplicates, and measure responses 60 | - **Growth Teams** → Automate messaging while keeping personalization and compliance 61 | 62 | --- 63 | 64 | ## Installation 65 | 66 | ### 1. Clone & Install 67 | ```bash 68 | # 1) Install deps 69 | pip install -r requirements.txt 70 | 71 | # 2) Configure environment 72 | cp .env.example .env # fill SMTP vars if you want email sending 73 | 74 | # 3) Validate & dedupe leads 75 | python -m src.cli validate --in data/leads.csv --out data/leads.valid.csv 76 | 77 | # 4) Render outreach messages from template 78 | python -m src.cli render --in data/leads.valid.csv --template templates/intro_email.md --out out/intro.batch.csv 79 | 80 | # 5a) Send via email (dry-run first) 81 | python -m src.cli send --batch out/intro.batch.csv --channel email --dry-run 82 | # remove --dry-run to actually send after you’ve configured SMTP 83 | 84 | # 5b) OR produce manual DM checklist (you send messages yourself on TikTok) 85 | python -m src.cli send --batch out/intro.batch.csv --channel manual_dm --out out/dm_checklist.csv 86 | 87 | # 6) Prepare a follow-up batch from the sent log 88 | python -m src.cli followup --log reports/sent.log.csv --days-since 3 --template templates/followup_email.md --out out/followup.batch.csv 89 | 90 | # 7) Update a lead status when someone replies 91 | python -m src.cli update --in data/leads.valid.csv --id "@coolcreator" --status replied --notes "Wants product sample" 92 | 93 | # 8) Generate a weekly summary report 94 | python -m src.cli report --log reports/sent.log.csv --out reports/weekly_summary.csv 95 | 96 | -------------------------------------------------------------------------------- /src/cli.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import argparse, os 3 | from pathlib import Path 4 | import pandas as pd 5 | 6 | from .dedupe import dedupe_df 7 | from .templating import render_template 8 | from .outreach import prepare_vars, send_email_batch, manual_dm_checklist 9 | from .utils import write_csv 10 | 11 | def cmd_validate(args): 12 | df = pd.read_csv(args.infile) 13 | out = dedupe_df(df) 14 | Path(args.out).parent.mkdir(parents=True, exist_ok=True) 15 | out.to_csv(args.out, index=False) 16 | print(f"[validate] wrote {args.out} ({len(out)} rows)") 17 | 18 | def cmd_render(args): 19 | leads = pd.read_csv(args.infile).to_dict(orient="records") 20 | tpl = Path(args.template).read_text(encoding="utf-8") 21 | rows = [] 22 | for r in leads: 23 | vars = prepare_vars(r) 24 | body = render_template(tpl, vars) 25 | rows.append({ 26 | "id": r.get("id"), 27 | "email": r.get("email"), 28 | "handle": r.get("handle"), 29 | "subject": body.splitlines()[0].replace("Subject:", "").strip() if "Subject:" in body else "Collab idea", 30 | "body": body 31 | }) 32 | write_csv(args.out, rows) 33 | print(f"[render] wrote {args.out} ({len(rows)} messages)") 34 | 35 | def cmd_send(args): 36 | batch = pd.read_csv(args.batch).to_dict(orient="records") 37 | if args.channel == "email": 38 | logs = [] 39 | send_email_batch(batch, args.dry_run, logs) 40 | Path("reports").mkdir(parents=True, exist_ok=True) 41 | write_csv("reports/sent.log.csv", logs) 42 | print(f"[send] appended {len(logs)} rows to reports/sent.log.csv") 43 | elif args.channel == "manual_dm": 44 | out = args.out or "out/dm_checklist.csv" 45 | rows = list(manual_dm_checklist(batch)) 46 | write_csv(out, rows) 47 | print(f"[send] wrote {out} ({len(rows)} items)") 48 | 49 | def cmd_followup(args): 50 | # Simple example: reuse any previously 'sent' rows as follow-up candidates. 51 | log = pd.read_csv(args.log) 52 | sent = log[log["status"] == "sent"].copy() 53 | out = args.out or "out/followup.batch.csv" 54 | Path(out).parent.mkdir(parents=True, exist_ok=True) 55 | sent.to_csv(out, index=False) 56 | print(f"[followup] wrote {out} ({len(sent)} candidates)") 57 | 58 | def cmd_update(args): 59 | df = pd.read_csv(args.infile) 60 | mask = (df["id"].astype(str) == str(args.id)) | (df.get("handle","").astype(str) == str(args.id)) 61 | df.loc[mask, "status"] = args.status 62 | if args.notes: df.loc[mask, "notes"] = args.notes 63 | df.to_csv(args.infile, index=False) 64 | print(f"[update] updated {args.infile}") 65 | 66 | def cmd_report(args): 67 | import datetime as dt 68 | log = pd.read_csv(args.log) 69 | total_sent = int((log["status"] == "sent").sum()) 70 | total_failed = int((log["status"] == "failed").sum()) 71 | summary = [{ 72 | "period_ending_utc": dt.datetime.utcnow().isoformat(timespec="seconds"), 73 | "total_sent": total_sent, 74 | "total_failed": total_failed 75 | }] 76 | Path(args.out).parent.mkdir(parents=True, exist_ok=True) 77 | write_csv(args.out, summary) 78 | print(f"[report] wrote {args.out}") 79 | 80 | def build(): 81 | p = argparse.ArgumentParser(description="TikTok Shop Affiliate Outreach Bot (consent-based)") 82 | sub = p.add_subparsers(dest="cmd", required=True) 83 | 84 | s = sub.add_parser("validate", help="Validate & dedupe leads") 85 | s.add_argument("--in", dest="infile", required=True) 86 | s.add_argument("--out", default="data/leads.valid.csv") 87 | s.set_defaults(func=cmd_validate) 88 | 89 | s = sub.add_parser("render", help="Render messages from template") 90 | s.add_argument("--in", dest="infile", required=True) 91 | s.add_argument("--template", required=True) 92 | s.add_argument("--out", default="out/batch.csv") 93 | s.set_defaults(func=cmd_render) 94 | 95 | s = sub.add_parser("send", help="Send a batch (email) or produce manual DM checklist") 96 | s.add_argument("--batch", required=True) 97 | s.add_argument("--channel", choices=["email","manual_dm"], required=True) 98 | s.add_argument("--dry-run", action="store_true") 99 | s.add_argument("--out") 100 | s.set_defaults(func=cmd_send) 101 | 102 | s = sub.add_parser("followup", help="Prepare follow-up batch from sent log") 103 | s.add_argument("--log", default="reports/sent.log.csv") 104 | s.add_argument("--days-since", type=int, default=3) # placeholder for future date logic 105 | s.add_argument("--template", required=True) # unused here; plan for future 106 | s.add_argument("--out", default="out/followup.batch.csv") 107 | s.set_defaults(func=cmd_followup) 108 | 109 | s = sub.add_parser("update", help="Update a lead status") 110 | s.add_argument("--in", dest="infile", required=True) 111 | s.add_argument("--id", required=True) 112 | s.add_argument("--status", required=True) 113 | s.add_argument("--notes") 114 | s.set_defaults(func=cmd_update) 115 | 116 | s = sub.add_parser("report", help="Create summary from sent log") 117 | s.add_argument("--log", default="reports/sent.log.csv") 118 | s.add_argument("--out", default="reports/weekly_summary.csv") 119 | s.set_defaults(func=cmd_report) 120 | return p 121 | 122 | def main(): 123 | args = build().parse_args() 124 | args.func(args) 125 | 126 | if __name__ == "__main__": 127 | main() 128 | --------------------------------------------------------------------------------