Protocol · legitimacy
Polls weighed in coins, counted by anyone.
Sats as weight. One ballot. Anyone tallies. In an OC Vote poll, each ballot is signed by a Bitcoin address and weighed by the coins that address holds. Anyone can collect the ballots and count them, and get the same answer.
{
"v": 0,
"kind": "oc-vote/poll",
"creator": "bc1qalice",
"question": "Do we ship?",
"options": [
{ "id": "yes", "label": "Ship it" },
{ "id": "no", "label": "Hold" }
],
"mode": "public",
"weight_mode": "sats",
"weight_params": null,
"min_sats": 100000,
"min_days": 30,
"snapshot_block": 900000,
"deadline": "2026-05-08T00:00:00Z",
"tiebreak": "latest",
"reveal_pk": null,
"notes": null,
"created_at": "2026-04-22T12:00:00Z"
}- Open spec, MIT
- No account, no token
- Voting moves no coins
- Anyone can count it
The problem
An online poll is easy to stuff.
One person can open a thousand accounts, and a form counts each one.
The usual fixes cost something else. Token voting needs a token. A hosted poll asks you to trust whoever runs the server that counts. Formal election systems need an authority to keep the voter roll.
A group that wants a quick signal needs two things: faking a crowd should cost real money, and nobody should have to be trusted to count.
How it works
Write a poll. Sign a ballot. Anyone counts.
- 01
Someone writes a poll
It has a question, options, a deadline and a snapshot block, which is a Bitcoin block height. It says how votes are weighed: one per address, by sats held, or by sats × days held (with a cap), plus optional minimums. The creator signs it, and its id is a hash of its contents, so nobody can change it after people vote. - 02
Voters sign a ballot
You pick an option and sign with your wallet. Signing moves nothing. The ballot goes to public Nostr relays. Most polls count your latest ballot, so you can change your vote until the deadline. A poll can choose to count your first ballot instead. - 03
Anyone counts
After the deadline, anyone collects the poll and its ballots, checks every signature, looks up each voter’s coins at the snapshot block and adds the weight per option. Two people running the count on the same inputs get byte-identical results.
- The Bitcoin part
- A vote’s weight is the value and age of the coins an address holds at a named block, and the ballot is a BIP-322 signature from that address. A plain keypair has no balance and no age to weigh.
- Secret ballots are optional
- Each choice is sealed with OC Lock to a key the creator publishes after the deadline.
Why it exists
Every other way misses at least one of these.
The spec’s own audit of the field, in words.
- A bot can’t make a voter for freeOC VoteYesSnapshotNo: liquid tokens can be boughtHeliosYes, with an authority’s voter listGitcoin PassportYes, through stampsNostr pollsNo
- WeightedOC VoteYesSnapshotYesHeliosOne voter, one voteGitcoin PassportYesNostr pollsNo
- Anyone can count it, offlineOC VoteYesSnapshotPartlyHeliosYesGitcoin PassportPartlyNostr pollsYes
- Secret ballotOC VoteYes, if the poll asks for itSnapshotNoHeliosYesGitcoin PassportNoNostr pollsNo
- No token, no KYCOC VoteYesSnapshotNo: needs a tokenHeliosNo: needs an authorityGitcoin PassportPartly: stamps lean on KYCNostr pollsYes
The full argument is in the spec’s WHY.md.
Where it stands
Open, stable, and free to use.
The OC Vote spec is v0 and stable. Later fixes have been clarifications that change no test-vector result. The reference library, @orangecheck/vote-core 1.5.0, is on npm under MIT and checks itself against the spec’s five test vectors. The tools at vote.ochk.io work today and are free. You need no account and no token: your wallet signs. OC Vote never holds your coins, and voting broadcasts no transaction. orangecheck as a whole has not launched publicly yet.
Try it
What you can do today.
One signature from UniSat, Xverse, Leather, OKX or Phantom, or paste a signature from any wallet that signs BIP-322 messages.
vote.ochk.io/createThe playground runs the reference library in your browser. Nothing leaves the page.
vote.ochk.io/playground
Each poll lives at vote.ochk.io/p/<poll_id>, where you vote and watch the count; the page re-runs it in your browser. The creator of a secret poll reveals it at vote.ochk.io/reveal/<poll_id>, after the deadline.
For developers
Build on it.
npm install @orangecheck/vote-core @orangecheck/bip322Core
@orangecheck/vote-corecanonical form, poll and ballot ids, the three weight modes and tally(); no network, no wallet
React
@orangecheck/vote-react<OcPoll>, <OcTallyBadge> and useTally() to show a live count on any site
import { verifySignature } from '@orangecheck/bip322';
import { mempoolBlockTimeSource, resolvePollSnapshot, tally } from '@orangecheck/vote-core';
// A poll may name its snapshot as "deadline": resolve it to a block first.
const snapshot = await resolvePollSnapshot(poll, mempoolBlockTimeSource());
if (!snapshot.ok) throw new Error(snapshot.reason);
const result = await tally({
poll, // the signed poll (Nostr kind 30080)
ballots, // every ballot for it (kind 30081)
snapshotBlock: snapshot.height,
utxosAt: (address, height) => utxosFromMyNode(address, height),
verify: ({ address, message, signature }) => verifySignature(address, message, signature),
tipHeight, // refuses a snapshot with fewer than 6 confirmations
});
// { state: 'tallied', snapshot_block, turnout: { voters, weight }, tallies: { yes: …, no: … } }tally() refuses to run without a signature verifier. In a secret poll, pass unseal once the reveal is out.Check one
Check a result yourself.
Nobody’s count is official, including ours. Paste a poll id or link to open its page, which fetches the poll and ballots from the relays and counts them on your machine.
What the count checks, per the spec:
- the creator’s signature over the poll id;
- for each ballot: it names this poll, comes from a mainnet address, was signed before the deadline, names a real option, and carries a valid signature from the voter;
- one ballot per voter, chosen by the poll’s rule (latest or first);
- in a secret poll: the reveal is signed by the creator and published after the deadline, and each opened choice matches the commitment in its ballot;
- the snapshot block has at least 6 confirmations;
- each voter’s weight under the poll’s mode and minimums, summed per option.
Over HTTP: curl "https://vote.ochk.io/api/tally?poll=<poll_id>". The answer names the relays and the coin source it used. Any site may call it.
On your own machine: npx -y @orangecheck/vote-cli tally <poll_id>. Add --mempool-base to read coins from your own mempool.space instance. oc-vote verify <poll_id> checks the signatures only.
What you trust
Every party you rely on, by name.
- A source of Bitcoin data
- The count needs each voter’s coins at the snapshot block. vote.ochk.io and the CLI read them from mempool.space by default. Run your own node or explorer and you trust no one.
- One known gap in the reference tools
- They count coins that are still unspent today and were confirmed by the snapshot block. A coin spent after the snapshot drops out, so a count run long after the deadline can come out lower than the spec’s answer. The tally API says so in every response.
- Nostr relays
- Polls and ballots are found on relays, and a relay can drop a ballot. The reference set is relay.ochk.io, nos.lol, relay.primal.net, offchain.pub and relay.snort.social. Publishing to several limits the damage.
- The creator of a secret poll
- The creator holds the reveal key. They can read ballots before the deadline, and if they never reveal, the poll is abandoned with no count. Use a public poll if that is not acceptable.
What it is not
The limits, plainly.
- Not one person, one vote
- One person can hold many addresses. Splitting coins does not add weight when votes are weighed by sats, but it does in one-per-address mode, so that mode needs a minimum high enough to make splitting expensive.
- Not coercion-proof
- Once a secret poll is revealed, every choice is linked to its address. Anyone can ask you to prove how you voted.
- Not a legal election
- It is a signal for voluntary groups.
- Not anonymous
- Questions, options and voter addresses are public. A fresh address hides you but usually carries no weight.
Where it is used
Apps that use it.
- No showcase app yet
- None of the showcase apps (vault, chat, cosign, the demo) or ochk.app uses OC Vote yet. vote.ochk.io is the reference tool.
- oc-vote-examples (opens in a new tab)
- Copy-ready templates: a Discord bot that posts counts, a GitHub Action that runs a job only when a poll passes, and shell recipes.
Run your first poll.
The tools are free and need no account or token. The spec is open to anyone who wants to build their own.
With thanks to Bram Kanstein, whose work on Bitcoin as a sovereignty layer shaped the premise.