live · mainnetoc · docs
specs · api · guides
docs / library + cli

api

The web UI is convenience. The curl output is the authoritative result.

http

GET /api/tally?poll=<64-char hex poll_id>

Fetches the poll from the default Nostr relay set, pulls every matching ballot, verifies every BIP-322 signature, resolves the snapshot block, looks up UTXOs from mempool.space, runs the pure tally function, and returns JSON.

200 tallied:

{
    "poll_id": "3054…",
    "state": "tallied",
    "snapshot_block": 900412,
    "turnout": { "voters": 47, "weight": 2814300000 },
    "tallies": {
        "split_a": 812300000,
        "split_b": 1102900000,
        "split_c": 899100000
    },
    "ballot_count": 47
}

200 awaiting_reveal (secret-mode poll pre-close):

{ "poll_id": "cc27…", "state": "awaiting_reveal" }

4xx:

  • 400 — poll query param missing or not 64 hex.
  • 404 — poll not found on any default relay.
  • 422 — poll event content did not match poll_id (relay corruption).

Response is cached for 60 seconds at the edge. Live tallies on the poll page recompute client-side against the same function.

library

npm i @orangecheck/vote-core

Full API surface:

import type {
    Ballot,
    // types
    Poll,
    Reveal,
    TallyResult,
    Utxo,
} from '@orangecheck/vote-core';

import {
    ageDays,
    ballotId,
    buildCommitMessage,
    canonicalBytes,
    // canonicalization
    canonicalize,
    // secret-mode commit
    commit,
    isSupportedMode,
    // ids (content-addressed, SHA-256 of canonical bytes with sig.value = "")
    pollId,
    qualifyingUtxos,
    revealId,
    // the tally
    tally,
    totalQualifyingSats,
    // weight computation
    voterWeight,
} from '@orangecheck/vote-core';

example: build and sign a poll

import { pollId } from '@orangecheck/vote-core';

const draft: Poll = {
    v: 0,
    kind: 'oc-vote/poll',
    creator: 'bc1q…',
    question: 'Should we ship?',
    options: [
        { id: 'yes', label: 'Yes' },
        { id: 'no', label: 'No' },
    ],
    deadline: '2026-05-08T00:00:00Z',
    snapshot_block: 'deadline',
    weight_mode: 'sats',
    weight_params: null,
    min_sats: 100_000,
    min_days: 30,
    mode: 'public',
    reveal_pk: null,
    tiebreak: 'latest',
    notes: null,
    created_at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
    sig: { alg: 'bip322', pubkey: 'bc1q…', value: '' },
};

const id = pollId(draft);
// ask wallet to BIP-322 sign `id`, then set draft.sig.value = <signature>

example: tally

import { tally } from '@orangecheck/vote-core';

const result = await tally({
    poll,
    ballots,
    utxosAt: (addr, snapshotHeight) => myUtxoSource.fetch(addr, snapshotHeight),
    skipSignatures: false,
    verifyBip322: async (address, message, signatureB64) => {
        const { Verifier } = await import('bip322-js');
        return Verifier.verifySignature(address, message, signatureB64);
    },
});

The library is pure. Same inputs → byte-identical output across implementations. 28/28 tests pass against the canonical fixtures in oc-vote-protocol/test-vectors/.

dispute resolution

If the vote.ochk.io page tally disagrees with yours:

npx -y @orangecheck/vote-cli tally <poll_id>
# or: git clone oc-vote-protocol && implement tally() yourself

Whoever matches the canonical fixtures + the spec is correct. The web page is a convenience over the same function any client can run.