The fastest way to contribute is to add a broker — it requires only a YAML file, no Python.
Before writing any YAML, go through the broker's opt-out flow yourself in a browser. Note:
- Is the opt-out a web form or an email address?
- If a web form: what fields does it have? What selectors?
- Does it require finding your listing URL first?
- Does it send a verification email or SMS?
- Does it show a CAPTCHA?
- What does the confirmation screen look like?
Do not guess at selectors. Inspect the live page.
cp src/optout/data/brokers/whitepages.yml src/optout/data/brokers/your-broker.yml # web-form with listing search
# or
cp src/optout/data/brokers/spokeo.yml src/optout/data/brokers/your-broker.yml # simple web formslug: your-broker # MUST match the filename (your-broker.yml)
name: Your Broker # human-readable name shown in the CLI
domain: yourbroker.com
category: people_search # people_search | marketing | background_check | other
parent_company: null # or "Acme Data Corp" if applicable
method: web_form # web_form | email | postal | manual
# Exactly one of these:
opt_out_url: "https://yourbroker.com/optout"
opt_out_email: null
opt_out_postal_address: nullrequired_fields: [full_name, current_address, email_current]
optional_fields: [dob, aliases, previous_addresses]
requires_listing_url: true # true if user must find their listing URL first
requires_phone_verification: false
requires_email_verification: true
requires_id_upload: false
requires_notarization: falseThese flags drive the CLI's pre-flight checklist. Set them exactly — if the broker requires phone verification, requires_phone_verification must be true and the steps must include an sms_verification_prompt step.
legal_basis: [CCPA, CPRA] # from: CCPA, CPRA, GDPR, VCDPA, CPA, CTDPA, UCPA
statutory_response_days: 45
re_add_window_days: 60 # how soon data typically reappears; null if unknown
last_verified_at: 2026-04-20 # today's date — update when you re-verify
notes: |
Free-form notes about quirks, caveats, and anything a future contributor
should know. Mention if selectors are fragile, if the broker frequently
rejects or ignores requests, etc.The scan block lets optout scan check whether your data appears on this broker:
scan:
search_url_template: "https://yourbroker.com/search?q={name_slug}&state={state}"
result_selector: ".person-card, .search-result"
match_fields: [full_name, current_address]Template variables available in search_url_template:
{name_slug}— URL-encoded full name (e.g.Jane-Public){state}— two-letter address state code (e.g.TX)
Steps are executed in order by the Playwright interpreter in engine/methods/web_form.py. Email-method brokers leave steps: [].
navigate — load a URL
- id: open_optout_form
type: navigate
url: "https://yourbroker.com/optout"search — search for the user's listing and prompt them to pick one
- id: find_listing
type: search
search_url_template: "https://yourbroker.com/search?q={name_slug}&state={state}"
result_selector: ".person-card"
pick_strategy: prompt_user # always prompt_user — never auto-pickRequired when requires_listing_url: true. The selected URL is stored as {state.found_listing_url} for use in later steps.
form_fill — fill one or more fields
- id: fill_form
type: form_fill
fields:
listing_url:
selector: "input[name='url'], input[placeholder*='yourbroker.com']"
value: "{state.found_listing_url}"
email:
selector: "input[name='email'], input[type='email']"
value: "{profile.emails.current[0]}"Field name (e.g. listing_url, email) is a label for logging only. The selector is a CSS selector. Use comma-separated alternatives for resilience.
prompt_user_if_present — pause and prompt the user when a selector is visible
- id: handle_captcha
type: prompt_user_if_present
selector: ".g-recaptcha, iframe[src*='recaptcha'], .recaptcha-checkbox"
message: "Please solve the CAPTCHA in the browser window, then press Enter."Use this for: CAPTCHAs, listing-selection UI, email-verification instructions. The step is a no-op if the selector is not found on the current page.
click — click a button or element
- id: submit
type: click
selector: "button[type='submit'], button.optout-submit"sms_verification_prompt — enter an SMS code
- id: phone_verify
type: sms_verification_prompt
code_selector: "input[name='code'], input[type='tel']"
message: "Enter the SMS code YourBroker sent to the phone number on the listing:"Required when requires_phone_verification: true.
capture — take a confirmation screenshot (always the last step)
- id: capture_confirmation
type: capture
method: screenshot
save_as: "{submission.id}-your-broker-confirmation.png"| Variable | Resolves to |
|---|---|
{name_slug} |
URL-encoded full name |
{state} |
Two-letter address state code OR step state object (context-dependent) |
{state.found_listing_url} |
URL picked during a search step |
{profile.legal_name} |
User's full legal name |
{profile.emails.current[0]} |
User's primary email address |
{profile.phones.current[0]} |
User's primary phone number |
{profile.current_address.city} |
City from the user's current address |
{submission.id} |
UUID of the current submission record |
{broker.slug} |
The broker's slug |
# Install dev dependencies (requires uv)
uv sync --extra dev
# Validate your new file against the schema
uv run python -c "
from pathlib import Path
from optout.brokers.loader import load_broker
b = load_broker(Path('src/optout/data/brokers/your-broker.yml'))
print(f'OK: {b.slug} ({b.method})')
"
# Run the full broker test suite
uv run pytest tests/test_production_brokers.py -vThe CI pipeline runs validate-brokers on every push and PR, so any schema error will be caught there too.
Before committing a new or updated broker YAML, confirm:
-
slugexactly matches the filename stem (your-broker.yml→slug: your-broker) -
methodis consistent withopt_out_url/opt_out_email -
requires_listing_url: true↔ there is asearchstep -
requires_phone_verification: true↔ there is ansms_verification_promptstep - All step
idvalues are unique within the broker - Web-form brokers end with a
capturestep -
last_verified_atis set to today's date - You personally ran through the opt-out flow in a browser and confirmed the selectors match the live page
- Notes explain any known quirks (fragile selectors, re-add behavior, response time)
-
uv run pytest tests/test_production_brokers.pypasses locally
When a broker changes their opt-out flow:
- Go through the new flow manually.
- Update the affected
stepsand any flags. - Update
last_verified_atto today. - Add a note in the
notesblock describing what changed and when. - Run the verification checklist above.
For anything beyond broker YAMLs, open an issue first to discuss the approach. The project has a deliberate scope:
- No SaaS, no multi-tenancy, no accounts.
- No programmatic CAPTCHA solving.
- No submission on behalf of users other than the operator.
- No bot-detection evasion (stealth plugins, proxy rotation, fingerprint spoofing).
These are not gaps to fill — they are the project's legal foundation.
git clone https://github.com/Blake104/OptOut.git
cd OptOut
uv sync --extra dev
uv run playwright install chromium
uv run pytest
uv run ruff check src/ tests/
uv run mypy src/optout/uv run ruff check src/ tests/ # lint
uv run ruff format --check src/ tests/ # format
uv run mypy src/optout/ # types
uv run pytest # testsBe straightforward. The project exists to help people remove their personal data from commercial databases. Keep contributions focused on that.