🔒 Self-hosted anti-detect browser profiles. The open-source alternative to AdsPower & Multilogin. Run locally, own your data, no subscriptions.
Like n8n for automation or Affine for notes, this is AdsPower for developers — self-hosted, open-source, and privacy-first.
| ❌ AdsPower/Multilogin | ✅ browser-profiles |
|---|---|
| $99+/month | Free forever |
| Cloud storage (data not yours) | Local storage (your data) |
| GUI only | Code-first (Puppeteer/Playwright) |
| Vendor lock-in | Open source (MIT) |
| No customization | Full control |
- 🏠 Self-hosted - Data stays on your machine, no cloud dependency
- 🔐 Privacy-first - Zero telemetry, zero tracking
- 🛡️ Anti-detect - WebRTC, Canvas, WebGL, Audio fingerprint protection
- 🌐 Proxy support - HTTP, HTTPS, SOCKS5 with auto timezone detection
- 📦 Profile management - Create, update, delete browser profiles
- 🎭 Puppeteer & Playwright - First-class integration
- ⚡ Zero config - Works out of the box
- 🪶 Lightweight - No extensions, pure CDP injection
# Recommended: rebrowser-puppeteer-core (better anti-detect!)
npm install @aitofy/browser-profiles rebrowser-puppeteer-core
# Or standard puppeteer
npm install @aitofy/browser-profiles puppeteer-core| Site | Score | Notes |
|---|---|---|
| browserleaks.com | ✅ 100% | All checks passed |
| pixelscan.net | ✅ 100% | Hardware fingerprint passed |
| browserscan.net | Bot Control -5% (Puppeteer limitation) | |
| creepjs | Minor deductions |
Note: 95% is the best achievable with Puppeteer/Playwright. 100% requires modified Chromium (like AdsPower).
Install globally to use the CLI:
npm install -g @aitofy/browser-profiles# List all profiles
browser-profiles list
# Create a new profile
browser-profiles create my-account
browser-profiles create my-account --proxy http://user:pass@proxy.com:8080
# Open browser with a profile
browser-profiles open <profile-id>
# Quick launch (no profile needed)
browser-profiles launch
browser-profiles launch --proxy http://proxy.com:8080
# Show profile details
browser-profiles info <profile-id>
# Delete a profile
browser-profiles delete <profile-id>
# Show storage path
browser-profiles pathimport { quickLaunch } from '@aitofy/browser-profiles';
// Launch anti-detect browser with proxy (timezone auto-detected!)
const { page, close } = await quickLaunch({
proxy: { type: 'http', host: 'your-proxy.com', port: 8080 },
});
await page.goto('https://browserscan.net');
await page.screenshot({ path: 'proof.png' });
await close();That's it! 🎉 Browser fingerprint is protected, IP is hidden, timezone matches proxy location.
import { BrowserProfiles } from '@aitofy/browser-profiles';
const profiles = new BrowserProfiles();
// Create a profile (saved to ~/.aitofy/browser-profiles/)
const profile = await profiles.create({
name: 'My Profile',
proxy: {
type: 'http',
host: 'proxy.example.com',
port: 8080,
},
});
// Launch browser
const { wsEndpoint, close } = await profiles.launch(profile.id);
// ... automation work ...
await close();You can define your own custom profile IDs instead of using auto-generated ones:
// Create profile with custom ID
const profile = await profiles.create({
id: 'google-main', // Custom ID (alphanumeric + hyphen/underscore)
name: 'Google Account',
});
// Launch by custom ID directly
const { wsEndpoint } = await profiles.launch('google-main');Custom ID rules:
- 1-64 characters
- Alphanumeric with hyphens and underscores only (a-z, A-Z, 0-9, -, _)
- Must be unique
You can now launch browsers using the profile name instead of ID:
// Create profile
await profiles.create({ name: 'Facebook Account' });
// Launch by name (case-insensitive)
const { wsEndpoint } = await profiles.launchByName('Facebook Account');
// Or use launchByIdOrName (works with both ID and name)
await profiles.launchByIdOrName('google-main'); // By ID
await profiles.launchByIdOrName('Facebook Account'); // By nameimport { withPuppeteer } from '@aitofy/browser-profiles/puppeteer';
const { browser, page, close } = await withPuppeteer({
profile: 'my-profile-id', // or profile name
});
await page.goto('https://whoer.net');
await page.screenshot({ path: 'screenshot.png' });
await close();import { quickLaunch } from '@aitofy/browser-profiles/puppeteer';
const { browser, page, close } = await quickLaunch({
proxy: {
type: 'http',
host: 'proxy.example.com',
port: 8080,
},
// timezone is now AUTO-DETECTED from proxy IP!
// No need to specify manually. If no proxy, system timezone is used.
fingerprint: {
platform: 'Win32', // Spoof as Windows
hardwareConcurrency: 8, // Spoof CPU cores
language: 'en-US',
},
});
// Output: [browser-profiles] 🌍 Auto-detected timezone: America/New_York (New York, United States)
await page.goto('https://browserscan.net');
await close();import { withPlaywright } from '@aitofy/browser-profiles/playwright';
const { browser, page, close } = await withPlaywright({
profile: 'my-profile-id',
});
await page.goto('https://example.com');
await close();import { createSession } from '@aitofy/browser-profiles/puppeteer';
// Quick session with random fingerprint - perfect for scraping
const session = await createSession({
temporary: true,
randomFingerprint: true, // Random platform, language, CPU cores
proxy: { type: 'http', host: 'proxy.com', port: 8080 },
});
await session.page.goto('https://example.com');
await session.close(); // Cleanupimport { patchPage } from '@aitofy/browser-profiles/puppeteer';
// Apply anti-detect patches to any page
await patchPage(page, {
webdriver: true, // Hide webdriver flag
plugins: true, // Spoof Chrome plugins
chrome: true, // Fix chrome object
webrtc: true, // WebRTC leak protection
fingerprint: { platform: 'Win32', hardwareConcurrency: 8 },
});import { generateFingerprint, getFingerprintScripts } from '@aitofy/browser-profiles';
// Generate a realistic fingerprint
const fp = generateFingerprint({
platform: 'macos',
gpu: 'apple',
screen: 'retina',
language: 'ja-JP',
});
console.log(fp.userAgent); // Mozilla/5.0 (Macintosh...
console.log(fp.webgl.renderer); // ANGLE (Apple, Apple M1 Pro...
// Use with any page
const scripts = getFingerprintScripts(fp);
await page.evaluateOnNewDocument(scripts);import { launchChromeStandalone } from '@aitofy/browser-profiles';
import puppeteer from 'puppeteer-core';
// Launch Chrome without profile management
const { wsEndpoint, close } = await launchChromeStandalone({
headless: false,
proxy: { type: 'http', host: 'proxy.com', port: 8080 },
});
const browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
await close();import puppeteer from 'rebrowser-puppeteer-core';
import { withPuppeteer } from '@aitofy/browser-profiles/puppeteer';
// Use your own puppeteer instance
const { browser, page } = await withPuppeteer({
profile: 'my-profile',
puppeteer, // ← Inject here
});Profiles are saved locally to ~/.aitofy/browser-profiles/ and persist between sessions.
import { BrowserProfiles } from '@aitofy/browser-profiles';
const profiles = new BrowserProfiles();
// ===== CREATE =====
const profile = await profiles.create({
name: 'Facebook Account 1',
proxy: { type: 'http', host: 'proxy.example.com', port: 8080 },
tags: ['facebook', 'marketing'],
});
// ===== LIST ALL =====
const allProfiles = await profiles.list();
console.log(`Total: ${allProfiles.length} profiles`);
// ===== LIST BY TAG =====
const fbProfiles = await profiles.list({ tags: ['facebook'] });
// ===== GET BY ID =====
const myProfile = await profiles.get(profile.id);
// ===== UPDATE =====
await profiles.update(profile.id, {
name: 'Facebook Account 1 - Updated',
proxy: { type: 'socks5', host: 'new-proxy.com', port: 1080 },
});
// ===== LAUNCH BROWSER =====
const { wsEndpoint, close } = await profiles.launch(profile.id);
// ... do automation ...
await close();
// ===== DUPLICATE =====
const cloned = await profiles.duplicate(profile.id, 'Facebook Account 2');
// ===== EXPORT / IMPORT =====
const json = await profiles.export(profile.id);
const imported = await profiles.import(json);
// ===== DELETE =====
await profiles.delete(profile.id);// Create groups
const group = await profiles.createGroup('TikTok Shop', 'All TikTok accounts');
// Move profile to group
await profiles.moveToGroup(profile.id, group.id);
// List groups
const groups = await profiles.listGroups();
// List profiles in group
const tikTokProfiles = await profiles.list({ groupId: group.id });import { ExTowerClient } from '@aitofy/browser-profiles/extower';
import puppeteer from 'puppeteer-core';
const client = new ExTowerClient({
baseUrl: 'http://localhost:50325', // default
});
// Create profile in ExTower
const { id } = await client.createProfile({
name: 'TikTok Shop 1',
proxy: {
type: 'http',
host: 'proxy.example.com',
port: 8080,
},
});
// Launch browser
const { puppeteer: wsEndpoint } = await client.launchBrowser(id);
// Connect Puppeteer
const browser = await puppeteer.connect({
browserWSEndpoint: wsEndpoint,
});
const page = await browser.newPage();
await page.goto('https://seller-us.tiktok.com');Main class for managing browser profiles.
const profiles = new BrowserProfiles({
storagePath: '~/.aitofy/browser-profiles', // Default: ~/.aitofy/browser-profiles
chromePath: '/path/to/chrome', // Custom Chrome path (optional)
defaultTimezone: 'UTC', // Default timezone for new profiles
});Default storage locations:
- macOS:
~/.aitofy/browser-profiles - Linux:
~/.aitofy/browser-profiles - Windows:
C:\Users\<user>\.aitofy\browser-profiles
| Method | Description |
|---|---|
create(config) |
Create a new profile (supports custom ID via config.id) |
get(id) |
Get profile by ID |
getByName(name) |
Get profile by name (case-insensitive) |
getByIdOrName(idOrName) |
Get profile by ID or name |
list(options?) |
List all profiles |
update(id, updates) |
Update profile |
delete(id) |
Delete profile |
launch(id, options?) |
Launch browser by profile ID |
launchByName(name, options?) |
Launch browser by profile name |
launchByIdOrName(idOrName, options?) |
Launch browser by ID or name |
close(id) |
Close running browser |
closeAll() |
Close all browsers |
duplicate(id) |
Duplicate profile |
export(id) |
Export to JSON |
import(json) |
Import from JSON |
interface ProfileConfig {
id?: string; // Custom profile ID (auto-generated if omitted)
name: string; // Profile name
proxy?: ProxyConfig; // Proxy settings
timezone?: string; // e.g., "America/New_York"
cookies?: ProfileCookie[]; // Cookies to inject
fingerprint?: FingerprintConfig; // Fingerprint settings
startUrls?: string[]; // URLs to open on launch
tags?: string[]; // Tags for organization
groupId?: string; // Group for organization
}interface ProxyConfig {
type: 'http' | 'https' | 'socks5';
host: string;
port: number | string;
username?: string;
password?: string;
}interface LaunchOptions {
headless?: boolean; // Run headless (default: false)
chromePath?: string; // Custom Chrome path
args?: string[]; // Additional Chrome args
extensions?: string[]; // Extension paths to load
defaultViewport?: { width: number; height: number } | null;
slowMo?: number; // Slow down by ms
timeout?: number; // Launch timeout
}All fingerprint protections are hardcoded and injected via CDP - no extensions required!
Comprehensive protection against bot detection:
| Check | Status | Description |
|---|---|---|
navigator.webdriver |
✅ Hidden | Returns undefined |
window.chrome |
✅ Faked | Complete chrome object |
chrome.runtime |
✅ Faked | Runtime object present |
chrome.csi() |
✅ Faked | Timing function |
chrome.loadTimes() |
✅ Faked | Page load metrics |
navigator.plugins |
✅ Faked | 3 default Chrome plugins |
navigator.connection |
✅ Faked | 4G connection info |
navigator.getBattery() |
✅ Faked | Battery status API |
navigator.permissions |
✅ Mocked | Permission query handling |
Automatically prevents WebRTC from leaking your real IP address. Works even when using proxy!
Adds random noise to canvas data to prevent tracking.
Spoofs WebGL parameters and adds noise to buffer data:
- Randomizes vendor/renderer strings
- Spoofs GPU parameters
- Adds noise to WebGL buffer data
Adds tiny noise to audio data without affecting audio quality.
const profile = await profiles.create({
name: 'US Profile',
timezone: 'America/New_York', // Browser reports this timezone
});Customize browser navigator properties:
const profile = await profiles.create({
name: 'Custom Profile',
fingerprint: {
language: 'en-US',
platform: 'Win32',
hardwareConcurrency: 8,
deviceMemory: 16,
},
});Handles authenticated proxies transparently:
const profile = await profiles.create({
name: 'With Auth Proxy',
proxy: {
type: 'http',
host: 'proxy.example.com',
port: 8080,
username: 'user', // ✅ Auth handled automatically
password: 'password',
},
});Tested on 2026-01-08 with the following fingerprint testing sites:
| Property | Spoofed Value | Verification |
|---|---|---|
| Timezone | America/New_York | ✅ BrowserScan |
| Platform | Win32 | ✅ BrowserLeaks |
| CPU Cores | 8 | ✅ PixelScan |
| Device Memory | 16GB | ✅ PixelScan |
| Language | en-US | ✅ All sites |
| WebRTC IP | Hidden | ✅ BrowserLeaks |
| Webdriver | Hidden | ✅ All sites |
| Plugins | 5 | ✅ BrowserLeaks |
| Connection API | 4g | ✅ Verified |
| Chrome Object | Complete | ✅ Verified |
| Chrome.csi | Present | ✅ Verified |
| Chrome.loadTimes | Present | ✅ Verified |
Sites tested:
| Feature | browser-profiles | AdsPower | Multilogin | puppeteer-extra |
|---|---|---|---|---|
| Open Source | ✅ MIT | ❌ Paid | ❌ Paid | ✅ MIT |
| Price | FREE | $9-50/mo | $99-199/mo | Free |
| Anti-Detect Score | 95% | 100% | 100% | ~80% |
| Profile Storage | ✅ | ✅ | ✅ | ❌ |
| Proxy Auth | ✅ | ✅ | ✅ | ❌ |
| Auto Timezone | ✅ | ✅ | ✅ | ❌ |
| WebRTC Protection | ✅ | ✅ | ✅ | |
| Canvas Noise | ✅ | ✅ | ✅ | |
| TypeScript | ✅ | ❌ | ❌ | |
| npm Package | ✅ | ❌ | ❌ | ✅ |
| Puppeteer Integration | ✅ | ✅ | ✅ | |
| Playwright Integration | ✅ | ❌ | ✅ | |
| Cloud Sync | 🔜 Coming | ✅ | ✅ | ❌ |
| GUI | ❌ | ✅ | ✅ | ❌ |
AdsPower and Multilogin achieve 100% by using modified Chromium binaries. Our library uses standard Chrome with JS injection, which has a fundamental ~5% detection limitation on advanced sites like BrowserScan.
For most use cases (social media, e-commerce, scraping), 95% is sufficient.
- All data stored locally at
~/.aitofy/browser-profiles/ - Zero telemetry - no data sent to any server
- Profile configs stored as JSON files
- Chrome user data (passwords, autofill) encrypted by Chrome itself
const profiles = new BrowserProfiles({
encryption: {
enabled: true,
masterKey: 'your-secret-key',
}
});- AES-256-GCM encryption for sensitive data
- Optional cloud sync with end-to-end encryption
- Only you (or people you share the key with) can decrypt
Starting from v0.2.5, PuppeteerPage and PlaywrightPage are native types re-exported from puppeteer-core and playwright. You now have access to ALL APIs directly:
import { withPuppeteer, PuppeteerPage } from '@aitofy/browser-profiles';
const { page, close } = await withPuppeteer({ profile: 'my-profile' });
// ✅ Full API access - no workarounds needed!
await page.setRequestInterception(true);
page.on('request', (req) => {
if (req.resourceType() === 'image') {
req.abort();
} else {
req.continue();
}
});
const cookies = await page.cookies();
await page.setCookie({ name: 'session', value: '123', domain: '.example.com' });
await close();For Playwright:
import { withPlaywright, PlaywrightPage } from '@aitofy/browser-profiles';
const { page, close } = await withPlaywright({ profile: 'my-profile' });
// ✅ Full Playwright API!
await page.route('**/*', (route) => {
if (route.request().resourceType() === 'image') {
route.abort();
} else {
route.continue();
}
});
await page.reload();
await page.waitForTimeout(1000);
await close();All commonly used types are re-exported for convenience:
| Puppeteer Types | Playwright Types |
|---|---|
PuppeteerPage |
PlaywrightPage |
PuppeteerBrowser |
PlaywrightBrowser |
HTTPRequest |
PlaywrightContext |
HTTPResponse |
PlaywrightRequest |
Cookie |
PlaywrightResponse, Route |
Contributions are welcome! Please read our Contributing Guide.
MIT © Aitofy
Made with ❤️ by Aitofy