live · mainnetoc · vote
stake-weighted · sybil-resistant
oc · vote·stake-weighted signaling

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.

· from mind of@bramk· no token· no custody· bip-322 identity
ballot.json · { "v": 0, "kind": "oc-vote/ballot" }
{
  "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 }
§ how it works

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.

[01]

create

Question + options + deadline + snapshot block + weight mode + thresholds. Sign the canonical poll with BIP-322. Publishes to Nostr kind 30080.

[02]

cast

Voters pick an option and sign a canonical ballot with BIP-322. Replaceable per voter per poll — change your mind until deadline.

[03]

snapshot

At deadline, any observer resolves the snapshot block (≥ 6 confirmations) and looks up each voter’s UTXO set from any public explorer.

[04]

tally

Pure function: de-dup per voter, apply weight_mode, sum per option. Two observers with the same inputs produce byte-identical results.

§ why it exists

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.

systembot-resistantweightedoffline-tallysecret ballotno token / no kyc
Google Forms / Typeform
Snapshot~✗ (token)
Helios✗ (authority)
Gitcoin Passport~~ (stamps)
Nostr poll (NIP-88)
oc vote✓ (sats × days)✓ (opt-in)
[01]

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.

[02]

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.

[03]

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.

§ integration

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.

§ create + tally
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.

§ layered with the family

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.

§ compose example
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,
});
§ cast a vote that carries weight

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.