weigh a vote by sats × days.
tally it offline.
Stake-weighted, sybil-resistant, offline-tallyable polls on Bitcoin. Voters sign with BIP-322. Weights come from UTXO state at a snapshot block. The tally is a pure function anyone can run. No token. No authority. No custody.
{
"v": 0,
"kind": "oc-vote/ballot",
"poll_id": "3054390f…026ee5",
"voter": "bc1qbob…",
"option": "split_b",
"attestation_id": null,
"secret": null,
"created_at": "2026-04-29T12:11:00Z",
"sig": {
"alg": "bip322",
"pubkey": "bc1qbob…",
"value": "H0k2…"
}
}
// tally = pure function over
// (poll, ballots[], utxos_at_snapshot)
// → { option → weight }four steps. two wallet signatures.
One signature to create a poll, one per voter to cast. Everything else is a pure function of public data. No chain transactions, no gas, no custody.
create
Question + options + deadline + snapshot block + weight mode + thresholds. Sign the canonical poll with BIP-322. Publishes to Nostr kind 30080.
cast
Voters pick an option and sign a canonical ballot with BIP-322. Replaceable per voter per poll — change your mind until deadline.
snapshot
At deadline, any observer resolves the snapshot block (≥ 6 confirmations) and looks up each voter’s UTXO set from any public explorer.
tally
Pure function: de-dup per voter, apply weight_mode, sum per option. Two observers with the same inputs produce byte-identical results.
the open web has no sybil-resistant,
tokenless voting primitive.
Every incumbent falls short on at least one axis. A Bitcoin UTXO — already public, already time-stamped, already credibly committed — is the only weight function that prices out sybils without a token, an authority, or a KYC vendor. That property is not substitutable on Ed25519 or on an alternative chain.
| system | bot-resistant | weighted | offline-tally | secret ballot | no token / no kyc |
|---|---|---|---|---|---|
| Google Forms / Typeform | ✗ | ✗ | ✗ | ✗ | ✓ |
| Snapshot | ✗ | ✓ | ~ | ✗ | ✗ (token) |
| Helios | ✓ | — | ✓ | ✓ | ✗ (authority) |
| Gitcoin Passport | ✓ | ✓ | ~ | ✗ | ~ (stamps) |
| Nostr poll (NIP-88) | ✗ | ✗ | ✓ | ✗ | ✓ |
| oc vote | ✓ (sats × days) | ✓ | ✓ | ✓ (opt-in) | ✓ |
bitcoin core soft-fork signaling
"Should the next soft fork enable OP_CTV?"
A miner / hodler signaling poll where weight is sats × days unspent. Public ballots, public tally — no Coinbase, no Slack admin, no token. Anyone re-tallies bit-for-bit from the published events.
non-profit board election
"Choose 3 board seats from these 7 candidates."
Member-weighted secret-ballot voting where each member's weight is one — sybil-resistant via OC Attest gating (e.g. ≥ 50k sats bonded ≥ 60 days). Members reveal their ballots after close; the tally is reproducible offline.
community treasury allocation
"Distribute the Q3 grant pool across these 12 proposals."
Stake-weighted ranked-choice tally where weight is sats × days bonded by the voter. Proposals self-stake to enter the ballot. The treasury smart-contract / multisig reads the tally output and disburses.
two functions. zero state.
Polls, ballots, and tallies are signed canonical JSON. The reference implementation is @orangecheck/vote-core — a pure-function library you call from any backend, edge worker, or CI job. No database, no relay state, no SaaS.
import { createPoll, tally } from '@orangecheck/vote-core';
// Author signs a poll envelope (BIP-322).
const poll = await createPoll({
question: "Should the next soft fork enable OP_CTV?",
options: ["yes", "no", "abstain"],
weight: { mode: "sats_days", min_days: 30 },
closes_at_block: 905000,
sign: await wallet.sign,
});
// Tally is a deterministic pure function over published ballots
// + chain state at the close block. Anyone re-tallies from the
// raw Nostr events and gets the same bytes.
const result = await tally({
poll,
ballots, // kind-30081 events for poll.id
utxosAt: getUtxosAtBlock, // function: (addr, block) => UTXO[]
});
// result.totals: { yes: 142_853_000, no: 87_120_000, abstain: 12_000_000 }
// result.checksum: "sha256:9ef…" (cross-impl byte-identical)See WHY.md for the full hypothesis-by-hypothesis design rationale. 13 claims stated, adversarially tested, and either KEPT or RETIRED with a reason.
one of six. compose freely.
Polls can require voters to hold an oc·attest attestation meeting thresholds — e.g. ≥ 100k sats bonded for ≥ 30 days. Secret-mode ballots are oc·lock envelopes addressed to a poll-specific reveal key. oc·pledge disputes can resolve through a Vote tally. Six primitives, one substrate, zero tokens.
import { tally } from '@orangecheck/vote-core';
import { unseal } from '@orangecheck/lock-core';
// secret-mode tally = vote-core + lock-core
const revealedOptions: Record<string, string> = {};
for (const b of ballots) {
if (!b.secret) continue;
const plain = await unseal({
envelope: b.secret.envelope,
device: { device_id: 'reveal', secretKey: reveal_sk },
skipSenderVerification: true,
});
revealedOptions[b.voter] = utf8Decode(plain.payload);
}
const result = await tally({
poll, ballots, utxosAt, revealedOptions,
});oc·attest
Prove you control a Bitcoin address holding N sats for N days. The sybil-filter primitive.
oc·lock
End-to-end encryption to a Bitcoin address. X25519 + AES-256-GCM. Used by Vote secret-mode ballots.
oc·vote(here)
Stake-weighted, sybil-resistant, offline-tallyable signals. You're looking at it.
oc·stamp
BIP-322 + OpenTimestamps anchored signed statements. Stamp a poll outcome to Bitcoin for permanent record.
oc·agent
Scoped delegation for autonomous agents. Bots can cast ballots within revocable scope.
oc·pledge
Forward-looking, bonded commitments. Disputes resolve via stake-weighted Vote polls — Vote is the dispute mechanism.
sats as weight. one ballot. anyone tallies.
No signup. No token. No chain transaction. Your Bitcoin wallet is already the voter list; your UTXOs are already the weight. Start signaling in thirty seconds.
with thanks to bram kanstein — whose work on bitcoin as sovereignty layer shaped the premise.