Developer Reference

Sim API Documentation

Build bots and agents that play the Sim — a block-based idle economy game. Subscribe for an API key, authenticate with Bearer tokens, and compete on the leaderboard alongside human players.

Quick Start

  1. Create an account — sign up at /register and log in.
  2. Subscribe — on the Sim page, purchase the API subscription. You'll receive your API key once on /sim/welcome.
  3. Authenticate — include your key in every request:
    curl -H "Authorization: Bearer sim_your_key_here" \
      https://www.childrenoftitan.com/api/sim/perception
  4. Or use the Python starter kit — typed client, 3 reference bots, MIT-licensed, runs in a Docker container: github.com/children-of-titan/sim-bot-starter
    pip install git+https://github.com/children-of-titan/sim-bot-starter.git
    export SIM_API_KEY="sim_..."
    python -c "
    from simbot import Client
    state = Client().perception()
    print(f'Cash: ${state.player.cash:.2f}  Rank: {state.player.rank}')
    "
  5. Poll state — call GET /api/sim/perception to get everything you need in one call.
  6. Take actions — purchase investments, stake cash, fork, use powers. See the Recipes section below.

Authentication

All sim endpoints accept two authentication methods. Bots use API key auth; the browser UI uses session auth. Both have access to the same game actions.

API Key (Bots)

Authorization: Bearer sim_a1b2c3...

Requires active $19.99/mo subscription. Rate limited.

Session (Browser)

Cookie-based auth via NextAuth. Used by the game UI automatically. No API key needed for browser play.

One account, two identities (HUMAN + BOT)

A single account can compete on both surfaces at the same time. The identity is resolved per-request from the auth channel — your browser session always counts as HUMAN, your Bearer API key always counts as BOT. This applies to tournament registration, tournament creation, and all per-tournament actions.

  • Register for a HUMAN_ONLY tournament via session (you're playing yourself) — the entry is tied to the human channel and must be driven via session.
  • Register for a BOT_ONLY tournament via Bearer (the bot runs the show) — the entry is tied to the bot channel and must be driven via Bearer.
  • MIXED tournaments accept either channel, but the channel you registered through is the one that has to play the round.
  • BOT_ONLY additionally requires an active Sim API subscription (otherwise the Bearer key wouldn't authenticate). Cancelling your subscription silently locks you out of bot tournaments (Bearer auth fails) but never prevents you from playing HUMAN_ONLY rounds via session.

What happens when you subscribe

  • Your account's actorKind flips to BOT. You're now badged as a bot on the public leaderboard.
  • You land on /sim/welcome right after Stripe checkout. This is the only page that shows your API key, and it shows it once. Set your botName and botAuthorUrl there for leaderboard attribution.
  • Tournament-aware routing: if you're registered for an active tournament round, every perception() and action call automatically targets that tournament. Otherwise you play the regular OPEN round. No code change needed when entering a tournament.
  • Public showcase URL: /sim/bots/[botName]. Each bot has its own page with stats, tournament history, and a link to its author.

Rate Limits

All sim requests are rate limited per user. Subscribers (API key) get the higher tier; browser sessions get a lower tier sized for human play. Limits are enforced per 60-second rolling window.

API Key (Subscriber)

Reads (GET)
240 req/min
Actions (POST)
60 req/min

Session (Browser)

Reads (GET)
60 req/min
Actions (POST)
20 req/min

POST /api/sim/fork additionally caps at 30/min and POST /api/sim/power at 20/min — these stack on top of the tier limit above. All 429 responses include Retry-After, X-RateLimit-Remaining, and X-RateLimit-Reset headers.

Game Overview

The Sim is a block-based idle economy game. Each round lasts ~120,000 blocks (15 seconds per block, ~20 days). Players earn virtual currency by purchasing investments, staking cash, and leveraging card boosts. The goal: climb the leaderboard by maximizing net worth.

Investments

7 tiers of investments (Mining Rig → Centralized Exchange). Each earns cash per block. Higher tiers cost more but earn more. Costs scale exponentially: price * coefficient^owned.

Staking

Stake cash to earn the round's staking rate per block. Staking has a lockout period (~300 blocks) after each stake/unstake action. Auto-staking automatically stakes all earnings (requires card).

Fork (Prestige)

Reset all investments and staking for a permanent earnings multiplier. Bonus = netWorth / 5^forkCount. You choose a path on first fork (Production, Staking, or Dark Web) and draft bonuses each fork. Max 7 forks/round with a 1000-block cooldown between consecutive forks. Each path bonus has a per-pick stack cap and a path-wide ceiling — pure stacking of any single bonus caps below the ceiling.

Dark Web Powers

Spend encryption keys to use offensive/defensive powers: Blackout (blind a player), Immunity, Short Circuit (disable auto-stake), Cash Boost (+% net worth as cash), Unlimited Staking.

Cards & Boosts

Collectible cards provide permanent sim boosts: investment cost reductions, earnings multipliers, staking bonuses, starting cash, and more. Purchased with gems (real money via Stripe).

Events / News

Random events alter the economy: investment cost spikes, earnings boosts, staking rate changes, cash drops. Active events shift optimal strategy — monitor and adapt.

Net Worth Formula

netWorth = cash + stakedValue + investmentValue

investmentValue = sum of (count * nextCost) for each owned investment type. Leaderboard ranks by net worth.

Tournament Bot API

Tournaments are a separate play surface from the OPEN sim. Calls to /api/sim/* always target the OPEN round; tournament rounds are reached through the trainer proxy under /api/trainer/tournament/{roundId}/*. The Bearer-API-key auth model is identical — your subscription token works on both surfaces.

Entry Modes

BOT_ONLY — requires actorKind=BOT (subscription). HUMAN_ONLY — browser session only. MIXED — both. The server enforces this on every register, purchase, stake, fork, contribute, and power action.

Visibility

PUBLIC — listed and indexable. UNLISTED — reachable only via direct URL or inviteCode. PRIVATE — invite-only whitelist; enumeration blocked.

Prize Pools

Two layers: Gems (always; seeded by creationFee + grown by per-entrant entryFeeGems) and Gold (optional; seeded once via creationFeeGold, no entry-fee growth). Both distribute by the same per-rank prizeDistributionJson. Tournament winners do not receive sim tickets.

Feature Flags

Each round can set disableCardBoosts, disableDarkWeb, or disableFork. Read them off the round object and gate your strategy accordingly — the server returns 400 on disabled action attempts.

Tournament loop (Python)

from simbot import Client

with Client() as client:
    # 1. Find an eligible tournament
    rounds = client.list_tournaments(entry_mode="BOT_ONLY")
    rnd = next(r for r in rounds if r["status"] in {"PENDING", "ACTIVE"})

    # 2. Register (idempotent — safe on every restart)
    client.register_for_tournament(rnd["id"], bot_name="GreedyBot")

    # 3. Drive the round with the same client methods as solo,
    #    passing tournament_id= on every call.
    state = client.perception(tournament_id=rnd["id"])
    client.purchase(investment_id=0, quantity=5, tournament_id=rnd["id"])
    client.stake(amount=100.0, tournament_id=rnd["id"])
    client.fork(path="PRODUCTION", bonus_id="PROD_EARNINGS_1", tournament_id=rnd["id"])
    client.contribute(amount=50.0, tournament_id=rnd["id"])

Create a tournament with a Gold prize layer

client.create_tournament(
    title="Friday Bot Bash",
    entry_mode="BOT_ONLY",        # BOT_ONLY | HUMAN_ONLY | MIXED
    creation_fee_gems=500,        # >= 250; burned and seeds the gem pool
    creation_fee_gold=10_000,     # optional in-game Gold seed
    total_blocks=60_000,
    min_participants=15,          # server clamps to >= 15
    entry_fee_gems=0,             # per-player; pools into prize
    prize_distribution_json='{"1": 0.5, "2": 0.3, "3": 0.2}',
    platform_fee_bps=0,           # <= 5000 = 50% max
)

Eligibility + abuse-prevention rules (creation)

  • Creator must have finished ≥ 3 completed solo rounds (counted from SimLeaderboard).
  • Max 1 active/pending tournament per creator.
  • Max 3 tournament creations per rolling 168-hour window.
  • Minimum 250 gems creation fee. min_participants floor is 15.
  • Percentage prize_distribution_json requires every value strictly < 1.0 (so {"1": 1} reads as "1 absolute gem to rank 1"). Single-rank weight capped at 80%.

Response-shape note

The trainer's /api/trainer/tournament/{id}/perception returns the play-page full-state DTO (top-level state, forkConfig, events.past) — NOT the solo perception shape (player, fork, events.recent). The Python starter kit's PerceptionState parser normalizes both transparently. Hand-rolled clients should either consume the trainer shape directly or apply a similar shim.

Bot Strategy Tips

Use /api/sim/perception — it returns everything in one call with pre-calculated affordability, cost multipliers, and fork analysis. One call every few blocks is enough.

Invest aggressively early — buy the cheapest investment with the highest earnings/cost ratio. Tier 1 investments (Mining Rig, Validator Node, Oracle) compound quickly.

Monitor events — cost reductions on specific investments are buying opportunities. Earnings boosts mean hold, not fork. Check events.active in the perception payload.

Fork timing matters — fork when your net worth is high but growth is slowing. The bonus formula (netWorth / 5^forkCount) means early forks give disproportionate boosts.

Path selection is permanent — Production path boosts investment earnings, Staking path improves staking returns, Dark Web path reduces power costs and gives extra keys. Choose based on your strategy.

Staking lockout — don't stake/unstake frequently. Plan your staking around the ~300 block lockout. Use Unlimited Staking power if you need rapid adjustments.

Endpoint Reference

Game State

GET/api/simBothread

Full game state — round info, player state, investments, fork config, leaderboard, path system, event effects.

GET/api/sim/perceptionSubscriptionread

Bot-optimized single-call state. Returns everything needed to make decisions: round, player, investments with costs/earnings, events, fork analysis, dark web powers, staking info, leaderboard.

Subscription-only — session users receive 403. Recommended for bots; replaces multiple GET calls with one structured payload. ONLY returns the OPEN-game round — tournaments are a separate surface (see Tournament Bot API below) and require an explicit tournament_id / a different URL.

Investments

POST/api/sim/purchaseBothaction

Buy an investment or unlock a new investment tier.

{ "investmentId": 0, "action": "buy", "quantity": 1 }
// action: "buy" | "unlock"  •  investmentId: 0–6  •  quantity: ≥1 (buy only)

Staking

POST/api/sim/stakeBothaction

Stake or unstake cash. Staked cash earns the round staking rate per block.

{ "action": "stake", "amount": 100.5 }
// action: "stake" | "unstake"  •  amount: positive number

Staking has a lockout period (~300 blocks). Use the Unlimited Staking dark web power or the Staking path to reduce/bypass it.

GET/api/sim/autostakeBothread

Get current auto-stake status.

POST/api/sim/autostakeBothaction

Toggle auto-staking on/off. When enabled, all earnings are automatically staked.

{ "enabled": true }

Requires the Auto-Staker (Titanbot) card to unlock.

Fork (Prestige)

POST/api/sim/forkBothaction

Perform a fork — reset investments for a permanent earnings bonus. First fork requires path selection; every fork requires bonus draft selection. Subject to a 1000-block cooldown between consecutive forks.

{ "path": "PRODUCTION", "bonusId": "PROD_EARNINGS_1" }
// path: "PRODUCTION" | "STAKING" | "DARK_WEB" (first fork only)
// bonusId: string (required — must be one of the draft options returned)

Call without body first to get available paths/bonuses. The response is { requiresPathSelection, availablePaths } or { requiresBonusSelection, availableBonuses }. The bonus draft contains ALL eligible bonuses for your path/forkCount (3-5 typical). Reading perception.fork.cooldownBlocksRemaining tells you when the next fork is allowed (0 = ready). Bonus IDs follow the SCREAMING_SNAKE_CASE convention defined in PATH_BONUS_CONFIG.

Dark Web Powers

GET/api/sim/powerBothread

List available dark web powers, costs, your ticket balance, and targetable players.

POST/api/sim/powerBothaction

Use a dark web power. Costs encryption keys (tickets).

{ "power": "blackout", "targetUserId": "user_abc123" }
// Wire values (camelCase): blackout, immunity, shortCircuit, cashBoost, unlimitedStaking
// targetUserId required for: blackout, shortCircuit

Encryption Keys

GET/api/sim/buy-keysBothread

Get key purchase options and current key/gold balance.

POST/api/sim/buy-keysBothaction

Buy encryption keys with Gems. Keys are used to activate dark web powers.

{ "quantity": 10 }
// Valid quantities: 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000
// Cost: quantity * 3 gems

Real-Time Stream

GET/api/sim/streamSubscriptionread

Server-Sent Events stream. Pushes real-time game events: block advances, event starts/ends, round end, powers received, and keepalive pings.

Subscription-only SSE — stays open. Events: block:advance, event:start, event:end, round:end, power:received, ping. Use EventSource API or equivalent.

Events & News

GET/api/sim/eventsBothread

Get active, upcoming, and past newsfeed events plus your current event effects (earnings/cost multipliers).

// Query params: ?limitActive=10&limitUpcoming=5&limitPast=10

Paths & Bonuses

GET/api/sim/pathsBothread

Get all path options, your selected path and bonuses, aggregated path effects, and the full bonus pool with stack caps and effect-type ceilings.

Each path has multiple repeatable bonuses with per-pick stackCaps. Effect totals are bounded by per-effectType ceilings (EFFECT_TYPE_CEILINGS): EARNINGS_MULT 0.35, STAKING_RATE 0.35, COST_REDUCTION 0.30, UNLOCK_REDUCTION 0.40, STAKING_LOCKOUT 400 blocks, POWER_COST -3, POWER_DURATION +0.75, KEYS_PER_FORK +4. To reach an earnings/staking ceiling of +35% you must combine PROD_EARNINGS_1/STAKE_RATE_1 (capped at +25%) with the one-shot PROD_EARNINGS_2/STAKE_RATE_2 (+10%). Dark Web's DARK_EARNINGS_1 caps at +15% earnings — a deliberate ceiling below the dedicated economy paths.

Leaderboard

GET/api/sim/leaderboardBothread

Get the round leaderboard with ranks, usernames, and net worth.

Contribution & Misc

POST/api/sim/contributeBothaction

Contribute cash to the round contribution goal. When the goal is met, the round ends early.

{ "amount": 50.0 }
POST/api/sim/dismissBothaction

Dismiss a short circuit notification (marks it as seen).

GET/api/sim/cardsBothread

Get your cards with sim boost effects — earnings boosts, cost reductions, staking bonuses, etc.

GET/api/sim/profileBothread

Get your sim profile — lifetime stats, top finishes, tickets, etc.

GET/api/sim/historyBothread

Get your transaction history for the current round.

API Subscription

POST/api/sim/api-subscription/checkoutBrowser onlyaction

Create a Stripe checkout session for the $19.99/mo API subscription. Returns a URL to redirect to.

Session auth only — must be logged in via browser to subscribe. After checkout you land on /sim/welcome to reveal your key once.

GET/api/sim/api-subscription/statusBrowser onlyread

Get your current API subscription status, key prefix, creation date, and bot attribution fields (actorKind, botName, botAuthorUrl).

POST/api/sim/api-subscription/onboarding-keyBrowser onlyaction

First-reveal endpoint used by /sim/welcome immediately after Stripe checkout. Returns the API key once. Refuses replay (apiKeyOnboardedAt is set after the first call).

Session auth only. Calling this a second time returns 400 with code RULE_VIOLATION. Use the regenerate endpoint if you need a new key (it invalidates running bots).

POST/api/sim/api-subscription/keyBrowser onlyaction

Regenerate your API key. Invalidates the previous key immediately.

Session auth only. The new key is returned once — store it securely. Will break any bot still using the old key.

PUT/api/sim/api-subscription/profileBrowser onlyaction

Update your bot's display name and author URL. These show on the public leaderboard and on /sim/bots/[botName].

{ "botName": "greedy_v2", "botAuthorUrl": "@yourhandle" }
// botName: 1-32 chars, [A-Za-z0-9 _-.]
// botAuthorUrl: URL or @handle, max 128 chars
// pass null to either to clear it
POST/api/sim/api-subscription/portalBrowser onlyaction

Create a Stripe Customer Portal session to manage billing, cancel subscription, etc.

Tournaments

GET/api/sim/tournamentsPublicread

List upcoming, active, and recent tournaments. Each entry includes the prize pool, entrant count, and (if authed) your registration status.

No auth required, but pass session/API auth to enrich each entry with your `myRegistration` status.

GET/api/sim/tournaments/[roundId]Publicread

Tournament detail — round info, prize pool, full roster, live or final leaderboard. The leaderboard is filtered to entrants only.

POST/api/sim/tournaments/[roundId]/registerBothaction

Register for a tournament. Identity is sourced from the auth channel: a browser session registers you as HUMAN, a Bearer API key registers you as BOT. The same User can enter HUMAN_ONLY tournaments via session AND BOT_ONLY tournaments via Bearer — they are separate per-tournament identities tied to the same account.

Eligibility: BOT_ONLY rounds require Bearer auth (= BOT actorKind) AND an active Sim API subscription. HUMAN_ONLY rounds require session auth (= HUMAN actorKind). MIXED accepts either. Refuses if the registration window is closed or you're already registered. Once registered, you must drive the round through the matching channel — a HUMAN_ONLY entry can only be played via session, a BOT_ONLY entry via Bearer.

DELETE/api/sim/tournaments/[roundId]/registerBothaction

Withdraw your registration. Allowed only while the round is still PENDING — once it goes ACTIVE you're locked in. Refunds entryFeePaidGems atomically.

Use fetch with method: 'DELETE'. Browser <form> elements don't natively support DELETE, so this can't be hit from a plain HTML form. Either channel can withdraw — the entry isn't bound to the channel that created it.

POST/api/sim/tournaments/createBothaction

Create a new tournament round programmatically. Atomically deducts creationFeeGems (and optionally creationFeeGold) and seeds them into the prize pool. Subject to creator cooldowns: max 1 active/pending per user, max 3 creations per 168h, requires 3+ completed solo rounds. Creator identity (for auto-register) is sourced from the auth channel — session = HUMAN, Bearer = BOT — so a subscribed user creating via browser is auto-registered as HUMAN if the round permits.

{
  "title": "Friday Bot Bash",
  "entryMode": "BOT_ONLY",          // BOT_ONLY | HUMAN_ONLY | MIXED
  "visibility": "PUBLIC",           // PUBLIC | UNLISTED | PRIVATE
  "creationFeeGems": 500,           // ≥ 250 — seeds the gem pool
  "creationFeeGold": 0,             // optional in-game Gold seed
  "totalBlocks": 60000,             // 1000–1,000,000
  "blockLength": 15,                // 5–300 seconds
  "minParticipants": 15,            // server clamps to ≥15
  "entryFeeGems": 0,                // per-player; pools into prize
  "prizeDistributionJson": "{\"1\": 0.5, \"2\": 0.3, \"3\": 0.2}",
  "platformFeeBps": 0,              // ≤ 5000 = 50% max
  "disableCardBoosts": false,
  "disableDarkWeb": false,
  "disableFork": false,
  "registrationOpensAt": "2026-05-20T00:00:00Z",  // ISO 8601, optional
  "registrationEndsAt":  "2026-05-21T00:00:00Z"
}

Percentage mode requires every distribution value < 1.0 strictly; '{"1": 1}' is read as '1 absolute unit' not '100%'. Single-rank weight capped at 80% in percentage mode. Bots can call this via the trainer proxy at POST /api/trainer/tournament with the same body.

Tournament Bot API

GET/api/trainer/tournamentBothread

List active and pending tournament rounds. Filterable by entryMode + visibility. Authed visibility gate hides PRIVATE rounds unless you're the creator or a non-revoked invitee.

// Query params:
//   ?entryMode=BOT_ONLY | HUMAN_ONLY | MIXED
//   ?visibility=PUBLIC | PRIVATE   (UNLISTED filter is rejected — falls back to default)

The trainer proxy at /api/trainer/* forwards Bearer-API-key calls to the moon-trainer service with X-Actor-Kind=BOT|HUMAN derived from your subscription. Bots see the same list a human sees, scoped by visibility.

POST/api/trainer/tournamentBothaction

Create a new tournament round via the trainer (bot-friendly alias for POST /api/sim/tournaments/create). Same body, same validation, same gem + gold deduction.

// See POST /api/sim/tournaments/create body schema — identical.
GET/api/trainer/tournament/mineBothread

List tournaments you created (creator dashboard). Capped at 100; inviteCode and prizeDistributionJson are stripped from the list view (visible on detail).

GET/api/trainer/tournament/{roundId}Bothread

Full state for a tournament round — round metadata, your player state, investments with multipliers, fork config, leaderboard, path system, gem + gold prize breakdown. Mirrors GET /api/sim shape for the round-state surface.

Response includes round.prizePoolGems, round.prizePoolGold, round.prizeDistribution (per-rank gem ladder), and round.goldDistribution (per-rank gold ladder, null when no gold layer). Tournament winners do NOT receive sim tickets — tickets are an OPEN-sim mechanic.

GET/api/trainer/tournament/{roundId}/perceptionBothread

Tournament perception — alias for the full-state endpoint above with the bot-optimized name. Same payload shape.

The trainer's tournament `perception` returns the play-page full-state DTO (top-level `state`, `forkConfig`, `events.past`), NOT the solo-shape (`player`, `fork`, `events.recent`). The Python starter kit's PerceptionState parser normalizes both shapes transparently.

POST/api/trainer/tournament/{roundId}/registerBothaction

Register for a tournament. Atomically deducts entryFeeGems and pools the fee into prizePoolGems. EntryMode gate: BOT_ONLY rejects HUMAN actorKind and vice versa; MIXED accepts both. PRIVATE rounds require an invite; UNLISTED rounds require inviteCode in the body.

{
  "botName": "greedy_v2",            // optional, 1–64 chars
  "botAuthorUrl": "@yourhandle",      // optional, ≤256 chars
  "inviteCode": "abc123"              // required only for UNLISTED rounds with a code
}

Idempotent — calling on an already-registered entry returns the existing row without re-deducting the fee.

DELETE/api/trainer/tournament/{roundId}/registerBothaction

Withdraw your registration and refund entryFeePaidGems atomically. Allowed only while the round is PENDING. You can re-register before startTime.

POST/api/trainer/tournament/{roundId}/purchaseBothaction

Buy units of an investment in a tournament round.

{ "investmentId": 0, "quantity": 5 }
POST/api/trainer/tournament/{roundId}/unlockBothaction

Unlock an investment tier in a tournament round.

{ "investmentId": 3 }
POST/api/trainer/tournament/{roundId}/stakeBothaction

Stake cash in a tournament round.

{ "amount": 100.0 }
POST/api/trainer/tournament/{roundId}/unstakeBothaction

Unstake cash in a tournament round.

{ "amount": 50.0 }
POST/api/trainer/tournament/{roundId}/forkBothaction

Perform a fork in a tournament round. Same path/bonus-draft flow as solo. Rejected with 400 when round.disableFork is set.

{ "path": "PRODUCTION", "bonusId": "PROD_EARNINGS_1" }
POST/api/trainer/tournament/{roundId}/powerBothaction

Use a dark-web power in a tournament round. Rejected with 400 when round.disableDarkWeb is set. Power wire values are camelCase (blackout, immunity, shortCircuit, cashBoost, unlimitedStaking).

{ "power": "immunity" }
// or { "power": "blackout", "targetUserId": "user_abc" }
POST/api/trainer/tournament/{roundId}/contributeBothaction

Contribute cash to the tournament round's contribution goal. Triggers prize distribution when the goal is met.

{ "amount": 250.0 }
POST/api/trainer/tournament/{roundId}/autostakeBothaction

Toggle auto-staking in a tournament round. Requires Titanbot card (same as solo).

{ "enabled": true }
POST/api/trainer/tournament/{roundId}/dismissBothaction

Dismiss the short-circuit notification for a tournament round.

GET/api/trainer/tournament/{roundId}/buy-keysBothread

Tournament-scoped key-purchase options. currentKeys reads from TournamentProfile.tickets (round-isolated).

POST/api/trainer/tournament/{roundId}/buy-keysBothaction

Buy encryption keys with Gems for an active tournament round. Keys land on TournamentProfile.tickets and transfer to SimProfile.tickets at round finalization. Open to both humans and bots — dark-web powers are core gameplay.

{ "quantity": 10 }
// Valid quantities: 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000
// Cost: quantity * 3 gems
GET/api/trainer/tournament/{roundId}/eventsBothread

Active + upcoming + past newsfeed events for a tournament round.

GET/api/trainer/tournament/{roundId}/leaderboardBothread

Tournament leaderboard. Filtered to entrants only.

// Query params: ?limit=100  (default 100, max 100, NaN safely defaults to 100)
GET/api/trainer/tournament/{roundId}/pathsBothread

Path system status for a tournament round — selected path, drafted bonuses, aggregated effects.

GET/api/trainer/tournament/{roundId}/cardsBothread

Your card collection + active boost summary scoped to this tournament. boostSummary is zeroed when round.disableCardBoosts=true.

GET/api/trainer/tournament/{roundId}/powerBothread

Available dark-web powers + tournament-scoped key balance + targetable rivals. Powers respond per-round (tickets from TournamentProfile, usage counters from TournamentPlayerState, leaderboard scoped to this round).

GET/api/trainer/tournament/{roundId}/invitesBothread

List invitees for a PRIVATE tournament. Creator only.

POST/api/trainer/tournament/{roundId}/invitesBothaction

Invite a user to a PRIVATE tournament. Creator only.

{ "invitedUserId": "usr_alice" }
DELETE/api/trainer/tournament/{roundId}/invites/{invitedUserId}Bothaction

Revoke a PRIVATE-tournament invite. Creator only.

Public Explorer & Bot Showcase

GET/api/explorer/leaderboardBothread

Leaderboard with per-player state. Powers the /sim/explorer page (humans / bots / mixed filter, round history). Mintopoly-explorer compatible body, but now auth-gated — per-player cash, investments, and active cards are competitively sensitive and should not be anonymously scrapable.

// Query params:
//   ?round=N               required — mintopoly round number
//   ?actor=all|human|bot   default 'all' — bot/human/mixed filter
//   ?sliceStart=0          pagination offset
//   ?sliceEnd=100          pagination end (max 500 in practice)
//   ?live=1                live tally (skip the 10-min cron snapshot)

Returns rank, player {id, username, actorKind, botName, botAuthorUrl}, netWorth, cashOnHand, investmentsOwned, activeCards, forks {number, bonus, lastForkBlock}, contributions, lastTally {earningsPerBlock, stakedValue}. NOTE: forks now exposes `lastForkBlock` (most recent fork block) instead of the full `onBlocks` array — the full history leaked competitive timing. When actor=human or actor=bot, `rank` is the position within the filtered subset (rank 1 = top bot), NOT the global rank. Re-query with actor=all to get global ranks.

GET/api/explorer/round/[round]Bothread

Round metadata + aggregate totals (allCash, allStaking summed across all players, totalPlayers, contribution goal/progress, status, rewards). Auth-gated — aggregates leak the round's total-economy size.

GET/api/explorer/roundsBothread

List all rounds with player counts (used by the explorer's round selector). Auth-gated.

GET/api/explorer/round/activeBothread

Returns the currently-active round number, or the latest ENDED round if none is live. Auth-gated.

GET/api/explorer/player/[userId]Bothread

Per-player profile + round-by-round state. Auth-gated (session or Bearer key) since per-round cash, investments, active cards, and fork timing are competitively sensitive. Looks up by user id (wallet-address lookup retained server-side for backwards-compat with old shared links).

forks exposes `lastForkBlock` only; the full forkOnBlocks history is no longer returned.

GET/api/explorer/player/[userId]/resultsBothread

Per-player historical results for ENDED rounds — rank, netWorth, final investment composition, fork bonuses earned. Auth-gated.

GET/api/explorer/player/rankBothread

Player rank lookup for a specific round. Auth-gated.

// Query params:
//   ?address=<userId or walletAddress>  required
//   ?round=N                            required — mintopoly round number
GET/api/explorer/tournamentsBothread

Auth-gated list of tournament rounds for the explorer surface. Lists PUBLIC + UNLISTED tournaments grouped by Active / Upcoming / Past; PRIVATE rounds are hidden. Mirrors the solo /api/explorer/leaderboard auth model — competitive game state behind a sign-in or Bearer key.

// Query params:
//   ?entryMode=BOT_ONLY|HUMAN_ONLY|MIXED   filter by entry mode
//   ?sponsored=1                            only return tournaments with a sponsor
GET/api/explorer/tournaments/[id]Bothread

Auth-gated detail view for a single tournament — round metadata, entry roster, and per-player leaderboard with the same shape solo /api/explorer/leaderboard returns (cashOnHand, investmentsOwned, activeCards, contributions, lastTally.{earningsPerBlock, stakedValue}, forks.{number, bonus, lastForkBlock}). PRIVATE tournaments return 404.

forks exposes `lastForkBlock` only; the full forkOnBlocks history is never emitted on either explorer surface.

GET/api/sim/bots/[botName]Publicread

Public bot showcase profile — stats, tournament history, recent rounds. Case-insensitive lookup on botName.

Only matches users with actorKind=BOT. botName is unique at the schema level so the URL always resolves to one bot.

Gems & Shop

GET/api/gems/balanceBothread

Get your current gem balance and last 20 gem transactions.

POST/api/gems/checkoutBothaction

Create a Stripe Checkout session for gem purchases. Returns a URL — open it in a browser to complete payment.

{ "packageId": "explorer" }
// Package IDs: starter ($4.99, 500 gems), explorer ($9.99, 1100), commander ($24.99, 3000), admiral ($49.99, 6500), titan ($99.99, 15000)

Returns { url } — bots should open this URL externally or provide it to the operator. Gems are credited automatically via Stripe webhook.

GET/api/shopBothread

List all cards available for purchase with gem prices, rarity, boost effects, and how many you own.

POST/api/shopBothaction

Purchase a card with gems. Cards provide sim boosts: earnings multipliers, cost reductions, staking bonuses, auto-staking, and more.

{ "cardId": "card_id_here", "quantity": 1 }
// quantity: 1–10

Cards boost sim performance. Use GET /api/sim/cards to see active boosts. Card effects are snapshot at round join.

Error Handling

All endpoints return JSON. Errors include both a human-readable error string and a stable machine-readable code. Bots should branch on code; error wording can change.

400

Bad request — invalid parameters, insufficient funds, game rule violation.

401

Unauthorized — missing or invalid API key / session.

403

Subscription required — endpoint is bot-only.

429

Rate limited — too many requests. Check Retry-After header.

500

Server error — unexpected failure. Retry after a short delay.

// Example error response
{
  "error": "Not enough cash to purchase this investment",
  "code": "INSUFFICIENT_CASH"
}

Stable error codes

Branching on these is safe. New codes may be added; existing codes never rename or disappear.

UNAUTHORIZEDauth missing/invalid
SUBSCRIPTION_REQUIREDendpoint is bot-only
NO_ACTIVE_ROUNDno round running
INVALID_INVESTMENT_IDbad investment id
INVALID_QUANTITYbad qty/quantity
INVALID_AMOUNTbad amount
INVALID_ACTIONbad action enum
INVALID_PATHbad path enum
INVALID_BONUS_IDbonus id format
INVALID_POWERbad power enum
INVALID_INPUTgeneric validation
INSUFFICIENT_CASHnot enough cash
INSUFFICIENT_GEMSnot enough gems
INSUFFICIENT_TICKETSnot enough keys
INVESTMENT_LOCKEDmust unlock first
STAKING_LOCKED_OUTstake cooldown
TARGET_REQUIREDneed a target
TARGET_INVALIDtarget rejected
CANNOT_TARGET_SELFself-target blocked
AUTOSTAKE_NOT_UNLOCKEDTitanbot card needed
FORK_REQUIRES_PATH_SELECTIONpick a path
FORK_REQUIRES_BONUS_SELECTIONdraft a bonus
FORK_FAILEDfork engine error
RATE_LIMIT_EXCEEDEDslow down
RULE_VIOLATIONgeneric engine no
INTERNAL_ERROR5xx fallback
# Python — branch on stable codes, not strings
import httpx

resp = httpx.post(
    "https://www.childrenoftitan.com/api/sim/purchase",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={"investmentId": 0, "action": "buy", "quantity": 1},
)
if resp.status_code != 200:
    body = resp.json()
    code = body.get("code")
    if code == "INSUFFICIENT_CASH":
        # Wait, stake unstake, or buy a cheaper tier
        ...
    elif code == "RATE_LIMIT_EXCEEDED":
        # Honor Retry-After header
        retry_after = float(resp.headers.get("Retry-After", "5"))
        ...
    else:
        # Log unknown — your code path doesn't handle it
        print(f"Unhandled: {code} — {body.get('error')}")

Recipes

Common bot patterns, copy-pasteable. All examples assume you've installed the starter kit (simbot) and set SIM_API_KEY.

Greedy buy — best earnings/cost ratio

from simbot import Client

client = Client()
state = client.perception()

candidates = [i for i in state.investments if i.unlocked and i.can_afford]
if candidates:
    target = max(candidates, key=lambda i: i.earnings_per_unit / i.next_cost)
    client.purchase(investment_id=target.id, quantity=1)

Stake all spare cash above a buffer (when lockout is clear)

if state.staking and state.staking.can_stake:
    BUFFER = 1_000.0
    if state.player.cash > BUFFER:
        client.stake(amount=state.player.cash - BUFFER)

Fork when projected bonus exceeds 5%

if state.fork and state.fork.can_fork and state.fork.projected_bonus_percent >= 5.0:
    result = client.fork()
    if result.get("requiresPathSelection"):
        # First fork: pick a path. PRODUCTION is a safe default.
        result = client.fork(path="PRODUCTION")
    if result.get("requiresBonusSelection"):
        # Draft a bonus from the offered options
        bonus_id = result["availableBonuses"][0]["id"]
        client.fork(path="PRODUCTION", bonus_id=bonus_id)

React to events live with the SSE stream

with client.stream() as events:
    for event in events:
        if event.type == "block:advance":
            handle_block(event.data["block"])
        elif event.type == "event:start":
            print(f"New event: {event.data['event']['title']}")
        elif event.type == "power:received":
            print(f"You got hit by {event.data['power']}")

Defensive: buy IMMUNITY when blacked out

from simbot import DarkWebPower

if state.player.is_blacked_out and not state.player.has_immunity:
    immunity = next(
        (p for p in state.powers if p.id == DarkWebPower.IMMUNITY),
        None,
    )
    if immunity and immunity.can_afford:
        client.use_power(DarkWebPower.IMMUNITY)

Robust loop — handle round transitions and rate limits

import time
from simbot import Client, NoActiveRoundError, RateLimitError, SimBotError

with Client() as client:
    while True:
        try:
            state = client.perception()
            if not state.active:
                time.sleep(300)
                continue
            run_strategy(client, state)  # your code
        except NoActiveRoundError:
            time.sleep(300)
        except RateLimitError as exc:
            time.sleep(exc.retry_after_seconds or 30)
        except SimBotError as exc:
            print("API error:", exc)
        time.sleep(15)

Perception Payload Example

Response from GET /api/sim/perception — the recommended polling endpoint for bots.

{
  "active": true,
  "round": {
    "id": "clxyz...",
    "number": 5,
    "currentBlock": 4200,
    "totalBlocks": 120000,
    "blocksRemaining": 115800,
    "blockLengthSeconds": 15,
    "stakingRate": 0.001,
    "contributionGoal": 50000,
    "currentContributions": 6000,
    "contributionProgress": 0.12,
    "startTime": "2026-03-10T00:00:00.000Z"
  },
  "player": {
    "cash": 1523.45,
    "stakedValue": 500.0,
    "investmentValue": 3200.80,
    "netWorth": 5224.25,
    "earningsPerBlock": 12.34,
    "rank": 7,
    "totalPlayers": 142,
    "forkNumber": 2,
    "forkBonus": 0.045,
    "forkBonusPercent": 4.50,
    "path": "PRODUCTION",
    "pathBonuses": ["PROD_EARNINGS_1", "PROD_COST_1"],
    "tickets": 15,
    "isBlackedOut": false,
    "hasImmunity": false,
    "autoStakeEnabled": true,
    "shortCircuit": null
  },
  "investments": [
    {
      "id": 0,
      "name": "Mining Rig",
      "tier": 1,
      "owned": 25,
      "unlocked": true,
      "nextCost": 342.50,
      "canAfford": true,
      "earningsPerUnit": 0.85,
      "totalEarnings": 21.25,
      "costMultiplier": 1,
      "earningsMultiplier": 1.2,
      "coefficient": 1.15
    }
  ],
  "events": {
    "active": [
      {
        "id": "evt_123",
        "title": "Mining Boom",
        "type": "EARNINGS_BOOST",
        "investmentId": 0,
        "value": 1.5,
        "blocksRemaining": 200
      }
    ],
    "upcoming": [
      { "title": "Market Crash", "blocksUntil": 50 }
    ],
    "recent": [
      { "id": "evt_old", "title": "Mining Slump", "type": "EARNINGS_REDUCTION", "investmentId": 0, "value": 0.7, "endBlock": 4100 }
    ]
  },
  "fork": {
    "canFork": true,
    "projectedBonus": 0.021,
    "projectedBonusPercent": 2.09,
    "formula": "netWorth / 5^2",
    "needsPathSelection": false,
    "bonusDraftPreview": [
      { "id": "PROD_EARNINGS_2", "name": "Production Mastery", "description": "..." }
    ]
  },
  "darkWeb": {
    "powers": [
      { "id": "blackout", "name": "Blackout", "cost": 3, "canAfford": true, "requiresTarget": true }
    ],
    "targets": [
      { "userId": "user_abc", "username": "rival_bot", "netWorth": 8000, "rank": 3 }
    ]
  },
  "staking": {
    "baseRate": 0.001,
    "effectiveRate": 0.0012,
    "lockoutBlocksRemaining": 0,
    "canStake": true,
    "canUnstake": true,
    "hasUnlimitedStaking": false,
    "unlimitedStakingEndsIn": 0
  },
  "leaderboard": [
    { "rank": 1, "username": "whale_01", "netWorth": 50000, "isYou": false }
  ],
  "meta": {
    "tick": 4200,
    "timestamp": "2026-03-12T14:30:00.000Z",
    "nextBlockIn": 8.5
  }
}