← All articles

Registering and warming 109 accounts on GitHub and Reddit: eight days of logs

12 min read automationaccountsfingerprintsdk

Signing up is the easy half. The account is worth something only if it is still usable a week later, from the same identity, without a re-login or a verification loop. So the only measurement worth taking runs over days, not over one launch.

Here is one: a harness built on the SDK that registers accounts on GitHub, Reddit and Hacker News, then re-opens each one every day or two and uses it the way a person would. Eight days, 2026-08-05 to 2026-08-13. Every launch went through AntiDetectBrowser.launch(), every action landed in Postgres, and the batch runs left 4,125 lines of logs on disk. The numbers below come out of that database and those logs, not out of a demo.

The eight-day numbers

0
interactive challenges
across 109 signups
0
re-logins across
32 warm-up sessions
0
exit-IP blocks
at signup
62/63
device probes matching
the intended device
Accounts created by the harness 109
Still active at the end 89 (81.7%)
Logged actions 1,989
Repository pages opened during warm-up 661
Warm-up sessions started / completed 146 / 141 (96.6%)

The cohort is lopsided on purpose. GitHub got 103 accounts because it is the harder target: a signup form behind an overlay-based bot check, an email code, and a username namespace where most short handles are already taken. Reddit got 5 and Hacker News 1, which is enough to confirm the flow works and nowhere near enough to quote a rate for.

activeparked for a humanfailed
GitHub n=103 84 active 10 parked for a human 9 failed 84 10 9 Reddit n=5 4 active 1 parked for a human 4 1
Signup outcome by platform, each bar scaled to its own total. Hacker News (n=1) is omitted. Parked means the run stopped and left the window open for a person; it is not a ban.

Zero challenges, and what that actually means

grep -ci 'arkose\|hcaptcha\|recaptcha\|captcha' across all six batch logs returns 0. That is the claim, and it is worth being precise about what it does not include.

Email verification codes still happen. GitHub asked for one on essentially every signup, and the harness read it out of the mailbox and typed it in. That is a possession check, not a human check, and no fingerprint work makes it go away.

What did not happen is the other kind: the puzzle, the slider, the "select all images with a bus". GitHub ships an overlay bot check on /signup and it never escalated to a visible challenge. That escalation fires when the browser looks wrong - when the UA says Android and maxTouchPoints says 0, when the timezone is Shanghai and the exit IP is in Oregon, when canvas and WebGL hash to a value the site has seen 4,000 times this week. None of those fired, across 109 signups, because none of those contradictions existed.

The harness is also built to stop at a challenge rather than solve one:

// A challenge means the site wants a human. Hand it a human.
if (await captchaPresent(page)) {
  account.status = 'NEEDS_HUMAN'
  await saveAccount(account)
  await page.waitForTimeout(600_000)   // window stays open for a person
  return account
}

That branch never ran in these logs. It stays in because the day it does run, the right move is to stop.

The identity is decided once, at profile creation

The single most common way to lose an account is to hand it a slightly different device on the second visit. A profile's persona - user agent, GPU strings, screen, touch points, fonts, language - is generated once when the profile directory is created and pinned to disk. Every later launch replays the same one. There is no drift to manage because nothing regenerates.

That is also why deviceType is a creation-time decision:

const { page, browser } = await launch({
  profile: `gh-${email}`,            // profile dir == identity, forever
  proxy,                             // this account's own exit IP
  label: email,                      // shows in the window, not in the page
  deviceType: 'android',             // only honoured on first creation
})
await page.goto('https://github.com/signup')

Pass deviceType: 'android' and the spoofing happens in the engine, not in an injected script - the page sees a phone from the first byte of the first request, including in headers the page never gets to read. Which is also why the GitHub signup form the harness drives is the mobile one, with its fields inside web components; the selectors had to be written against that, not against the desktop layout.

Do not take the device on faith. Read it back from inside the page:

const d = await page.evaluate(() => ({
  ua: navigator.userAgent,
  platform: navigator.platform,
  mobile: navigator.userAgentData?.mobile ?? null,
  touch: navigator.maxTouchPoints,
  screen: `${screen.width}x${screen.height}`,
  dpr: devicePixelRatio,
  tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
}))

Logged on every launch, that produced 62 consistent readings out of 63:

[device:register] ✓ Android | platform=Linux armv81 mobile=true touch=5 412x917@1.75x tz=America/Los_Angeles
[device:register] UA=Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 ... Mobile Safari/537.36

The one reading that came back Win32 was a desktop profile created before the phone runs started. It reported desktop because it is desktop. That is the assertion working, not failing.

Warming is mostly about the session surviving

Across 32 warm-up sessions in the logs, the string "session not preserved" appears zero times. Every one of them opened the profile and landed on github.com/dashboard already logged in - no password, no device-verification email, no second factor. The login fallback in the code is dead weight so far, and that is the desired state.

That is the part people underestimate. An account that has to log in fresh every visit is an account generating a login event from a new session every visit, which is exactly the signal a re-login is supposed to avoid. Cookies, local storage, IndexedDB and passkeys all live in the profile directory and come back with it.

The behaviour on top of that is unglamorous. 661 repository pages were opened over the eight days, and nothing about how they were read is instant or uniform:

export async function typeLikeHuman(page: Page, selector: string, text: string) {
  const el = page.locator(selector).first()
  await el.click()
  await sleep(120, 400)
  for (const ch of text) {
    await page.keyboard.type(ch, { delay: rand(60, 180) })   // per character
  }
  await sleep(150, 500)
}

export async function humanScroll(page: Page, steps = randInt(4, 9)) {
  for (let i = 0; i < steps; i++) {
    await page.mouse.wheel(0, rand(300, 800))
    await sleep(1200, 3800)                                  // reading pause
    if (Math.random() < 0.2) {
      await page.mouse.wheel(0, -rand(80, 250))              // glance back up
      await sleep(600, 1500)
    }
  }
}

A page.fill() writes a 40-character string in one event with no keydowns. Per-character typing at 60-180ms with pauses either side is not clever; it is just the difference between an input event trace that looks like a person and one that does not.

One account, one exit IP, forever

Each account is created behind a residential proxy and that proxy is stored on the account row, so every later session goes out the same way:

const { page } = await launch({
  profile: account.browserProfile,
  proxy: account.proxy,     // the same exit IP as the day it was created
  label: account.email,
})

Timezone follows the proxy automatically - the SDK looks up the exit IP's geo before launch and writes it into the fingerprint, which is why the probe above shows America/Los_Angeles while the host machine sits in UTC+8. Locale, timezone and IP agree without anyone maintaining a mapping table.

Zero signups were turned away with GitHub's Access is temporarily restricted interstitial over the eight days. That page is keyed on the exit IP, not on the browser, so it is a property of the proxy pool - and it is the input worth spending money on if you have to choose.

Where the failures actually were

81.7% survived. The rest is worth reading carefully, because none of it was detection.

The nine hard failures were mundane: eight or more candidate usernames all rejected as taken, or a form field that timed out. The ten parked accounts were signups where GitHub's overlay intercepted the pointer on Create account and the page simply stayed on /signup - a selector problem, fixable, and unrelated to fingerprinting.

The batches were also not uniform, and this is the figure to budget against:

activeparked for a humanfailed
Aug 12 10:35 7 active 2 parked 1 failed 7 2 1 Aug 13 01:01 3 active 5 parked 2 failed 3 5 2 Aug 13 09:52 7 active 1 parked 1 failed 7 1 1
Three consecutive GitHub batches, same code, same proxy pool, one common scale. The middle run lost five accounts to one intercepted click.

Best run of ten: 7 active. Worst: 3, with all five parked accounts stuck on the same intercepted button. If you are budgeting, budget on the worse number. A per-batch success rate that swings between 30% and 70% on frontend selectors is normal, and it is not what the fingerprint is doing.

What the free tier covers

The interesting part: nearly all of the above runs on the free plan.

Free Paid
Engine and fingerprint spoofing Identical binary, identical config Identical
Local profiles Unlimited Unlimited
Persona pinned to disk, replayed every launch Yes Yes
Android device spoofing Yes Yes
Concurrent browsers 1 5 to 100
Your own proxies via launch({ proxy }) Yes Yes
Cloud profile sync, managed proxies, Live View No Yes

The spoofing is not a paid tier. A free key downloads the same engine build and gets the same generated persona; there is no degraded fingerprint mode.

What the free plan caps is throughput and convenience: one browser at a time, and nothing backed up off the machine. A serial loop - register one, warm one, close, next - fits inside that limit exactly, which is how the alternating batches in this data were shaped. Drop sync: true, bring your own proxy, and the whole eight-day run reproduces on a free key. (The measurement key here was a high-concurrency one, because two batches were run in parallel lanes and because cloud backup was wanted for the profiles worth keeping. Neither is required for the loop itself.)

What you give up is real, and it is worth naming: on free, the profile exists only on that disk. Lose the machine, lose every identity on it - accounts that each cost a signup, a verification email and a week of history. That is what cloud sync is for, and it is the one line on the paid side that is hard to work around.

Reproducing this

Point it at a site you are allowed to automate, and check the three things that actually matter:

  1. Consistency - log the device probe on every launch and diff it across sessions. Anything that changes between visits is the bug.
  2. Persistence - close the browser, reopen the same profile, and land on a logged-in page without typing a password. If you cannot, warming is not happening no matter how human the scrolling looks.
  3. Agreement - exit IP country, timezone and navigator.language should tell one story. Check it against a detector rather than against your own code.

Then run it for a week, not for an afternoon. Everything in the tables above only became visible on about day three.

More on why the pinning matters in why accounts still get flagged, on the profile-per-run tradeoff in temporary profiles for automation, and on the session half in passkeys and multi-account workflows.

One boundary worth stating plainly, since this post names real platforms: every site here has terms, and automating signups is not automatically within them. Read them, stay inside them, and when a service puts a human check in front of you, treat it as the answer rather than the obstacle. Everything measured above works because the browser tells the truth about itself consistently - not because it beats a challenge.

Try it on the free tier.

Unlimited local profiles, no credit card. Check it against the detectors yourself.