EnvironmentProd

Sports

icon

Marketplace

icon

Rewards

back

Provably Fair

Verify every game outcome with full transparency

shield

Fairness Verification

Enter the details below to verify your game result. You can find these values in your game history or by clicking the Fairness button in any game.

This is a passphrase or random string set by the player or browser, which can be changed anytime.

To reveal the hashed server seed, the player must rotate it, replacing it with a new seed. This allows verification of any bets made with the previous seed using the provided encrypted hash.

code iconCode

After entering the information above and clicking Apply, proceed to the next section.

package main

import (
	"crypto/sha256"
	"encoding/binary"
	"encoding/json"
	"errors"
	"strconv"
	"fmt"
)

const CASE_ROLL_TICKETS_NUM uint64 = 1000000

type CaseItem struct {
	ItemID         int64   `json:"item_id"`
	ItemName       string  `json:"item_name"`          // not required for rolling
	MarketHashName string  `json:"market_hash_name"`   // not required for rolling
	ItemCategory   string  `json:"item_cat,omitempty"` // not required for rolling
	Color          string  `json:"color,omitempty"`    // not required for rolling
	MinRand        uint32  `json:"min_rand"`
	MaxRand        uint32  `json:"max_rand"`
	Price          float64 `json:"price"`
	ImageURL       string  `json:"image_url,omitempty"` // not required for rolling
}

func getWinningTicket(serverSeed, externalSeed string, nonce int) (uint32, uint64) {
	seed := serverSeed + externalSeed + "#" + strconv.FormatInt(int64(nonce), 10)
	hash := sha256.Sum256([]byte(seed))
	rolledUInt64 := binary.LittleEndian.Uint64(hash[:])
	return uint32(rolledUInt64%CASE_ROLL_TICKETS_NUM) + 1, rolledUInt64
}

func rollItem(serverSeed, externalSeed string, nonce int, itemsMap map[int64]*CaseItem) (*CaseItem, uint32, uint64, error) {
	rolledTicket, rolledUInt64 := getWinningTicket(serverSeed, externalSeed, nonce)
	if itemsMap == nil {
		return nil, rolledTicket, rolledUInt64, nil
	}
	for _, item := range itemsMap {
		if item.MinRand <= rolledTicket && rolledTicket <= item.MaxRand {
			return item, rolledTicket, rolledUInt64, nil
		}
	}
	return nil, rolledTicket, rolledUInt64, errors.New("rollItem: case item not found")
}

func GetDroppedItems(serverSeed, clientSeed string, nonce int, caseItems string) (*CaseItem, uint32, error) {
	if len(serverSeed) != 64 {
		return nil, 0, errors.New("error: serverSeed must have length 64")
	}
	var itemsMap map[int64]*CaseItem
	if len(caseItems) > 0 {
		if err := json.Unmarshal([]byte(caseItems), &itemsMap); err != nil {
			return nil, 0, err
		}
	}
	caseItem, winningTicket, _, err := rollItem(serverSeed, clientSeed, nonce, itemsMap)
	return caseItem, winningTicket, err
}

func main() {
	serverSeed := ""
	clientSeed := ""
	nonce := 0
	caseItems := ``

	caseItem, winningTicket, err := GetDroppedItems(serverSeed, clientSeed, nonce, caseItems)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("ticket:", winningTicket)
	if len(caseItems) > 0 {
		fmt.Println("item:", caseItem.ItemName)
		fmt.Printf("price: %.2f", caseItem.Price)
	}
}
package main

import (
	"crypto/sha256"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"strings"
)

type MinesGamePublic struct {
	Width               int    `json:"width"`
	Height              int    `json:"height"`
	MinesCount          int    `json:"mines"`
	ClientSeed          string `json:"clientSeed"`
	Nonce               int    `json:"nonce"`
	ProvablyFairVersion int    `json:"provablyFairVersion"`
	ServerSeedPublic    string `json:"serverSeed,omitempty"`
	serverSeed          string
	field               []byte
}

// returns the index of the new mine, when enumerating all remaining tiles
func getRandomMine(game *MinesGamePublic, remainingTiles uint64) int {
	maxValidRoll := uint64(math.MaxUint64)
	extraVals := (maxValidRoll % remainingTiles) + 1
	if extraVals != remainingTiles {
		maxValidRoll -= extraVals
	}
	for attempt := 0; ; attempt++ {
		seed := game.serverSeed + game.ClientSeed + fmt.Sprintf("#%d#%d#%d", game.Nonce, remainingTiles, attempt)
		if game.ProvablyFairVersion > 0 {
			seed = "mines" + seed
		}
		hash := sha256.Sum256([]byte(seed))
		rolledUInt64 := binary.LittleEndian.Uint64(hash[:])
		if rolledUInt64 <= maxValidRoll {
			return int(rolledUInt64 % remainingTiles)
		}
	}
}

func populateField(game *MinesGamePublic) {
	tiles := make([]int, game.Width*game.Height)
	for i := range tiles {
		tiles[i] = i
	}
	for mine := 0; mine < game.MinesCount; mine++ {
		// select random remaining tile and place it at index `mine` in list of tiles:
		mineOffset := getRandomMine(game, uint64(game.Width*game.Height-mine))
		tiles[mine], tiles[mine+mineOffset] = tiles[mine+mineOffset], tiles[mine]
	}

	game.field = []byte(strings.Repeat("*", game.Width*game.Height))
	// use first `game.MinesCount` tiles in list as mine positions:
	for _, minePos := range tiles[:game.MinesCount] {
		game.field[minePos] = 'x'
	}
}

func isValidFieldConfig(width, height, minesCount int) error {
	if width <= 0 || height <= 0 || width > 100 || height > 100 {
		return errors.New("invalid width/height")
	}
	if minesCount < 1 || minesCount >= width*height {
		return errors.New("invalid mines count")
	}
	return nil
}

type tile struct {
	row    int
	column int
}

func initFromJSONandGetMines(gameJSON string) (mines []tile, err error) {
	var game MinesGamePublic
	if err = json.Unmarshal(json.RawMessage(gameJSON), &game); err != nil {
		return nil, err
	}
	if err = isValidFieldConfig(game.Width, game.Height, game.MinesCount); err != nil {
		return nil, err
	}

	game.serverSeed = game.ServerSeedPublic
	if len(game.serverSeed) != 64 {
		return nil, errors.New("server seed must be specified an have length 64")
	}
	populateField(&game)

	for i, t := range game.field {
		if t == 'x' {
			mines = append(mines, tile{i / game.Width, i % game.Width})
		}
	}
	return
}

func main() {
	gameJSON := "{\"serverSeed\":\"\",\"clientSeed\":\"\",\"nonce\":0,\"width\":5,\"height\":5,\"mines\":3,\"provablyFairVersion\":0}"

	mines, err := initFromJSONandGetMines(gameJSON)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Mine positions:")
	for _, m := range mines {
		fmt.Printf("Row: %d, Column: %d\n", m.row, m.column)
	}
}
package main

import (
	"crypto/sha256"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"slices"
)

const (
	totalTiles = 40
	drawnTiles = 10
)

type KenoGamePublic struct {
	ClientSeed          string `json:"clientSeed"`
	Nonce               int    `json:"nonce"`
	ProvablyFairVersion int    `json:"provablyFairVersion"`
	ServerSeed          string `json:"serverSeed,omitempty"`
	serverSeed          string
}

func getRandomTile(tileIndex int, game *KenoGamePublic) int {
	remainingTiles := uint64(totalTiles - tileIndex)
	maxValidRoll := uint64(math.MaxUint64)
	extraVals := (maxValidRoll % remainingTiles) + 1
	if extraVals != remainingTiles {
		maxValidRoll -= extraVals
	}
	for attempt := 0; ; attempt++ {
		seed := game.serverSeed + game.ClientSeed + fmt.Sprintf("#%d#%d#%d", game.Nonce, tileIndex, attempt)
		if game.ProvablyFairVersion > 0 {
			seed = "keno" + seed
		}
		hash := sha256.Sum256([]byte(seed))
		rolledUInt64 := binary.LittleEndian.Uint64(hash[:])
		if rolledUInt64 <= maxValidRoll {
			return int(rolledUInt64 % remainingTiles)
		}
	}
}

func getTiles(game *KenoGamePublic) []int {
	tiles := make([]int, totalTiles)
	for i := range tiles {
		tiles[i] = i
	}
	for tileIndex := 0; tileIndex < drawnTiles; tileIndex++ {
		tileOffset := getRandomTile(tileIndex, game)
		tiles[tileIndex], tiles[tileIndex+tileOffset] = tiles[tileIndex+tileOffset], tiles[tileIndex]
	}
	slices.Sort(tiles[:drawnTiles])
	return tiles[:drawnTiles]
}

func initFromJSONandGetTiles(gameJSON string) ([]int, error) {
	var game KenoGamePublic
	if err := json.Unmarshal(json.RawMessage(gameJSON), &game); err != nil {
		return nil, err
	}
	game.serverSeed = game.ServerSeed
	if len(game.serverSeed) != 64 {
		return nil, errors.New("server seed must be specified and have length 64")
	}
	return getTiles(&game), nil
}

func main() {
	gameJSON := "{\"serverSeed\":\"\",\"clientSeed\":\"\",\"nonce\":0,\"provablyFairVersion\":0}"

	tiles, err := initFromJSONandGetTiles(gameJSON)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Printf("Server tiles: %v\n", tiles)
}
package main

import (
	"crypto/sha256"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"slices"
	"strconv"
	"strings"
)

const battleProvablyFairVersion = 2

type CaseItem struct {
	ItemID         int64   `json:"item_id"`
	ItemName       string  `json:"item_name"`          // not required for rolling
	MarketHashName string  `json:"market_hash_name"`   // not required for rolling
	ItemCategory   string  `json:"item_cat,omitempty"` // not required for rolling
	Color          string  `json:"color,omitempty"`    // not required for rolling
	MinRand        uint32  `json:"min_rand"`
	MaxRand        uint32  `json:"max_rand"`
	Price          float64 `json:"price"`
	ImageURL       string  `json:"image_url,omitempty"` // not required for rolling
}

type Case struct {
	ID            int64               `json:"id"`              // not required for rolling
	Name          string              `json:"name"`            // not required for rolling
	Type          string              `json:"type"`            // not required for rolling
	Color         string              `json:"color,omitempty"` // not required for rolling
	Price         float64             `json:"price"`
	PublishStatus bool                `json:"publish_status"`       // not required for rolling
	RiskLevel     *string             `json:"risk_level,omitempty"` // not required for rolling
	Level         *int                `json:"level,omitempty"`      // not required for rolling
	ImageURL      *string             `json:"image_url,omitempty"`  // not required for rolling
	Items         map[int64]*CaseItem `json:"items"`
}

type BattleStep struct {
	Slot           string  `json:"slot"`
	ItemID         int64   `json:"itemId"`
	Price          float64 `json:"price"`
	JackpotCurrent float64 `json:"jackpotCurrent"`
}

type WinnerSummary struct {
	Slots       []string `json:"Slots,omitempty"`
	SlotPayout  float64  `json:"SlotPrizes"`
	TotalPayout float64  `json:"TotalPrizes"`
}

type BattleSummary struct {
	Steps          map[int][]BattleStep `json:"steps"`
	Winners        WinnerSummary        `json:"winners"`
	DropValue      map[string]float64   `json:"prizes"`
	Jackpot        map[string]float64   `json:"jackpot,omitempty"`
	JackpotWinner  string               `json:"jackpotWinner,omitempty"`
	rawRolls       []uint64
	jackpotRawRoll uint64
}

type PFair struct {
	ServerSeed     string `json:"serverSeed,omitempty"`
	ServerSeedHash string `json:"serverSeedHash"`
	DrandRoundID   uint64 `json:"drandRoundId,omitzero"`
	DrandSeed      string `json:"drandSeed,omitempty"`
	ClientSeed     string `json:"clientSeed,omitempty"`
	Nonce          int    `json:"nonce"`
	LogicVersion   int    `json:"logicVersion"`
}

type playerTypeInfo struct {
	numPlayers     int
	playersPerTeam int
}

var playerTypeInfos = map[string]playerTypeInfo{
	"solo":    {1, 1},
	"1v1":     {2, 1},
	"1v1v1":   {3, 1},
	"1v1v1v1": {4, 1},
	"1v5":     {6, 1}, // means "1v1v1v1v1v1"
	"2v2":     {4, 2},
	"2v2v2":   {6, 2},
	"3v3":     {6, 3},
}

func (ptInfo playerTypeInfo) slotIndexToTeam(slotIdx int) int {
	return slotIdx / ptInfo.playersPerTeam
}

type BattleInfo struct {
	PlayerType    string          `json:"playerType"`
	ptInfo        playerTypeInfo
	Options       []string        `json:"options"`
	jackpot       bool
	madness       bool
	equality      bool
	CaseSequence  []int64         `json:"cases"`
	CasesSnapshot map[int64]*Case `json:"casesSnapshot"`
	PFair         PFair           `json:"pFair"`
}

const CASE_ROLL_TICKETS_NUM uint64 = 1000000

func getWinningTicket(serverSeed, externalSeed string, nonce int) (uint32, uint64) {
	seed := serverSeed + externalSeed + "#" + strconv.FormatInt(int64(nonce), 10)
	hash := sha256.Sum256([]byte(seed))
	rolledUInt64 := binary.LittleEndian.Uint64(hash[:])
	return uint32(rolledUInt64%CASE_ROLL_TICKETS_NUM) + 1, rolledUInt64
}

func rollItem(serverSeed, externalSeed string, nonce int, itemsMap map[int64]*CaseItem) (*CaseItem, uint32, uint64, error) {
	rolledTicket, rolledUInt64 := getWinningTicket(serverSeed, externalSeed, nonce)
	if itemsMap == nil {
		return nil, rolledTicket, rolledUInt64, nil
	}
	for _, item := range itemsMap {
		if item.MinRand <= rolledTicket && rolledTicket <= item.MaxRand {
			return item, rolledTicket, rolledUInt64, nil
		}
	}
	return nil, rolledTicket, rolledUInt64, errors.New("rollItem: case item not found")
}

func roundPrice(val float64) float64 {
	return math.Round(val*100) / 100
}

func RollCasesPublic(battle *BattleInfo) (*BattleSummary, error) {
	var extraSeed string
	// use drand seed for multi-player and client seed for single-player
	if battle.PlayerType != "solo" {
		extraSeed = battle.PFair.DrandSeed
	} else {
		extraSeed = battle.PFair.ClientSeed
	}

	var summary BattleSummary
	var slotTotals []float64
	var curJackpotWeights []float64 // updated to current jackpot weights after every round (case)
	summary.Steps = make(map[int][]BattleStep)
	slotTotals = make([]float64, battle.ptInfo.numPlayers)
	nonce := battle.PFair.Nonce
	for caseIndex, caseID := range battle.CaseSequence {
		curSteps := make([]BattleStep, battle.ptInfo.numPlayers)
		for slotIdx := 0; slotIdx < battle.ptInfo.numPlayers; slotIdx++ {
			item, _, rawRoll, err := rollItem(battle.PFair.ServerSeed, extraSeed, nonce, battle.CasesSnapshot[caseID].Items)
			nonce++
			if err != nil {
				return nil, err
			}
			summary.rawRolls = append(summary.rawRolls, rawRoll)
			curSteps[slotIdx] = BattleStep{
				Slot:   fmt.Sprintf("s%d", slotIdx+1),
				ItemID: item.ItemID,
				Price:  item.Price,
			}
			slotTotals[slotIdx] += item.Price
		}
		if battle.jackpot {
			curJackpotWeights = slices.Clone(slotTotals)
			totalWeight := 0.0
			for i, v := range curJackpotWeights {
				if battle.madness {
					curJackpotWeights[i] = 1.0 / v
				}
				totalWeight += curJackpotWeights[i]
			}
			for i := range curSteps {
				curJackpotWeights[i] = 100.0 * (curJackpotWeights[i] / totalWeight)
				curSteps[i].JackpotCurrent = curJackpotWeights[i]
			}
		}
		summary.Steps[caseIndex] = curSteps
	}

	for i := range slotTotals {
		slotTotals[i] = roundPrice(slotTotals[i]) // for accurate comparison
	}

	winnerTeam := -1
	// determine winning team and check whether it is unique (if no modifiers or madness only)
	if !battle.jackpot && !battle.equality {
		bestTotal := -1.0
		better := func(total float64) bool {
			if bestTotal == -1.0 {
				return true
			}
			if battle.madness {
				return total < bestTotal
			}
			return total > bestTotal
		}
		bestTeams := []int{}
		curTotal := 0.0
		for i, v := range slotTotals {
			curTotal += v
			if (i+1)%battle.ptInfo.playersPerTeam == 0 { // if last player in team
				if better(curTotal) {
					bestTotal = curTotal
					bestTeams = []int{battle.ptInfo.slotIndexToTeam(i)}
				} else if bestTotal == curTotal {
					bestTeams = append(bestTeams, battle.ptInfo.slotIndexToTeam(i))
				}
				curTotal = 0.0
			}
		}
		if len(bestTeams) > 1 { // set up tie-breaker jackpot to uniformly choose any player from best teams
			curJackpotWeights = make([]float64, battle.ptInfo.numPlayers)
			weight := 100.0 / float64(len(bestTeams)*battle.ptInfo.playersPerTeam)
			for i := range curJackpotWeights {
				if slices.Contains(bestTeams, battle.ptInfo.slotIndexToTeam(i)) {
					curJackpotWeights[i] = weight
				}
			}
		} else {
			winnerTeam = bestTeams[0]
		}
	}

	// run jackpot
	var jackpotWinner int
	if len(curJackpotWeights) > 0 {
		baseSeedHash := sha256.Sum256([]byte(battle.PFair.ServerSeed + extraSeed))
		summary.jackpotRawRoll = binary.LittleEndian.Uint64(baseSeedHash[:])
		randVal := 100.0 * (float64(summary.jackpotRawRoll) / float64(1<<64))
		tmpWeight := 0.0
		for i, v := range curJackpotWeights {
			tmpWeight += v
			if winnerTeam == -1 && (tmpWeight >= randVal || i+1 == len(curJackpotWeights)) {
				jackpotWinner = i
				winnerTeam = battle.ptInfo.slotIndexToTeam(i)
			}
		}
		summary.Jackpot = make(map[string]float64)
	}

	// populate summary.DropValue and summary.Winners
	summary.DropValue = make(map[string]float64)
	for i, v := range slotTotals {
		slotName := fmt.Sprintf("s%d", i+1)
		summary.DropValue[slotName] = v
		summary.Winners.TotalPayout += v
		if battle.equality || battle.ptInfo.slotIndexToTeam(i) == winnerTeam {
			summary.Winners.Slots = append(summary.Winners.Slots, slotName)
		}
		if len(curJackpotWeights) > 0 {
			summary.Jackpot[slotName] = curJackpotWeights[i]
			if i == jackpotWinner {
				summary.JackpotWinner = slotName
			}
		}
	}
	summary.Winners.SlotPayout = roundPrice(summary.Winners.TotalPayout / float64(len(summary.Winners.Slots)))

	return &summary, nil
}

const (
	optionJackpot  = "jackpot"
	optionMadness  = "madness"
	optionEquality = "equality"
)

func InitFromJSONandRoll(battleJSON string) (*BattleSummary, error) {
	var battle BattleInfo
	if err := json.Unmarshal(json.RawMessage(battleJSON), &battle); err != nil {
		return nil, err
	}
	if battle.PFair.LogicVersion != battleProvablyFairVersion {
		return nil, errors.New(fmt.Sprintf("wrong battle provably fair logic version: expected %d, got %d", battleProvablyFairVersion, battle.PFair.LogicVersion))
	}

	var ok bool
	battle.ptInfo, ok = playerTypeInfos[battle.PlayerType]
	if !ok {
		return nil, errors.New("invalid playerType")
	}
	for _, opt := range battle.Options {
		switch strings.TrimSpace(strings.ToLower(opt)) {
		case optionJackpot:
			battle.jackpot = true
		case optionMadness:
			battle.madness = true
		case optionEquality:
			battle.equality = true
		}
	}

	return RollCasesPublic(&battle)
}

func main() {
	battleJSON := ``

	summary, err := InitFromJSONandRoll(battleJSON)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	summaryJSON, _ := json.Marshal(summary)
	fmt.Println(string(summaryJSON))
	// fmt.Println(summary.rawRolls, summary.jackpotRawRoll)
	fmt.Println("Winners:")
	for _, winnerSlot := range summary.Winners.Slots {
		fmt.Printf("%s: %.2f\n", winnerSlot, summary.Winners.SlotPayout)
	}
}
package main

import (
	"crypto/sha256"
	"encoding/binary"
	"fmt"
)

func houseEdge(roundId int) float64 {
	if roundId >= 223777 {
		return 0.05
	}
	return 0.077
}

func CalculateCrashMultiplier(serverSeed, drandSeed string, roundId int) float64 {
	combinedSeedHash := sha256.Sum256([]byte(serverSeed + drandSeed))

	const MAX = float64(1 << 32)
	value := float64(binary.LittleEndian.Uint32(combinedSeedHash[:]))
	result := (MAX / (value + 1)) * (1 - houseEdge(roundId))

	return max(result, 1.0)
}

func main() {
	serverSeed := ""
	drandSeed  := ""
	roundId    := 0

	if len(serverSeed) != 64 {
		fmt.Println("error: serverSeed must have length 64")
		return
	}
	if len(drandSeed) != 64 {
		fmt.Println("error: drandSeed must have length 64")
		return
	}
	if roundId <= 0 {
		fmt.Println("error: roundId must be positive")
		return
	}

	fmt.Println(CalculateCrashMultiplier(serverSeed, drandSeed, roundId))
}
package main

import (
	"crypto/sha256"
	"encoding/binary"
	"math"
	"fmt"
)

type Card string // Card identifies a bettable outcome on the roulette wheel.

const (
	CardSilver Card = "Silver"
	CardGold   Card = "Gold"
	CardCS2    Card = "CS2"

	cs2Prob          = 1.0 / 17
	goldOrSilverProb = 1.0 - cs2Prob
	silverProb       = 0.5 * goldOrSilverProb

	cs2SuperProb    = 0.009375
	silverSuperProb = 0.01875
)

var superMinGameID int64 = 671222

func toSuper(card Card) Card {
	return "Super" + card
}

func pickResultFromSeed(serverSeed, drandSeed string, gameID int64) Card {
	combinedSeedHash := sha256.Sum256([]byte(serverSeed + drandSeed))

	num := binary.LittleEndian.Uint64(combinedSeedHash[:])
	ratio := float64(num) / float64(math.MaxUint64)

	var card Card
	switch {
	case ratio < silverProb:
		card = CardSilver
	case ratio < goldOrSilverProb:
		card = CardGold
	default:
		card = CardCS2
	}
	if gameID >= superMinGameID {
		isSuper := false
		num = binary.LittleEndian.Uint64(combinedSeedHash[8:])
		ratio = float64(num) / float64(math.MaxUint64)
		if card == CardCS2 {
			isSuper = (ratio < cs2SuperProb)
		} else {
			isSuper = (ratio < silverSuperProb)
		}
		if isSuper {
			card = toSuper(card)
		}
	}
	return card
}

func main() {
	serverSeed := ""
	drandSeed  := ""
	gameID     := 0

	if len(serverSeed) != 64 {
		fmt.Println("error: serverSeed must have length 64")
		return
	}
	if len(drandSeed) != 64 {
		fmt.Println("error: drandSeed must have length 64")
		return
	}
	if gameID <= 0 {
		fmt.Println("error: gameID must be positive")
		return
	}

	fmt.Println(pickResultFromSeed(serverSeed, drandSeed, int64(gameID)))
}
package main

import (
	"cmp"
	"crypto/sha256"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"slices"
	"strconv"
)

const giveawayProvablyFairVersion = 1

// GiveawayPublic contains the inputs needed to reproduce the original draw.
// Royale registration can subsequently skip selected participants without drawing replacements.
type GiveawayPublic struct {
	NumWinners          int    `json:"num_winners"`
	ParticipantCount    int    `json:"participant_count"`
	ServerSeed          string `json:"server_seed,omitempty"`
	DrandSeed           string `json:"drand_seed,omitempty"`
	ProvablyFairVersion int    `json:"provably_fair_version"`
}

// DrawWinners returns distinct participant indices, starting at 1, in draw order.
func DrawWinners(giveaway *GiveawayPublic) ([]int, error) {
	if giveaway.ProvablyFairVersion != giveawayProvablyFairVersion {
		return nil, fmt.Errorf("unsupported provably fair version: %d", giveaway.ProvablyFairVersion)
	}
	if giveaway.NumWinners <= 0 || giveaway.ParticipantCount < 0 {
		return nil, fmt.Errorf("num_winners must be positive; participant_count must be non-negative")
	}
	if len(giveaway.ServerSeed) != 64 || len(giveaway.DrandSeed) != 64 {
		return nil, fmt.Errorf("server_seed and drand_seed must be specified and have length 64")
	}

	type scoredParticipant struct {
		index int
		score int64
	}
	participants := make([]scoredParticipant, giveaway.ParticipantCount)
	for i := range participants {
		index := i + 1
		hash := sha256.Sum256([]byte(giveaway.ServerSeed + giveaway.DrandSeed + strconv.Itoa(index)))
		participants[i] = scoredParticipant{index: index, score: int64(binary.LittleEndian.Uint64(hash[:8]))}
	}
	slices.SortFunc(participants, func(a, b scoredParticipant) int {
		if order := cmp.Compare(a.score, b.score); order != 0 {
			return order
		}
		return cmp.Compare(a.index, b.index)
	})
	winners := make([]int, min(giveaway.NumWinners, giveaway.ParticipantCount))
	for i := range winners {
		winners[i] = participants[i].index
	}
	return winners, nil
}

func initFromJSONandDraw(giveawayJSON string) ([]int, error) {
	var giveaway GiveawayPublic
	if err := json.Unmarshal(json.RawMessage(giveawayJSON), &giveaway); err != nil {
		return nil, err
	}
	return DrawWinners(&giveaway)
}

func main() {
	giveawayJSON := "{\"num_winners\":1,\"participant_count\":0,\"server_seed\":\"\",\"drand_seed\":\"\",\"provably_fair_version\":0}"

	winners, err := initFromJSONandDraw(giveawayJSON)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Printf("Winners: %v\n", winners)
}
play iconRun Code

After copying the code above, go to https://go.dev/play, paste the code there, and click the Run button to see the result.

help icon

Provably Fair FAQs

What does "Provably Fair" mean ?

It's a cryptographic system that guarantees every game result is transparent and cannot be manipulated by us.

How does it work ?

  • Before each round, we generate a secret Server Seed and show you its SHA-256 hash.
  • Your browser contributes a Client Seed.
  • A counter called Nonce increases with every bet.
  • These values are combined with a public formula to generate the final result.

Can I verify the result myself ?

Yes. After the round ends, we reveal the Server Seed. You can re-calculate the outcome using the seeds and nonce. If your calculation matches the game result, you know it was 100% fair.

Why should I trust this ?

Because the hash of the Server Seed is published before you play. That means the result is locked in advance, and we cannot alter it after seeing your bet.

What's the benefit for me ?

Peace of mind — you know every spin, every crash, every case is fair, transparent, and verifiable forever.