Bir Çekilişi Kendiniz Doğrulayın

Her teknik bilgi düzeyinde herhangi bir yarışmanın adilliğini izlemek ve bağımsız olarak test etmek için adım adım rehber.

Çoğu çekiliş aracı ile Pick a Winner karşılaştırması

Kendiniz kontrol edebilir misiniz? Tipik çekiliş aracı Pick a Winner
Kimin kazandığını görün
Yayımlanan bir tohumdan çekilişi kendiniz yeniden çalıştırın
Her işlemin kurcalamaya karşı korumalı, hash zinciriyle bağlı kaydı
Sonucun bağımsız Bitcoin zaman damgası
Herkese açık doğrulama bağlantıları — giriş gerektirmez

Çoğu araç bir kazanan gösterir ve buna güvenmenizi ister. Pick a Winner ise sonucu kendiniz doğrulamak için ihtiyacınız olan her şeyi yayımlar.

At a glance: seven progressive checks

Start at Level 1; stop wherever your skepticism is satisfied. Each level answers a different question and rests on a different trust assumption. Reading top-to-bottom, the trust required of the operator decreases; by Level 5 you trust only the Bitcoin blockchain, by Level 6 you trust only your own disk.

Level What it proves Trust required Effort
1Server's own verifier says the draw is intactTrust the operator's server10 seconds
2Published seed reproduces the published winnersSHA-256 + your Ruby interpreter5 minutes
3The full event history is visible to youTrust the operator's server1 minute
4Audit chain is internally hash-consistentSHA-25610 minutes
5Chain head is committed to the Bitcoin blockchainBitcoin20 minutes (one-time CLI setup)
6No past event has silently changed over timeYour own snapshot storageOngoing (cron)
7Long-run draw distribution is statistically uniformStatistics~30+ contests

Levels 2 and 7 require contests drawn on or after 2026-05-27, when the seeded-shuffle hardening landed (no seed was recorded before that date). Earlier contests show a Pre-hardening draw badge at Level 1 and can still progress through Levels 3–6 — the hash chain was backfilled across all prior audit events, so their history remains independently verifiable. See What this guide does not prove.

Fast path: If you have a specific contest in mind, the per-contest helper at https://pickawinner.pro/results/<token>/verify gathers every URL below into one auto-populated page for that contest. Levels 1, 2, 3, and 5 can be completed from there without copying any URL by hand.

Where everything lives

Every URL on this page is public, requires no authentication, and has no rate limit beyond ordinary abuse protection. Most are keyed by the contest's token (a random 24-character identifier); the audit-log JSON is additionally reachable at a slug-keyed twin (/facebook_page_contests/<slug>/audit-log.json) so a verifier who only has the operator's SEO share URL can reach the audit log without first opening the results page to recover the token. The two audit-log URLs return byte-identical JSON. To find the token, open any contest results page and click How to verify yourself — the URL you land on contains the token in the path.

Project-wide pages

Per-contest pages and machine-readable endpoints

URL pattern Returns Used in
/facebook_page_contests/<slug> SEO-friendly contest page (the URL operators typically share). Renders the verification badge inline. Level 1
/results/<token>/<slug> Canonical token-keyed contest page. Identical content; participates in HTTP 304 Not Modified. Level 1
/results/<token>/verify Per-contest verification helper: the badge, seed, hash, eligible-IDs link, audit-log link, latest Bitcoin-anchor proof link — all auto-populated for one contest. All levels (fast path)
/results/<token>/eligible-ids.txt The sorted canonical list of internal IDs of every comment that passed every filter — the exact input the seeded shuffle saw. One ID per line, plain text. Level 2
/results/<token>/audit-log.json Full hash-chained audit history (every event with previous_hash, entry_hash, metadata, timestamps) + a self-describing verification block + the anchors array. Sent with Cache-Control: no-store. Levels 3, 4, 5, 6
/facebook_page_contests/<slug>/audit-log.json Same JSON as the token-keyed URL above — byte-identical payload, identical no-store caching. Reachable by appending one segment to the slug URL the operator typically shares, so you don't need to open the results page first to recover the token. Levels 3, 4, 5, 6
/results/<token>/audit-anchors/<id>.ots Raw OpenTimestamps proof bytes for one anchor (binary). Feed to ots verify. Level 5

Pre-hardening contests (drawn before 2026-05-27) have no seed recorded; eligible-ids.txt returns 404 for those. The other endpoints still work — the audit log for a pre-hardening contest simply shows fewer events.

Level 1 — Check the badge (zero effort)

Open any contest's public results page (the URL looks like /results/<token>/<slug>, or the SEO equivalent at /facebook_page_contests/<slug>) and find the Independently verify this draw strip. The badge shows one of four states, computed server-side from the live database on each full response. (Cached responses revalidate against the contest record via HTTP 304, so a fresh visitor always gets a fresh check.)

  • Verified — the server's verifier confirmed the recorded seed reproduces the published winners and the audit chain is unbroken.
  • Redrawn since draw — the original draw is still verifiable, but one or more winners have been replaced via the audited redraw flow. Check the audit log (Level 3) to see what changed and why.
  • Tampering detected — the recorded seed does not reproduce the current winners, or the audit chain is broken. Critical signal — see If a check fails below.
  • Pre-hardening draw — contest drawn before 2026-05-27. No seed recorded; cannot be independently verified. Winners are real but not retroactively re-runnable.

Trust threshold: you trust the operator's server to honestly report the result of its own verifier. The remaining levels reduce that trust progressively to zero.

Level 2 — Re-run the draw yourself (the math check)

Every drawn contest publishes its full RNG transcript: a 256-bit random seed and the canonical list of eligible comment IDs. With those two inputs and five lines of Ruby, you can recompute the winner list yourself and confirm it matches what's on the page.

Prefer not to install anything? The browser-native verifier runs this exact recompute client-side — drop the eligible-IDs file, paste the seed, and read off the winners. No Ruby required; the page's JavaScript is a faithful port of Ruby's Mersenne Twister and Array#shuffle, so it produces identical results. The manual Ruby path below remains for anyone who wants to trust their own interpreter end-to-end.

Steps

  1. On the contest's /results/<token>/verify page, click Download eligible-ids.txt. Save the file.
  2. Copy the published Random seed and the SHA-256 of sorted eligible IDs.
  3. Open a Ruby interpreter (Ruby ≥ 3.0 — pre-installed on macOS and most Linux distros; on Windows, install via RubyInstaller). Run irb.
  4. Paste the snippet below, replace the placeholder, and check both the hash and the winners match.
require "digest"
seed = "PASTE_THE_PUBLISHED_SEED_HERE"
ids  = File.read("eligible-ids.txt").split("\n").map(&:to_i).sort

# (1) Sanity check the input: must equal the published SHA-256.
Digest::SHA256.hexdigest(ids.join("\n"))

# (2) Re-run the shuffle. First N items are winners 1..N (in order);
#     the next 3N items are reserves 1..3N.
ids.shuffle(random: Random.new(seed.to_i(16))).first(N_winners + N_reserves)

If both checks match, the draw is cryptographically confirmed. If either fails, see If a check fails.

Trust threshold: you trust your own Ruby interpreter and the standard-library implementations of SHA-256 and Array#shuffle. No remote service is involved beyond the initial file download. Cross-language reproduction is theoretically possible but requires re-implementing Ruby's specific Mersenne Twister seeding and rand(n) rejection rules — MRI Ruby ≥ 3.0 is the path of least resistance.

Level 3 — Read the audit log via the public API

Every administrative action on a contest — draws, redraws, cancellations, winner notifications — is recorded in an append-only, hash-chained audit log. The log is readable at two equivalent public URLs with no authentication; both return byte-identical JSON, so pick whichever you have in hand:

# Token-keyed (canonical):
curl https://pickawinner.pro/results/<token>/audit-log.json | jq

# Slug-keyed (reachable from the SEO results URL the operator typically shares):
curl https://pickawinner.pro/facebook_page_contests/<slug>/audit-log.json | jq

The response gives you:

  • chain_status — either the literal string "ok" or the exception message naming the offending event (e.g., "previous_hash mismatch at event #5...")
  • events — every audit event in chain order, each with action, metadata, previous_hash, entry_hash, timestamps
  • anchors — the OpenTimestamps proofs covering each chain head (used in Level 5)
  • verification — the canonical algorithm spec (so you can recompute hashes yourself in Level 4)

The endpoint returns Cache-Control: no-store — every request recomputes the chain status. A tamper is visible the moment a poller pulls.

Trust threshold: same as Level 1 — you're still trusting the server's verifier. But you can now see the events instead of taking the badge's word for it, and you can save the JSON for use in Levels 4, 5, and 6.

Level 4 — Recompute the chain hashes yourself

The audit-log JSON exposes every field that contributes to each event's hash. You can rebuild the canonical payload from the published fields and recompute the chain end-to-end. If your computed hashes match the published ones, the chain is internally consistent regardless of what the server's chain_status field claims.

require "digest"
require "json"
require "net/http"

token = "PASTE_CONTEST_TOKEN"
url   = URI("https://pickawinner.pro/results/#{token}/audit-log.json")
data  = JSON.parse(Net::HTTP.get(url))

prev = data["verification"]["genesis_hash"]
data["events"].each do |e|
  payload = {
    "previous_hash" => prev,
    "action" => e["action"],
    "facebook_page_contest_id" => e["facebook_page_contest_id"],
    "user_id" => e["user_id"],
    "metadata" => e["metadata"],
    "created_at" => e["created_at"]
  }
  computed = Digest::SHA256.hexdigest(JSON.generate(payload))
  raise "previous_hash mismatch at event #{e["id"]}" unless prev == e["previous_hash"]
  raise "entry_hash mismatch at event #{e["id"]}"    unless computed == e["entry_hash"]
  prev = e["entry_hash"]
end
puts "OK — #{data["events"].size} events, chain verified."

Trust threshold: you trust SHA-256 and your own Ruby. This is the threshold most cryptographic protocols ultimately rest on. A passing check at this level confirms the chain is mathematically consistent with the published fields — but does not rule out the case where someone with database access has rewritten every record AND recomputed every downstream hash in a self-consistent way. Level 5 and Level 6 close that gap.

Level 5 — Verify the Bitcoin anchor

Levels 1–4 confirm internal consistency. They do not rule out a sophisticated attacker who rewrites every record AND recomputes every downstream hash to keep the chain internally consistent. To catch that class of attack we anchor every contest's chain head to the Bitcoin blockchain via OpenTimestamps. Once an anchor has been confirmed in a Bitcoin block (typically several hours after submission, sometimes up to a day), the chain head it covers cannot be moved — the Bitcoin blockchain is not under the operator's control.

Recommended path: one Ruby script

The audit-log JSON's verification block tells you exactly how the digest input is constructed. This script pulls the JSON, picks the most recent Bitcoin-confirmed anchor, rebuilds the digest, downloads the proof, and prints the ots verify command to run:

require "digest"
require "json"
require "net/http"

token = "PASTE_CONTEST_TOKEN"
host  = "https://pickawinner.pro"
data  = JSON.parse(Net::HTTP.get(URI("#{host}/results/#{token}/audit-log.json")))

# Pick the most recent Bitcoin-confirmed (upgraded) anchor.
anchor = data["anchors"].find { |a| a["block_height"] }
abort "No Bitcoin-confirmed anchor yet — pending anchors typically upgrade within a few hours." unless anchor

# Rebuild the exact digest the system submitted to OpenTimestamps.
namespace = data["verification"]["anchor_digest_namespace"]
payload   = "#{namespace}\n#{data["contest_id"]}\n#{anchor["anchored_entry_hash"]}"
digest    = Digest::SHA256.digest(payload)
File.binwrite("digest.bin", digest)

# Download the raw proof bytes.
File.binwrite("anchor.ots", Net::HTTP.get(URI("#{host}#{anchor["proof_url"]}")))

puts "Saved digest.bin (32 bytes) and anchor.ots."
puts "Block height: #{anchor["block_height"]}"
puts "Now run: ots verify -d digest.bin anchor.ots"

Install the OpenTimestamps CLI one-time: pip install opentimestamps-client (Python) or npm install -g opentimestamps (Node). Then run the command the script printed. A successful verification looks like:

Success! Bitcoin block 947,182 attests existence as of 2026-05-27 14:12:08 UTC

Shell-only alternative

If you prefer to keep everything in the terminal:

TOKEN="PASTE_CONTEST_TOKEN"
JSON=$(curl -s "https://pickawinner.pro/results/$TOKEN/audit-log.json")

CONTEST_ID=$(echo "$JSON"  | jq -r '.contest_id')
ANCHOR_ID=$(echo "$JSON"   | jq -r '[.anchors[] | select(.block_height)][0].id')
ANCHOR_HASH=$(echo "$JSON" | jq -r --argjson id $ANCHOR_ID '.anchors[] | select(.id == $id) | .anchored_entry_hash')

printf 'pickawinner.audit.v1\n%s\n%s' "$CONTEST_ID" "$ANCHOR_HASH" \
  | shasum -a 256 | awk '{print $1}' | xxd -r -p > digest.bin

curl -s -o anchor.ots "https://pickawinner.pro/results/$TOKEN/audit-anchors/$ANCHOR_ID.ots"
ots verify -d digest.bin anchor.ots

Trust threshold: you trust SHA-256 and the Bitcoin blockchain. This is the strongest external trust anchor in the guide; for any event whose anchor has been upgraded (block_height present), it closes the PostgreSQL-superuser scenario named in the fairness analysis. Anchors still inside the confirmation window (typically several hours after submission) appear with block_height: null — Bitcoin hasn't confirmed yet, so trust shifts entirely to Bitcoin only after upgrade.

Level 6 — Monitor over time (detect retroactive rewrites)

Levels 4 and 5 catch a chain that's broken or anchored-then-altered. They do not catch a sophisticated rewrite within the 6-hour pending-anchor window where someone with database access rewrites both the events AND the calendar submission. To catch that — and any other retroactive change you might want to detect — you need your own snapshots from before the rewrite.

The pattern: pull the audit log on a schedule, save each pull as a timestamped file, and diff successive pulls. Any change to a past event (not just an appended new one) is evidence of retroactive modification.

#!/usr/bin/env bash
# Save a snapshot every run; alert if any past event has changed.

TOKEN="PASTE_CONTEST_TOKEN"
URL="https://pickawinner.pro/results/$TOKEN/audit-log.json"
DIR="$HOME/.pickawinner-audit/$TOKEN"
mkdir -p "$DIR"

now=$(date -u +%Y%m%dT%H%M%SZ)
curl -sf "$URL" -o "$DIR/$now.json" || { echo "fetch failed"; exit 2; }

# Compare past events between the latest snapshot and the one before.
prev=$(ls -1 "$DIR" | sort | tail -2 | head -1)
[ "$prev" = "$now.json" ] || ruby -rjson -e '
  a = JSON.parse(File.read(ARGV[0]))["events"].to_h { |e| [e["id"], e] }
  b = JSON.parse(File.read(ARGV[1]))["events"]
  changed = b.select { |e| a[e["id"]] && a[e["id"]] != e }
  if changed.any?
    puts "TAMPER: #{changed.size} past event(s) changed between snapshots"
    changed.each { |e| puts "  event #{e["id"]} action=#{e["action"]}" }
    exit 1
  end
  puts "ok: no past events changed"
' "$DIR/$prev" "$DIR/$now.json"

Run from cron every 15–60 minutes per contest you care about. Mail or page yourself on non-zero exit.

Trust threshold: you trust your own snapshot storage. This is the strongest practically-deployable check — it defeats even an attacker who has full database control, because the historical record they're trying to rewrite is already on your disk.

Level 7 — Statistical sanity across many contests

For academic auditors or organizations monitoring an operator over time, three statistical properties should hold for a uniformly-random draw:

  • Seed distribution. Concatenate the draw_seed values from many contests; the bits should be statistically indistinguishable from uniform. Standard randomness test batteries (NIST STS, dieharder) apply.
  • Winner-position distribution. Within each contest, the winner's position in the sorted eligible-IDs list, normalized by pool size, should be uniform on [0, 1). Across many contests, run a Kolmogorov–Smirnov test against the uniform distribution.
  • No covert correlation. The strongest possible test: gather an external attribute about each winner (account age, follower count, brand affinity) and check for non-zero correlation with their normalized winner position. A meaningful correlation in any direction is a signal worth investigating.

Trust threshold: you trust statistics, and you've collected enough draws (typically ≥ 30) for tests to have meaningful power. This is the layer that detects systematic bias the cryptographic checks cannot — for example, a hypothetical operator who manipulated the input list before hashing it.

If a check fails: what to do

Each level's failure has a specific meaning. The table below maps what you saw to what it means and what to do next. For any failure, the most important thing is to preserve the evidence — your snapshot, your computed values, the raw responses you received. The cryptographic checks are designed so the evidence speaks for itself; you do not need to convince the operator, only to retain a record they cannot rewrite.

If you saw What it means Next step
L1 badge says Tampering detected The server's own verifier disagrees with the published winners. Re-run Level 2 yourself. If your hash matches but the shuffle doesn't, the published winners have been altered.
L2 hash mismatch The eligible-IDs file you downloaded does not hash to the published value — either the file or the published hash has been tampered with. Save a copy of the file and the hash. Re-download both from a different network 24 hours later; if either changes, you have a record of the change.
L2 shuffle mismatch (hash OK, winners wrong) The seed produces a different winner list than the page shows — the strongest single-check signal of post-draw modification. Save the seed, the IDs file, and the current winners list. Contact the operator; if no resolution, contact the platform (Facebook/Instagram) with your saved evidence.
L3 / L4 chain_status"ok" The audit chain has a broken hash link — either an event was modified or one was inserted out of order. The chain_status message names the offending event id. Save the full audit-log.json response. The exception message identifies which event broke the chain and how.
L5 ots verify reports digest mismatch The chain head you computed the digest from does not match what was anchored to Bitcoin. Either the chain has changed since the anchor was created, or the proof file has been swapped. Re-download the proof from the published URL. If it still fails, you have cryptographic evidence — the Bitcoin-confirmed proof — that the operator's current chain state differs from the anchored historical state.
L6 snapshot diff shows a past event changed The audit log's content for an event you previously snapshotted is now different — a retroactive rewrite has occurred. Your prior snapshot is the evidence. Save both snapshots and the diff output side-by-side.
Contest page returns 404 The contest record has been deleted. Audit events and chain anchors cascade with it (see Fairness Analysis §8.3 and §12.6). Any prior snapshots you took remain valid evidence; Bitcoin-confirmed .ots proofs you downloaded earlier still verify against the blockchain regardless of the deletion.

What this guide does not prove

Every honest verification framework names the threats it does not defeat. Three remain after Levels 1–7:

  • Pre-hardening contests (drawn before 2026-05-27) have no recorded seed and cannot be retroactively re-run. Level 1 returns "Pre-hardening draw"; Levels 2 and 7 do not apply (no seed, no eligible-IDs list). Levels 3–6 still apply — the hash chain was backfilled across all prior audit events — so the chain history of these contests remains independently verifiable. Winners are real but not reproducible.
  • The ~6-hour pending-anchor window. An audit event newer than its Bitcoin confirmation is structurally vulnerable to a determined PostgreSQL superuser who could rewrite both the event and the calendar submission. Once Bitcoin confirms (typically several hours after submission, occasionally up to a day), this window closes for that event. Level 6 (snapshot monitoring) covers the pending window; Level 5 covers everything past it.
  • Source data integrity. The Graph API response from Facebook/Instagram is not cryptographically signed by Meta. The system can prove what it drew from (every byte goes into the eligible-IDs file) but cannot independently prove that input matched what was publicly posted. The Facebook/Instagram post itself remains the source of truth for the comment list; comparing it against eligible-ids.txt is the practical check, and any contestant can do it.

See §12 of the Fairness Analysis for the formal framing of each limitation.

Glossary

Term Meaning
SeedA 256-bit random integer used to initialize the shuffle. Recorded once per draw; printed as 64 hexadecimal characters.
SHA-256A cryptographic hash function. Maps any input bytes to a fixed 256-bit output. Standardized by NIST; the de-facto industry-standard hash.
Eligible IDsThe internal database IDs of comments that passed every filter and were considered for the draw. Sorted ascending for canonicalization, then hashed.
Fisher–Yates shuffleA standard algorithm (used by Ruby's Array#shuffle) that produces a uniformly random permutation of an array. Deterministic given a fixed RNG.
Entry hashThe SHA-256 of an audit event's canonical payload, including the predecessor's entry hash. This is what chains events together.
Previous hashThe entry hash of the immediately prior audit event in the same contest's chain. The first event's previous hash is the genesis hash.
Genesis hashA fixed all-zero 256-bit value ("0" * 64) that anchors the start of every contest's chain.
Canonical payloadA byte-stable serialization of an audit event — sorted JSON keys, UTC microsecond timestamps, fixed field order. Required so SHA-256 produces the same hash everywhere.
Chain statusThe result of walking the chain and recomputing every hash — either "ok" or a specific exception message naming the broken event.
RedrawReplacement of a primary winner with the next reserve, triggered by an operator with a written reason. Audited; visible in the chain; flagged with a "Redrawn" badge.
OpenTimestamps (OTS)An open protocol that anchors arbitrary 256-bit digests to the Bitcoin blockchain. Free to use, no fees, no account. Pick a Winner uses it to anchor every contest's audit-chain head. See opentimestamps.org.
Calendar serverA free public OpenTimestamps server that collects pending digests, builds a Merkle tree, and submits the root to Bitcoin. The .ots proof file we publish embeds the calendar server's commitment plus (after Bitcoin confirms) the Merkle path to the Bitcoin block.
Chain anchorA row in our chain_anchors table linking a contest's audit-chain head to an OpenTimestamps proof. Each anchor's proof bytes are downloadable; running ots verify against them confirms the chain head existed at a specific Bitcoin block time.
Anchor digestSHA-256 of "pickawinner.audit.v1\n" + contest_id + "\n" + entry_hash. The 32-byte input submitted to OpenTimestamps. The namespace prefix prevents replay of our anchors as proofs of unrelated content.
Block heightThe position of a confirmed block in the Bitcoin blockchain. An anchor's block_height appears in the audit-log JSON once the proof has been upgraded (Bitcoin-confirmed). Pending anchors show null here.
Confirmation windowThe interval between an anchor's submission to OpenTimestamps and its Bitcoin confirmation. Typically several hours, occasionally up to a day. Events within this window are not yet Bitcoin-anchored — see What this guide does not prove.
TokenThe 24-character random identifier in each contest's URL (/results/<token>/...). Sufficient to construct every API URL on this page; eligible-ids.txt and audit-anchors/<id>.ots are token-only, while audit-log.json is additionally mounted under the slug (see Where everything lives).