shardx (Python)
Self-contained Python SDK for the ShardX anti-detect browser by the ProxyShard team.
This package does not depend on the desktop launcher. On first use it downloads the patched Chromium 152 engine, Widevine CDM, and the 220-profile fingerprint library from our CDN into a local cache, then launches isolated browser sessions on demand.
Driven by patchright
(stealth-patched Playwright) — sdk.session() returns a ready-to-use
Browser instance, no manual connect_over_cdp plumbing.
Install
pip install shardx
Supported hosts: macOS arm64, Windows x64, Linux x64.
Linux system dependencies
The bundled Chromium engine needs unzip + the standard set of shared
libraries any Chromium fork links against. On a fresh Debian / Ubuntu:
sudo apt install -y \
unzip ca-certificates fonts-liberation \
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 \
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
libgbm1 libpango-1.0-0 libcairo2 libasound2 libxshmfence1
When launching as root or inside Docker, pass --no-sandbox and
--disable-dev-shm-usage via extra_args=:
await sdk.session(..., extra_args=["--no-sandbox", "--disable-dev-shm-usage"])
Quick start
import asyncio
from shardx import ShardX
async def main():
sdk = ShardX()
# Engine + Widevine + fingerprint library auto-download from CDN on
# the first `session`/`launch`/`list_profiles` call (~170 MB once,
# etag-cached afterward). No separate install step.
# Create a persistent profile from a library template (or create_profile()
# for a random one). Library templates aren't launched directly — this
# freezes an enriched copy under a unique id you can return to. Do it once.
profile = sdk.create_profile("win-rtx4060")
# Launch + drive in one call. Yields a patchright `Browser`.
async with sdk.session(profile, proxy="socks5://user:pass@host:port") as browser:
ctx = browser.contexts[0]
page = await ctx.new_page()
await page.goto("https://browserleaks.com/quic")
print(await page.title())
# Inspect what the SDK resolved before launch:
sess = browser._shardx
print(sess.geo) # GeoInfo(...) from ip-api / ipapi.co
print(sess.proxy_udp_ms, # UDP RTT in ms or None
sess.quic_enabled, # bool
sess.webrtc_mode) # "auto" | "tcp_only" | "block"
# browser + udd shut down cleanly on exit
asyncio.run(main())
Random profile
# create_profile() with no id freezes a random library template (filter the
# pool with platform="Windows"). It already randomises hardware_concurrency /
# device_memory / platform_version once, at creation.
profile = sdk.create_profile(platform="Windows")
async with sdk.session(profile) as browser:
page = await browser.contexts[0].new_page()
...
Discover bundled profiles
print(sdk.list_profiles()[:5])
# ['linux-gt1030', 'linux-gtx1050', 'mac-m1-air13', 'mac-m1-imac24', 'mac-m1-max-mbp14']
print(sdk.list_profiles(platform="Windows")[:5])
profile = sdk.random_profile(platform="macOS")
print(profile.id, profile.config["webgl"]["renderer"])
Validate a proxy before binding
print(sdk.check_proxy("socks5://user:pass@host:port"))
# {
# 'udp_ms': 142.3,
# 'geo': GeoInfo(country_code='DE', timezone='Europe/Berlin', ...),
# 'would_enable_quic': True,
# 'would_set_webrtc': 'auto',
# }
Persistent profiles
list_profiles() / random_profile() hand back library templates — launch
one and it re-reads the same template every time. For a profile you can
return to (same fingerprint and cookies/cache) or delete, create a
saved profile: it freezes a template (or a random one) with randomized
hardware/platform_version under a fresh unique id in its own folder
<cache>/profiles/<id>/, exactly like the desktop launcher's "create profile".
sdk = ShardX()
# Create once (random template, or pass a library id like "win-rtx4060").
profile = sdk.create_profile()
print(profile.id)
print(sdk.list_saved_profiles()) # ['<id>', ...]
# Launch it. randomize stays False → the frozen fingerprint is stable;
# cookies/cache persist in the profile's folder across runs.
async with sdk.session(profile) as browser:
...
# Later — even a different process — reopen by id: same fingerprint + state.
profile = sdk.open_profile("<id>")
# Remove the profile and all its state when you're done.
sdk.delete_profile("<id>")
Saved profiles never touch the bundled S3 library: templates stay read-only in
<cache>/fingerprints/*.json, saved profiles live in <cache>/profiles/<id>/
(holding profile.json + the browser's user-data-dir).
Human input and finger gestures
Motion is the engine's own input domain: pointer trajectories that obey
Fitts's law, key-by-key typing with log-normal gaps, and the gestures a cursor
cannot make. It runs inside the browser process — nothing is injected into the
page — and it does not appear in /json/protocol, so a page cannot discover it.
from shardx import ShardX, Motion
async with sdk.session(profile) as browser:
page = await browser.new_page()
# Motion acts on a page's widget, so it attaches to a page.
motion = await Motion.attach(page)
# desktop profile
await motion.create_pointer(20, 20)
await motion.glide_to(640, 360, target_width=220) # the field's real width
await motion.tap()
await motion.enter_text("hello there")
await motion.destroy_pointer()
# phone profile
await motion.touch_swipe(200, 600, 200, 200, flick=True) # scroll and fling
await motion.touch_long_press(200, 300) # context menu
await motion.pinch(200, 400, 2) # zoom in
o = await motion.set_orientation(90)
# o → {'angle': 90, 'type': 'landscape-primary', 'screenWidth': 844, ...}
A profile that claims a touchscreen has no cursor, and the core enforces it
both ways: the pointer commands are refused on such a profile, and the finger
commands are refused on every other. There is no touchScroll and no fling
command — gestures are built in the browser out of the touch stream, so a real
swipe produces the scroll and the fling with the velocity the browser's own
tracker fitted. touch_drag differs from touch_swipe in the wait before the
travel: that wait is when a page's drag-and-drop starts, and a stroke that
begins earlier is a scroll.
set_orientation is physical and takes time — the sensors move first and the
picture commits at the end, the order a handset produces. It returns once the
new angle has reached the page, so screen.width read straight after is
already the turned one.
Anti-fingerprint noise
Per-vector noise (canvas / WebGL / audio / DOMRect / sensors / fonts) is off
by default. set_noise(...) is declarative — exactly the vectors you list
are enabled (with soft defaults), every other one is turned off:
profile.set_noise("canvas", "audio", "webgl") # only these three on
profile.set_noise("canvas") # audio + webgl now off again
profile.set_noise() # all off
sdk.save_profile(profile) # persist the choice
Seeds are derived per-profile at launch — stable across runs, unique per
profile — so two profiles with the same vectors enabled still produce different
canvas/audio/WebGL fingerprints. Soft defaults: WebGL intensity 0.0005,
DOMRect max_offset 1.
Pre-launch checks
Every call to sdk.session() / sdk.launch() runs the same pre-spawn
pipeline the desktop launcher uses:
resolve_auto_fields— if the profile has"auto"sentinels fortimezone,navigator.language, orgeolocation.mode, the SDK makes a live geo lookup through the bound proxy (ip-api.comby default). It then writes concrete values: timezone (from the API, never a static table),accept_languagechain,languages,icu_locale(always overwritten soIntl.*matchesnavigator.language), and lat/lng. Proxy-via failure → direct geo → host$LANG/$TZas last-resort fallback. The chosen geo is surfaced onsession.geo.apply_screen_strategy— see below.probe_udp— SOCKS5 UDP_ASSOCIATE round-trip. If it fails, QUIC is force-disabled and WebRTC switches totcp_onlyautomatically.
Screen strategy
screen_mode kw to session() / launch():
"profile"— keep whatever the fingerprint claims."cap_to_host"— macOS default. If the host monitor is smaller than the FP screen, scalescreen.*+window.*down proportionally; otherwise no-op."use_host"— Windows/Linux default. Overwritescreen.*with the real monitor (minus a 40 px Windows taskbar) and recomputewindow.outer_*/window.inner_*accordingly.
Default mode is picked from navigator.platform. Override per launch:
async with sdk.session(profile, screen_mode="profile") as browser:
...
Host-aware hardware randomisation
randomize=True re-picks hardware_concurrency, device_memory, and
platform_version before the launch — using the same logic as the
desktop launcher (randomize_hardware in lib.rs):
- macOS profiles use the curated
MAC_HW_CONFIGStable by id. - Windows / Linux profiles bracket the host's logical CPU count
within
[host − 4, host + 2]from the real x86 set[4, 6, 8, 12, 16, 20, 24, 28, 32];device_memoryis floored by core count (≥ 12 → 16, else 8) and capped byhost_ram_bucket_gb()(8 / 16 / 32 GiB bucketed fromsysctl hw.memsize//proc/meminfo/Get-CimInstance Win32_ComputerSystem).
So a profile launched on an 8-core / 16 GB laptop will never claim 32 cores / 128 GB of RAM — keeps fingerprints internally consistent with real-world hardware.
Override fingerprint fields
profile = sdk.library.load("win-rtx4060").with_override(
name="my-account",
timezone="Europe/Berlin",
navigator={"language": "de-DE"},
)
async with sdk.session(profile, proxy="socks5://...") as browser:
...
Use your own fingerprint JSON
from shardx import Profile
profile = Profile.from_file("/path/to/my-custom.json")
async with sdk.session(profile) as browser:
...
WebRTC policy
async with sdk.session(
profile,
proxy="socks5://...",
webrtc="tcp_only", # or "block" | "auto" (default)
webrtc_public_ip="203.0.113.42", # advertised in ICE candidates
) as browser:
...
Advanced: raw launch without patchright
If you'd rather drive the browser with a different CDP client (raw
pychrome, pyppeteer, your own WebSocket), skip session() and use
launch() directly:
profile = sdk.create_profile("win-rtx4060")
sess = sdk.launch(profile, proxy="socks5://...", cdp=True)
print(sess.cdp_url) # ws://127.0.0.1:54113/devtools/browser/c0a3…
# … drive it yourself …
sess.stop()
launch() does the same pre-launch pipeline (auto-resolve, screen
strategy, UDP probe, hw randomisation) and returns a BrowserSession
with cdp_url, geo, proxy_udp_ms, quic_enabled, webrtc_mode,
user_data_dir, and stop().
Cache layout
~/Library/Application Support/shardx-sdk/ (mac)
%LOCALAPPDATA%\shardx-sdk\ (win)
~/.cache/shardx-sdk/ (linux)
├── manifest.json ← etag cache for browser/widevine/fingerprints
├── ShardX-Mac-arm64/ ← extracted engine
│ └── ShardX.app/…
├── fingerprints/ ← 220 bundled .json profiles
│ ├── win-rtx4060.json
│ └── …
└── profiles/ ← saved profiles (create_profile) + their state
└── <profile-id>/
├── profile.json ← the frozen fingerprint config
└── … ← user-data-dir: cookies, IndexedDB, cache
Override the cache root:
sdk = ShardX(cache_dir="/data/shardx")
Update the runtime
The SDK auto-checks remote etags on the first session/launch/list_profiles
call of each process and re-downloads anything that changed. To force a
re-download mid-process (e.g. CI scenarios):
sdk.runtime.install(force=True)
License
MIT (this SDK). The Chromium-fork engine binary downloaded at runtime is a closed-source product — see the main repo for engine licensing.
Metadata
Release files for shardx 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| shardx-2.1.0.tar.gz | 35.8 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| shardx-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 76.4 kB
Security for shardx 2.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.