Two Hands
Execution trace

Two real forms, end to end

Two forms of the pre-registered random 300, followed from the request to the receipt: 080310B0100, which the gate accepted, and 122655C0100, which the gate routed to a person because the arithmetic refused a misread. Vote rows are Option NN. Generated by scripts/build_trace.py from stored data (no model call), checked by make eval; the same trace as docs/TRACE.md.

The request (the same for both forms)

modelGemma-4-31B-IT run record (results/calls_gemma.jsonl)
endpointAMD Token Factory, OpenAI-compatible POST /chat/completions the key comes from the environment and is never stored
prompt sha2563664253035b912184e24a205852e7b8e686f0abb36a4340e91834d60dc71193f run record; rebuilt from twohands/reader.py: the same
temperature0
max_tokens2000
response_formatnone
images sent7: the masked page, then 6 masked tiles (left_column, party_rows, coalition_rows, bottom_rows_sec7, party_logos, coalition_logos); JPEG, longest side at most 2400 px, quality 88
image tokens billed1,843 per form

(a) Accepted: form 080310B0100, Unused ballots

1. The photo

Unused ballots on form 080310B0100, masked crop

The exact crop bytes the receipt hashes: /crop/080310B0100/SOBRANTES.jpg?pad=0.02&label=0 · masked page

2. The read

One request, 2026-10-15T05:59:07Z: 53.6 s, 1 attempt, finish stop, 2,609 tokens in, 810 out, $0.000145957. Raw output: 1,765 characters, sha256 45bd81cd8b39daf863eb110fc143c528e65333e24380063ed260e99951a30b6d, 23 items. Item 1 (Unused ballots), as returned:

{"label": "BOLETAS SOBRANTES", "n": 180, "w": 180}

3. The gate

twohands/gate.py, run again on the raw output. Shape: 23 values, the form has 23. Row labels: consistent. Digits read: 23 of 23. Words: 22 of 22 fields with words read: digits = words (Unused ballots: digits = words).

identityleft sideright sideresult
rows sum to TOTAL277
Option 01 117 + Option 02 21 + Option 03 6 + Option 04 6 + Option 05 7 + Option 06 16 + Option 07 65 + Option 08 10 + Option 09 5 + Option 10 0 + Option 11 0 + Option 12 8 + Option 13 3 + Option 14 4 + Option 15 2 + Unregistered candidates 0 + Null votes 7
277
TOTAL 277
pass
votaron + representantes = section 5277
Voters on the list 273 + Representatives who voted 4
277
Voters + representatives 277
pass
section 5 = section 7277
Voters + representatives 277
277
Ballots taken from the boxes 277
pass
section 7 = TOTAL277
Ballots taken from the boxes 277
277
TOTAL 277
pass

4. Decision

Accepted, no person: every check holds; the same as the stored record. Unused ballots is in no identity; on this field the check that holds is digits = words.

5. Against the official record

The official first capture keyed 480; Gemma read 180 (digits) and 180 (words); the paper says 180 (the AI adjudicator's reading of the crop: words "CIENTO OCHENTA", boxes 1 8 0); certified count: none (the certified count covers the vote rows and TOTAL, not this field). Disputed fields on this form in the sample audit: 1.

6. The receipt

Leaf 43 of 267 in the sample audit's Merkle tree (twohands/merkle.py: leaf = SHA-256(0x00 || canonical JSON), node = SHA-256(0x01 || left || right)).

{"crop_sha256": "e1bf660ece41a082946b06e211c5be41684abca692dbfa113bd4b01b5033507d", "field": "SOBRANTES", "form": "080310B0100", "keyed": "480", "model": 180, "tier": "medium", "words": 180}

Leaf hash 6d5fc26fd391fb0c70375495ed54a6d1511dc3342e2d4c442e4a0fdca3c5b7b1. Proof path (9 steps):

stepsibling on thesibling hash
1right7fc780500562c07d3a7b9f4a9f93429925cd3e5ecd30d6729cf53bd2e91b099b
2left85eebdae9fd6ebe2cec48060a5b64ce05aa8486e8286023eebf94cca2efcfda3
3right8a56a66a35be65eae539f6e5732f3eb23dd2bee3cf483f3e3f8cfece56cba7e3
4left800d2893661b694bf2fa322f30c45545085c9ee7c5e5792ed6500183714dd4be
5right2a39cdb982914400e0d253d9be880440d1bd58207663bb6c8a2d3ce788e67013
6left12940b49c48d44f585f54395196af75f6685bc898cfe45a2ce64d9d1092de02d
7right2d220120869fd6862e540b4ad140d8c1239f51aceec69482fba04bb17bf97435
8right297c803d134dee3409fbf0e80b997ce9d9c14780bcf38bf4a4b04919392176fa
9right355a9c831b9a43c89a9d2fef79211aececbc49d556d971c2861dc3f6bf7b07f4

Root e56a8ef4f011b9e5a3b7bd91dd708580fe1a8912e3bd65f51d780a6dd5898721 (published in results/sample_audit_root.txt: the same; anchored with OpenTimestamps). Open the receipt: /verify/080310B0100/SOBRANTES. Verify it offline:

curl -sL https://<host>/verify/080310B0100/SOBRANTES -o receipt.json
curl -s "https://<host>/crop/080310B0100/SOBRANTES.jpg?pad=0.02&label=0" -o crop.jpg
python -m twohands.verify_receipt receipt.json --crop crop.jpg --root e56a8ef4f011b9e5a3b7bd91dd708580fe1a8912e3bd65f51d780a6dd5898721
  • ok: leaf recomputed from the record (6d5fc26fd391fb0c)
  • ok: crop SHA-256 matches the record (e1bf660ece41a082)
  • ok: path of 9 steps reaches the receipt's root (e56a8ef4f011b9e5)
  • ok: receipt's root matches the root given (e56a8ef4f011b9e5)

VERIFIED. The receipt proves this record (form, field, keyed value, Gemma's digits and words, tier, crop hash) is unchanged since the report was sealed. It does not prove the reading is right; the paper settles that.

(b) Routed: form 122655C0100, Option 02

1. The photo

Option 02 on form 122655C0100, masked crop

The exact crop bytes the receipt hashes: /crop/122655C0100/OPT02.jpg?pad=0.02&label=0 · masked page

2. The read

One request, 2026-10-15T06:38:24Z: 60.7 s, 1 attempt, finish stop, 2,609 tokens in, 815 out, $0.000146307. Raw output: 1,766 characters, sha256 e8fc7875af26ec52cde18733b4ac7b1f92b103174850976a6f757bb2aea4aa22, 23 items. Item 7 (Option 02), as returned:

{"label": "(printed row logos; shown here as Option 02)", "n": 23, "w": 23}

3. The gate

twohands/gate.py, run again on the raw output. Shape: 23 values, the form has 23. Row labels: consistent. Digits read: 23 of 23. Words: 23 of 23 fields with words read: digits = words (Option 02: digits = words).

identityleft sideright sideresult
rows sum to TOTAL366
Option 01 1 + Option 02 23 + Option 03 3 + Option 04 26 + Option 05 22 + Option 06 98 + Option 07 165 + Option 08 0 + Option 09 0 + Option 10 0 + Option 11 0 + Option 12 7 + Option 13 0 + Option 14 0 + Option 15 0 + Unregistered candidates 0 + Null votes 21
466
TOTAL 466
FAIL
votaron + representantes = section 5466
Voters on the list 466 + Representatives who voted 0
466
Voters + representatives 466
pass
section 5 = section 7466
Voters + representatives 466
466
Ballots taken from the boxes 466
pass
section 7 = TOTAL466
Ballots taken from the boxes 466
466
TOTAL 466
pass

4. Decision

Routed to a person. Cause: rows sum to TOTAL: 366 != 466 (the only reason; the same as the stored record). Option 02: digits = words, so only the arithmetic could catch it. With the paper's 123, rows sum to TOTAL gives 466 = 466.

5. Against the official record

The official first capture keyed 123; Gemma read 23 (digits) and 23 (words); the paper says 123 (confirmed by the certified count); the certified count says 123 (settled by comparison with the paper, Cotejo). Disputed fields on this form in the sample audit: 1.

6. The receipt

Leaf 146 of 267 in the sample audit's Merkle tree (twohands/merkle.py: leaf = SHA-256(0x00 || canonical JSON), node = SHA-256(0x01 || left || right)).

{"crop_sha256": "0ddd5b7520df372496cd137744a8d31be0cdffdffcfef9812b1358a3de809d5b", "field": "OPT02", "form": "122655C0100", "keyed": "123", "model": 23, "tier": "weak", "words": 23}

Leaf hash 40c90012f177f089c23b4a4f5f56558fc2c9ed489fee719cef3c60d0e847361f. Proof path (9 steps):

stepsibling on thesibling hash
1left134876b1756ecee927e725ee60966f13cbbfc8b3955072095e2f2639b2cf5fce
2rightb87781825f1cd133901c74b698399fc539475725db6ca863342536e3eb6f0f63
3right224d88fda1f6144a223a537702ae1bc1a958a13718479a1351240ab985f8842d
4rightba58cd05893fcc607a40fba41422ce3db362ebd3befb49cf3f54fe398824b1a6
5left9382ecef0e39550e659cd450202a30f81250e7b871f6c6892116dc010521dffe
6rightf29cd346d804255b42d45aac2b6b0a381b6ae6ae2e47836ae072d63ee3d3cd73
7righte3433d72e84e9f9836b00bcbef6a9132281a17a7b5551f203dc42917839b2f1f
8left0691cdcdd271df7c343f3ac2eecf2b20957ea05c53c1fe4f73aeb06f49d67581
9right355a9c831b9a43c89a9d2fef79211aececbc49d556d971c2861dc3f6bf7b07f4

Root e56a8ef4f011b9e5a3b7bd91dd708580fe1a8912e3bd65f51d780a6dd5898721 (published in results/sample_audit_root.txt: the same; anchored with OpenTimestamps). Open the receipt: /verify/122655C0100/OPT02. Verify it offline:

curl -sL https://<host>/verify/122655C0100/OPT02 -o receipt.json
curl -s "https://<host>/crop/122655C0100/OPT02.jpg?pad=0.02&label=0" -o crop.jpg
python -m twohands.verify_receipt receipt.json --crop crop.jpg --root e56a8ef4f011b9e5a3b7bd91dd708580fe1a8912e3bd65f51d780a6dd5898721
  • ok: leaf recomputed from the record (40c90012f177f089)
  • ok: crop SHA-256 matches the record (0ddd5b7520df3724)
  • ok: path of 9 steps reaches the receipt's root (e56a8ef4f011b9e5)
  • ok: receipt's root matches the root given (e56a8ef4f011b9e5)

VERIFIED. The receipt proves this record (form, field, keyed value, Gemma's digits and words, tier, crop hash) is unchanged since the report was sealed. It does not prove the reading is right: here Gemma's reading is wrong, and the receipt seals it all the same; the arithmetic is what caught it.

Source: scripts/build_trace.py from results/calls_gemma.jsonl, app/data/audit_sample_prep300.csv, results/adjudication/, data/masked/ (no model call).