Skip to content

Call-a-bot: the real code

Everything below is pulled straight from the source files this server is running right now — not a copy — so you can see exactly how the house bot plays. Every bot game can also be checked on the verify page.

  • • The bot stakes real pets from the house stock, sized so it's 48–52% of the pot, and never a pet worth more than your best pet.
  • • Your win chance is your share of the pot. The bot gets no extra edge — same odds as playing a person. The exact chances are saved on the flip before it rolls.
  • • The result is sha256(serverSeed:flipId:1) turned into a number from 0 to 1. The server commits to sha256(serverSeed) before the flip and reveals the seed after, so the roll can't be changed once you've played.
  • • If you win, you get the pot minus the 15% house tax. If the bot wins, the pot goes back to the house stock.
  • • Blackjack bot: plays dealer rules (hits until 17), plays its hand before you act and never sees your cards. Every card comes from the committed seed. No house tax on blackjack.

Live results — every bot game ever played

Bot coinflips
2,288
Bot actually won
1,217 (53.2%)
Expected from its odds
1,158 (50.6%)
Difference
+59

"Expected" adds up the bot's shown win chance on every flip. The bot has been luckier than expected lately. Every single flip below can still be checked on the verify page.

Bot blackjack games
395
Bot won
196
Players won
137
Pushes (stakes back)
62

Latest bot coinflips

PlayerPlayer stakeBot stakeBot chanceRollWinner
extractt3350.0%0.7774BotVerify
extractt4450.0%0.9074BotVerify
extractt3350.0%0.5289BotVerify
extractt5550.0%0.4372extracttVerify
extractt3350.0%0.7809BotVerify
extractt3350.0%0.0465extracttVerify
extractt2250.0%0.0134extracttVerify
extractt2250.0%0.3864extracttVerify
extractt2250.0%0.5732BotVerify
Jet16.816.850.0%0.7924BotVerify
Jet4.124.1250.0%0.7573BotVerify
NotBreakingAnythimg393950.0%0.8173BotVerify
NotBreakingAnythimg1150.0%0.8116BotVerify
NotBreakingAnythimg404050.0%0.7161BotVerify
NotBreakingAnythimg1150.0%0.3438NotBreakingAnythimgVerify

The player sits on the coin's creator side: they win when the roll is below their chance (1 − bot chance).

1 · The provably-fair roll

Where every result comes from. The server seed is random, its hash is published first, and the roll is just a SHA-256 hash turned into a number between 0 and 1. The creator (you) wins when the roll is below your chance.

lib/fair.ts · generateServerSeed
export function generateServerSeed(): string {
  return randomBytes(32).toString("hex");
}
lib/fair.ts · commit
export function commit(serverSeed: string): string {
  return sha256Hex(serverSeed);
}
lib/fair.ts · hashToFloat
/**
 * Map a hash to a float in [0,1) using the first 13 hex chars (52 bits),
 * which is exactly representable by an IEEE-754 double.
 */
export function hashToFloat(hashHex: string): number {
  const slice = hashHex.slice(0, 13);
  const int = parseInt(slice, 16);
  return int / Math.pow(2, 52);
}
lib/fair.ts · computeRoll
export function computeRoll(serverSeed: string, clientSeed: string, nonce: number): number {
  return hashToFloat(sha256Hex(`${serverSeed}:${clientSeed}:${nonce}`));
}
lib/fair.ts · creatorWins
export function creatorWins(roll: number, creatorChance: number): boolean {
  return roll < creatorChance;
}

2 · Calling the bot

What happens when you press Call. The bot's pets come from house stock, chances are set from the pot shares (no bot edge), then it settles through the same code as a player-vs-player flip.

lib/game.ts · callBot
/**
 * The creator of an open flip asks the house bot to take the other side. The
 * bot matches the bet with house money at exactly the same odds as a player
 * would get — win chance is each side's share of the pot, no house edge.
 */
export async function callBot(flipId: string, callerUserId: string, rawPlayerSeed?: unknown): Promise<Flip> {
  await ensureBotUser();
  const flip = await withState((s) => {
    const flip = s.flips.find((f) => f.id === flipId);
    if (!flip) throw new GameError("Flip not found");
    if (flip.status !== "open") throw new GameError("This flip is no longer open");
    if (flip.creator.userId !== callerUserId) throw new GameError("Only the creator can call a bot");
    if (flip.joiner) throw new GameError("This flip has already been matched");
    const caller = findUser(s, callerUserId);
    if (!caller || caller.banned) throw new GameError("Account banned");
    guardWager(s, caller, 0); // honours a self-exclusion that started after the flip opened
    const bot = s.users.find((u) => u.username === BOT_USERNAME);
    if (!bot) throw new GameError("The bot isn't ready yet — try again");
    if (gameOf(flip) === "legacy") throw new GameError("This is an old flip");
    flip.joiner = sideFrom(bot, [], flip.creator.side === "blue" ? "gold" : "blue");
    flip.joiner.value = flip.creator.value;
    computeChances(flip.creator, flip.joiner);
    applyPlayerSeed(flip, rawPlayerSeed);
    settle(flip, s);
    audit(s, "system", "flip.bot-matched", flip.id, `${BOT_USERNAME} matched ${flip.creator.value} gems`);
    return flip;
  });
  try {
    await notifyFlipSettled(flip);
  } catch {
    // already audited; the flip itself stands
  }
  void postBigWin(flipBigWin(flip));
  return flip;
}
lib/game.ts · computeChances
function computeChances(creator: FlipSide, joiner: FlipSide) {
  const total = creator.value + joiner.value;
  creator.chance = total > 0 ? creator.value / total : 0.5;
  joiner.chance = 1 - creator.chance;
}

3 · How the bot picks its pets

The bot answers like with like: it mirrors your pets one-for-one from stock, always lands between 48% and 52% of the pot, and is never allowed a pet worth more than your best one.

lib/game.ts · botStakeFor
/**
 * Pick the bot's counter-stake for a game whose creator staked targetValue.
 *
 * Aim: answer like with like. If the player puts in one good pet, the bot
 * should put in one similar pet — not a pile of junk that happens to add up.
 * Always inside the 48–52% band, never using a pet worth more than
 * `maxPetValue` (the player's best pet), max MAX_STAKE_ITEMS pets.
 *
 *  1. Mirror: for each of the player's pets (biggest first) take the unused
 *     stock pet closest in value. If that lands in the band, done; if not, try
 *     single swaps that pull it into the band while staying close per pet.
 *  2. Otherwise build the sum from stock with a subset-sum search that only
 *     uses "similar" pets (at least a share of the player's average pet value),
 *     relaxing that floor step by step. Among valid sums it prefers the FEWEST
 *     pets (i.e. the biggest ones), then the closest to 50/50.
 *
 * Returns [] when the stock can't cover the band — the caller then tells the
 * player the bot can't join.
 */
export function botStakeFor(
  pool: Item[],
  targetValue: number,
  maxPetValue = Infinity,
  creatorItems: Item[] = [],
): Item[] {
  if (pool.length === 0 || targetValue <= 0) return [];
  const lo = Math.ceil((BOT_MIN_SHARE / (1 - BOT_MIN_SHARE)) * targetValue);
  const hi = Math.floor((BOT_MAX_SHARE / (1 - BOT_MAX_SHARE)) * targetValue);
  if (lo > hi || hi > 1_000_000) return [];
  const eligible = pool.filter((i) => i.value > 0 && i.value <= hi && i.value <= maxPetValue);
  if (eligible.length === 0) return [];
  const inBand = (items: Item[]) => {
    const sum = items.reduce((a, i) => a + i.value, 0);
    return sum >= lo && sum <= hi;
  };

  // ---- 1. mirror the player's pets one-for-one ----
  const mine = [...creatorItems].sort((a, b) => b.value - a.value);
  if (mine.length > 0 && mine.length <= MAX_STAKE_ITEMS) {
    const used = new Set<string>();
    const picked: Item[] = [];
    for (const want of mine) {
      let best: Item | null = null;
      for (const c of eligible) {
        if (used.has(c.id)) continue;
        if (!best || Math.abs(c.value - want.value) < Math.abs(best.value - want.value)) best = c;
      }
      if (!best) break;
      used.add(best.id);
      picked.push(best);
    }
    if (picked.length === mine.length) {
      if (inBand(picked)) return picked;
      // One swap at a time: replace a pick with another unused pet that pulls
      // the total into the band, choosing the swap that stays closest per pet.
      const sum = picked.reduce((a, i) => a + i.value, 0);
      let bestSwap: { at: number; with: Item; cost: number } | null = null;
      for (let k = 0; k < picked.length; k++) {
        for (const c of eligible) {
          if (used.has(c.id)) continue;
          const next = sum - picked[k].value + c.value;
          if (next < lo || next > hi) continue;
          const cost = Math.abs(c.value - mine[k].value);
          if (!bestSwap || cost < bestSwap.cost) bestSwap = { at: k, with: c, cost };
        }
      }
      if (bestSwap && bestSwap.cost <= Math.max(1, mine[bestSwap.at].value * 0.6)) {
        const out = [...picked];
        out[bestSwap.at] = bestSwap.with;
        return out;
      }
    }
  }

  // ---- 2. build from similar pets, relaxing "similar" step by step ----
  const avg = mine.length ? targetValue / mine.length : 0;
  for (const share of avg > 0 ? [0.5, 0.25, 0.1, 0] : [0]) {
    const floor = avg * share;
    // Pets closest to the player's typical pet are tried first, so the search
    // reaches good combinations with them before falling back to others.
    const cands = eligible
      .filter((i) => i.value >= floor)
      .sort((a, b) => Math.abs(a.value - avg) - Math.abs(b.value - avg) || b.value - a.value);
    const found = subsetStake(cands, targetValue, lo, hi);
    if (found.length) return found;
  }
  return [];
}

4 · Deciding the winner and paying out

The roll is compared to the creator's saved chance. The winner gets the pot minus the house tax; if the bot wins, the pot goes to house stock.

lib/game.ts · settle
function settle(flip: Flip, s: State) {
  if (!flip.joiner) return;
  const roll = computeRoll(flip.serverSeed, flip.clientSeed, flip.nonce);
  const creatorWon = creatorWins(roll, flip.creator.chance);
  flip.roll = roll;
  flip.status = "settled";
  flip.settledAt = Date.now();
  flip.winnerUserId = creatorWon ? flip.creator.userId : flip.joiner.userId;
  accrueReferral(s, flip.creator.userId, flip.creator.value);
  accrueReferral(s, flip.joiner.userId, flip.joiner.value);

  const winner = findUser(s, flip.winnerUserId);
  const game = flip.game as GameKey;
  const potValue = gem2(flip.creator.value + flip.joiner.value);
  if (!winner || winner.isBot) {
    audit(s, "system", "flip.bot-won", flip.id, `house kept the ${potValue} gem pot`);
    flipNotify(s, flip);
    return;
  }
  const taxPct = clampTaxPct(s.settings.coinflipTaxPct);
  const taxValue = gem2((potValue * taxPct) / 100);
  const vsBot = [flip.creator, flip.joiner].some((x) => findUser(s, x.userId)?.isBot);
  const myBet = winner.id === flip.creator.userId ? flip.creator.value : flip.joiner.value;
  // Against the bot it's house money, so the max-profit cap applies.
  const paid = vsBot ? payout(s, winner, game, myBet, potValue - taxValue) : (addGems(winner, game, potValue - taxValue), potValue - taxValue);
  flip.taxValue = gem2(potValue - paid);
  flip.taxItems = [];
  s.houseTaxCredit += flip.taxValue;
  audit(s, "system", "flip.tax", flip.id, `tax ${taxPct}% · ${flip.taxValue} of ${potValue} gem pot`);
  const w = flipBigWin(flip);
  if (w) announceBigWin(s, w);
  flipNotify(s, flip);
}

5 · The blackjack bot

Cards come from the committed seed (card n for a side is fixed before the game starts). The bot plays its whole hand at the start by dealer rules — hit below 17 — before you act, so it can't react to your cards.

lib/blackjack.ts · drawCard
function drawCard(m: Blackjack, side: "creator" | "joiner", n: number): number {
  return Math.floor(hashToFloat(sha256Hex(`${m.serverSeed}:${m.clientSeed}:${side}:${n}`)) * 52);
}
lib/blackjack.ts · handTotal
export function handTotal(cards: number[]): { total: number; soft: boolean } {
  let total = 0;
  let aces = 0;
  for (const c of cards) {
    const r = c % 13;
    if (r === 0) {
      aces += 1;
      total += 11;
    } else total += Math.min(10, r + 1);
  }
  while (total > 21 && aces > 0) {
    total -= 10;
    aces -= 1;
  }
  return { total, soft: aces > 0 };
}
lib/blackjack.ts · start
/** Deal both hands; the bot plays its whole hand straight away. */
function start(m: Blackjack): void {
  m.status = "playing";
  m.startedAt = Date.now();
  for (const side of ["creator", "joiner"] as const) {
    hit(m, side);
    hit(m, side);
    m[side]!.done = handTotal(m[side]!.cards).total >= 21;
    m[side]!.lastActionAt = Date.now();
  }
  const j = m.joiner!;
  if (j.username === BOT_USERNAME) {
    while (!j.done && handTotal(j.cards).total < 17) hit(m, "joiner");
    j.done = true;
  }
}
Call-a-bot code | AMPSPIN