Static review of online survey instruments for resistance to AI/bot respondents
Project description
๐ก๏ธ Survey Shield
Static review of online survey instruments for resistance to AI/bot respondents โ plus an optional live runtime that drives a real browser through your survey.
What is Survey Shield?
Survey Shield gives researchers feedback on whether their survey instrument is hardened against AI respondents. Two paths:
- Instrument Review (primary, static, no browser) โ point it at a Qualtrics
.qsfexport. Multi-agent LLM reviewers fan out across bot-resistance dimensions (attention checks, identity questions, visual-perceptual traps), produce a peer-review-style verdict with verbatim-grounded findings, and render a self-contained HTML report with a copy-paste Methods statement and APA/BibTeX citation. - Take Survey (live runtime) (optional
[live]extra) โ drives a real browser (browser-use) through a live Qualtrics URL, reports detected mechanisms after the fact. Costs ~5โ10 minutes and real LLM credit per run, so the hosted demo doesn't expose it; install the[live]extra to run it locally.
Researchers using Survey Shield mostly want Instrument Review. Reach for the live runtime when you need to exercise the survey end-to-end.
Install
pip install surveyshield-py # static review only โ small, no browser
pip install "surveyshield-py[live]" # adds browser-use + Playwright
playwright install chromium # only if you installed [live]
The package name on PyPI is surveyshield-py (after openreview-py); the import name is surveyshield.
Set an LLM provider key in your environment (or in a .env file in the working directory โ Survey Shield loads it via python-dotenv):
OPENAI_API_KEY=sk-... # used unless the model name starts with "gemini"
GOOGLE_API_KEY=... # used for Gemini models
CLI
surveyshield review your_survey.qsf
# โ writes your_survey.report.html next to the input
surveyshield review your_survey.qsf --output report.html --json review.json \
--model gpt-4o-mini
surveyshield take https://qualtrics.com/jfe/form/SV_xxx # requires [live]
--model gemini-3-flash-preview --max-steps 150
surveyshield serve --host 127.0.0.1 --port 8000
# โ boots the FastAPI app + bundled React SPA
surveyshield --help lists every command and flag.
Python API
import asyncio
import surveyshield
review, parsed = asyncio.run(
surveyshield.review_qsf(
"your_survey.qsf",
model="gpt-4o-mini",
# api_key="sk-...", # or rely on env vars
# dimensions=["attention_checks"], # default = all
)
)
print(review.overall_score, review.overall_feedback.headline)
with open("report.html", "w") as f:
f.write(surveyshield.render_html(review, parsed))
The review object is a surveyshield.InstrumentReview Pydantic model. Power users can compose the lower-level seams directly: parse_qsf, run_review, drop_unverified_quotes, consolidate_and_summarize, aggregate. See surveyshield/__init__.py for the public surface.
For live runtime:
import asyncio, surveyshield # surveyshield-py[live] installed
result = asyncio.run(surveyshield.take_survey(
"https://qualtrics.com/jfe/form/SV_xxx",
model="gemini-3-flash-preview",
max_steps=150,
))
print(result.success_probability, [m.name for m in result.detected_mechanisms])
If the [live] extra isn't installed, surveyshield.take_survey resolves to None and the CLI's take command exits with a clear install hint.
Self-host the hosted UI
git clone https://github.com/kiante-fernandez/survey-shield
cd survey-shield
./setup.sh # creates ../.conda env
echo "OPENAI_API_KEY=sk-..." > backend/.env # or GOOGLE_API_KEY
cd backend && ./start.sh # โ http://localhost:8000
The Take Survey tab is gated on live_take_enabled โ GET /api/v1/survey/config flips it to true once a key is detected in the env.
Endpoints
- Web UI: http://localhost:8000
- Interactive API docs: http://localhost:8000/docs
- Health check: http://localhost:8000/health
- Live-runtime config: http://localhost:8000/api/v1/survey/config
Models
The hosted UI does not expose a model picker โ Instrument Review reviewers run on a sensible default (gpt-4o-mini). Self-hosters who want a different model can pass model_name directly to the API or CLI. The backend has no allowlist; any model name langchain-openai's ChatOpenAI or langchain-google-genai's ChatGoogleGenerativeAI accept will be routed by prefix:
- Names starting with
geminiโ Google (requiresGOOGLE_API_KEY) - Everything else โ OpenAI (requires
OPENAI_API_KEY)
API usage (self-host)
Instrument Review (primary)
# Submit a QSF for review
curl -F "file=@your_survey.qsf" \
http://localhost:8000/api/v1/instrument/review
# โ {"review_id": "<uuid>", "status": "queued", ...}
# Poll
curl http://localhost:8000/api/v1/instrument/status/<uuid>
# queued โ running โ completed (~30โ90 s)
# Structured JSON
curl http://localhost:8000/api/v1/instrument/results/<uuid>
# Human-readable HTML report
curl "http://localhost:8000/api/v1/instrument/report/<uuid>"
# Download as a file
curl -OJ "http://localhost:8000/api/v1/instrument/report/<uuid>?download=1"
Live runtime (self-host only)
curl -X POST "http://localhost:8000/api/v1/survey/analyze" \
-H "Content-Type: application/json" \
-d '{
"survey_url": "https://example.com/survey",
"model_name": "gpt-4o-mini",
"max_steps": 150,
"use_vision": true
}'
# Then poll /api/v1/survey/status/<id> and fetch /api/v1/survey/results/<id>.
What Survey Shield evaluates
Instrument Review dimensions (primary)
The reviewer fans out across plug-in dimensions defined in surveyshield/review/dimensions.py. v1 ships three:
attention_checksโ explicit IMCs and instructional manipulation checks (Westwood, 2025; PNAS).identity_questionsโ direct LLM-resistance items (identity probes, reverse-shibboleth questions, impossible-event questions).visual_perceptual_trapsโ image- and layout-based cognitive traps that exploit vision-language model architectural constraints (Affonso, 2026; JCR).
Adding a new dimension is one entry in dimensions.py plus a Pydantic output schema โ the reviewer fan-out, aggregator, and HTML report are all plug-in-driven.
Findings are grounded: every reviewer finding must cite a verbatim excerpt from the source survey. Survey Shield substring-checks each excerpt against the parsed survey and drops anything that can't be located. What reaches the report is grounded in real survey content.
The product is scoped strictly to bot resistance. We do not critique a survey's substantive research design, theoretical framing, or question wording โ those remain the researcher's domain.
Live-runtime mechanisms
When you run a live analysis against a real Qualtrics URL, Survey Shield's browser-use Agent navigates the survey end-to-end and inventories detected mechanisms by category โ hover traps, invisible text, timing requirements, CAPTCHAs, honeypot fields, attention checks, and behavioural / mouse-tracking signals. Per-mechanism severity weights (1โ10) feed into success-probability and difficulty scores. See surveyshield/live/analyzer.py for the taxonomy and surveyshield/live/prompts.py for the agent task.
Development
Project structure
survey-shield/
โโโ surveyshield/ # the importable package
โ โโโ __init__.py # public API (review_qsf, render_html, take_survey, โฆ)
โ โโโ cli.py # Typer CLI (review / take / serve)
โ โโโ models/ # Pydantic schemas
โ โโโ review/ # static review pipeline
โ โ โโโ parser.py # QSF โ ParsedSurvey
โ โ โโโ dimensions.py # plug-in dimension registry
โ โ โโโ reviewer.py # fan-out, verify, consolidate, aggregate
โ โ โโโ mechanism_context.py
โ โ โโโ templates/ # Jinja2 self-contained HTML report
โ โโโ live/ # browser-use runtime ([live] extra)
โ โ โโโ analyzer.py # SurveyAnalyzer / take_survey
โ โ โโโ prompts.py
โ โ โโโ patches.py
โ โโโ serve/ # FastAPI app + bundled React SPA
โ โโโ app.py
โ โโโ config.py
โ โโโ api/{survey,instrument}.py
โ โโโ static/ # built React (populated by bin/build.sh)
โโโ frontend/ # React/TypeScript source (CRA)
โโโ tests/ # pytest suite + tiny QSF fixture
โโโ backend/start.sh # convenience wrapper around `surveyshield serve`
โโโ pyproject.toml # canonical package metadata
โโโ Procfile # web: gunicorn surveyshield.serve.app:app
โโโ bin/build.sh # React build โ surveyshield/serve/static/
โโโ .github/workflows/ # test.yml + release.yml (PyPI trusted publishing)
Local dev
./setup.sh # one-time: conda env at ../.conda
pip install -e ".[dev,live]" # editable install + tests + browser-use
pytest -q # ~30 tests, no LLM calls
cd frontend && npx tsc --noEmit && npm run build
Releasing
git tag v0.1.0 && git push --tags
# .github/workflows/release.yml builds the wheel + sdist (with the React SPA
# bundled into surveyshield/serve/static/) and publishes to PyPI via OIDC.
The PyPI project must be configured with this repo + release.yml as a Trusted Publisher before the first push.
Contributing
- Fork the repository
- Create a feature branch
- Make changes with tests
- Submit a pull request
License
MIT License โ see LICENSE.
Citation
If you use Survey Shield in published work:
@misc{fernandez2026surveyshield,
author = {Fernandez, K. and Low, A. and Bogard, J. and Fox, C. R.},
title = {Survey Shield: Static review of online survey instruments for resistance to non-human responses},
year = {2026},
note = {Manuscript in preparation},
}
Every report includes the same citation pre-formatted (APA + BibTeX).
Disclaimer
Survey Shield is intended for research and testing purposes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file surveyshield_py-0.1.0.tar.gz.
File metadata
- Download URL: surveyshield_py-0.1.0.tar.gz
- Upload date:
- Size: 858.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0921df8d566248c95e407821e36417df7e4cd0fb32f38a7c2ae7828c580de444
|
|
| MD5 |
5e53244791f017cbe893768101e6fe67
|
|
| BLAKE2b-256 |
1b8e31b0cf256336c7b6b7322ec1264ec301223fe4f62d37f80bcce4fa18c88e
|
Provenance
The following attestation bundles were made for surveyshield_py-0.1.0.tar.gz:
Publisher:
release.yml on kiante-fernandez/survey-shield
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
surveyshield_py-0.1.0.tar.gz -
Subject digest:
0921df8d566248c95e407821e36417df7e4cd0fb32f38a7c2ae7828c580de444 - Sigstore transparency entry: 1488527638
- Sigstore integration time:
-
Permalink:
kiante-fernandez/survey-shield@65fd8f625f00e535b93952582ec8b2090cc29492 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kiante-fernandez
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@65fd8f625f00e535b93952582ec8b2090cc29492 -
Trigger Event:
push
-
Statement type:
File details
Details for the file surveyshield_py-0.1.0-py3-none-any.whl.
File metadata
- Download URL: surveyshield_py-0.1.0-py3-none-any.whl
- Upload date:
- Size: 865.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
337006c694fb993d2a674af68b83b8f105e13cc47c8f4f6c062bf43a2c56f262
|
|
| MD5 |
d652c64d43ab52cea38cbf10906508a8
|
|
| BLAKE2b-256 |
8e055c7761772a3915eb5f5e94e8d55b3866608f4bdac1fc25577859652075ea
|
Provenance
The following attestation bundles were made for surveyshield_py-0.1.0-py3-none-any.whl:
Publisher:
release.yml on kiante-fernandez/survey-shield
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
surveyshield_py-0.1.0-py3-none-any.whl -
Subject digest:
337006c694fb993d2a674af68b83b8f105e13cc47c8f4f6c062bf43a2c56f262 - Sigstore transparency entry: 1488527656
- Sigstore integration time:
-
Permalink:
kiante-fernandez/survey-shield@65fd8f625f00e535b93952582ec8b2090cc29492 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kiante-fernandez
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@65fd8f625f00e535b93952582ec8b2090cc29492 -
Trigger Event:
push
-
Statement type: