APIs are great until they aren't. Instagram's API won't let you post Reels from a personal account. X's API costs money and restricts features. LinkedIn's API requires a company page and an OAuth dance that takes longer than just clicking the button yourself.
So I stopped using APIs entirely. Every social platform my agent posts to — Instagram, X, Pinterest, LinkedIn, Threads, Facebook — is controlled through the browser. Playwright + Chrome DevTools Protocol. The platform sees a normal browser session. Because it is one.
Here's exactly how I set it up.
Why Browser Automation Beats APIs
The browser is the universal API. Every platform has a web interface. If a human can click it, Playwright can click it. No rate limits (beyond what a normal user would hit), no business verification, no waiting 6 weeks for API access approval.
I run three Instagram accounts (@billy_kennedy_bmx, @bmx4beginners, @nepa_ai), two X accounts, Pinterest, LinkedIn, Threads, and Facebook — all through browser automation. Zero API keys for any social platform.
The Setup: Playwright + CDP
I run Chrome with remote debugging enabled:
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug-profile
This gives Playwright a Chrome instance it can connect to via CDP (Chrome DevTools Protocol). The key advantage: the browser keeps your login sessions. Log in once manually, and the agent reuses those cookies forever.
from playwright.sync_api import sync_playwright
def connect_to_chrome():
p = sync_playwright().start()
browser = p.chromium.connect_over_cdp("http://localhost:9222")
context = browser.contexts[0]
page = context.new_page()
return p, browser, page
I actually run two Chrome instances: port 9222 for Instagram/YouTube (Google sessions) and port 9223 for X/Twitter (separate Brave browser). Keeps the sessions isolated.
The Hard Parts (and How I Solved Them)
Instagram's Create Button
This one took days to figure out. The "New post" button is an SVG icon nested inside an anchor tag. If you click the SVG element directly, Instagram navigates to Stories instead of opening the Create menu.
The fix: walk up the DOM tree from the SVG to its <A> ancestor and click that instead.
# Find the SVG, then walk up to the <A> tag
create_svg = page.locator('[aria-label="New post"]')
create_link = create_svg.locator('xpath=ancestor::a')
await create_link.click()
Instagram's Share Button
mouse.click() on the Share button does nothing. I tried coordinates, I tried force-clicking, I tried JavaScript injection. None of it works.
The only method that works: Tab navigation to the Share button, then Enter.
await page.keyboard.press("Tab")
await page.keyboard.press("Tab") # may need multiple tabs
await page.keyboard.press("Enter")
File Uploads
Instagram's file input is hidden. You can't click it normally. Use Playwright's wait_for with the "attached" state, not "visible":
file_input = page.locator('input[type="file"]')
await file_input.wait_for(state="attached")
await file_input.set_input_files(video_path)
Account Switching
For multi-account Instagram, navigate to More → Switch accounts, then find the right account by its text content in the DOM:
await page.click('text=Switch accounts')
await page.click(f'text={account_name}')
My Production Setup
My social_poster.py handles all platforms through a single interface. Each platform has its own posting function, but they all follow the same pattern:
- Connect to Chrome via CDP
- Navigate to the platform
- Find the compose/create element
- Fill in content (caption, media upload)
- Click post/share
- Verify success
The orchestrator (brand_cron.py) wraps these in a ThreadPoolExecutor so multiple brands can post concurrently. It also handles thumbnail extraction from video files via ffmpeg before uploading to platforms that need a cover image.
from concurrent.futures import ThreadPoolExecutor
def run_brand_crons():
with ThreadPoolExecutor(max_workers=3) as executor:
executor.submit(post_bmx_content)
executor.submit(post_axon_content)
executor.submit(post_nepa_content)
What Breaks (and How to Handle It)
Platforms change their DOM constantly. Instagram updates their button classes, X moves elements around, LinkedIn redesigns their post composer every few months.
My approach:
- Use aria-labels and text content for selectors, not CSS classes.
[aria-label="New post"]survives redesigns better than.x1i10hfl - Add screenshots on failure so I can see exactly what the page looked like when something broke
- Log everything — every click, every navigation, every error goes to a log file
- Retry with backoff — if a post fails, wait 30 seconds and try again. Most failures are transient
The system isn't bulletproof. I still fix selector issues maybe once a month. But the fix is usually a one-line change, and all accounts benefit immediately.
The Rule
No platform APIs. Ever. Browser only. This is a permanent architectural decision.
APIs can be revoked, rate-limited, or priced out of reach. The browser is always there. As long as the platform has a web interface, my agent can use it.
The full browser automation toolkit — social posting, multi-account management, CDP connection handling — is part of the Axon stack at axon.nepa-ai.com. If you're tired of fighting API restrictions, this is the way.
