Two Hands
API

Everything on screen is an endpoint, a command and a tool

Plain JSON over HTTP, a CLI, and an MCP server (CLI · MCP). Field codes are neutral: vote rows are OPT01 to OPT15 in printed order, the other lines by name (TOTAL, NULOS, ...).

POST/api/gatetoken

Check a set of values against the paper's identities

Send the values you captured (any source: a model, a keyer, another system). Get back every identity, the failing fields, and the verdict. Needs the gate token (Authorization: Bearer).

curl -s -X POST https://twohands.pytr.dev/api/gate \ -H "Authorization: Bearer $TWOHANDS_GATE_TOKEN" -H "Content-Type: application/json" \ -d '{"values": {"OPT01": 61, "OPT02": 9, "OPT07": 113, "NULOS": 10, "TOTAL": 262}}'
POST/api/live/{form}GET/api/jobs/{job}

Queue a live read on AMD and follow it

One live read runs at a time; a job waits in a short queue and reports its place, stages and elapsed time. The same events stream as server-sent events at /api/live/events/{job}.

JOB=$(curl -s -X POST https://twohands.pytr.dev/api/live/211294C0100 | python -c "import sys,json; print(json.load(sys.stdin)['job'])") curl -s https://twohands.pytr.dev/api/jobs/$JOB

Without a key, read the stored verdict of any form: curl -s https://twohands.pytr.dev/api/form/211294C0100

GET/api/export.csvGET/api/export.json

Export the captured records

One record per form: status, fields to key, reason, and every value. Filter with ?status=accepted,resolved to take only what is final.

curl -s "https://twohands.pytr.dev/api/export.csv?status=accepted,resolved" -o records.csv
GET/api/station/{station}POST/api/station/{station}/queue

Any polling station

What I have for one station: a measured read with its receipts, the live audit's read, or "not audited yet"; queue a live read and get its place. Search ids with /api/station/search?q=2024; the live strip's numbers at /api/live-strip.

curl -s https://twohands.pytr.dev/api/station/202451B0100

CLI

pip install -e ".[mcp]" (from the repo) gives the twohands command (also python -m twohands). It reuses the core and the web app's audit module, so it gives the same disputes, order, CSV, fingerprint and Merkle root as Audit your capture.

twohands audit --keyed capture.csv --scans scans/ --schema mx_presidencia_2024 --out report/ twohands audit --keyed app/data/audit_sample_prep300.csv --from-stored --out report/ # no model call twohands read form.jpg [--masked] [--layout movil|escaner] # locate, mask, one read, the gate twohands gate values.json [--variant v1|v2|v2b] # exit 0 accepted, 2 routed to a person twohands verify-receipt receipt.json [--crop crop.jpg] [--root <hex>]

audit masks each scan in memory, reads only the forms without a read through the configured OpenAI-compatible endpoint, and writes report.html, report.csv, merkle_root.txt, summary.json and disputes.json. --from-stored uses the stored pre-registered reads only, and reproduces the published sample root.

MCP

An MCP server on the official Python SDK (twohands/mcp_server.py) with four tools: read_form, gate, audit_capture and verify_receipt.

twohands mcp # stdio (Desktop MCP clients: command twohands, args ["mcp"]) twohands mcp --http --port 8765 # streamable HTTP at /mcp

File paths must lie under the repo, the working directory or TWOHANDS_MCP_ROOTS; over HTTP, tools that would call the model are refused unless started with --allow-reads.