operator field manual

Learn the techniques

Every technique from the talk — description, working code, and a worked example — mapped to the target it defeats. Code is verified against current (2026) library APIs. Use it only against targets you're authorized to scrape.

00

Orientation

You submit the data, not the code. A validation engine scores what you extract against hidden ground truth. Every defense here is a real-world scraping obstacle, beatable by exactly the techniques below.

ethicsscoring

Rules of engagement

Only scrape targets you are authorized to. greynet's seven sites are synthetic practice targets built for this — nothing here touches a real third party. Register a team, get a bearer token, beat the defenses, POST your scraped JSON to /api/submit/<challenge>.

beatsAll seven targets

How you're scored

Completeness (primary): records × fields matched vs ground truth — OCR/text fields use fuzzy matching, so minor OCR errors still earn partial credit. Speed (tiebreak): first authenticated request → best submission. Stealth (modifier): request-timing humanization; hammering or perfectly-periodic traffic grades poorly.

Submit the WHOLE dataset as one JSON. Partial dumps score only what they contain — the response tells you `matched X / N`.

01

Architect Execution Pipelines

Pick the cheapest tool that works: a raw HTTP client against the hidden API beats a browser every time — until the site needs JavaScript or a real TLS fingerprint. Escalate only as far as you must.

requests/httpxcurl_cffiPlaywrightPatchrightXvfb

Direct API requests (the fast path)

Most sites render from a JSON endpoint their own front-end calls. Open DevTools → Network, find it, and hit it directly — no browser needed. This is the single highest-leverage move in scraping.

python · requests → hidden JSON API
import requests

# The page is JS-rendered, but the data is here:
r = requests.get(
    "https://target.example/api/megacorp/listings",
    params={"region": "apac", "page": 1},
    headers={"Authorization": "Bearer <token>", "Cookie": "og_session=..."},
)
for job in r.json()["items"]:
    print(job["uuid"], job["title"])
beatsOmniCorp hidden APISpecimen nested dossier APIASIC settlement JSON

curl_cffi — impersonate a browser's TLS/JA3 fingerprint

Plain requests/httpx have a Python TLS fingerprint that anti-bot systems flag instantly. curl_cffi sends Chrome's exact TLS/JA3 + HTTP/2 fingerprint, so a header-screened API treats you like a real browser — without the cost of running one.

python · pip install curl_cffi
from curl_cffi import requests

# Impersonate the latest Chrome (TLS/JA3 + HTTP2 + headers):
r = requests.get("https://target.example/api/specimen/dossier/<uuid>",
                 impersonate="chrome")           # or "chrome124", "safari17_0", ...
print(r.json())

# A Session keeps cookies AND the impersonated fingerprint across requests:
s = requests.Session(impersonate="chrome")
s.get("https://target.example/")                 # collect cookies
data = s.get("https://target.example/api/asic/lots").json()

Use `impersonate="chrome"` for the newest profile; pin a version (e.g. `chrome124`) for reproducibility. This is how you beat the Specimen/ASIC fingerprint gates from a plain script.

beatsSpecimen fingerprint gateASIC background-JSON gate

Playwright — a real browser for JS-rendered content

When data only appears after JavaScript runs (AJAX tables, detail popups), drive a real browser. Playwright is the modern standard across Python/Node.

python · pip install playwright && playwright install
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://target.example/salvage")
    page.wait_for_selector("table tbody tr")
    rows = page.locator("table tbody tr")
    for i in range(rows.count()):
        print(rows.nth(i).inner_text())
    browser.close()

Patchright — undetected, drop-in Playwright

Vanilla Playwright leaks automation via the Chrome DevTools Protocol; many WAFs detect it. Patchright is a patched drop-in — change only the import. For maximum stealth its docs recommend a persistent context with the real Chrome channel, headed.

python · pip install patchright && patchright install chrome
from patchright.sync_api import sync_playwright   # only the import changes

with sync_playwright() as p:
    ctx = p.chromium.launch_persistent_context(
        user_data_dir="/tmp/pw-profile",
        channel="chrome",        # real Chrome, not bundled Chromium
        headless=False,          # headed is far less detectable
        no_viewport=True,
    )
    page = ctx.new_page()
    page.goto("https://target.example")
beatsOmniCorp headless nudgeSpecimen fingerprint gateASIC fingerprint gate

Headed Chrome on a server with Xvfb

Headless mode has tells. To run a *headed* browser on a server with no display, wrap it in Xvfb (a virtual framebuffer). Launch the browser with headless=False.

bash
# Run a headed browser on a headless box:
xvfb-run -a --server-args="-screen 0 1920x1080x24" python scrape.py
# inside scrape.py:  p.chromium.launch(headless=False)

Speed reality check: headless is usually FASTER, not slower. It skips compositing/paint and the display pipeline, so on a recurring million-record job the throughput gap is large. Headed-via-Xvfb is a *detection* tool, not a performance one — reach for it only when a target actually flags headless (the OmniCorp nudge, fingerprint gates). Default to headless; the wall-clock you feel scraping is the origin's latency (slow servers, streamed/drip responses, full-screen loaders), not your render mode. Scale workers to hide that latency — don't switch to headed hoping it's quicker.

02

Stealth & Behavior

Looking like a browser isn't enough — you must behave like a human. Spoof a coherent fingerprint and emulate real mouse/scroll/idle behavior before you touch protected endpoints.

fingerprintmouse wanderscroll + idle

Spoof a coherent browser fingerprint

A real visitor has a consistent UA, locale, timezone, viewport and geolocation. Mismatches (a 'US tourist' on a Manila timezone) get flagged. Set them all on the Playwright context.

python · Playwright context fingerprint
ctx = browser.new_context(
    user_agent=("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"),
    locale="en-US",
    timezone_id="America/Los_Angeles",       # match your proxy's country
    viewport={"width": 1920, "height": 1080},
    geolocation={"latitude": 36.17, "longitude": -115.14},  # Las Vegas
    permissions=["geolocation"],
    extra_http_headers={"Accept-Language": "en-US,en;q=0.9"},
)
# Some sites read navigator.webdriver — hide it (Patchright does this for you):
ctx.add_init_script("Object.defineProperty(navigator,'webdriver',{get:()=>undefined})")
beatsCasino en-US / America-Los_Angeles gateOmniCorp headless nudge

Humanized mouse wander

Behavioral gates reject instant jumps, perfectly straight lines, and perfectly periodic timing. Move in a curved path with jitter and a variable cadence.

python · beat a mouse-movement gate
import random, time

def human_move(page, x0, y0, x1, y1, steps=40):
    for i in range(steps):
        t = i / steps
        x = x0 + (x1 - x0) * t + random.uniform(-6, 6)   # curve + jitter
        y = y0 + (y1 - y0) * t + random.uniform(-6, 6)
        page.mouse.move(x, y)
        time.sleep(random.uniform(0.012, 0.05))          # variable cadence
beatsSyndicate behavioral clearance gate

Scroll + idle reading ('floor surveillance')

Some gates require evidence you browsed before hitting the data — scrolling and dwelling. Emulate it, then make the protected call.

python
for _ in range(6):
    page.mouse.wheel(0, random.randint(200, 500))   # scroll the page
    time.sleep(random.uniform(0.4, 1.2))            # idle-read
time.sleep(4)                                        # dwell before the cage
beatsCasino floor-surveillance gate
03

Manage Digital Identities

Rotate IPs to survive per-IP limits and reach geo-locked content, build session trust the way a real user does, and burn through email-verified signups with disposable inboxes.

proxiesburner emailcookie prefetchgeo

Proxy rotation (datacenter / mobile / residential)

A site that blocks an IP after N hits forces you to rotate egress IPs. Residential/mobile pools assign a fresh IP per request; datacenter pools are cheaper but easier to flag. Rotate from a pool and back off on failure.

python · rotate a pool
import itertools, requests

POOL = [
    "http://user:pass@p1.residential.example:8000",
    "http://user:pass@p2.residential.example:8000",
    # free lists (free-proxy-list.net, ...) work too  just flakier
]
rotor = itertools.cycle(POOL)

def fetch(url):
    proxy = next(rotor)
    return requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=20)
python · Playwright per-context proxy
ctx = browser.new_context(proxy={
    "server": "http://p1.example:8000", "username": "user", "password": "pass",
})
beatsSpecimen + ASIC per-IP rotation gateGeo-locked regions (proxy in an allowed country)

Session trust-building (cookie prefetch)

Don't jump straight to the data. Visit the pages a real user would first to collect the cookies the protected endpoint requires — then carry them on every request.

python
s = requests.Session()
s.get("https://target.example/megacorp")                 # sets og_session
for hotel in ("mirage-royale", "oasis-grand", "zenith-tower"):
    s.post(f"https://target.example/api/casino/partner/{hotel}")   # comp cookies
# now the ledger trusts the session:
ledger = s.get("https://target.example/api/casino/ledger?page=1").json()
beatsOmniCorp session gateCasino partner-hotel trust chain

Burner emails to clear signup walls

Email-verified registration is beaten with a disposable inbox: register with a throwaway address, poll the inbox for the code, verify. Use any online temp-mail (mailinator, temp-mail.org, mail.tm's API).

python · register → read code → verify (greynet)
import requests, re
BASE = "https://target.example"

# 1) solve the signup CAPTCHA image (see §05), then register:
requests.post(f"{BASE}/api/megacorp/account/register", json={
    "email": "ghost@temp-mail.org", "password": "p@ssw0rd",
    "captchaId": cid, "captchaText": solved})

# 2) read the code from your burner inbox (here, the site's own inbox API):
msgs = requests.get(f"{BASE}/api/mail/inbox",
                    params={"address": "ghost@temp-mail.org"}).json()["messages"]
code = re.search(r"\b(\d{6})\b", msgs[0]["text"]).group(1)

# 3) verify + sign in:
requests.post(f"{BASE}/api/megacorp/account/verify",
              json={"email": "ghost@temp-mail.org", "code": code})
beatsOmniCorp register → verify-email → sign-in wall
05

Bypass Automated Verification

CAPTCHAs are solvable programmatically: transcribe audio challenges with Whisper, OCR text/arithmetic challenges, and classify image-grid challenges with a vision model.

ffmpegFaster WhisperGoogle STTTesseractCLIPSigLIP

Audio CAPTCHA → ffmpeg + Faster Whisper

Audio challenges (reCAPTCHA's audio option, or our 'intercept' clips) are speech. Normalize with ffmpeg to 16 kHz mono, then transcribe with Faster Whisper (a CTranslate2 reimplementation — ~4× faster than openai-whisper).

python · pip install faster-whisper
import subprocess
from faster_whisper import WhisperModel

subprocess.run(["ffmpeg", "-y", "-i", "intercept.wav",
                "-ar", "16000", "-ac", "1", "clean.wav"], check=True)

# device="cuda", compute_type="float16"  on GPU; "cpu"/"int8" works too.
model = WhisperModel("large-v3", device="cpu", compute_type="int8")
segments, info = model.transcribe("clean.wav", beam_size=5)
transcript = " ".join(s.text for s in segments).strip()
print(transcript)        # -> "victor echo zulu kilo"

Google Cloud Speech-to-Text is the managed alternative (`speech.SpeechClient().recognize(...)`) if you'd rather not run a model. Faster Whisper is free and offline.

beatsSyndicate inner-circle audio CAPTCHAASIC auction CAPTCHA

Text / arithmetic CAPTCHA → OCR

Distorted-text and 'x + y = ?' challenges are images: OCR them, then read the characters or compute the sum.

python
import pytesseract, re
from PIL import Image

txt = pytesseract.image_to_string(Image.open("challenge.png"))
nums = list(map(int, re.findall(r"\d+", txt)))   # "9 + 9 = ?" -> [9, 9]
answer = nums[0] + nums[1]                          # -> 18
beatsOmniCorp signup CAPTCHAASIC per-record arithmetic gate

Image-grid challenge → CLIP

For 'select all squares with a bus' style challenges, a vision-language model scores each tile against text labels and picks the best match — zero-shot, no training. CLIP (OpenAI) uses a softmax over labels: the probabilities sum to 1, so it answers 'which one'.

python · pip install git+https://github.com/openai/CLIP.git
import torch, clip
from PIL import Image

device = "cuda" if torch.cuda.is_available() else "cpu"
model, preprocess = clip.load("ViT-B/32", device=device)

img = preprocess(Image.open("tile.png")).unsqueeze(0).to(device)
labels = ["a bus", "a traffic light", "a crosswalk", "a bicycle"]
text = clip.tokenize([f"a photo of {l}" for l in labels]).to(device)

with torch.no_grad():
    logits_per_image, _ = model(img, text)
    probs = logits_per_image.softmax(dim=-1).cpu().numpy()[0]   # softmax: sums to 1
print(labels[int(probs.argmax())])

Image challenge → SigLIP (sigmoid, better zero-shot)

SigLIP (Google) replaces CLIP's softmax with a sigmoid loss, so each label gets an INDEPENDENT probability — it answers 'is X present?' per tile, which is exactly what 'select all squares with a bus' needs (multiple tiles can match). It usually beats CLIP at zero-shot. SigLIP 2 adds stronger multilingual encoders. Run it via 🤗 Transformers.

python · pip install transformers torch pillow
import torch
from transformers import AutoProcessor, AutoModel
from PIL import Image

model = AutoModel.from_pretrained("google/siglip2-base-patch16-224")
processor = AutoProcessor.from_pretrained("google/siglip2-base-patch16-224")

image = Image.open("tile.png")
labels = ["a bus", "a traffic light", "a crosswalk", "a bicycle"]
# SigLIP was trained with this template + padding="max_length":
inputs = processor(text=[f"a photo of {l}" for l in labels],
                   images=image, padding="max_length", return_tensors="pt")

with torch.no_grad():
    outputs = model(**inputs)

# sigmoid (NOT softmax) -> independent per-label probabilities:
probs = torch.sigmoid(outputs.logits_per_image)[0]
present = [l for l, p in zip(labels, probs) if p > 0.5]   # select ALL matches
print(present, "| best:", labels[int(probs.argmax())])

Rule of thumb: CLIP softmax → pick the single best label; SigLIP sigmoid → threshold each label independently (ideal for multi-select grids). `google/siglip-base-patch16-224` is the v1 id.

06

Extract High-Value Data (OCR)

Sites hide prices, emails, coordinates and IDs inside images and scanned PDFs to deter scrapers. OCR lifts the text back out — strictly for data you're authorized to extract.

TesseractRapidOCRPyMuPDF

Tesseract (pytesseract)

The classic OCR engine — fast, tiny, great on clean rasterized text like our salary/slip/chart images.

python · apt install tesseract-ocr · pip install pytesseract pillow
import pytesseract
from PIL import Image

text = pytesseract.image_to_string(Image.open("salary.png"))
print(text)   # "$109,000 - $178,000  /  h.recruiter@omnicorp.example"

RapidOCR (ONNX, robust on noisy images)

An ONNX-Runtime OCR toolkit that's more robust than Tesseract on low-contrast / noisy scans — ideal for the muddy sonar logs and watermarked surveillance slips.

python · pip install rapidocr-onnxruntime
from rapidocr_onnxruntime import RapidOCR

engine = RapidOCR()
result, elapse = engine("sonar.png")
for box, text, score in (result or []):
    print(text, round(score, 2))   # "LAT -22.706", "LON 73.705", ...

The newer meta-package is `pip install rapidocr` (`from rapidocr import RapidOCR`), which can swap ONNX/OpenVINO/Torch backends. `rapidocr-onnxruntime` shown above is the stable, widely-used build.

beatsOmniCorp salary/email imagesSalvage sonar coordsCasino watermarked slipsSpecimen charts

Scanned PDFs & SVG-rendered fields — rasterize → OCR

PyMuPDF (import name fitz) extracts a PDF's text layer instantly. Image-only 'scanned' PDFs have no text layer — detect that, rasterize the page, then OCR it. Real-world targets lean on scanned PDFs constantly.

python · pip install pymupdf pytesseract
import fitz  # PyMuPDF
import pytesseract
from PIL import Image
import io

doc = fitz.open("scan.pdf")
page = doc[0]
text = page.get_text()
if not text.strip():                       # image-only (scanned) PDF
    pix = page.get_pixmap(dpi=200)
    img = Image.open(io.BytesIO(pix.tobytes("png")))
    text = pytesseract.image_to_string(img)
print(text)

greynet renders its OCR-only fields (agent emails, vitals charts, comp slips, settlement docs, dossiers) as **runtime SVG** drawn from glyph *outlines* — there's no selectable text in the markup, so reading the <svg> source gets you nothing. Same move as a scanned page: rasterize the SVG, then OCR the raster.

python · pip install cairosvg pytesseract
import cairosvg, pytesseract, io, requests
from PIL import Image

svg = requests.get(f"{BASE}/api/syndicate/dossier/{uuid}").content
png = cairosvg.svg2png(bytestring=svg, scale=2)   # rasterize (or use resvg / a headless screenshot)
text = pytesseract.image_to_string(Image.open(io.BytesIO(png)))
print(text)                                        # "REAL NAME ...  STANDING BOUNTY $260,000"

Numbers render with thousands separators ($260,000) and some fields are redacted ("——") — strip commas before casting, and submit an empty value for a redaction rather than guessing.

beatsSyndicate dossiersASIC documentsCasino slipsSpecimen chartsStatehouse agent email
07

Orchestrate & Scale

A single sequential worker can't finish in time — but hammering trips the breaker. Scale with capped concurrency + exponential backoff, then fan out across processes and VMs, each with its own identity.

asynciobackoffmultiprocessingmulti-VM queue

Capped concurrency + exponential backoff

Run many requests at once, but cap them with a semaphore so you don't trip the fragile-origin breaker. On 429/503, back off exponentially with jitter and retry — never hammer.

python · pip install httpx
import asyncio, random, httpx

async def fetch(client, url, sem):
    async with sem:
        for attempt in range(5):
            r = await client.get(url)
            if r.status_code in (429, 503):
                await asyncio.sleep(2 ** attempt + random.random())  # backoff + jitter
                continue
            return r.json()

async def main(urls):
    sem = asyncio.Semaphore(8)              # tune below the breaker threshold
    async with httpx.AsyncClient(timeout=30) as client:
        return await asyncio.gather(*(fetch(client, u, sem) for u in urls))
beatsFragile-origin breaker (all sites)OmniCorp 600-listing scaleCasino live-odds scale

When (and why) to scale — the ladder

Don't reach for VMs first. Climb the ladder only when the rung below it is the actual bottleneck — each step adds cost and operational surface.

  1. 1.

    Single worker, sequential — correct first. If it finishes in time, stop here.

  2. 2.

    Concurrency (async + semaphore) — when latency-bound: the origin is slow / drip-streamed and you're waiting, not computing. One IP, many in-flight requests, capped under the breaker.

  3. 3.

    Multi-process — when CPU-bound: OCR, CLIP/SigLIP, and PoW solving saturate a core. Fan across cores on one box.

  4. 4.

    Multi-VM — when IP-budget-bound: you've hit the per-(team, IP) rotation/rate gate, or need geo-diverse egress. Each VM carries its own identity + proxy.

Diagnose before you climb: if doubling concurrency doesn't speed you up, you're not latency-bound — adding VMs won't help either. The challenges here are mostly latency-bound (clunky origins, drip-streamed registries, full-screen loaders), so rung 2 buys the most; rung 4 only matters once a rotation/geo gate caps a single IP.

Single worker → multi-VM

When one box (or one IP budget) isn't enough, fan out: a coordinator pushes work to a shared queue; N workers across VMs pull it, each behind its own proxy/identity so no single (token, IP) trips the rotation gate.

python · Redis work queue
import redis, json
r = redis.Redis(host="coordinator.internal", port=6379)

# producer (once):
for uuid in all_uuids:
    r.lpush("queue:lots", uuid)

# worker (run on each VM, with a DISTINCT proxy/identity):
while (item := r.rpop("queue:lots")):
    data = scrape_lot(item.decode(), proxy=MY_PROXY)   # your scraper + your IP
    r.lpush("results", json.dumps(data))

Distributing across IPs is exactly how you beat the per-(team, IP) rotation gate at speed: each VM/proxy gets its own fresh request budget.

beatsSpecimen + ASIC rotation gatemulti-VM scaling test

Orchestrate & monitor the fleet over SSH

A control node provisions the worker VMs, ships code + a per-VM identity (proxy, locale, fingerprint seed), starts the workers, and watches their health — all over SSH. Keep it boring: one command to deploy, one stream to monitor. asyncssh fans the same op across every host concurrently.

python · pip install asyncssh — deploy + launch on every worker
import asyncio, asyncssh

HOSTS = ["10.0.0.11", "10.0.0.12", "10.0.0.13"]   # worker VMs
PROXIES = {h: f"http://proxy-{i}.pool:8000" for i, h in enumerate(HOSTS)}

async def deploy(host):
    async with asyncssh.connect(host, username="ops") as c:
        await asyncssh.scp("worker.py", (c, "/opt/worker.py"))     # ship code
        # each VM gets a DISTINCT egress identity -> beats the rotation gate
        await c.run(f"COORD=coordinator.internal PROXY={PROXIES[host]} "
                    f"nohup python /opt/worker.py >/var/log/w.log 2>&1 &", check=True)
        return f"{host}: launched"

print(await asyncio.gather(*(deploy(h) for h in HOSTS)))

Monitoring is the other half: tail each worker's heartbeat (rate, error %, last-success age) so a wedged or blacklisted VM is visible immediately — not three hours into a million-record run. Pull the queue depth + per-host counters the workers push to Redis.

python · Live fleet health from the control node
import asyncssh, asyncio

async def health(host):
    async with asyncssh.connect(host, username="ops") as c:
        r = await c.run("tail -n1 /var/log/w.log", check=False)
        return host, r.stdout.strip()

async def watch():
    while True:
        rows = await asyncio.gather(*(health(h) for h in HOSTS))
        for host, line in rows:
            print(f"[{host}] {line}")          # rate / errors / last-ok age
        await asyncio.sleep(10)                # 10s poll, jittered in practice

Treat a flagged VM like a flagged identity: rotate it out (new proxy / fresh box), don't retry through the same egress and deepen the ban. The control node should drain that host's in-flight work back to the queue so no records are silently dropped.

beatsmulti-VM scaling testFragile-origin breaker (all sites)
08

Defeat Payload & Markup Tricks

Real sites don't hand you clean JSON. They base64/XOR the payload, drip it over a slow stream, charge CPU at the door with proof-of-work, salt the HTML with honeypots, and leave fields blank. None of it is real protection — each is reversible with a few lines. Here's how to read through all of it.

base64XORNDJSON streamingALTCHA PoWDOM honeypots

Deobfuscate the payload (base64 / XOR)

Obfuscation is encoding, not encryption — the client must be able to read it, so you can too. Spot it in the response shape: an encoding/alg field and a single opaque data/enc blob. base64 is a straight decode. XOR-with-a-known-key (here the record's own uuid) is a byte-wise loop — fully reversible because the key ships with the data.

python · base64 (Specimen dossier) + XOR-base64 (ASIC settlement)
import base64, json

# Specimen: { "encoding": "base64", "data": "<blob>" }
body = session.get(f"/api/specimen/dossier/{uuid}", headers=fp).json()
dossier = json.loads(base64.b64decode(body["data"]))

# ASIC: { "alg": "xor-b64", "enc": "<blob>" }   key is the lot uuid
def xor_b64(enc: str, key: str) -> dict:
    raw = base64.b64decode(enc)
    out = bytes(b ^ ord(key[i % len(key)]) for i, b in enumerate(raw))
    return json.loads(out)

detail = xor_b64(resp["enc"], lot_uuid)   # buyer id, clearing price, documents

The browser does the exact same reversal in JS (`atob` + a charCodeAt XOR loop) — read the site's own client code to recover the scheme, then port those few lines to your scraper.

beatsSpecimen base64 dossierASIC xor-b64 settlement

Drain a simulated-slow stream (NDJSON)

A clunky origin doesn't return the page at once — it drips records line-by-line over a long-lived response. Don't .json() it (that buffers and stalls); stream it and parse each NDJSON line as it lands. You must read to completion to get every record, and the per-record delay is wall-clock you hide with concurrency across pages — not by switching to a headed browser.

python · requests stream=True — Salvage registry
import requests, json

records = []
with requests.get(url, headers=hdrs, stream=True, timeout=120) as r:
    for line in r.iter_lines():        # arrives one wreck at a time
        if not line:
            continue
        obj = json.loads(line)
        if "stream" in obj:            # first line is meta {page,totalPages,total}
            continue
        records.append(obj)            # drip  keep reading to the end
beatsSalvage drip-streamed registry

Solve proof-of-work at the door (ALTCHA · Argon2id)

A PoW gate makes every client burn CPU before it serves data — a hashcash challenge. This site runs ALTCHA with the **Argon2id** key-derivation (not the SHA-256 default): the server publishes a salt, nonce, the Argon2 cost parameters, and the key *prefix* of a secret counter; you brute-force counters — running Argon2id over nonce‖uint32be(counter) — until the derived key matches, then submit the winning counter for a clearance cookie. Argon2id is *memory-hard* (~19 MB per attempt), so a GPU/ASIC farm can't out-run a normal browser; read the cost params off the challenge instead of hard-coding them.

python · Solve ALTCHA Argon2id hashcash — Casino cage clearance
import requests, struct
from argon2.low_level import hash_secret_raw, Type   # pip install argon2-cffi

s = requests.Session()
ch = s.get("/api/casino/pow").json()              # {parameters, signature}
p = ch["parameters"]
salt, nonce, prefix = bytes.fromhex(p["salt"]), bytes.fromhex(p["nonce"]), p["keyPrefix"]

def derive(counter):                               # mirror the server's KDF exactly
    return hash_secret_raw(
        secret=nonce + struct.pack(">I", counter), # message = nonce ‖ uint32be(counter)
        salt=salt,
        time_cost=p["cost"], memory_cost=p["memoryCost"],
        parallelism=p["parallelism"], hash_len=p["keyLength"], type=Type.ID,
    )

def solve():
    for counter in range(0, 5_000):                # ~tens of attempts; each is memory-hard
        key = derive(counter)
        if key.hex().startswith(prefix):
            return counter, key.hex()

counter, derived = solve()
s.post("/api/casino/pow",
       json={"challenge": ch, "solution": {"counter": counter, "derivedKey": derived}})
# pow_ok cookie is now set on the session -> the ledger opens
bets = s.get("/api/casino/ledger?page=1").json()

In a browser the same work runs over a WASM Argon2id (a few dozen memory-hard attempts, ~2 s). Solve once, reuse the cookie for the whole run — re-solving per request is the trap the gate is counting on.

beatsCasino proof-of-work gate

See through HTML honeypots & obfuscated markup

Hard-to-parse HTML salts the DOM with decoy rows (hidden by CSS) and wedges junk text into real values, so a naive tbody tr + textContent scrape ingests garbage and a SP-\d+ regex mis-captures. Three robust fixes: (1) prefer the clean JSON API the table is built from; (2) read *rendered* text (inner_text), which honors display:none; (3) if you must parse raw HTML, drop nodes that aren't displayed before extracting.

python · Strip hidden honeypots — BeautifulSoup
from bs4 import BeautifulSoup
import re

soup = BeautifulSoup(html, "html.parser")
# 1. skip decoy rows (hidden via opaque utility classes)
for junk in soup.select(".idx-decoy, .tok-x"):
    junk.decompose()
rows = []
for tr in soup.select("tbody tr"):
    cells = [td.get_text(strip=True) for td in tr.select("td")]
    if not cells:
        continue
    rows.append(cells)
# real designations now read clean: SP-3001, not SP-30<junk>01
python · Or just use rendered text — Playwright
# inner_text() excludes display:none honeypots; visible rows only
for tr in page.locator("tbody tr:visible").all():
    print(tr.inner_text())     # decoys + junk fragments never appear

The decoy rows carry fake uuids that match no record, so even if you do ingest them the scorer drops them — the only cost is wasted requests if you go on to fetch their detail pages. Filter early.

beatsSpecimen index honeypotsASIC lots decoy rows

Page through ASP.NET __VIEWSTATE / __doPostBack

Classic WebForms registries (Secretary-of-State business search, court dockets) have no ?page=N. The current search lives in an opaque __VIEWSTATE hidden field you must echo back on every postback, validated by an __EVENTVALIDATION tag. To turn the page: scrape both hidden fields out of the HTML and re-POST them with __EVENTARGUMENT=Page$Next. Hand-editing the page number fails the tag — round-trip the blob verbatim.

python · VIEWSTATE postback pagination — requests + BeautifulSoup
import requests, re
from bs4 import BeautifulSoup

s = requests.Session()           # (clear the search CAPTCHA first; route via a US proxy)
html = s.post(URL, data={"txtBusinessName": "", "SearchType": "StartsWith",
                         "ddlNameType": "Business Name"}).text

def hidden(html, name):
    return re.search(rf'name="{name}"\s+value="([^"]*)"', html).group(1)

rows = []
while True:
    soup = BeautifulSoup(html, "html.parser")
    rows += [[td.get_text(strip=True) for td in tr.select("td")] for tr in soup.select("tbody tr")]
    m = re.search(r"page <b>(\d+)</b> of <b>(\d+)</b>", html)
    cur, last = (int(m[1]), int(m[2])) if m else (1, 1)
    if cur >= last:
        break
    html = s.post(URL, data={                       # re-POST the scraped state
        "__VIEWSTATE": hidden(html, "__VIEWSTATE"),
        "__EVENTVALIDATION": hidden(html, "__EVENTVALIDATION"),
        "__EVENTARGUMENT": "Page$Next",
    }).text

The registry runs to 100k entities — sequential postback paging is a single worker's whole afternoon. This is the canonical place to fan out across workers / VMs (split the alphabet or the page range) and merge.

beatsStatehouse __doPostBack pagination

Handle sparse & incomplete records honestly

Not every record is complete: some wrecks have unknown coordinates (the sonar log reads "———"), some charts have a redacted vital, some dossiers have no protocol on file. The ground truth for those fields is empty — so leave them empty. The scorer gives full credit for a correctly-empty field and ZERO for inventing a value. Don't hallucinate to fill a blank.

python · Emit empty, never guess
def parse_coord(ocr_text: str):
    m = re.search(r"-?\d+\.\d+", ocr_text)
    return float(m.group()) if m else ""   # "———" -> "" (matches empty truth)

wreck["latitude"]  = parse_coord(lat_text)
wreck["longitude"] = parse_coord(lon_text)
# A confident wrong number scores 0; an honest blank scores 1.
beatsSalvage unknown coords/cargoSpecimen redacted vitals/protocol

All data is synthetic. greynet teaches these techniques for authorized scraping education only.