Open source · Not for profit · Self-hostable

Ballots designed so nobody can read them.

ZK-Voting lets any group run its own elections and polls. When creating one, the operator chooses a poll or a vote and whether re-voting is allowed. That setting can't be changed afterward. Turnout is published on a delayed hourly schedule for both kinds. A poll also publishes per-choice counts on that schedule once 100 voters have cast their first ballot, or at the scheduled end, whichever comes first. A vote publishes choices only at the deadline. Zero-knowledge proofs are designed to show that only eligible people voted, each one once, and that the published tally matches the ballots (tests currently use a mock verifier). Individual choices are designed not to be published.

  • 0choices published
  • 1counted ballot per voter
  • 100%of the tally checkable by anyone
  • $0to voters: no token, no fees

The big picture

Many civic L2s, one shared record on Ethereum

Anyone can run a civic L2 for their own elections. Each one batches votes and settles them to Ethereum L1, so the record outlives any single operator.

Macro view: many independent civic L2s settling to Ethereum L1 Three civic L2 boxes labeled A, B and C, plus a dashed box for unlimited more. Each holds election chips with no limit on how many. Arrows carry vote batches from every L2 down to one Ethereum L1 box, where each batch is keyed by hash of the L2 ID and the election UUID. A two-way arrow notes that results can be rebuilt from either the L2 or L1. Anyone can run one · unlimited L2s Civic L2 A election 1 election 2 + unlimited Civic L2 B election 1 election 2 + unlimited Civic L2 C election 1 election 2 + unlimited ∞ more L2s vote batches, each keyed by hash(L2 ID, election UUID) Ethereum L1 Shared settlement layer and permanent public record L2 data L1 record Results can be rebuilt from either one No single operator has to be trusted to keep them

Macro view

  • Anyone can run a civic L2, and there's no limit on how many exist.
  • Each L2 can hold any number of elections.
  • Every L2 settles its vote batches to Ethereum L1, keyed by hash(L2 ID, election UUID).
  • Results can be rebuilt from the L2's data or from the L1 record.
L2 view: one election team's L2 Voters at the edge vote for free and send ballots to the sequencer. A voter who is not pre-approved is rejected by the prover. The sequencer and prover batch the accepted votes. The operator's minimally funded L1 key pays to submit each batch to Ethereum L1. The L2 ID is bound to the operator key plus a salt, and election UUIDs are unique within the L2. One election team's L2 L2 ID = operator key + salt Voters at the edge · vote free ✓ ✓ ✓ ✓ ? not pre-approved Sequencer orders and batches votes Prover proves each batch ✕ rejected by the prover Election UUIDs are unique within this L2 Operator L1 key minimally funded, pays to submit batch Ethereum L1 batch settled under hash(L2 ID, election UUID)

L2 view: one election team

  • Voters at the edge vote for free.
  • Only pre-approved voters are accepted. The prover rejects everyone else. (with the mock verifier in tests, this check is not yet enforced)
  • The sequencer and prover batch the votes.
  • The operator's minimally funded L1 key pays to submit each batch to L1.
  • The L2 ID is bound to the operator key plus a salt, and election UUIDs are unique within the L2.

Why it matters

Most elections still ask voters, candidates and courts to trust a few operators: who is on the roll, who was turned away, whether a ballot was counted, whether the result matches. ZK-Voting turns those claims into things anyone can check.

Private by design

The option you pick is designed never to be a readable field in a transaction or a database. Receipts are hashes, designed so they don't show how you voted.

Publicly verifiable

Registrations and disqualification codes are public. Turnout is published on a delayed hourly schedule for both kinds. A poll also publishes per-choice counts on that schedule once 100 voters have cast their first ballot, or at the scheduled end, whichever comes first. A published hourly block is designed to stay as first published. A vote's choices stay sealed until the deadline. An independent auditor is designed to be able to rebuild a published result without the operator's private database.

One person, one vote

Every ballot carries a proof that the voter is on the frozen roll and not disqualified. The operator decides at creation whether re-voting is allowed, and that setting can't be changed afterward. If it is allowed, the last ballot before the deadline counts. If not, a second ballot is rejected.

Built to resist coercion

Nothing a voter is given is designed to prove their choice to someone pressuring them. Choose a vote when a running choice count would add pressure. Turnout is published on a delayed hourly schedule, as in a poll. A poll also publishes per-choice counts on that schedule once 100 voters have cast their first ballot, or at the scheduled end, whichever comes first. The choices stay encrypted until the deadline. If the operator allowed re-voting at creation, a voter can replace their ballot until then.

Due process for disqualification

Barring someone is a public, coded, on-chain change with a stated reason, not a silent database flag.

No vendor lock-in

AGPL-licensed, run at cost, built to be forked and rebranded. Your group owns the node, the roll and the record.

Who it's for

Any body that needs a roll, a ballot and a result people will accept.

Cities, counties and districts

Run civic elections and referendums with nested districts, measures and articles. Observers follow turnout while the election is open and the choices after the deadline.

Unions and member associations

Hold leadership elections and contract ratification votes where members trust the count without trusting the officers.

Cooperatives and boards

Take board votes and member polls, including weighted and ranked-choice ballots, with an auditable outcome report.

Schools and campuses

Run student government and faculty votes with passkey sign-in and a ballot that's accessible on any device.

Communities and clubs

Quick yes/no, single-choice or multiple-choice polls for any group that wants a private ballot and an honest show of hands. Turnout is published on a delayed hourly schedule for both kinds. A poll also publishes per-choice counts on that schedule once 100 voters have cast their first ballot, or at the scheduled end, whichever comes first.

Observers and auditors

Follow registrations and disqualification codes on the public explorer, and verify an outcome report once that contest is allowed to show it. Turnout is published on a delayed hourly schedule for both kinds. A poll also publishes per-choice counts on that schedule once 100 voters have cast their first ballot, or at the scheduled end, whichever comes first. A vote publishes choices only at the deadline.

How it works

Zero-knowledge proofs let the system check facts about a voter without putting the ballot in the clear. What anyone can see, and when, depends on the kind the creator chose.

ZK-Voting · Poll vs Vote

Poll vs Vote

The creator chooses a poll or a vote. A poll publishes per-choice counts hourly, with a 30-minute delay, after a gate. A vote keeps choices sealed until the deadline. Both end with one final batch, a challenge window, and the final tally.

Poll

Hourly per-choice counts, 30 minutes delayed, after the gate.

  • Counts stay hidden until 100 voters have cast their first ballot or the scheduled end arrives, whichever comes first.
  • After that gate, per-choice counts publish for each hourly block, 30 minutes after the hour closes.
  • An hour with fewer than 10 ballots merges forward until a block has at least 10.
  • Published hourly blocks never change: each block is designed to stay as first published. Replacement ballots, if re-voting is allowed, show up only in the final tally.

Vote

Choices stay sealed until the deadline.

  • Choices stay sealed until the deadline.
  • They are designed to be decrypted only as an aggregate, by k-of-n guardians. The design gives the operator no key share.
  • Choices are encrypted to a public key posted on the L2.
  • The matching private key is split among the guardians with Shamir secret sharing, designed so no single party, including the operator, can decrypt early.
  • There are no interim per-choice counts.

Both

  • The operator decides at creation whether re-voting is allowed, and that setting can't be changed afterward. If it is allowed, the last ballot before the deadline counts. If not, a second ballot is rejected.
  • Every ballot, including a re-vote, gets a receipt.
  • Turnout is published on a delayed hourly schedule, with no padding.
  • Ballots batch hourly while open, one final batch goes to L1 at the deadline, and one supplementary batch is posted before the tally.
  • A challenge window, which must be longer than zero, comes before the final tally.

How a ballot moves

Ballot flow for a poll and a vote Cast, then a receipt, then an hourly batch. A poll publishes hourly results. A vote stays sealed and shows turnout only. Then the deadline, a challenge window, and the final tally. Cast Receipt Hourly batch Poll Hourly results Vote Sealed Turnout only Deadline Challenge window Final tally

A poll publishes hourly results after the batch. A vote stays sealed and shows turnout only. The deadline, the challenge window, and the final tally are shared.

  1. 1

    Register

    Members sign up with a passkey, wallet, email or ID. The roll stores an identity commitment, never an address or document number.

  2. 2

    Freeze the roll

    Officials commit the eligible set and any disqualifications for the election.

  3. 3

    Vote with a proof

    Each ballot proves eligibility. The operator decides at creation whether re-voting is allowed, and that setting can't be changed afterward. If it is allowed, the last ballot before the deadline counts. If not, a second ballot is rejected.

  4. 4

    While it is open

    A poll publishes per-choice counts once 100 voters have cast their first ballot, or the scheduled end arrives, whichever comes first. Each hour publishes 30 minutes after it closes. An hour with fewer than 10 ballots merges forward until a block has at least 10. A vote publishes turnout on the same delayed hourly schedule and keeps choices sealed.

  5. 5

    After the closing date

    No more ballots are accepted. A poll's published hourly blocks are designed to stay as first published. If re-voting was allowed, replacement ballots show up only in the final tally. A vote's choices are designed to be decrypted only as an aggregate by k-of-n guardians. One final batch goes to L1 at the deadline, then a challenge window, then the final tally. Anyone can check that the published totals match the accepted ballots.

Run it yourself

Your group runs its own node with Docker Compose, Coolify, Dokploy or CapRover, optionally kept private behind a Tailscale sidecar. Every node serves its self-hosting guides at /docs.

Day-to-day activity lands on the ZK-Voting L2, a purpose-built civic sequencer and prover that settles to Ethereum. Final tallies can be mirrored on Ethereum mainnet.

# run your own node
cp .env.example .env
docker compose up -d --build
curl -fsS http://127.0.0.1:3000/health

Honest status

ZK-Voting is under active development. Tests use a mock verifier, the first proving path is in progress and the trusted-setup ceremony hasn't happened yet. Don't use it for a binding election today.

Passing a vote never auto-executes anything. Outcomes are verifiable; acting on them stays with the people.

community.zk-voting.org

Talk about how your group could use it

The community forum is where people discuss real use cases, ask questions about running a node, and shape what gets built next.