Lemon8 has no public posting API in October 2026. Every post goes through the mobile app: tap the plus button, pick photos from the gallery, choose a template, type a headline and a description, put the hashtags at the end, publish. For one account that takes two minutes. For ten accounts every day, somebody on your team loses a good part of each morning tapping through the same five screens.

Appium can do the tapping. The harder part is where the ten phones live, because a Lemon8 account needs a device that stays signed in, keeps its own network identity and still answers to an automation server next month. Below is the setup I'd use: one Appium 3 server, ten BitCloudPhone Android phones reached over ADB, a human approval step before anything goes live, and a pacing plan that keeps each account behaving like what it is.

Who this is for: social teams and agencies that run several Lemon8 accounts they own or manage for clients. If the plan is fake engagement or a ring of throwaway accounts, stop here. Lemon8 bans that, and no device setup changes the outcome.

Table of contents

  1. Why Lemon8 still needs a phone
  2. What Lemon8 lets you automate
  3. Three ways to host ten phones
  4. Where iOS cloud phones fit
  5. The architecture
  6. Prepare the ten phones
  7. Build the content queue
  8. Write the posting script
  9. Run all ten with pacing
  10. Logs and failure rules
  11. The browser side of each account
  12. Common errors
  13. FAQ

Why Lemon8 still needs a phone

Lemon8 is ByteDance's photo-and-text discovery app, somewhere between Pinterest and Instagram. A data.ai estimate put it at about 23.4 million monthly active users in March 2026, and paid ads now run in six markets: the US, Japan, Thailand, Singapore, Malaysia and Indonesia. Since 22 January 2026 the US side has been run by the TikTok USDS Joint Venture, and the US supplemental terms that took effect that day say every Lemon8 profile has to be linked to a TikTok account.

Three facts shape the automation work. Posting happens in the app: there is no public posting API and no documented partner tool for publishing. Each US account carries a TikTok link, so ten Lemon8 accounts means ten TikTok accounts behind them, and the guidelines warn that penalties can apply on both apps when you sign in to both with the same account. And the format is vertical (3:4 photos, a headline, a long description with the hashtags inside it).

So the job is UI automation on a phone. On Android that means Appium with the UiAutomator2 driver, pointed at the Lemon8 package, which is com.bd.nproject.

What Lemon8 lets you automate

Lemon8's community guidelines don't set a number of accounts per person. What they ban is plain: activity that inflates popularity artificially, buying or selling likes and followers, promoting fake-traffic services, using automated scripts to collect information from Lemon8, and getting around measures that restrict access. The supplemental terms add inauthentic commercial behavior such as spam accounts. One severe violation, or several smaller ones, can get an account removed.

That splits the work cleanly:

Task Automate it? Reason
Upload an approved post (photos, headline, description) Yes Same taps a person makes, on content you own
Choose the posting time Yes A publishing decision, not engagement
Like, follow, comment, save No This is the center of the fake-engagement ban
Read feeds, collect profiles or stats No The guidelines name automated data collection directly
Sign in, pass a verification, answer a security prompt No A person handles these, every time
Write the post Human, or human-approved Near-identical captions across accounts look like spam

Everything in this article automates the first two rows and nothing else.

Three ways to host ten phones

You need ten Android devices that stay signed in, each with its own IP, all reachable by adb. Teams usually end up with one of three setups.

Option Keeps Lemon8 signed in Own IP per device Appium access Upkeep
Ten physical phones on a USB hub Yes Needs ten SIMs or a proxy app on each Yes, over USB Cables, batteries, OS updates, a desk
Test device farms (BrowserStack, AWS Device Farm and similar) No, devices are cleaned after a session Provider IPs Yes, it's what they are built for Low, billed per test minute
Persistent Android cloud phones Yes Proxy set per device Yes, when the provider opens ADB Low

Device farms are excellent for testing an app and wrong for running accounts. The device you get tomorrow is a clean one, so every session starts at the login screen. Physical phones work, and plenty of teams start there, but ten handsets turn into a small hardware project with its own failure modes.

For this job I'd pick BitCloudPhone Android devices. Each phone keeps its state between sessions and takes its own proxy. Language, time zone and location follow that IP, and ADB and root can be switched on per device. It also has an API for creating and starting phones, uploading files and installing apps, which pays off once the pool grows past ten.

BitCloudPhone Android phones listed in the BitBrowser client, each with its own IP address
There's a built-in RPA recorder as well (added in May 2026, on Computing Power 12 and 15 devices) that replays recorded taps on a schedule, even with your own computer switched off. That covers simple flows. Appium is the better fit when you want the logic in code, under version control, with waits and checks you can test.

Where iOS cloud phones fit

Appium can drive iPhones too, through its XCUITest driver. The catch is WebDriverAgent: a small server that has to be built and signed with Xcode, installed on the device and reachable from your Appium machine (it listens on port 8100 by default). Test farms set that up for you. Cloud iPhones sold for account work usually don't.

BitCloudPhone-iOS, launched on 11 July 2026, is the kind built for accounts. You get a native iOS environment on iPhone SE hardware (an SE2 or SE3 is assigned when the device is created), a SOCKS5 proxy per phone that you set before first boot, App Store installs, file upload, and a QR handoff that moves a session to a physical iPhone. As of October 2026 its documentation describes no WebDriverAgent endpoint, API or Appium support for the iOS phones.

Two BitCloudPhone-iOS devices open side by side in the BitBrowser client

In this setup, then, the iOS devices hold the accounts a person runs by hand. Think of a creator who shoots and edits on an iPhone and wants to post from iOS, or a client account where someone answers comments and messages manually. Same client, same proxy rules, no script.

Before planning automation on any iOS cloud phone, run this check:

# Ask the provider for the WebDriverAgent address, then: curl -s http://<wda-host>:8100/status # A JSON reply containing "ready": true means the XCUITest driver can attach. # No address, or no reply, means the phone is manual-only for now. 

The architecture

 +------------------------------+ | Controller (Mac/Win/Linux) | | Appium 3 + UiAutomator2 | | run_all.py + approved/ | +--------------+---------------+ | adb over TCP +---------+---------+---+-----+---------+---------+ | phone01 | phone02 | ... | phone09 | phone10 | | SOCKS5 | SOCKS5 | | SOCKS5 | SOCKS5 | | US-NY | US-CA | | SG | TH | +---------+---------+---------+---------+---------+ one Lemon8 account per phone, each with its TikTok link 

One Appium server can hold all ten sessions at once. Two capabilities keep them apart: appium:udid says which device, and appium:systemPort sets the local port the UiAutomator2 server on that device is forwarded to (8200 by default). Give each phone its own systemPort or parallel sessions collide.

Proxies live on the phones, not in Appium. The controller never touches network identity, so a bug in your script can't route phone03's traffic through phone07's IP.

Prepare the ten phones

Do this once per phone, by hand:

  1. Create the phone in the BitBrowser client (cloud phones are managed from its Phone Profiles section) and assign a SOCKS5 proxy in the region the account posts for.
  2. Install Lemon8 on the phone and turn off auto-update for it. A new app version in the middle of a run is the most common way locators break.
  3. Sign in to the Lemon8 account and its linked TikTok account yourself. Finish any verification yourself too.
  4. Turn on ADB for the phone and note the address you'll connect to.

Then set up the controller:

# Appium 3 needs Node.js ^20.19.0, ^22.12.0 or >=24, plus npm 10+ node -v && npm -v npm install -g appium appium driver install uiautomator2 appium driver doctor uiautomator2 # runs the driver's environment checks pip install Appium-Python-Client # Connect each phone with the ADB address you noted adb connect 203.0.113.11:5555 adb devices -l 

UiAutomator2 driver 5.x works only with Appium 3, so upgrade both together. Start the server with the one insecure feature the script uses (shell access, needed to refresh the gallery after photos are pushed):

appium --allow-insecure uiautomator2:adb_shell 

Appium 3 wants the driver prefix on that flag. The bare adb_shell form from Appium 2 is no longer accepted.

Last, a device map. One line per phone:

[ {"account": "brand-a-us-east", "serial": "203.0.113.11:5555", "system_port": 8201, "tz": "America/New_York"}, {"account": "brand-a-us-west", "serial": "203.0.113.12:5555", "system_port": 8202, "tz": "America/Los_Angeles"}, {"account": "brand-b-sg", "serial": "203.0.113.19:5555", "system_port": 8209, "tz": "Asia/Singapore"}, {"account": "brand-b-th", "serial": "203.0.113.20:5555", "system_port": 8210, "tz": "Asia/Bangkok"} ] 

Build the content queue

Posts sit in folders. A person writes or approves each one, then moves it from queue/ to approved/. The script only ever reads approved/, and it moves a post to posted/ once it's live.

queue/ brand-a-us-east/ 2026-10-14-autumn-tableware/ 01.jpg 02.jpg 03.jpg post.json approved/ brand-a-us-east/ ... posted/ brand-a-us-east/ ... 

Each post.json holds the text:

{ "headline": "Three tablescapes for a small apartment", "description": "Plates from our autumn line on a 70 cm table. Swipe for the linen version. #tablescape #smallapartment #autumndecor", "images": ["01.jpg", "02.jpg", "03.jpg"] } 

Three rules for the queue. Images are 3:4. No caption or image appears on two accounts. And nothing reaches approved/ without a person reading it on a phone-sized screen first, because a headline that looks fine in a JSON file can get cut off in the feed.

Write the posting script

Lemon8 changes its interface without notice, so don't hard-code element IDs. Open Appium Inspector against one phone, walk through a post by hand, and copy each element's locator into a file:

{ "create_button": ["accessibility id", "FILL_ME"], "gallery_items": ["id", "com.bd.nproject:id/FILL_ME"], "next_button": ["id", "com.bd.nproject:id/FILL_ME"], "headline_field": ["id", "com.bd.nproject:id/FILL_ME"], "description_field": ["id", "com.bd.nproject:id/FILL_ME"], "post_button": ["id", "com.bd.nproject:id/FILL_ME"] } 

post_one.py pushes the photos, opens Lemon8, fills the post and stops before the final tap unless you switch dry-run off:

import json import random import time from pathlib import Path from appium import webdriver from appium.options.android import UiAutomator2Options from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait LEMON8 = "com.bd.nproject" REMOTE_DIR = "/sdcard/Pictures/lemon8-queue" LOC = json.loads(Path("locators.json").read_text()) def by(key): strategy, value = LOC[key] return (strategy, value) def pause(low=1.2, high=3.5): time.sleep(random.uniform(low, high)) def start_session(device): opts = UiAutomator2Options() opts.set_capability("appium:udid", device["serial"]) opts.set_capability("appium:systemPort", device["system_port"]) opts.set_capability("appium:noReset", True) opts.set_capability("appium:newCommandTimeout", 300) return webdriver.Remote("http://127.0.0.1:4723", options=opts) def push_images(driver, post_dir, images): for name in images: remote = f"{REMOTE_DIR}/{post_dir.name}-{name}" driver.push_file(remote, source_path=str(post_dir / name)) # Deprecated since Android 10 but still honored by many builds.  driver.execute_script("mobile: shell", { "command": "am", "args": ["broadcast", "-a", "android.intent.action.MEDIA_SCANNER_SCAN_FILE", "-d", f"file://{remote}"], }) def post(device, post_dir, dry_run=True, shots=Path("shots")): meta = json.loads((post_dir / "post.json").read_text()) shots.mkdir(exist_ok=True) tag = f'{device["account"]}-{post_dir.name}' driver = start_session(device) wait = WebDriverWait(driver, 20) try: push_images(driver, post_dir, meta["images"]) driver.activate_app(LEMON8) pause(3, 6) wait.until(EC.element_to_be_clickable(by("create_button"))).click() pause() thumbs = wait.until(EC.presence_of_all_elements_located(by("gallery_items"))) for thumb in thumbs[:len(meta["images"])]: # newest photos first  thumb.click() pause(0.6, 1.4) # Gallery -> template screen -> details. Match this to your app version.  for _ in range(2): wait.until(EC.element_to_be_clickable(by("next_button"))).click() pause() wait.until(EC.element_to_be_clickable(by("headline_field"))).send_keys(meta["headline"]) pause() wait.until(EC.element_to_be_clickable(by("description_field"))).send_keys(meta["description"]) pause() driver.get_screenshot_as_file(str(shots / f"{tag}-before-post.png")) if dry_run: return "dry-run: stopped before Post" wait.until(EC.element_to_be_clickable(by("post_button"))).click() pause(5, 8) driver.get_screenshot_as_file(str(shots / f"{tag}-after-post.png")) return "posted" finally: driver.quit() 

A few choices in there are deliberate. noReset keeps the phone's sign-in between sessions. The random pauses aren't there to trick anything; they keep the script from firing taps faster than the app's own animations finish, which is the usual cause of flaky clicks. The gallery picks the newest photos, which are the ones just pushed, but confirm the order on your Lemon8 version during the dry runs.

Run all ten with pacing

run_all.py takes the oldest approved post for each account, runs three phones at a time, spreads the start times and logs every result:

import json import os import random import shutil import time from concurrent.futures import ThreadPoolExecutor from datetime import datetime from pathlib import Path from post_one import post DEVICES = json.loads(Path("devices.json").read_text()) GROUP = os.environ.get("TZ_GROUP") # run one time zone per schedule DEVICES = [d for d in DEVICES if GROUP is None or d["tz"] == GROUP] APPROVED, POSTED = Path("approved"), Path("posted") DRY_RUN = True # flip after a clean week of dry runs MAX_PARALLEL = 3 def next_post(account): folder = APPROVED / account if not folder.exists(): return None posts = sorted(p for p in folder.iterdir() if p.is_dir()) return posts[0] if posts else None def job(device): time.sleep(random.uniform(0, 600)) # spread starts over ten minutes  post_dir = next_post(device["account"]) if post_dir is None: return device["account"], "nothing approved" try: result = post(device, post_dir, dry_run=DRY_RUN) if result == "posted": dest = POSTED / device["account"] dest.mkdir(parents=True, exist_ok=True) shutil.move(str(post_dir), str(dest / post_dir.name)) except Exception as exc: # stop this account, keep the rest going  result = f"error: {exc!r}" with open("runs.jsonl", "a", encoding="utf-8") as log: log.write(json.dumps({"ts": datetime.now().isoformat(), "account": device["account"], "post": post_dir.name, "result": result}) + "\n") return device["account"], result with ThreadPoolExecutor(max_workers=MAX_PARALLEL) as pool: for account, result in pool.map(job, DEVICES): print(f"{account:<20} {result}") 

Schedule one run per time zone group. With cron on a server set to UTC, during US daylight time:

# 23:30 UTC = 19:30 in New York (EDT) 30 23 * * * cd /opt/lemon8 && TZ_GROUP=America/New_York python3 run_all.py # 11:30 UTC = 19:30 in Singapore 30 11 * * * cd /opt/lemon8 && TZ_GROUP=Asia/Singapore python3 run_all.py 

On Windows, Task Scheduler does the same job with one task per group.

The pacing rules I'd start with:

Rule Starting value Why
Posts per account per day 1, at most 2 About what a team posts by hand
Start spread 0 to 10 minutes, random Ten accounts don't publish in the same second
Parallel sessions 3 Leaves headroom and keeps the controller light
Posting window Evening in each account's audience time zone When that audience is scrolling
Same photo on two accounts Never Reads as duplicate content

Raise volume only when the content team can actually produce more original posts. The script makes posting cheap. It doesn't make good posts cheap.

Logs and failure rules

Every run writes a line to runs.jsonl and a screenshot before the final tap (plus one after it, when live). During the dry-run week, open the before-post screenshot for every account and check the headline, the photo order and the hashtags.

The failure rules are short:

  • A failure stops that account for the day. No retry loops.
  • Any unexpected screen ends the run for that phone: a login prompt, a verification, a policy notice, an unusual-activity warning. The script fails on the missing element, and a person opens that phone in the client to deal with it. Never script around a verification step.
  • After a Lemon8 update, switch DRY_RUN back on, run one phone, re-check the locators, then go live again.
  • If two accounts fail on the same day with the same error, assume the app changed before you assume anything else.

The browser side of each account

Each Lemon8 account has a TikTok account behind it, and usually a brand email and a couple of web logins someone checks for messages and analytics. Those web sessions belong in BitBrowser profiles: one profile per account, using the same proxy region as that account's phone. The phone and the browser for brand-a-us-east both sit in New York, and nothing for that account ever opens from Singapore. Since the same client manages the browser profiles and the cloud phones, phone03 and profile 03 sit next to each other, and mixing two accounts by accident gets a lot harder.

Common errors

Error Usual cause Fix
offline next to a serial in adb devices ADB session dropped adb disconnect <addr>, then adb connect <addr>
"Could not find a connected Android device" Wrong udid, or the phone isn't connected Copy the serial from adb devices -l into devices.json
Port errors around 8200 Two sessions share a systemPort One systemPort per phone
NoSuchElementException after weeks of clean runs Lemon8 changed its UI Re-inspect locators, dry-run one phone
Pushed...Source:https://dev.to/claudedel/appium-on-cloud-phones-posting-to-lemon8-from-10-devices-57ga

Related posts