Since late 2023, Google has put a gate in front of every new indie Android developer. If you open a personal Play Console account (created after November 13, 2023), you can’t publish to production until you’ve run a closed test with at least 12 testers who stayed opted in for 14 days in a row. Then you fill in a form about that test and wait for Google to review it, which Google says “usually takes seven days or less.”
Most solo developers don’t have 12 friends with Android phones who’ll open a half-finished app every day for two weeks. So they pay for it. Search Fiverr for “12 testers” and you’ll find pages of gigs, plus a handful of dedicated sites built around this one rule.
That’s the opportunity: run a pool of real testers, sell the 14-day closed test to developers, and use a small bot to keep everyone active and produce the feedback summary Google asks for. It’s a grey area. Google wrote the rule so developers would test properly, not so they could buy a pass. But being paid to test an app is not illegal, and Google’s own help page suggests recruiting testers through outside channels such as messaging groups.
In this guide:
- what the rule says, word for word, and what makes Google reject an application
- real prices competitors charge today
- a Python + Telegram script that sends daily reminders, logs check-ins and bug reports, flags testers going quiet, and drafts the production-access answers with Gemini
- costs, realistic earnings, and the risks: account bans, refund fights, and where this tips from grey into plain fraud
The rule, exactly
From Google’s Play Console Help page “App testing requirements for new personal developer accounts” (checked October 2026):
- Applies to personal developer accounts created after November 13, 2023. Organization accounts aren’t affected.
- The developer “must run a closed test for their app with a minimum of 12 testers who have been opted in continuously for at least 14 days.”
- To apply for production, the developer answers three groups of questions: About your closed test, About your app/game, About your production readiness. The first group asks whether testers used all the features, whether usage matched what real users would do, and for a summary of tester feedback and how it was collected.
- Google names two reasons for sending an app back for more testing: fewer than 12 opted-in testers or “insufficient tester engagement.”
That last point is your product. Anyone can get 12 email addresses onto a list. Getting 12 people to actually use an app for 14 days, and documenting it, is the part developers pay for.
What competitors charge (October 2026)
Prices taken from the vendors’ own pricing pages:
| Service | Entry offer | Bigger packages |
|---|---|---|
| 12testers.com | $11.99 per app | 5 apps $34.99, 10 apps $64.99 |
| Testers14 | $15.99 (15 testers) | $19.99 (20), $24.99 (25) |
| closedtest.app | $19 (12 testers) | $29 (16), $39 (20) |
| TestFi | $39.99 flat (16 testers, 12 guaranteed on day 14) | Separate per-tester sessions: $3.99 written feedback, $5.99 screen recording |
Note that 12testers.com’s packages include “feedback and rating on Google Play”, which is exactly the kind of thing you should not copy (see Risks below).
Two things stand out. First, the price is already low: $12–$40 per app for two weeks of work. Second, everyone sells a buffer (15, 16 or 20 testers instead of 12), because testers drop out and one dropout can reset the clock. Plan your pool the same way.
Your edge, if you’re in Indonesia or another lower-cost country, is that tester pay in rupiah is cheap compared with those dollar prices, and Indonesian developer communities (Facebook groups, Telegram groups, Discord servers) are a market the English sites don’t serve in Bahasa.
What you need
| Item | Cost | Notes |
|---|---|---|
| 15–20 real people with Android phones | Tester pay (see below) | Students, family, a kos-kosan group. One person, one Google account, one phone. |
| A Google Group | $0 | groups.google.com. The client adds the group as their tester list once. |
| Telegram bot token | $0 | Message @BotFather, send /newbot |
Python 3.10+, requests, google-genai | $0 | Any laptop or a $5 VPS |
| Gemini API key | $0 to start | aistudio.google.com, for the report step |
| Fiverr seller account (optional) | 20% of each order | Fiverr’s standard seller fee |
Gemini pricing (check ai.google.dev/pricing before relying on it): Gemini 3.5 Flash-Lite paid tier is $0.30 per 1M input tokens and $2.50 per 1M output tokens. One report is a few thousand tokens, well under $0.01.
Step 1: Set up the tester pool with a Google Group
This is the trick that makes the service run smoothly. In Play Console, a closed test’s tester list can be email addresses or Google Groups. Use a group:
- Create a group at groups.google.com, e.g.
solvinc-testers@googlegroups.com. Add your testers’ Gmail addresses as members. - The client opens Play Console → their app → Test and release → Testing → Closed testing → Manage track → Testers tab → selects Google Groups and enters your group’s address. They save, then copy the opt-in link, which looks like
https://play.google.com/apps/testing/com.client.app. Google only shows the opt-in link once the closed-test release is published, and testers must already be in the group before they opt in. - Every tester opens that link while signed in with the Gmail that’s in the group, taps Become a tester, and installs the app from Play.
Now when a tester quits, you swap them in the group yourself. The client never touches anything and never has to give you Play Console access. Don’t accept a client’s Play Console password, and don’t ask for it.
Write down the date all testers have opted in. That’s day 1, not the date the client paid.
Step 2: Create the bot and install the script
mkdir testerops && cd testerops
python3 -m venv .venv && source .venv/bin/activate
pip install requests google-genai
export TG_TOKEN="123456:ABC-your-botfather-token"
export GEMINI_API_KEY="paste-your-key-here"
Ask each tester to open your bot in Telegram and press Start. You’ll grab their chat IDs in Step 4.
Step 3: The script
Save this as testerops.py. It stores everything in one SQLite file:
import argparse, datetime as dt, os, sqlite3, sys
import requests
DB = os.environ.get("TESTEROPS_DB", "testerops.db")
API = f"https://api.telegram.org/bot{os.environ.get('TG_TOKEN', '')}"
MODEL = os.environ.get("REPORT_MODEL", "gemini-3.5-flash-lite")
PRICE_IN, PRICE_OUT = 0.30, 2.50 # USD per 1M tokens, Gemini 3.5 Flash-Lite paid tier
TEST_DAYS = 14
db = sqlite3.connect(DB)
db.executescript("""
CREATE TABLE IF NOT EXISTS apps(package TEXT PRIMARY KEY, client TEXT, started TEXT, closed INTEGER DEFAULT 0);
CREATE TABLE IF NOT EXISTS testers(chat_id INTEGER PRIMARY KEY, name TEXT, email TEXT, active INTEGER DEFAULT 1);
CREATE TABLE IF NOT EXISTS events(ts TEXT, chat_id INTEGER, package TEXT, kind TEXT, text TEXT);
CREATE TABLE IF NOT EXISTS state(k TEXT PRIMARY KEY, v TEXT);
""")
def tg(method, **params):
r = requests.post(f"{API}/{method}", json=params, timeout=60)
r.raise_for_status()
return r.json()["result"]
def day_of(started):
return (dt.date.today() - dt.date.fromisoformat(started)).days + 1
def open_apps():
return db.execute("SELECT package, client, started FROM apps WHERE closed = 0").fetchall()
def remind(_):
apps = open_apps()
if not apps:
return print("No open apps.")
lines = ["Today's apps. Open each one and use a real feature for 2-3 minutes.",
"Then reply: /done <package> Bugs or ideas: /fb <package> <what happened>", ""]
for pkg, _, started in apps:
lines += [f"- {pkg} (day {day_of(started)}/{TEST_DAYS})",
f" Opt in: https://play.google.com/apps/testing/{pkg}",
f" Install: https://play.google.com/store/apps/details?id={pkg}"]
testers = db.execute("SELECT chat_id FROM testers WHERE active = 1").fetchall()
for (chat_id,) in testers:
tg("sendMessage", chat_id=chat_id, text="\n".join(lines), disable_web_page_preview=True)
print(f"Sent {len(apps)} apps to {len(testers)} testers.")
def poll(_):
known = {row[0] for row in db.execute("SELECT chat_id FROM testers")}
packages = {row[0] for row in open_apps()}
row = db.execute("SELECT v FROM state WHERE k = 'offset'").fetchone()
offset = int(row[0]) if row else 0
print("Polling Telegram. Ctrl+C to stop.")
while True:
for upd in tg("getUpdates", offset=offset, timeout=50):
offset = upd["update_id"] + 1
db.execute("INSERT OR REPLACE INTO state VALUES ('offset', ?)", (str(offset),))
db.commit()
msg = upd.get("message") or {}
chat_id, text = msg.get("chat", {}).get("id"), (msg.get("text") or "").strip()
if chat_id not in known:
print(f"Unknown chat {chat_id} ({msg.get('from', {}).get('first_name')}): {text}")
continue
parts = text.split(maxsplit=2)
if len(parts) < 2 or parts[0] not in ("/done", "/fb") or parts[1] not in packages:
tg("sendMessage", chat_id=chat_id, text="Format: /done <package> or /fb <package> <text>")
continue
kind = "checkin" if parts[0] == "/done" else "feedback"
note = parts[2] if len(parts) == 3 else ""
if kind == "feedback" and not note:
tg("sendMessage", chat_id=chat_id, text="Add what happened after the package name.")
continue
db.execute("INSERT INTO events VALUES (?, ?, ?, ?, ?)",
(dt.datetime.now().isoformat(timespec="seconds"), chat_id, parts[1], kind, note))
db.commit()
tg("sendMessage", chat_id=chat_id, text=f"Logged {kind} for {parts[1]}. Thanks!")
def status(_):
since = (dt.datetime.now() - dt.timedelta(days=3)).isoformat()
testers = db.execute("SELECT chat_id, name FROM testers WHERE active = 1").fetchall()
for pkg, client, started in open_apps():
d = day_of(started)
note = " -> 14 full days done, client can apply" if d > TEST_DAYS else ""
print(f"\n{pkg} ({client}) day {d}/{TEST_DAYS}{note}")
for chat_id, name in testers:
recent, fb = db.execute(
"SELECT SUM(ts >= ?), SUM(kind = 'feedback') FROM events WHERE chat_id = ? AND package = ?",
(since, chat_id, pkg)).fetchone()
flag = "" if recent else " <-- no activity for 3 days"
print(f" {name:<14} last 3 days: {recent or 0} feedback total: {fb or 0}{flag}")
def report(args):
from google import genai
from google.genai import types
rows = db.execute("""SELECT e.ts, t.name, e.kind, e.text FROM events e
JOIN testers t ON t.chat_id = e.chat_id
WHERE e.package = ? ORDER BY e.ts""", (args.package,)).fetchall()
if not rows:
sys.exit("No events logged for this package.")
checkins = sum(r[2] == "checkin" for r in rows)
testers = len({r[1] for r in rows})
feedback = "\n".join(f"{ts[:10]} {name}: {text}" for ts, name, kind, text in rows if kind == "feedback")
prompt = f"""You help an Android developer fill in Google Play's production access form
after a closed test. Use ONLY the facts below. Do not invent devices, numbers or features.
If something is unknown, write [DEVELOPER: fill in].
Facts: {testers} testers logged activity, {checkins} daily check-ins, test length {TEST_DAYS}+ days.
Tester feedback log:
{feedback or "(no written feedback)"}
Write short, plain answers under these headings:
1. Tester engagement (did testers use the main features; how usage compared to real users)
2. Summary of feedback and how it was collected (Telegram bot + Google Play private feedback)
3. Changes made based on the test (list each bug or request; mark [DEVELOPER: confirm fixed])"""
resp = genai.Client().models.generate_content(
model=MODEL, contents=prompt,
config=types.GenerateContentConfig(
thinking_config=types.ThinkingConfig(thinking_level="minimal")))
u = resp.usage_metadata
tok_in = u.prompt_token_count or 0
tok_out = (u.candidates_token_count or 0) + (u.thoughts_token_count or 0)
print(resp.text)
print(f"\nTokens: {tok_in} in / {tok_out} out, "
f"about ${tok_in / 1e6 * PRICE_IN + tok_out / 1e6 * PRICE_OUT:.4f}")
def add_app(a):
db.execute("INSERT OR REPLACE INTO apps VALUES (?, ?, ?, 0)",
(a.package, a.client, a.started or dt.date.today().isoformat()))
db.commit()
def add_tester(a):
db.execute("INSERT OR REPLACE INTO testers VALUES (?, ?, ?, 1)", (a.chat_id, a.name, a.email))
db.commit()
def close_app(a):
db.execute("UPDATE apps SET closed = 1 WHERE package = ?", (a.package,))
db.commit()
ap = argparse.ArgumentParser()
sub = ap.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("add-app"); p.add_argument("package"); p.add_argument("client")
p.add_argument("--started", help="YYYY-MM-DD the testers opted in"); p.set_defaults(fn=add_app)
p = sub.add_parser("add-tester"); p.add_argument("chat_id", type=int); p.add_argument("name")
p.add_argument("email"); p.set_defaults(fn=add_tester)
p = sub.add_parser("close-app"); p.add_argument("package"); p.set_defaults(fn=close_app)
p = sub.add_parser("report"); p.add_argument("package"); p.set_defaults(fn=report)
for name, fn in (("remind", remind), ("poll", poll), ("status", status)):
sub.add_parser(name).set_defaults(fn=fn)
args = ap.parse_args()
args.fn(args)
What each command does:
add-app/add-tester/close-app: manage the lists.--startedis the day all testers opted in.remind: sends every active tester one Telegram message with each open app, its day count and both links. Run it from cron every morning.poll: long-polls Telegram. Testers reply/done com.client.appafter using an app, or/fb com.client.app Login button does nothing on Android 14. Anything from an unknown chat is printed so you can add new testers.status: per app, each tester’s activity in the last 3 days. Anyone with zero gets flagged before they become a problem.report: sends the logged check-ins and feedback to Gemini and drafts answers for the About your closed test and production readiness sections. It’s told to use only logged facts and to leave[DEVELOPER: fill in]where it doesn’t know something.
Step 4: Run it
# testers pressed Start in your bot; poll prints their chat IDs
python testerops.py poll
# Unknown chat 5512093344 (Rina): /start
# Ctrl+C, then register them
python testerops.py add-tester 5512093344 Rina rina.tester@gmail.com
# a client's app goes live in the group today
python testerops.py add-app com.budi.kasirapp "Budi (Fiverr #FO81A2)"
# run the poller permanently (tmux, screen or systemd), and the reminder daily at 08:00
crontab -e
# 0 8 * * * cd /home/you/testerops && TG_TOKEN=... .venv/bin/python testerops.py remind
Check every evening:
python testerops.py status
# com.budi.kasirapp (Budi (Fiverr #FO81A2)) day 9/14
# Rina last 3 days: 3 feedback total: 2
# Agus last 3 days: 0 feedback total: 0 <-- no activity for 3 days
Message Agus today, not on day 13. If someone has opted out or left the group, replace them immediately. That’s what your buffer is for.
On day 15, generate the report and send it to the client together with the raw feedback log:
python testerops.py report com.budi.kasirapp > budi-report.txt
python testerops.py close-app com.budi.kasirapp
The report costs a fraction of a cent. The developer edits it, confirms which bugs they really fixed, and pastes it into the production-access form. Also tell testers to send private feedback through Google Play itself (on the app’s Play listing). That feedback lands in the client’s Play Console, so it’s on record alongside your own log.
Costs and realistic earnings
The limit isn’t the bot. It’s how many apps one person will actually open every day. Two to three minutes per app is realistic. Ten apps is 20–30 minutes a day, which a student will do for some extra pocket money. Twenty-five apps is over an hour a day, and engagement falls apart. Cap each cycle at roughly 10 apps.
Example only, not market data (assumes Rp 16,500 per USD; check today’s rate):
| Per month | Amount |
|---|---|
| Apps sold (10 per 14-day cycle, ~2 cycles) | 20 apps × $15 = $300 |
| Fiverr fee (20%) | −$60 |
| 16 testers × Rp 100,000 | −Rp 1,600,000 ≈ −$97 |
| Gemini API + VPS | ~$5 |
| Net | ≈ $138/month |
| Your time | 20–30 min/day: status, chasing, replacing, client chat |
Selling directly to Indonesian developers (no Fiverr cut) at Rp 200,000–250,000 per app improves the margin, but finding those buyers takes longer. More testers let you sell more apps per cycle. They also mean more people to chase, and your income scales with your tester management, not with your code.
This is a side income in the low hundreds of dollars, with real daily work. It’s not passive, and anyone calling it passive is selling you something.
Risks: bans, refunds, and the line you don’t cross
What Google can see. Google doesn’t publish how it measures “engagement.” Assume it can see installs, how often the app is opened, on how many distinct devices, how long testers stayed opted in, and whatever private feedback was sent. Real people using real apps on their own phones look like real testing because they are.
What gets accounts banned:
- Device farms and bulk accounts. One phone or an emulator rotating through 20 Gmail accounts is exactly what Google’s anti-abuse systems are built to catch. Google’s account policies don’t allow bulk-created accounts used to abuse its services. You risk your testers’ Google accounts, and the client risks being rejected or flagged. One person, one account, one device.
- Ratings and reviews. Some gigs sell “feedback & rate on Google Play.” Paying for or rewarding ratings breaks Google Play’s Ratings and Reviews policy, and that can get the client’s app or developer account removed. Testers give private feedback only. Never sell stars.
- Made-up reports. Writing invented testing details into the form is lying to Google on the client’s behalf. The script refuses to invent facts for a reason. Keep it that way.
Refunds and chargebacks. A client whose production access is denied for an unrelated reason (policy problems, a broken app, missing privacy policy) will often blame the testers. Google says plainly that review “is not a troubleshooting step.” Protect yourself:
- Sell “12+ testers opted in for 14 days, with logs and a report.” Don’t sell “guaranteed approval,” because you don’t control Google’s decision.
- Offer what competitors offer: replace any tester who drops out, and rerun the test free if Google sends the app back over tester count or engagement.
- Keep the SQLite log and screenshots of the group membership. On Fiverr, disputes are settled on evidence.
Platform risk. Fiverr allows these gigs today, but marketplaces change policy. If Google changes or drops the requirement (it already cut the tester count from 20 to 12 in December 2024), this whole niche can disappear overnight. Don’t buy equipment for it.
Testers’ privacy. Clients see testers’ email addresses in Play Console. Tell testers that before they join, and suggest a separate Gmail used only for testing, on their own phone.
Reality check
You’re entering a crowded, cheap market. $11.99 per app is the price to beat on the English web. Competing on price alone is a race to the bottom. Compete on proof (logs, a feedback report, real bug findings) and on language (Bahasa support for local developers).
Testers drift off mid-test. Novelty wears off and storage fills up. As a rule of thumb, budget for a quarter or more of your pool going quiet and keep two or three standby testers who’ve already opted in.
Restarted clocks are your fault in the client’s eyes. If a tester leaves the group on day 12, that person’s 14 days start over. That’s why everyone sells 15–20 testers, not 12.
Don’t test apps you wouldn’t install. Gambling-style apps, “earn money” apps that farm ads, fake VPNs, anything asking for SMS or accessibility permissions without a clear reason: decline. Your testers’ phones and accounts are on the line.
The bot won’t make people engaged. It reminds and records. If testers are tapping /done without opening the app, the report is fiction and Google may notice the low usage. Spot-check: ask for a screenshot of something inside the app once a week.
Model names change. If gemini-3.5-flash-lite is retired, set REPORT_MODEL to the current Flash-Lite model and update PRICE_IN/PRICE_OUT.
Final checklist
- 15–20 real testers, one account and one phone each, told about email visibility
- Google Group created; clients add the group, not individual emails
-
pollrunning permanently,remindin cron -
statuschecked daily; standby testers ready - No ratings, no emulators, no invented report details
- Offer written as “12+ testers, 14 days, logs + report”, not “guaranteed approval”
- Logs and group screenshots kept for every order
Start with a pool of 15 people and one test app of your own (or a developer friend’s). Run one full 14-day cycle, fix what breaks, and only then put up a gig.