Integration DocsSpec v1

把 Provably Fair
接入你的平台。

Oripa、一番賞、抽卡、盲盒、卡牌合成等十三種玩法都適用。平台在每一筆紀錄公開 Server Seed、Client Seed、Nonce 和 Odds,玩家把資料貼到驗證頁,就能自己核對每一抽。

概覽

JACKZ.AI 公平驗證是一個獨立的驗證頁。平台在每一筆抽卡紀錄公開驗證需要的資料,玩家自己把資料貼到驗證頁,頁面會在瀏覽器裡重算那一抽,算出中還是不中。由玩家自己輸入、自己按 Verify,結果才由玩家親手核對。

平台不用安裝 SDK,也不用開 API。計算全部在玩家的瀏覽器完成,資料不會送到伺服器。想讓玩家少打字,可以另外做一鍵連結(見「驗證連結 v1」),但這不是必須的。

Commit抽之前公開 Server Seed Hash
Roll每一抽記下 Client Seed、Nonce 和結果
Reveal換種子時公開舊的 Server Seed
Verify玩家把資料貼到驗證頁

名詞

文件和驗證頁都用以下英文名詞。

Server Seed
平台產生的隨機字串。建議用 32 個位元組的密碼學隨機數,以十六進制表示。換種子之前保密。
Server Seed Hash
SHA-256(Server Seed) 的十六進制小寫字串,在抽之前公開。
Client Seed
玩家的種子,由玩家設定或平台隨機產生,區分大小寫。
Nonce
同一組種子下的抽卡序號,十進制整數,每抽一次加一。
Draw
同一個 Nonce 裡的第幾抽,由 0 開始。十連抽可以共用一個 Nonce,用 Draw 0 至 9 區分。
Roll
由以上資料算出的 32 位元無號整數,範圍是 0 至 4,294,967,295。
Odds(weights)
Pick 計法裡各獎項的整數權重。一番賞用抽這一張時各賞剩下的張數。
Threshold
Threshold 計法的成功區上限,Roll 小於它就成功。
Seed Rotation
換種子:公開舊的 Server Seed,同時公開新種子的 Hash,之後的抽卡改用新種子。
House Edge
平台抽成,每部機各自設定,可以隨時調整。以小數表示,例如 8% 寫 0.08,沒有抽成寫 0。卡牌合成用它計算成功率;只在選用的驗證連結裡傳給驗證頁,玩家不用填。

接入步驟

以下七步全部做到,玩家才可以獨立核對每一抽。

  1. 抽之前公開 Server Seed Hash產生 Server Seed,在玩家抽卡之前顯示它的 Hash。可以的話同時寫上鏈或存證,之後不可更改。
  2. 讓玩家設定 Client Seed預設可以隨機產生,但玩家要能自己修改。玩家改 Client Seed 時,建議同時換一組新的 Server Seed。
  3. 每一抽用下一個 Nonce每抽一次 Nonce 加一。不可重用、不可跳號,也不可因為結果而重抽;每一個 Nonce 都要有紀錄。
  4. 抽之前公開 OddsPick 計法公開各獎項權重,一番賞公開各賞剩餘張數;Threshold 計法公開成功率或計算方法。
  5. 保存每一抽的紀錄client_seed、nonce、draw、weights、ranges 或 threshold、roll、result,連同紀錄編號一起保存。
  6. Seed Rotation 時公開舊種子玩家按「換種子」或種子到期時,公開舊的 Server Seed,並開始使用新的種子。
  7. 在紀錄公開驗證資料每一筆紀錄顯示 Server Seed Hash、Server Seed(換種子後)、Client Seed、Nonce、Draw 和這一抽的 Odds 或成功率,每項最好可以一鍵複製;旁邊放一條連到驗證頁的普通連結,不用帶資料。

算法

所有計法都先算出 Roll,再按計法把 Roll 變成結果。全程用整數運算,不經浮點數。

Roll

Roll derivationHMAC-SHA256
block = HMAC-SHA256(key = server_seed, message = "{client_seed}:{nonce}:{floor(draw / 8)}")
roll  = block[4 × (draw mod 8) .. +4] 讀成大端(big-endian)無號 32 位元整數
Live · server_seed = 測試向量改數字即時重算
message block roll
  • server_seed 按 UTF-8 文字直接當密鑰,不要先由十六進制轉成位元組。
  • 冒號是半形;nonce 和 floor(draw / 8) 用十進制,不補 0。
  • 每個 HMAC 區塊有 32 個位元組,可以給 8 個 Draw 用。Draw 8 至 15 用第二個區塊,如此類推。

Threshold:成功/失敗(卡牌合成)

Threshold modeCard Upgrade
success   = roll < threshold
成功率     = threshold ÷ 4,294,967,296
threshold = floor(投入卡市價合計 × (1 − house_edge) × 2^32 ÷ 目標卡市價)
  • 市價用十進制字串,以分數運算,只在最後取整一次。投入卡和目標卡要用同一種幣別。
  • threshold 大於或等於 4,294,967,296 時一定成功。
  • 平台用其他合成公式的話,只公開 threshold;市價參數只用於選用的驗證連結。
  • 直接公佈成功率的玩法(例如卡牌強化),threshold = floor(成功率 × 2^32),同樣以分數運算。例如成功率 0.35,threshold 是 1,503,238,553。

Pick:抽獎品(Oripa、一番賞、抽卡、盲盒)

Pick modeWeighted
W      = 所有權重的總和
ticket = floor(roll × W ÷ 4,294,967,296)
result = 由第 0 項起累加權重,第一個累計值大於 ticket 的獎項編號

Pick(機率區間)

獎品表用小數區間表示機率的平台,用這個寫法。驗證頁的玩法選「抽獎品(按機率區間)」。

Pick modeRanges
f      = round(roll ÷ 4,294,967,296, 12),再 round(f, 6)
result = 第一個 begin ≤ f ≤ end 的區間編號(由 0 開始,begin 和 end 都包括在內)
  • 四捨五入都是「五入」:第 13 位是 5 或以上就進位,6 位時同理。
  • 區間用小數,最多 6 位,例如 0.000000-0.000999,0.001000-0.019999。
  • 平台用浮點數計算時,12 位小數剛好落在 5 的極少數 Roll 可能差一位;用整數或十進制計算可以避免。

遊戲種類對應

遊戲種類計法做法
Oripa オリパPick按公開的 Odds 抽出卡牌。
一番賞 Ichiban KujiPick用各賞剩餘張數做權重,每抽一張就更新。
抽卡 GachaPick按稀有度 Odds 抽出角色或卡牌。
盲盒 Blind BoxPick按款式 Odds 決定開出哪一款。
扭蛋 GashaponPick每粒扭蛋按款式 Odds 抽出。
福袋 Lucky BagPick按檔次 Odds 決定福袋內容。
神秘箱 Mystery BoxPick按公開的獎品表抽出內容。
開包 Pack OpeningPick每個卡位各用一個 Draw,按稀有度 Odds(權重或機率區間)抽出。分層的卡包每一層公開一組 weights、labels 和 Draw,用同一個 Nonce,Draw 按「卡位 → 層」順序連續編號(例如 5 個卡位、每位 2 層,用 Draw 0 至 9)。第一層的 weights 是各級別的權重;下一層的 weights 和 labels 是上一層抽中級別內的卡。
拆盒分配 Box BreakPick隨機分配隊伍或卡位。下一抽的 weights 和 labels 去掉已抽中的項目。
抽選 RafflePick每個登記者權重相同,抽出購買資格。抽多個名額時,同樣去掉已抽中的人。
抽獎活動 Lucky DrawPick活動獎品按名額和權重抽出。
卡牌合成 Card UpgradeThresholdRoll 小於平台紀錄的 Threshold 就成功;成功率由平台訂定並公開。
卡牌強化 EnhancementThreshold按公開的成功率決定強化成敗。
  • roll × W 可能超過 2^53,JavaScript 要用 BigInt。
  • 每個 ticket 對應的 Roll 數量最多相差 1。W 不超過 1,000,000 時,每項機率的相對誤差小於 0.03%。
  • 一番賞每抽一張,權重就換成新的剩餘張數;紀錄要公開抽那一張時的權重。

程式範例

三個版本的算法相同,都只用語言內建的函數,並已用下面的測試向量核對。PHP 版的卡牌合成 threshold 用 bcmath 擴展。

// JACKZ.AI Fair Verify v1:Node.js 18 以上,只用內建模組
import { createHash, createHmac } from "node:crypto";

const SPACE = 2n ** 32n;

// 抽之前公開:SHA-256(Server Seed),十六進制小寫
export function seedHash(serverSeed) {
  return createHash("sha256").update(serverSeed, "utf8").digest("hex");
}

// 第 draw 抽的 Roll:0 至 4,294,967,295
export function roll(serverSeed, clientSeed, nonce, draw = 0) {
  const block = createHmac("sha256", serverSeed)
    .update(`${clientSeed}:${nonce}:${Math.floor(draw / 8)}`)
    .digest();
  return BigInt(block.readUInt32BE((draw % 8) * 4));
}

// Threshold:Roll 小於 threshold 就成功
export function isSuccess(r, threshold) {
  return r < BigInt(threshold);
}

// 卡牌合成的 threshold;金額用十進制字串,避免浮點誤差
export function upgradeThreshold(inputValue, targetPrice, edgeRate) {
  if (!/^0(\.\d+)?$/.test(edgeRate ?? "")) throw new Error("edge_rate 必填,要是 0 至小於 1 的小數;沒有抽成填 0");
  if (![inputValue, targetPrice].every((s) => /^\d+(\.\d+)?$/.test(s ?? ""))) throw new Error("市價要用十進制數字");
  const frac = (s) => {
    const [i, f = ""] = String(s).split(".");
    return [BigInt(i + f), 10n ** BigInt(f.length)];
  };
  const [a, ad] = frac(inputValue), [t, td] = frac(targetPrice), [e, ed] = frac(edgeRate);
  return (a * (ed - e) * SPACE * td) / (ad * ed * t);
}

// Pick:weights 是各獎項的整數權重(一番賞填各賞剩餘張數),回傳獎項編號
export function pick(r, weights) {
  const w = weights.map(BigInt);
  const ticket = (r * w.reduce((x, y) => x + y, 0n)) / SPACE;
  let cum = 0n;
  for (let i = 0; i < w.length; i++) {
    cum += w[i];
    if (ticket < cum) return i;
  }
  throw new Error("weights 的總和必須大於 0");
}

// 一鍵驗證連結(Link Format v1)
export function verifyLink(base, params) {
  return `${base}#${new URLSearchParams({ v: "1", ...params })}`;
}

測試向量

接入前,用以下數字核對你的實作。全部數字都可以在驗證頁重算出來。

server_seedd790b1978d538dea96a16942291c4efa2d5e631ca48db984c1f2e9e7b738b434
server_seed_hashcae64244e12924e26d2eb5e9dd90820650d6cb377944a13eee82c2df31d56f93
client_seedMySeed2026

Roll

noncedrawroll
102079044362
50441912824
184185532748
192271708478

Threshold

input_valuetarget_market_priceedge_ratethreshold
401000.051632087572
30.00100.000.051224065679
50950.052147483648
401000.081580547964
41.80108.5001654650995
40100(缺)拒絕驗證
401001拒絕驗證

Pick

rollweightsresult
4419128241,4,20,752
20790443621,4,20,753

Pick(機率區間)

rollf(6 位)rangesresult
11631909750.2708270.000000-0.270826, 0.270827-1.0000001
11631909750.2708270.000000-0.270827, 0.270828-1.0000000
21668110.0005050.000000-0.000504, 0.000505-1.0000001

驗證範圍

驗證頁核對的是「結果由承諾的種子算出」。以下列明它能證明和不能單獨證明的事,平台公開說明時請照實寫。

驗證可以證明

  • 這一抽由輸入的 Server Seed、玩家的 Client Seed 和 Nonce 算出,而這組 Server Seed 符合輸入的 Server Seed Hash。
  • 平台紀錄的 Roll 和結果,和重算一致。
  • 附上市價時,卡牌合成的成功率和市價、House Edge 算出來的一致。

需要另外核對

  • Odds 是否和抽之前公開的一致:權重由平台提供,玩家要對照抽之前看到的 Odds。
  • 有沒有跳號或重抽:用 Batch Verify 重算連續的 Nonce,再和平台的抽卡紀錄逐筆對照。
  • 輸入的 Server Seed Hash,是否就是玩家抽之前看到的那一個。
  • 市價本身是否合理:市價和 House Edge 由平台提供,驗證頁只核對用這些數字算出來的成功率。
  • 一番賞剩餘張數是否真實,以及獎品有沒有交付。

版本

連結裡的 v 是格式版本,現行版本是 1。

2026-10-24 · v1Pick(權重和機率區間)和 Threshold 兩種計法、玩家自行輸入驗證、Batch Verify,以及選用的一鍵驗證連結。