from a live blocked-sites pass (no PII): - posture shift: blind opt-out is the DEFAULT, not a fallback -- submit on every site with an accessible removal channel even without first confirming a listing (own identifiers to the broker's own official channel = still least-disclosure). guided flows double as the authoritative search. - blocked-form rule: when a form is automation-hostile (hard captcha / cloudflare / datadome / slide-to-verify), default to the broker's CITED rights-email rather than recording blocked. - captcha policy clarified: never defeat behavioral/token/slider challenges; ok to read a static distorted-text or plain-arithmetic captcha on the subject's own opt-out; stop if the whole submission is rejected after a correct answer (fingerprinting the automation, not grading it). - intelius/peopleconnect: delete-wipes-suppression is field-confirmed -- a deletion-complete email means the suppression is gone and the subject re-lists cluster-wide; re-run suppression and verify the Control step reads "suppressed". guided-mode session persists; DOB is an <input type=date>. - new records: addresses.json (intelius front-end, cluster-covered) and socialcatfish.json (cited rights-email lane + automation-hostile form). - new references/site-playbooks.md: per-site game-plan matrix (8 blocked-tail sites), the meta-search no-op skip-list (idcrawl/lullar/yasni/webmii/namesdir/itools/skipease), and the infopay / peopleconnect backend clusters. OSINT-list triage taxonomy added to methods.md. - state-machine.md: fixed doc drift + documented submitted->not_found illegal (resolves as awaiting_processing), blocked->submitted via action_selected, operator_manual_check, --evidence & pitfall. tests: standalone 99, PR 97 (+1 cluster-coverage regression); ruff + windows-footguns clean.
27 KiB
Opt-out method playbooks
How the agent executes each broker optout.method using native Hermes tools. Obey least-disclosure:
submit only the subject's OWN identifiers, and only the fields a broker's official channel requires
(pdd.py plan lists them per broker). Never disclose more than that, and confirm a listing is really
the subject's before acting on any THIRD-PARTY / indirect record (see "Distinguish the subject" and
"Indirect exposure"). See the posture section below for when a confirmed listing is NOT a prerequisite.
Autonomy: pdd.py next <subject> sequences all of this - it decides which method applies, orders
parents first, and routes human-only work to the digest. In autonomy=full (default), execute its
actions without pausing per submission; the consent recorded at intake is the authorization. These
playbooks are the HOW for each action type.
Opt-out posture: blind opt-out is the default (not a fallback)
Operator-directed posture: submit an opt-out or deletion on EVERY site that exposes an accessible removal channel, even when a listing was not first confirmed - whichever of opt-out / deletion is optimal per site. Do not hand back a to-do list of "we could not search these."
- Why it is sound (does NOT violate least-disclosure): a blind opt-out sends only the subject's own identifiers to the broker's own official removal channel. You are giving the broker the subject's data to remove it, not exposing new data or acting on a third party. (Third-party / indirect records are the exception: those still require confirming the exposure first.)
- The opt-out flow doubles as the authoritative search. Guided flows that match on email + DOB +
legal name and then say "no results" are a stronger
not_foundthan any scrape - the broker ran its own matcher against real identifiers. On guided-flow sites, "run the opt-out" and "search" are one action (e.g. CheckPeople; seesite-playbooks.md).
Blocked form -> default to the cited rights-email (the headline rule)
When a removal form is automation-hostile (hard CAPTCHA, a Cloudflare wall that will not clear, a JS
paywall funnel), default to the broker's cited rights-request email rather than recording blocked
and deferring to a human - unless there is an easy in-browser solve. Decision order per site:
- Easy in-browser solve? (one-click remove; a guided flow whose CAPTCHA auto-clears on the residential browser; plain email-verify) -> do it in the browser.
- Form blocked but a cited rights-email exists? -> send a deletion/opt-out email from the operator's
webmail (name + state + contact email only). This is now preferred over recording
blocked. - No easy solve AND no cited email ->
blocked(orhuman_task_queuedwith the exact end-state). - Only lane requires gov-ID / physical mail -> do NOT pursue autonomously (least-disclosure); surface as a human decision.
"Cited" = published by the broker itself (privacy policy / opt-out page / a working deletion alias). Do
not email addresses sourced only from third-party blogs or Reddit. Per-site lanes and gotchas are
pre-recorded in references/site-playbooks.md so future runs execute rather than re-derive.
Triage an external OSINT list before scanning
When cross-checking any external "people OSINT" catalog, separate first-party brokers (removal
targets) from meta-search / link-out aggregators (no first-party data -> no-ops, do not file
opt-outs), cluster front-ends (covered by a parent, e.g. addresses.com -> Intelius), and
non-broker tools / APIs / wrong-jurisdiction (skip). The skip-lists live in site-playbooks.md.
Scan ladder (all methods)
Build the exposure map cheapest first (on a site with an accessible removal channel you may still
blind-opt-out even if the scan is inconclusive - see the posture section above). Run every
search_vectors entry from pdd.py plan (each name x location, phone, email, and address the broker's
search.by supports) - different vectors surface different listings for the same person; dedupe found
URLs.
web_extracton the brokersearch.url(fast HTML -> markdown). Look forsearch.match_signal. Build per-vector URLs fromsearch.url_patternsand heedsearch.url_format_quirks(see below). 1b.site:search-engine probe (cheap, do it early and in parallel).web_searchwithsite:<broker-domain> "First Last"(add a city/ZIP or a unique phone/address to cut namesake noise) often returns the exact profile-slug URL in one shot - which both confirms the listing exists AND hands you the opaque/find/person/<id>or/p/<slug>URL you'd otherwise have to derive. Two big wins seen in the field: (a) it disambiguates namesakes fast - the SERP snippet shows age/city so you can tell the subject from a same-name relative before fetching anything; and (b) a broad"First Last" <ZIP OR unique-address>search (nosite:) surfaces brokers not yet in your DB (e.g. information.com, peoplefinders.com) - record those as bonus exposures. Note: emptysite:results are INCONCLUSIVE (many broker pages aren't indexed / arenoindex), notnot_found.- If the page is JS-rendered or returns nothing useful,
browser_navigate+browser_snapshot(andbrowser_type/browser_clickto run the site's search box). - If blocked by stealth/Cloudflare, use the
scraplingskill viaterminal. If the broker record hassearch.antibotset (e.g.datadome), results are behind a device-check CAPTCHA: a cloud/stealth browser (Browserbase) orscraplingmay get through; if none is available, do not burn attempts -pdd.py record <subject> <broker> blockedand move on (a re-scan with a stealth backend can pick it up later). 3b. Operator-browser path (the reliable unblock for anti-bot sites). Cloudflare/DataDome key on datacenter IPs + headless fingerprints, soweb_extract, the proxyless agent browser, and even a cloud browser often fail - but the operator's own everyday browser (residential IP, real fingerprint) sails straight through. For anyblockedsite, hand the operator a paste-ready search URL (built fromsearch.url_patterns), give them the identity anchors to judge by (current- prior addresses, age, a distinguishing detail) and the namesake/relative watch-list, and ask for
the verdict or a screenshot (the agent can read screenshots). This is a first-class scan path, not
a fallback - treat the operator's live check as authoritative and record the real verdict
(
found/not_found/indirect_exposure), citingscanned_via: operator_browser. Same for opt-out forms the agent's browser can't reach: guide the operator field-by-field (least-disclosure), pausing before submit. (This is exactly why the same trick clears email-verification links the agent can't open - see the Verification loop.)
- prior addresses, age, a distinguishing detail) and the namesake/relative watch-list, and ask for
the verdict or a screenshot (the agent can read screenshots). This is a first-class scan path, not
a fallback - treat the operator's live check as authoritative and record the real verdict
(
- Capture evidence: save listing URLs and a
browserscreenshot into the subject'sevidence/dir, thenpdd.py record <subject> <broker> found --found true --evidence '{"listing_urls":[...]}'.
If a listing genuinely does not exist: pdd.py record <subject> <broker> not_found and move on.
A 404 (or empty body) is INCONCLUSIVE, not "not_found"
A constructed search URL that 404s almost always means the URL pattern is wrong, not that the
person is absent. Never record not_found off a 404. Instead:
- Re-check the broker's
search.url_patterns/url_format_quirksand rebuild the URL. - Fall back to the on-site search box:
browser_navigateto the search page,browser_typethe raw query,browser_clickSearch, then read the canonical result URL the site lands on. - Only after the site's own search returns an empty result set do you record
not_found. - If a pattern was wrong, fix it in
references/brokers/<id>.json(url_patterns+url_format_quirks) so the next run is correct - see the rule below.
Log URL/format quirks for every site you scrape
Whenever you discover how a broker's URLs are actually shaped (path layout, hyphen-vs-slash joins,
whether ZIP is required, abbreviation handling, query-param search, anti-bot gating), record it in
that broker's references/brokers/<id>.json under search.url_patterns (the templates) and
search.url_format_quirks (the gotchas, including which forms 404). Bump last_verified. This makes
the deterministic URL path reliable across runs and subjects instead of rediscovered each time. If the
opt-out form's real requirements differ from the record (extra required fields, a CAPTCHA, an account),
fix optout.requires / optout.inputs / optout.tier too - those drive tier selection and
least-disclosure. Log opt-out mechanics gotchas (a broker that needs a profile URL but doesn't expose
one for the subject, an email-only fallback, an authorized-agent toggle) in optout.quirks - the
planner surfaces these as optout_quirks per broker. Example: Radaris sometimes shows the subject only
as a static address-table row with no "View Profile" link, so /control-privacy (which needs a profile
URL) can't be used - fall back to optout.email rather than submitting a namesake's URL.
Distinguish the subject from namesakes and relatives
People-search sites are dense with namesakes and family clusters. Before recording found, confirm the
record is the subject themselves (corroborate via DOB, a known current/prior address, or the
identifier you searched). Two non-removable patterns to record as evidence but NOT as the subject's own
listing:
- Namesake: same name, different person (different DOB/location with no overlap). Not the subject.
- Relative record: the listing is about a different person (a relative) and merely names the subject in a "Family" field, or carries the subject's email/phone as a secondary datum. This is a third party's record - the consent gate correctly blocks acting on it. See "Indirect exposure" in the web_form section for what the subject can still request.
Two more false-positive traps that a naive scan records as found when it should not:
- Property record != PII (address-anchored sites). Reverse-address / property sites (rehold,
clustrmaps-style) can match on a public property record (build year, beds/baths, last sale
price, incidents) without exposing the subject's personal info - the resident/owner NAME is behind
a "View full report" paywall/signup. Distinguish "this address exists in a public property DB"
(non-removable,
not_found) from "the subject's personal profile is displayed" (removable,found). RecordfoundONLY if a resident name matching the subject is publicly shown; an address-only match isnot_found- there is nothing to opt out of, and public property records are not removable anyway. Seerehold.jsonsearch.match_signal_notes. - SEO-templated title/H1 fakes a "found". Many people-search sites auto-insert the query into the
page
<title>, H1, and intro copy ("FREE public records found for {Name} in {City}", "Over 100+ FREE public records found for {Name}"). That echo is templating, not a result - the actual result cards are often unrelated namesakes in other states. Amatch_signalon title/intro text yields false positives. Require a real result card corroborated by the subject's address or DOB, and ignore the templated title/intro/H1 entirely. Seetruepeoplesearch.json/fastpeoplesearch.jsonsearch.match_signal_notes.
Both are why the parent re-verifies every found before acting rule is load-bearing (pdd.py show <subject> <broker> reads back a subagent's recorded evidence so the parent can re-verify without
re-deriving the listing URL). If a found turns out to be a false positive, correct it with a fresh
record ... not_found carrying an evidence note explaining the retraction.
web_form
browser_navigatetooptout.url;browser_snapshotto read the form.- Fill only the planned
disclosure_fieldswithbrowser_type/browser_click; forprofile_url, paste the confirmed listing URL from evidence. - Submit;
browser_snapshotto confirm the success state; screenshot toevidence/. pdd.py record <subject> <broker> submitted --disclosed <field> --disclosed <field> --channel web_form.- If the broker requires email verification, follow Verification loop below.
Indirect exposure (named as a relative / your email on someone else's record)
You asked the right question: if a broker lists a relative and names you in their "Family" field, or shows your email/phone on their record, that IS personal information about you - even though the record's primary subject is a third party. Resolve it in two distinct lanes:
- The self-service opt-out form does NOT cover this. That form removes a record whose primary subject is you. It has no notion of "scrub my identifiers from this other person's record," and submitting it with the relative's address to force a match would be (a) disclosing data the listing doesn't tie to you and (b) acting on a third party's record. Don't. The consent gate exists to stop exactly that.
- What you CAN do - a targeted "delete my personal information" request (CCPA 1798.105 / GDPR Art.17).
These rights attach to your personal information wherever the business holds it, including as a
data point on another person's profile. So the subject may email the broker's privacy address and
request suppression of their own specific identifiers (this email address, this phone number, my
name in family/relative associations), citing the relative listings as the locations. This is a
narrower request than a full opt-out and does not require the relative's consent - you are only asking
them to delete data about you. Use
render-emailwith theccpa/gdprtemplate, list only the subject's own identifiers + the URLs where they appear, and record it as a normalsubmitted→awaiting_processingemail case. Verify by re-scanning those identifier vectors (email/phone) after the statutory window -confirmed_removedonly when the subject's identifier no longer appears. - Caveat: the broker may decline to alter a third party's record beyond removing your specific identifiers, and "your name in a family graph" can be derived from public records they'll re-list. Note residual exposure in the report rather than marking a clean removal. (Operational guidance, not legal advice.)
pdd.py send-email <subject> <broker> --listing <url> [--kind ccpa|gdpr|ccpa_indirect] always does
the deterministic parts (recipient locked to an address the broker record declares, refusing anything
else; --listing mandatory; records submitted, logs disclosure, stamps next_recheck_at). How it
actually sends depends on email_mode:
- browser mode (no password, autonomous): the command returns a recipient-locked
composepayload (to/subject/body). Compose a NEW message in the operator's logged-in webmail viabrowser_*(pastecompose.bodyexactly, disclosing nothing beyond it) and send. No credentials stored. Requires the inbox signed in in the browser Hermes uses. - programmatic mode (SMTP creds): the command SMTP-sends it directly, no human.
- draft_only fallback:
pdd.py render-email <subject> <broker> --listing <url>; a digest entry tells the operator to send it, and the agent recordssubmitted --channel emailafterward.
Then follow the Verification loop if the broker emails a confirmation link.
Verification loop (email_verification brokers)
- browser mode (autonomous, no password): open the broker's confirmation email in the operator's
webmail (
browser_*), thenpdd.py verify-link <subject> <broker> --text '<email body>'returns the anti-phishing-scored link.browser_navigateit in the same browser (several brokers, e.g. PeopleConnect, bind the session to the browser that opens the link), finish the flow, recordawaiting_processing. - programmatic mode (IMAP):
pdd.py poll-verification <subject>polls IMAP for every in-flight case, extracts the link (anti-phishing scored: only opt-out-looking links on the broker's own domains), and auto-advancessubmitted → verification_pending. Thenbrowser_navigatethe link in the agent's own browser, finish the flow, recordawaiting_processing. - draft_only: the digest tells the operator to click the link in the subject's inbox; the agent
records
awaiting_processingon their word. - Either way, the due queue (
pdd.py due) brings the case back after the broker's processing window for the verifying re-scan; only that re-scan justifiesconfirmed_removed.
phone_callback (e.g. Whitepages)
Submit the web form, then the site places an automated call with a numeric code. If the operator is available to read the code, capture it and complete the form (T2). Otherwise queue a human task.
phone (voice menu) / fax / mail / gov_id -> human task (T3)
Do not attempt to automate. Create a todo task and pdd.py record <subject> <broker> human_task_queued with exact instructions and an explicit withhold list (never SSN; never a
driver's-license number unless the subject chooses to and crosses out the ID number). Capture the
confirmation reference back into the ledger when the operator completes it.
captcha
Default: soft/managed CAPTCHAs clear automatically. The recommended baseline backend is the
Browserbase cloud browser (setup --auto selects it when BROWSERBASE_API_KEY is set). Being a
real browser on a residential IP, it passes managed challenges - Cloudflare Turnstile, hCaptcha /
reCAPTCHA checkbox - as normal operation, so those brokers stay T1 and you just proceed. This is
not CAPTCHA solving: no solver service, no fingerprint spoofing.
Only a hard challenge the browser genuinely can't pass (interactive image grids, behavioral
scoring that flags the session) becomes a fallback: record ... blocked and requeue it for the
stealth/operator-browser pass (methods.md → scan ladder 3b - the operator's own residential
browser is the reliable unblock). Without a cloud browser configured, soft-CAPTCHA brokers drop to
T2 and become human tasks. Never use a third-party CAPTCHA-defeating service.
CAPTCHA policy, clarified (on a consenting first-party opt-out)
- Do NOT defeat behavioral / token challenges: a Cloudflare Turnstile that will not auto-clear,
DataDome, and "slide-to-verify" gesture-entropy sliders (the InfoPay lane). These are hard
stops -> take the email lane (rule above) or record
blocked. - Acceptable to solve on the subject's own first-party opt-out: a static distorted-text image CAPTCHA (read it with the vision tool) or a plain arithmetic CAPTCHA ("8 + 13 = ?"). That is OCR / arithmetic on a consenting removal, not evasion of a bot-detection system.
- But if the site then rejects the whole submission ("Captcha verification failed / feature not
available") after a correct answer, it is fingerprinting the automation itself, not grading the answer
-> stop, do not loop (e.g. PrivateRecords' distorted-text-THEN-arithmetic double gate). If no cited
rights-email exists, that is a genuine
blocked.
Browser backends: scan vs execute
Two different jobs need two different browsers. Getting this wrong is the single biggest cause of a run stalling in Phase 2.
- Phase 1 (scan, read-only): a cloud stealth browser (Browserbase) or the
scraplingskill is ideal. On a residential IP with a real fingerprint it passes managed challenges (Cloudflare Turnstile, hCaptcha checkbox) and reads anti-bot people-search pages thatweb_extractand the proxyless agent browser cannot. This is what the skill'sbrowser_backendsetting governs (autopicks Browserbase whenBROWSERBASE_API_KEYis present - now also read from$HERMES_HOME/.env, not just the shell env, sodoctor/setup --autodetect the key Hermes already loads for its own tools). - Phase 2 (execute: opt-out forms, webmail sends, session-bound multi-step gates): the work must run in the operator's own everyday browser - real fingerprint, residential IP, AND the operator's logged-in sessions. A headless cloud browser is the WRONG default here for two reasons: (1) it is not signed into the operator's webmail, so browser-mode email sends and confirmation-link opens have no inbox to act in; and (2) it is itself Cloudflare/DataDome-gated on exactly the multi-step flows that matter (e.g. PeopleConnect guided-mode, whose verify link is session- and device-bound to the browser that opens it - a cloud browser both fails the challenge and breaks the binding).
- How to drive the operator's browser (CDP). Point Hermes's browser tools at the operator's real
Chrome over the DevTools protocol: launch
chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.hermes/chrome-debug"and connect the browser backend to127.0.0.1:9222. Use a dedicated debug profile (chrome-debug), NOT the operator's Default Chrome profile, and have the operator sign into their webmail (and any needed broker accounts) in that profile once. That single browser then carries residential IP + real fingerprint + logged-in sessions, which is precisely what Phase-2 flows need. (This is a Hermes-side browser setup, not apddconfig value;browser_backendabove only selects the Phase-1 scan browser.) The skill launches this for you:pdd.py cdpfinds a Chrome/Chromium/Brave/Edge binary, starts it detached on the dedicated profile, waits for the debug port, and prints the CDP endpoint (webSocketDebuggerUrl).pdd.py cdp --checkreports whether a debug browser is already live (and never launches a second one);pdd.py cdp --printjust emits the exact command for the operator to run themselves. Point the browser tools at theendpointit returns. - Always-available fallback: if no CDP browser is wired up, use the operator-in-the-loop path (scan ladder 3b) - hand over paste-ready URLs and field-by-field least-disclosure guidance, pausing before submit. It never fails; it just needs a human present.
Backend precedence, most to least autonomous: operator Chrome over CDP (Phase 2, hands-off once the profile is signed in) > Browserbase cloud stealth (Phase 1 scanning, plus managed-captcha forms that need no login) > proxyless agent browser (only already-unblocked sites) > operator-in-the-loop (paste-ready URLs; the last-resort unblock that always works).
Ownership clusters - DO PARENTS FIRST (playbooks live in the broker records)
Many brokers are resold shells of a few parents, so one parent removal clears a whole cluster of
children (see owns in each record). In Phase 2 you MUST work the cluster parents first, then
the standalone listings - doing a child before its parent wastes a submission the parent would have
covered. pdd.py plan <subject> --batch orders the found group parents-first and emits a
parent_playbook whose steps come verbatim from each record's optout.playbook - the single
source of truth, field-verified, updated as live runs discover mechanics. What follows is the
operating doctrine; the exact steps are in references/brokers/<id>.json.
Deletion USUALLY beats suppression, email lanes beat forms -- but check the record. Each parent
record carries a structured optout.deletion lane (via: in_flow | email | email_followup, a
privacy address, and prefer). The autopilot routes accordingly, and when deletion.prefer is
false it emits prefer_suppression instead of prefer_deletion:
in_flow(PeopleConnect,prefer: false): the deletion control lives inside the web flow, but for this cluster it is the WRONG lever for search-visibility (see the exception below). Complete the suppression flow and maintain it; do not press Delete unless the goal is a data-purge.via: email(Whitepages): the fully-autonomous lane -send-emailthe request (residency-picked kind: CCPA for US-CA, GDPR for EU/UK, generic otherwise), thenpoll-verificationfor their reply and answer identity questions with least-disclosure. This is also the rescue lane: any broker whose form demands a phone-callback/gov-ID/account but that declares a deletion email gets routed here instead of the human digest.email_followup(BeenVerified, Spokeo): the opt-out form is the fast primary (it clears the listing), and the playbook then sends a right-to-delete email for full erasure beyond suppression.
Verified parent facts (live-checked 2026-07-02; details + steps in the records):
- Intelius/PeopleConnect (~15+ sites in one flow) -- EXCEPTION to deletion-beats-suppression.
Portal entry asks only email + consent → verify link is session-bound to the browser that opens
it → guided-mode. Complete the SUPPRESSION flow and keep the account on file: suppression is
the do-not-display list that removes you. Per their privacy-center, 'DELETE MY USER DATA' deletes
your suppressions and does NOT stop the sites from showing you (public records re-list), so use it
only for a deliberate data-purge.
privacy@peopleconnect.usis the rights-request address for that path; published metrics: 33.5k deletion requests, median response < 1 day. - Whitepages:
privacyrequest@whitepages.com(or the Zendesk form) handles removal + CCPA deletion without the phone-callback tool - that phone call is only required by the automated tool. One removal also drops "all known connected listings". ≤15 days; check 411.com + Premium. - BeenVerified: opt-out tool (footer "Do Not Sell" link →
/svc/optout/search/optouts) + email verification; one opt-out per email address. Thenprivacy@beenverified.comdeletion follow-up - controller is The Lifetime Value Co., so name their sister properties (NeighborWho, Ownerly, NumberGuru, Bumper) in the same request, and verify each separately. - Spokeo: form takes ONE listing URL at a time and each listing must be opted out
individually - collect every listing URL from all search vectors first, then submit one opt-out
per URL. 24-48h processing.
privacy@spokeo.comfor full deletion beyond free-search suppression.
After each parent removal is confirmed, re-scan its children before submitting anything for them - usually they drop out and need no separate opt-out.
Any other parent
A parent without a hand-verified optout.playbook gets synthesised steps from its structured record
(URL/email, requires flags, deletion lane, notes/quirks). Follow those, and write what you learn
back into references/brokers/<id>.json (optout.playbook, optout.deletion, quirks,
last_verified) so the next run is exact - that file, not this one, is where per-broker knowledge
accrues.