VoteThisWay

publish-anonymized-voter-profile

Publish a user-reviewed, redacted version of the user's VOTER.md to votethisway.org. Published profiles contribute to aggregate data showing what people in a given area care about. Removing direct identifiers reduces privacy risk, but free-form text and a ZIP code cannot be guaranteed anonymous.

This skill is optional. The user must explicitly ask to publish.

---

Step 1: Prepare the redacted profile

Read VOTER.md and produce a stripped version. The goal is to keep the substance — values, priorities, tradeoffs — and remove anything that could identify the person.

VOTER.md is the only file this publishes. If BALLOT.md is also in the directory — the per-item recommendations research-ballot writes — it is not part of this and never gets sent. It pairs a jurisdiction with how the voter intends to vote on each item, which makes it more identifying than the profile, not less. Do not read from it, do not merge it in, and do not offer to publish it.

Remove:

Keep:

When in doubt, remove it. The value of publishing is in the positions, not the personal context.

Re-identification risk: a distinctive combination of positions can itself be identifying, especially in a rural or low-population ZIP, even with direct PII removed. ZIP-level aggregate results are suppressed until five distinct profiles exist, but the submitted profile remains retrievable by its hash. Explain this limitation and let the user decide whether to proceed.

Show the user the redacted text and ask them to confirm it looks right before continuing.

---

Step 2: Solve the proof-of-work challenge

Publishing requires solving a computational challenge. This is an anti-spam measure — it costs a small amount of compute, not identity.

2a. Request a challenge

` GET https://votethisway.org/v1/pow/challenge `

Response fields:

2b. Solve the challenge

Count up from 0, and for each candidate hash its decimal string form:

` SHA-256("{challenge_id}|{nonce_seed}|{nonce}") `

Stop at the first nonce whose digest has at least difficulty leading zero bits.

The nonce is a string, not a number, everywhere it appears. It is interpolated into the preimage above as text, and it goes into the JSON body as a JSON string — "nonce": "3502981", never "nonce": 3502981. A numeric literal there fails to decode and comes back as 400 {"error": "invalid JSON"}, which does not name the field that caused it. Whatever string you hash must be byte-identical to the string you send, since the server recomputes the digest from the JSON value verbatim. Max 64 characters.

Leading zero _bits_ on the raw 32-byte digest — not leading zero hex characters. The two tests coincide only when difficulty is a multiple of 4, and the current default of 22 is not: 22 bits is five zero hex characters plus the top two bits of the sixth. Counting hex characters there leaves you four times too lenient at five and four times too strict at six. Take the digest as bytes, most significant bit first, and require the first difficulty bits to be zero.

Check your bit test against this vector before spending real work on it:

` SHA-256("test-challenge|test-seed|4070840") = 0000039ff916a84862007536eda9dbf68754f8c0b2de759e2dbb688cd1622091 `

That digest has exactly 22 leading zero bits, so it solves difficulty 22 — even though only five of its hex characters are zero.

Write a script for the search; do not attempt it by reasoning. The expected number of hashes is 2^difficulty, so the current default of 22 averages around 4 million SHA-256s — a second or two of real compute, and nothing that can be shortcut by thinking about it. The count swings widely run to run, so a solve taking several times the average is ordinary luck rather than a bug. Read difficulty from the challenge response every time instead of hardcoding it; the server tunes it.

To confirm a solver before wiring it to a publish, POST the challenge_id and nonce to https://votethisway.org/v1/pow/verify. It answers {"ok": true, "reason": "verified"} and consumes nothing, so the challenge is still good for the publish that follows. This is a debugging aid, not a required step — don't call it on every publish.

---

Step 3: Submit

` POST https://votethisway.org/v1/profiles/publish Content-Type: application/json

{ "challenge_id": "...", "nonce": "...", "zip": "...", "profile_text": "..." } `

The profile_text is hashed server-side after PoW verification.

Send exactly the fields above. The endpoint rejects unknown ones, and reports that as 400 {"error": "invalid JSON"} — the same message a malformed body gets — so a field you added to be helpful is indistinguishable from broken JSON.

If the submit is rejected

Errors come back as {"error": "..."}. Which error you got decides whether the challenge you just solved is still usable, so read it before doing anything else.

validation, not proof-of-work. The ZIP was malformed, or the profile text still matched one of the direct-identifier patterns the server screens for; the message names which. This check runs *before* the challenge is consumed, so the challenge is untouched. Show the user the specific reason, revise the text with them, and resubmit with the same challenge_id and nonce. Never silently strip content just to force it through.

retrying it will never succeed. The reason is one of challenge expired, unknown challenge, invalid nonce, or challenge already used. None of these recover on their own — challenges are single-use, and an expired or already-spent one never becomes valid again. Request a fresh challenge and solve it again from Step 2a.

server could not determine whether the challenge had already been spent. That is a fault on our side, not a problem with your solution: the challenge is still valid and still unconsumed. Wait a few seconds and retry the identical request. Do not solve a new challenge — it costs seconds of CPU to replace something that was never broken.

Any other 5xx carries the same advice as the 503. The publish runs in a single transaction, so a server-side failure rolls back the challenge consumption along with everything else, leaving your challenge good for a retry.

---

Step 4: Confirm

A successful response returns a profile_hash. Show it to the user as their receipt — they can use it to retrieve the published profile at:

` GET https://votethisway.org/v1/profiles/{profile_hash} `

The response also contains created: true when this canonical ZIP/profile was first stored, or false when an identical profile already existed. Identical submissions count once.

Both are successes and both carry the same profile_hash; the status code tracks created, so a new profile returns 201 and a repeat of one already stored returns 200. Treat only a 4xx or 5xx as a failure — a 200 here does not mean the publish went wrong, it means someone had already published the identical text for that ZIP.

---

Output requirements

challenge, a PoW 400 requires a fresh one, and a 503 means retry the same request unchanged

Markdown source