Direct BMX to AI Code
Every big social media platform has an official API—depricated, rate-limited, or just plain blocks the features you need. No thanks.
Playwright browser automation lets you log in, navigate, and post like a human. Here's how to set it up.
Why Playwright Over Selenium?
Playwright is better for this job:
- Auto-wait for elements
- Supports Chromium, Firefox, WebKit
- Easier network interception
- Persistent sessions saved as JSON
The persistent session feature means you log in once and reuse forever.
Installation
pip install playwright
playwright install chromium # download browser binary
# For headed mode debugging
pip install pytest-playwright
Session Management: Log In Once, Post Forever
Store login sessions:
import asyncio
from playwright.async_api import async_playwright
import json
from pathlib import Path
SESSIONS_DIR = Path("./sessions")
SESSIONS_DIR.mkdir(exist_ok=True)
async def save_session(account_name: str, platform: str):
session_file = SESSIONS_DIR / f"{platform}_{account_name}.json"
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False) # headed for manual login
context = await browser.new_context()
page = await context.new_page()
urls = {
"instagram": "https://www.instagram.com/",
"tiktok": "https://www.tiktok.com/",
"facebook": "https://www.facebook.com/",
"youtube": "https://studio.youtube.com/"
}
await page.goto(urls[platform])
print(f"Log into {platform} as {account_name}. Press Enter when done...")
input()
# Save cookies and localStorage
storage_state = await context.storage_state()
session_file.write_text(json.dumps(storage_state))
print(f"Session saved to {session_file}")
await browser.close()
async def load_session(account_name: str, platform: str, playwright):
"""Load a saved session and return a ready browser context."""
session_file = SESSIONS_DIR / f"{platform}_{account_name}.json"
if not session_file.exists():
raise FileNotFoundError(f"No session for {platform}/{account_name}. Run save_session first.")
browser = await playwright.chromium.launch(headless=True)
context = await browser.new_context(
storage_state=str(session_file),
viewport={"width": 1280, "height": 800},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
)
return browser, context
Instagram Reel Upload
async def post_instagram_reel(
account_name: str,
video_path: str,
caption: str,
cover_frame_time: float = 1.0
):
async with async_playwright() as p:
browser, context = await load_session(account_name, "instagram", p)
page = await context.new_page()
await page.goto("https://www.instagram.com/")
await page.wait_for_load_state("networkidle")
# Click the + (Create) button
create_btn = page.locator('[aria-label="New post"]')
await create_btn.click()
# Click "Reel" option
await page.locator('text=Reel').click()
# Upload video file
async with page.expect_file_chooser() as fc_info:
await page.locator('text=Select from computer').click()
file_chooser = await fc_info.value
await file_chooser.set_files(video_path)
# Wait for upload + processing
await page.wait_for_selector('[aria-label="Trim"]', timeout=60000)
# Click Next (twice: trim screen, then settings screen)
await page.locator('text=Next').click()
await page.wait_for_timeout(1000)
await page.locator('text=Next').click()
# Fill caption
caption_box = page.locator('[aria-label="Write a caption..."]')
await caption_box.click()
await caption_box.type(caption, delay=30) # human-like typing speed
# Share
await page.locator('text=Share').click()
# Wait for confirmation
await page.wait_for_selector('text=Your reel has been shared', timeout=120000)
print(f"✓ Instagram Reel posted for @{account_name}")
await browser.close()
TikTok Upload
async def post_tiktok_video(
account_name: str,
video_path: str,
caption: str,
hashtags: list[str] = None
):
hashtag_string = " ".join(f"#{tag}" for tag in (hashtags or []))
full_caption = f"{caption}\n\n{hashtag_string}".strip()
async with async_playwright() as p:
browser, context = await load_session(account_name, "tiktok", p)
page = await context.new_page()
await page.goto("https://www.tiktok.com/upload")
await page.wait_for_load_state("domcontentloaded")
# Upload via file input
file_input = page.locator('input[type="file"]')
await file_input.set_files(video_path)
# Wait for video processing
await page.wait_for_selector('.upload-progress-bar', state="hidden", timeout=120000)
# Fill caption
caption_input = page.locator('[data-text="true"]').first
await caption_input.click()
await caption_input.type(full_caption)
# Post
await page.locator('button:text("Post")').click()
# Wait for success redirect
await page.wait_for_url("**/profile**", timeout=60000)
print(f"✓ TikTok posted for @{account_name}")
await browser.close()
Multi-Account, Multi-Platform Orchestrator
import asyncio
from dataclasses import dataclass
from typing import Optional
@dataclass
class PostJob:
platform: str
account: str
video_path: str
caption: str
hashtags: list[str] = None
async def run_post_job(job: PostJob) -> dict:
"""Run a single post job, return result."""
try:
if job.platform == "instagram":
await post_instagram_reel(job.account, job.video_path, job.caption)
elif job.platform == "tiktok":
await post_tiktok_video(job.account, job.video_path, job.caption, job.hashtags)
return {"platform": job.platform, "account": job.account, "status": "success"}
except Exception as e:
return {"platform": job.platform, "account": job.account,
"status": "failed", "error": str(e)}
async def broadcast_post(
video_path: str,
caption: str,
hashtags: list[str],
accounts: list[dict]
):
"""Post to multiple platform/account combos concurrently."""
jobs = [
PostJob(
platform=acc["platform"],
account=acc["account"],
video_path=video_path,
caption=caption,
hashtags=hashtags
)
for acc in accounts
]
# Run all posts concurrently (each opens its own browser)
results = await asyncio.gather(*[run_post_job(job) for job in jobs])
# Report
for result in results:
status = "✓" if result["status"] == "success" else "✗"
print(f"{status} {result['platform']} / {result['account']}: {result['status']}")
return results
# Example: post to all accounts
asyncio.run(broadcast_post(
video_path="./export/final_reel.mp4",
caption="New video dropping 🔥 Full breakdown in the link",
hashtags=["ai", "tech", "automation", "python"],
accounts=[
{"platform": "instagram", "account": "nepa_ai"},
{"platform": "tiktok", "account": "billy_kennedy_bmx"},
{"platform": "instagram", "account": "billy_kennedy_bmx"},
]
))
Rate Limiting and Anti-Detection
Platforms monitor for bot-like behavior. Here's how to avoid it:
import random
import asyncio
async def human_delay(min_ms: int = 500, max_ms: int = 2000):
"""Random delay to simulate human interaction timing."""
delay = random.uniform(min_ms, max_ms) / 1000
await asyncio.sleep(delay)
async def human_type(element, text: str):
"""Type with randomized speed to simulate human typing."""
for char in text:
await element.type(char, delay=random.uniform(50, 150))
if char in '.!?':
await asyncio.sleep(random.uniform(0.3, 0.8))
# Space out posts across accounts
async def staggered_broadcast(jobs: list[PostJob], stagger_seconds: int = 30):
"""Post to accounts with delay between each to avoid suspicious burst patterns."""
results = []
for i, job in enumerate(jobs):
if i > 0:
print(f"Waiting {stagger_seconds}s before next post...")
await asyncio.sleep(stagger_seconds)
result = await run_post_job(job)
results.append(result)
return results
Scheduling
Combine with APScheduler for time-based posting:
from apscheduler.schedulers.asyncio import AsyncIOScheduler
scheduler = AsyncIOScheduler()
# Schedule a post for 9am EST daily
scheduler.add_job(
broadcast_post,
'cron',
hour=9, minute=0,
timezone='America/New_York',
kwargs={
"video_path": "./queue/next_post.mp4",
"caption": "Daily post",
"hashtags": ["daily"],
"accounts": [{"platform": "instagram", "account": "nepa_ai"}]
}
)
scheduler.start()
The NEPA AI Social Poster Pack is a production-ready version of this system: session management, retry logic, queue management, and multi-account orchestration pre-built and tested across Instagram, TikTok, YouTube, Facebook, and Pinterest.
→ Get the Social Poster Pack at /shop/social-poster-pack
Post everywhere, once, automatically.



