Headers get you in the door. The DOM is what gives you away.
Meteor generates the User-Agent, the Client Hints and the navigator / screen / WebGL surface — as one identity that agrees with itself — ready to inject into Playwright or Selenium.
UA + Client Hints · navigator / screen / WebGL · Playwright & Selenium ready · zero dependencies
⋆ ˚ 。 ⋆ ୨ ⋆ ˚ 。 ⋆
Why Meteor · Install · Quick start · Coherence · Playwright · Filters · API
| Feature | What it does | |
|---|---|---|
| ☄ | Whole fingerprint | UA + Sec-CH-UA + navigator + screen + WebGL vendor/renderer, in one coherent object. |
| ◈ | JS surface | platform, hardwareConcurrency, deviceMemory, maxTouchPoints, languages, GPU strings — the values headless-detection actually reads. |
| ⟡ | Engine-aware | Client Hints and deviceMemory only for Chromium; Firefox / Safari get none, matching reality. |
| ✦ | Inject-ready | inject_script() spoofs navigator + WebGL getParameter; to_playwright() returns a ready context config. |
| ⌗ | HTTP-ready | headers() / header_lines() for requests, httpx, aiohttp — in real send order. |
| ⬡ | Zero deps | Pure standard library. Nothing to compile, nothing to pin. |
fake-useragent and friends give you a string. But a real anti-bot doesn't stop at
the UA header — it runs JavaScript and reads navigator.platform,
navigator.hardwareConcurrency, screen.width, the WebGL renderer, and checks
them against the UA you claimed. A Windows UA with navigator.platform === "MacIntel",
or a Chrome UA whose WebGL says "Apple GPU", is an instant flag.
Meteor is the Python member of the Nebula toolkit, and it goes past the header line into the surface that actually gets fingerprinted.
| Meteor | fake-useragent | ua-generator | user-agents | |
|---|---|---|---|---|
| UA string | ✓ | ✓ | ✓ | ✓ |
Client Hints (Sec-CH-UA) |
✓ | ✗ | ✓ | ✗ |
navigator surface |
✓ | ✗ | ✗ | partial |
screen + WebGL |
✓ | ✗ | ✗ | ✗ |
| Playwright / Selenium inject | ✓ | ✗ | ✗ | ✗ |
| UA ↔ JS coherence | ✓ | — | — | — |
| Runtime dependencies | none | data file | none | none |
pip install git+https://github.com/Vxsilisk/Meteor.gitOr vendor it — it's pure standard library, so the meteor/ package drops straight
into any project.
Requirements: Python ≥ 3.9. No third-party packages.
from meteor import Meteor
fp = Meteor.chrome("windows")
fp.user_agent # Mozilla/5.0 (Windows NT 10.0; Win64; x64) …Chrome/140.0.0.0 Safari/537.36
fp.sec_ch_ua # "Not?A_Brand";v="8", "Chromium";v="140", "Google Chrome";v="140"
fp.headers() # ordered dict, ready for requests/httpx
fp.navigator # {'platform': 'Win32', 'hardwareConcurrency': 16, 'deviceMemory': 8, ...}
fp.webgl # {'vendor': 'Google Inc. (Intel)', 'renderer': 'ANGLE (Intel, …)'}Feed the headers to any HTTP client:
import requests
requests.get("https://example.com", headers=Meteor.chrome().headers())Every layer is generated together, so they can't disagree:
fp = Meteor.random()
# 1. Client Hints exist iff the engine is Chromium.
assert (fp.sec_ch_ua is not None) == (fp.engine == "blink")
# 2. navigator.userAgent is literally the UA header.
assert fp.navigator["userAgent"] == fp.user_agent
# 3. navigator.platform matches the OS (Win32 / MacIntel / Linux …).
# 4. The mobile bit, maxTouchPoints and pixel ratio all agree.
# 5. WebGL renderer matches the platform's GPU family.
# 6. Impossible combos raise instead of lying.
Meteor.safari("windows") # MeteorError — Safari isn't on WindowsFirefox and Safari return None for every Client-Hints accessor and omit
deviceMemory from navigator — exactly like the real browsers.
to_playwright() returns a context config plus an init script that patches the
JS surface so it matches the UA you set:
from playwright.sync_api import sync_playwright
from meteor import Meteor
fp = Meteor.chrome("macos")
with sync_playwright() as p:
browser = p.chromium.launch()
pw = fp.to_playwright()
context = browser.new_context(
user_agent=pw["user_agent"],
viewport=pw["viewport"],
locale=pw["locale"],
timezone_id=pw["timezone_id"],
device_scale_factor=pw["device_scale_factor"],
)
context.add_init_script(pw["init_script"]) # spoofs navigator + WebGL
page = context.new_page()
page.goto("https://example.com")Selenium (CDP):
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": fp.inject_script()})
driver.execute_cdp_cmd("Network.setUserAgentOverride", {"userAgent": fp.user_agent})Meteor.chrome("android") # Chrome on Android
Meteor.edge("windows")
Meteor.firefox("linux")
Meteor.safari("ios")
Meteor.random("chrome", os="macos")
Meteor.desktop() # any desktop fingerprint
Meteor.mobile("chrome") # mobile Chrome
Meteor.user_agent("firefox") # just the string| Browser | Engine | Platforms |
|---|---|---|
chrome |
blink | windows · macos · linux · android |
edge |
blink | windows · macos |
firefox |
gecko | windows · macos · linux · android |
safari |
webkit | macos · ios |
Reproducible for tests and fixtures:
Meteor.seed(42)
Meteor.random() # deterministic from hereMeteor (factory, all class methods)
| Method | Returns |
|---|---|
random(browser=None, os=None, device=None) |
Fingerprint |
chrome(os=None) · edge · firefox · safari |
Fingerprint |
desktop(browser=None) · mobile(browser=None) |
Fingerprint |
user_agent(browser=None, os=None) |
str |
seed(int) |
None |
Fingerprint (frozen dataclass)
| Member | Type |
|---|---|
user_agent · browser · engine · version · platform |
str |
mobile |
bool |
sec_ch_ua · sec_ch_ua_mobile · sec_ch_ua_platform |
str | None |
headers() · header_lines() |
dict · list[str] |
navigator · screen · webgl |
dict |
inject_script() |
str (JS) |
to_playwright() · to_dict() |
dict |
Meteor/
├── pyproject.toml
├── examples/demo.py # runnable demo
├── tests/test_meteor.py # unittest suite (incl. 1000-fingerprint coherence fuzz)
└── meteor/
├── __init__.py
├── generator.py # the Meteor factory + coherence resolver
├── fingerprint.py # the Fingerprint dataclass (headers, JS surface, injection)
└── catalog.py # browser / OS / engine / hardware reference data
Run
python examples/demo.pyfor a live demo, orpython -m unittest discover -s testsfor the suite.