Ring signatures. A ring signature lets one member of a group sign a message on behalf of the whole group, without revealing which member actually signed. The verifier learns "some member of this ring signed" and nothing more.
LSAG (Liu–Wei–Wong, 2004). We use the LSAG construction adapted to secp256k1 / BIP-340 Schnorr. The signer picks a random α, computes a challenge chain c_i → c_{i+1} = H(m ‖ I ‖ s_i·G + c_i·P_i ‖ s_i·H_p(P_i ‖ ctx) + c_i·I) starting at their secret index, and closes the loop by setting s_π = α − c_π·x. Verification walks the same chain from c_0 and checks that c_n = c_0.
Key images. Every signature also publishes I = x·H_p(P_π ‖ poll_id), where H_p is a deterministic, context-bound hash-to-curve. Because I depends only on the signer's secret, their own public key, and the poll id (and not on the ring order), the same signer produces the same I for a given poll every time. Honest clients count only the first valid vote per I, so double-voting is detected without ever learning who the voter is. The full 33-byte compressed point is published so verification reconstructs the exact point (including its y-parity).
Cross-poll unlinkability. We bind poll_id into both the signed message m and the hash-to-curve context, so the same key signing two different polls produces two completely independent key images. Your votes across polls cannot be linked by anyone except you.
Event-level blinding. The outer Nostr event is signed by a fresh throwaway keypair generated per vote, so the event's pubkey never points at the voter. The ring signature inside the content is the only cryptographic link back to a ring member.
Threat model. Protects the link between a ring member and their vote. Does not protect against network-level correlation, relay-side timing analysis, or collusion among the non-signing ring members with a relay.
localStorage so you stay signed in across reloads. Any script running on this origin, or anyone with physical access to an unlocked device, can read it. Only use this on a device you trust. Use "Sign out" to remove it.created_at within a 1-hour window, but relays still see arrival time.