--- name: publish-digital-yard-sign description: Publish a small, curated set of the user's ballot picks and one-line advocacy reasoning as a public "digital yard sign" with a shareable link and QR code — use when user wants to publicly advocate for specific choices, share their vote, or create a shareable ballot placard license: MIT --- # publish-digital-yard-sign Publish a small, curated set of the user's ballot picks — what they're voting for and why — as a public "digital yard sign" at an unguessable link with a QR code, so they can share it the way a physical yard sign advertises a cause. Unlike `publish-anonymized-voter-profile`, this is not about aggregate statistics: it's one person's public advocacy for the specific choices they care about. This skill is optional. The user must explicitly ask to publish. --- ## Step 1: Name the election Ask which election these picks are for and record it as a short label — the election type and date, e.g. `General Election — November 3, 2026` or `Primary — March 3, 2026`. Max 80 characters. This matters more than it looks. The whole point is that a stranger scans the QR code weeks later. Without a date, "Yes on Measure B" is unreadable a year on — and there is no way to edit it in. **No location in this label.** Placards carry no ZIP, city, or county anywhere by design, and a free-text field is exactly where one creeps back in. `General Election — November 3, 2026` is right; `Alameda County General Election` is not. The server rejects a ZIP code in this field outright, but it cannot detect a city or county name — that's on you. The field is optional and can be omitted, but don't skip it silently. If the user would rather not say, publish without it and tell them the sign won't be dated. --- ## Step 2: Narrow down — this is not the whole ballot Assumes the user has already researched their ballot (e.g. via `research-ballot`) and has positions they're confident in. **Do not default to publishing everything they researched.** Ask which specific races or measures they actually want to publicly advocate for — frame it as "which of these do you feel strongly enough about to put on a yard sign?" A placard is a curated highlight reel of their highest-conviction picks, not a full ballot dump. Most placards should have a handful of items, not dozens. The server enforces a hard ceiling of 20 items, but treat that as a limit, not a target. For each item the user selects, collect exactly three things: - **Item** — the office or measure name, plus a short plain-language description (under 5 words) when the formal name alone wouldn't mean anything to a stranger — e.g. "Prop 40 — the wealth tax", not just "Prop 40". Skip the description when the name is already self-explanatory ("City Council District 4" doesn't need one). - **Position** — their vote or stance, short (e.g. "No", "Jane Doe") - **Reason** — one advocacy sentence, roughly tweet-length (~280 characters) The server rejects an item or position over 120 characters and a reason over 280, so keep them short rather than discovering the limit on submit. **Redaction guidance for the reason:** keep it issue-focused. Remove anything that could identify the person — no names, addresses, employer, or personal anecdotes tied to identity ("as a teacher at Lincoln Elementary…"). "Lower property taxes matter to me" is fine; identifying details are not. This placard carries no ZIP code or location field at all — don't add one. Show the assembled list back to the user verbatim and get explicit confirmation before continuing. State plainly: publishing is public, **unlisted** (not searchable anywhere on the site — no listing, no search, direct-link-only), **not editable** — there are no accounts on this site, so there is no way to change it afterwards — and **deleted after 30 days**. On the 30th day the placard is removed outright: the picks, the reasoning and the link all go at once, and anyone scanning a printed copy after that gets a "page not found, or this sign expired" page. Tell them the 30-day limit before they confirm, not after. --- ## Step 3: Solve the proof-of-work challenge Same mechanism as `publish-anonymized-voter-profile` — an anti-spam measure, not an identity check. **3a. Request a challenge** ``` GET https://votethisway.org/v1/pow/challenge ``` Response fields: - `challenge_id` — identifier for this challenge - `nonce_seed` — random seed included in the hash input - `difficulty` — number of leading zero bits required - `expires_at` — solve before this time **3b. 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 4: Submit ``` POST https://votethisway.org/v1/placards/publish Content-Type: application/json { "challenge_id": "...", "nonce": "...", "election": "General Election — November 3, 2026", "items": [ {"item": "...", "position": "...", "reason": "..."} ] } ``` `election` is optional — omit it or send `""` if the user declined to name one. 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. - **400 whose `error` does not begin with `pow verification failed:`** — content validation, not proof-of-work. Most likely a field that looks like it contains PII, a ZIP code in the election label, an over-length field, or too many items; the message names which item and which limit. This check runs *before* the challenge is consumed, so the challenge is untouched. Show the user the specific reason, let them revise, and resubmit with the **same** `challenge_id` and `nonce`. Never silently strip or invent content just to force a retry. - **400 with `pow verification failed: `** — the challenge is unusable and 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 3a. - **503 with `pow verification failed: verification temporarily unavailable`** — the 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 5: Show the result A successful response (201) returns: - `hash` — the placard's identifier - `url` — the public, shareable link: `https://votethisway.org/{hash}` **Give the user the `url`, prominently and as plain text they can copy.** That link is the whole result: the page it opens shows their picks, a scannable QR code, and a "Print this sign" button that produces a one-page sign to tape in a window or hand to a neighbor. Tell them what they can do with it — share the link, or open it and print the sign — and remind them it is public, unlisted, and deleted 30 days from now, so they know how long a printed copy stays useful. --- ## Output requirements - Never publish without explicit user confirmation after showing the assembled list and explaining that publishing is public, unlisted, not editable, and deleted after 30 days - Never add a ZIP code, city, or any location field — this feature carries none, and that includes the `election` label - Record the election the picks are for, so the sign still makes sense to someone scanning the QR code later - Help the user pick a small, curated set of items, not their whole researched ballot - Store the returned `url` locally if the user wants a record — there is no other way to retrieve it later - If the challenge expires before you finish solving, request a new one and start step 3b again - Send `nonce` as a JSON string, never a numeric literal - On a rejected submit, read the error before retrying — a content 400 keeps the challenge, a PoW 400 requires a fresh one, and a 503 means retry the same request unchanged