npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@prizemart/safedraw

v2.0.2

Published

Transparent, verifiable, reproducible draws powered by the drand randomness beacon. The same code that runs a draw verifies a draw.

Downloads

597

Readme

@prizemart/safedraw

Transparent, verifiable, reproducible raffle draws powered by the drand randomness beacon.

League of Entropy npm License: MIT


Why

Most raffle systems use Math.random() or a seeded PRNG to pick winners. These approaches have two problems:

  1. Not verifiable — you have to trust the operator ran the code they claim to have run.
  2. Not reproducible — even if the code is public, without the seed you can never re-derive the same winner.

@prizemart/safedraw solves both by using the drand Quicknet beacon — a distributed randomness service run by the League of Entropy (Cloudflare, Protocol Labs, Ethereum Foundation, and ~12 others). Each beacon round produces a BLS threshold signature that:

  • Is unpredictable before it fires
  • Is public and permanent once it fires
  • Is verifiable against a published public key

This means any draw result can be independently reproduced by anyone, forever — including by your users on their own phones.


How it works

The draw follows a commit-reveal model with three phases:

PUBLISH TIME            CLOSE TIME              DRAW TIME
─────────────────────────────────────────────────────────
  Commit drand round  → Lock entry pool       → Fetch beacon
  (before entries)      Hash entries          → Select winner
                         committed            → Publish result
  1. Commit the round at publish time — before any entries are accepted, you record which drand round will provide the randomness. This round is determined solely by the close date and is publicly known. Nobody — not even the operator — can change it after entries open.

  2. Hash the entry pool at close time — when the raffle closes, every ticket number is sorted alphabetically and SHA-256 hashed. This fingerprint is published before the draw runs.

  3. Fetch the beacon at draw time — the pre-committed drand round is fetched. The winner is selected: BigInt(randomness) % entries.length. Anyone with the entry list and round number can reproduce this exact result.


Quick start

npm install @prizemart/safedraw
import { conductDraw } from '@prizemart/safedraw';

const result = await conductDraw({
  entries: ['TICKET-001', 'TICKET-042', 'TICKET-107'],
  expectedHash: 'a3f8...', // SHA-256 committed when raffle closed
  drandRound: 1234567,     // committed when raffle was published
});

console.log(result.winner);           // 'TICKET-042'
console.log(result.selectionFormula); // 'BigInt("0xfe29...") % BigInt(3) = 1'
console.log(result.drand.randomness); // 'fe29...'
console.log(result.drand.signature);  // BLS signature hex — verifiable against drand public key

Committing the drand round at publish time

When you publish a raffle, compute the round that will fire at or just after the close date and store it:

import { computeDrandRound, QUICKNET_CHAIN_HASH } from '@prizemart/safedraw';

const closeTimestamp = Math.floor(new Date(raffle.close_date).getTime() / 1000);
const drand_committed_round = computeDrandRound(closeTimestamp);

// Store on your raffle record:
// { drand_committed_round, drand_chain_hash: QUICKNET_CHAIN_HASH }

Hashing the entry pool at close time

When the raffle closes, hash the entry pool and store it before running the draw:

import { hashEntryPool, verifyEntryHash } from '@prizemart/safedraw';

const entries = tickets.map(t => t.ticket_number);
const entry_hash = await hashEntryPool(entries);

// Store entry_hash on the raffle record — this is the commitment.
// The draw uses this hash to verify the pool hasn't changed.

Verifying a past draw

Given any published draw result, anyone can verify the winner independently:

Step 1 — Confirm your ticket was in the pool:

# entry list is published at GET /draws/{id}/entries
curl https://api.yourapp.com/draws/abc123/entries | jq '.[] | .ticket_number' | sort

Step 2 — Verify the entry pool hash:

import { hashEntryPool } from '@prizemart/safedraw';

const entries = ['TICKET-001', 'TICKET-042', ...]; // from the published entry list
const hash = await hashEntryPool(entries);
console.log(hash === publishedHash); // true

Step 3 — Fetch the drand round:

curl https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/1234567
# { "round": 1234567, "randomness": "fe29...", "signature": "b44..." }

Step 4 — Reproduce the winner:

import { selectWinner } from '@prizemart/safedraw';

const sorted = [...entries].sort((a, b) => a.localeCompare(b));
const { winner, winnerIndex, formula } = selectWinner(sorted, beacon.randomness);

console.log(winner);  // reproduces the published winner
console.log(formula); // 'BigInt("0xfe29...") % BigInt(1500) = 742'

Reproducing a winner from scratch (copy-paste script)

// reproduce-winner.mjs — run with Node.js 18+
import { conductDraw } from '@prizemart/safedraw';

const result = await conductDraw({
  entries: [
    // paste the full published entry list here
    'TICKET-001',
    'TICKET-042',
    // ...
  ],
  expectedHash: 'a3f8...', // from the draw result page
  drandRound: 1234567,     // from the draw result page
});

console.log('Winner:', result.winner);
console.log('Formula:', result.selectionFormula);
console.log('Verified hash:', result.entryHash);
node reproduce-winner.mjs
# Winner: TICKET-042
# Formula: BigInt("0xfe29...") % BigInt(3) = 1
# Verified hash: a3f8...

API reference

conductDraw(input: DrawInput): Promise<DrawOutput>

Main entrypoint. Verifies the entry hash, fetches the pre-committed drand round, and returns the winner with full proof data.

Throws if the entry hash doesn't match, or if the drand round cannot be fetched.


hashEntryPool(ticketNumbers: string[]): Promise<string>

Returns SHA-256(sorted_ticket_numbers.join(',')) as a lowercase hex string. Sorts the input internally.


verifyEntryHash(ticketNumbers: string[], expectedHash: string): Promise<boolean>

Returns true if hashEntryPool(ticketNumbers) === expectedHash.


selectWinner(sortedEntries: string[], randomnessHex: string): WinnerSelection

Synchronous. Computes BigInt('0x' + randomnessHex) % BigInt(sortedEntries.length) and returns the winner, its index, and a human-readable formula string.

sortedEntries must already be sorted — sorting is not applied here.


computeDrandRound(closeTimestampSeconds: number): number

Returns the first drand Quicknet round that fires at or after closeTimestampSeconds. Call this at raffle publish time and store the result.


fetchDrandRound(round: number, options?: DrandOptions): Promise<DrandBeacon>

Fetches a specific round from the drand Quicknet beacon, failing over across the official endpoints.


QUICKNET_CHAIN_HASH

'52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971' — the Quicknet chain hash. Store this alongside drand_committed_round so the chain is explicit in your records.


Types

interface DrawInput {
  entries: string[];       // ticket numbers (sorted internally)
  expectedHash: string;    // SHA-256 committed when raffle closed
  drandRound: number;      // committed at raffle publish time
  options?: DrandOptions;
}

interface DrawOutput {
  winner: string;
  winnerIndex: number;
  totalEntries: number;
  entryHash: string;
  drand: {
    round: number;
    randomness: string;   // hex — sha256(signature)
    signature: string;    // BLS threshold signature hex
    chainHash: string;
  };
  selectionFormula: string; // e.g. 'BigInt("0xfe29...") % BigInt(1500) = 742'
}

interface DrandOptions {
  chainHash?: string;    // defaults to QUICKNET_CHAIN_HASH
  apiUrls?: string[];    // defaults to 3 official drand endpoints
}

Security model

Why drand?

crypto.randomInt() is cryptographically secure but not reproducible — you can't re-derive the same random number from public information. Users have to take the operator's word for it.

drand Quicknet solves this: each round produces a BLS threshold signature over the round number, signed by a distributed network of 15+ independent organisations (the League of Entropy). To produce a valid signature, a threshold majority must participate. No single party — including the raffle operator — can influence the output.

The commit-reveal property

The critical trust property is that the drand round is committed before any entries are accepted, not at draw time. This means:

  • The operator cannot see the randomness and decide to open/close entry based on it
  • The operator cannot cherry-pick a "better" round after seeing the entries
  • The draw is fully determined at publish time — the only unknowns are who buys tickets

What you can independently verify

After any draw:

| Claim | How to verify | |---|---| | My ticket was in the pool | Hash the published entry list; compare to published hash | | The hash was committed before the draw | Check timestamps on the raffle record | | The winner was selected correctly | Fetch the drand round; run selectWinner locally | | The randomness is authentic | Verify BLS signature against the drand public key |


Platform support

@prizemart/safedraw has zero runtime dependencies. It uses only:

  • fetch (Node.js 18+, browsers, React Native)
  • globalThis.crypto.subtle — Web Crypto API (Node.js 18+, browsers, React Native/Hermes)
  • BigInt (Node.js 10.4+, all modern browsers, React Native/Hermes)

This means the full conductDraw function runs on a user's phone — no server required. Users can verify any past draw result entirely locally.


Contributing

This library is intentionally minimal — zero dependencies, three pure functions, one async HTTP call. Contributions that add dependencies or server-side state will not be accepted.

Bug reports and PRs welcome.


License

MIT — see LICENSE.