Latest pachinko-pybind: 0.9.3 — betting bots need 0.9.0 or newer.

Betting API

Write a bot that watches live matches and trades the win-probability market— separately from the bots that play them. A betting bot sees exactly what a human spectator sees (the whole map, every seat's army, items and chips, the scoreboard, the poker table), plus the live odds, the players' public records and its own wallet. Deterministic script or RL policy — same runtime, online and offline.

Overview

Every match has one winner market: an automated market maker (LMSR) quotes a price for each seat, between 0 and 1, and the prices sum to 1. A YES share on a seat pays 1 credit if that seat wins; a NO position pays 1 if it loses. You buy with an integer spend, you can sell YES shares back, and the market settles at the end of the match. When a seat folds or is eliminated its price goes to 0 and it stops trading.

Your bot runs on your machine with a bbot_…token. It subscribes to a stream that delivers one observation about twice a second per match — the spectator view (3 s behind the live game, like the watch page) with the live market attached — and sends orders through the REST endpoint. Betting bots have their own wallet, topped up from your account; they can never bet on a match you or your playing bots are in.

Quick start

1. Go to My betting bots, create a bot and copy its token (shown once). Top it up from your wallet.

2. Install the SDK and write a Bettor:

pip install -U pachinko-pybind

from pachinko_sdk import Bettor, Buy, Sell, run_bettor

class Favourite(Bettor):
    """Back the scoreboard leader once the game is a minute in."""

    def on_observation(self, obs):
        m = obs.market
        if m is None or not m.is_open or obs.tick < 24 * 60:
            return []
        scores = obs.view.scoreboard.scores
        leader = max(m.live_outcomes(), key=lambda s: scores[s])
        if obs.account.shares(leader) == 0 and m.price(leader) < 0.5:
            return [Buy(leader, spend=200)]          # YES on the leader
        return []

    def on_fill(self, fill):
        print("filled" if fill.ok else "rejected", fill.order, fill.trade or fill.error)

run_bettor(Favourite(), token="bbot_...paste_your_token_here...")

3. Run it. run_bettor subscribes to every live match (pass matches=[...] for specific ones), calls on_observation for each update, executes the orders you return and reconnects if the connection drops. Watch the market move on the watch page.

What your bot sees

on_observation(obs) receives a BettingObservation. The game part is the same frame human spectators get, decoded into the playing SDK's own Unit / Building / ResourceNode dataclasses — no fog, every seat.

NameTypeDescription
obs.tick / obs.phase / obs.delay_msint / str / intGame clock (24 ticks = 1 s); waiting · mulligan · running · betting · ended; how far behind live (3000).
obs.view.units, .buildingslistEvery unit and building on the map with its owner slot. units_of(slot), buildings_of(slot), upgrade_levels(slot).
obs.view.resources, .items, .buffsper slotHematite, the items each seat holds (as the Items tab shows them), the buffs they confer.
obs.view.scoreboardScoreboardscores (0–400), stakes, bounties, status (0 active, 1 folded, 2 eliminated).
obs.view.tablePokerTable | NoneThe in-match poker round: pot, current bet, each seat's bankroll / committed / status. Never the cards.
obs.view.resource_nodes, .stars, .dropped_items, .eventslistMap state and the events since the last observation.
obs.view.alive, .bankrolls, .cumulative_pot, .winner_slotper slotContention, poker chips, the running pot, the winner once ended.
obs.marketMarketStateLive: prices per seat, dead (collapsed) seats, q, b, volume, status, min/max spend.
obs.accountAccountYour balance, positions (shares per seat), their mark-to-market, and your cost basis on this match: net_spend, yes_spend / no_spend per seat (survives a reconnect).
obs.tradeslist[TapeTrade]Every market trade since the previous observation: outcome, side, action, shares, cost, prices after, and who placed it — house (a house bettor), bot or user. Model the house flows.
obs.rawdictThe observation exactly as it came off the wire, for your own featurizers.
obs.matchMatchInfoSeats with public profiles (matches, 1st rate, podium rate, P&L, fold rate, recent placements), the map, the rules: buy-in, blinds, b, spend limits, max_ticks (the time cap — at the cap the highest score wins), lobby_kind (standard 60 min, pybot 15 min, floor = all-AI), idle_end_secs, house_band.
obs.view.as_seat(slot, obs.match.map)GameStateThe frame as the playing SDK's GameState from that seat — reuse a playing-bot model or Featurizer on any seat.

on_match_start(match) gives you the MatchInfo when you subscribe; on_match_end(obs, end) the result. Profiles are the same numbers the leaderboard and profile pages show, built by the same code.

The market & orders

Prices

LMSR with liquidity b = 2500: cost C(q) = b · ln Σ e^(q_k/b), price p_i = e^(q_i/b) / Σ e^(q_k/b). A trade moves the price; the whole state (q, b) is public, so BettingClient.preview_buy(market, outcome, spend)tells you exactly what you'd get before you trade. The house seeds each market with ⌈b·ln N⌉ credits and takes no fee; whatever is left after settlement returns to the house.

Trading is open from the moment the market exists (the pre-game betting round, while the seats' buy-ins settle) until the match ends or the result is decided — including the paused phases (mulligan, the poker rounds), during which frames keep arriving with the table's statuses while prices stay live. The closed forms for sizing — the credits that move a price to a target, the shares to sell to reach one, the legs that move the whole vector — ship in pachinko_sdk.lmsr (yes_spend_to_price, no_spend_to_price, yes_shares_to_price, trade_to_legs).

Orders

NameTypeDescription
Buy(outcome, spend, side="yes", max_price=None)10 ≤ spend ≤ 100,000 creditsYES shares on that seat (pay 1 each if it wins). max_price rejects the order (409) instead of filling it if the average price per share would be higher — the slippage guard.
Buy(outcome, spend, side="no", max_price=None)sameA NO bundle: one YES share on every other live seat — pays 1 if this seat loses. Priced at 1 − p; max_price applies per bundle.
Sell(outcome, shares, side="yes", min_price=None)YES shares you holdRefunded at the current price, rounded down to whole credits; min_price rejects a worse average refund.
Sell(outcome, shares, side="no")NO bundles you holdSells one YES share of every other live seat in a single order (one rate-limit token, not one per leg).
Batch([legs…])1–8 Buy/SellAtomic: every leg fills or none does, prices move leg by leg, one rate-limit token. A Buy leg may name shares instead of spend.
TradeTo(prices, spend=None)a price vectorMove the live prices to your estimate in one atomic batch of YES buys sized by the closed form; costs exactly the minimum unless you pass a larger spend.

Trades are rejected on seats that are out (price 0), once the result is decided, and after the match ends. Draws and cancelled matches void the market: everyone gets their net spend back.

Offline testing

run_local_bettorsplays a match of built-in AIs with your bettors watching and trading an offline copy of the market. By default it is the live setup: the spectator frame is built by the server's own code, delivered 3 s (72 ticks) behind the game twice a second, seats drop out of the market the moment the delayed frame shows them folded or eliminated, and the market seeds, prices, fills, refunds and settles like the live one. Deterministic by seed.

from pachinko_sdk import run_local_bettors, house_bettors

bettors = {"mine": Favourite(), **house_bettors(seed=1)}
res = run_local_bettors(bettors, num_players=8, seed=42, balance=20_000)
print(res)            # winner, ticks, market status, trades, P&L per bettor
print(res.pnl["mine"])

Every rule is a keyword (delay_ticks, stride_ticks, lmsr_b, max_ticks, the engine's scenario knobs) for experiments; the defaults are the live values. Profiles are empty offline (the AIs have no public record); online and offline observations are otherwise built identically.

House bettors

Five deterministic bettors trade every live market under the House account, and are the default opponents offline. They are variants of one small fitted linear model of the spectator view (pachinko_sdk.linear_bettor, fitted on 1,149 offline all-AI matches) that look at different parts of the board and trade at different speeds, so the house's prices carry information without being five copies of one opinion. Each sizes an order from the market itself: it never moves a price by more than a set fraction of the gap to its own estimate, under a Kelly budget and a hard cap, and only closes a position once its edge has clearly flipped. Their wallets live in a band (obs.match.rules.house_band): refilled to 20,000 when below 5,000, skimmed back to 20,000 when above 40,000, so house money cycles rather than piling up. Read the code, extend it, beat it.

NameTypeDescription
fullevery feature, 30 sThe reference estimate: score, army and building shares, production, nodes, hematite, upgrades, threat in and out, army rank.
tacticalno scoreboard, 30 sBlind to the score and the rank: what the armies, buildings, nodes and threats say.
economyno threat or rank, 45 sWhat each seat owns and produces.
scoreboardscore, army share, rank, 60 sThe board at a glance; smaller Kelly fraction.
patientevery feature, 90 sSlow and sure: a wider edge threshold, smaller price moves, lower cap.

The whole line-up runs from one stream at a frame every 5 s (pachinko_sdk.run_bettors) and acts every 30–90 s. The simple bettors of the first releases (FormBettor, ScoreboardBettor,MomentumBettor, NoiseBettor) stay in the SDK assimple_house_bettors() if you want them as opponents.

Fairness

Everything game-derived is delayed 3 s on the server before anyone— human or bot — sees it; the sidebar on the watch page and the bettor stream read the same frames. Prices are live for everyone. Seats are collapsed out of the market from that same delayed frame, before it is sent out, so nobody can race the market maker to a dead seat. Hands are never visible until items are applied. You cannot trade on a match that you, or a playing bot you author, are seated in.

API reference

Base URL https://pachinko-rts.com/betting-api/v1. Reads need no token; trades, /me and the stream need Authorization: Bearer bbot_…. pachinko_sdk.markets.BettingClient wraps all of it.

REST

NameTypeDescription
GET /matches—Running matches: seats, market summary, feed (stream up), tick, phase.
GET /matches?status=ended&limit=50&before=—Finished matches, newest first: winner, end reason, placements, seats, the settled market with its final prices. Page with before (ISO time). For backtests.
GET /matches/:id—One match: every seat with its public profile, the market, and while the feed is up the map (every spawn is real), rules and mapSeed. Works after the match too.
GET /matches/:id/live—The newest BettingObservation (JSON), account omitted.
GET /markets/:id—Market state: q, b, prices, dead, volume, status, min/max spend.
GET /markets/:id/history—Price series (every trade and collapse), thinned to ≤ 300 points.
GET /markets/:id/trades—Last 50 trades, anonymous.
POST /markets/:id/tradetoken{outcome, side: yes|no, action: buy|sell, spend | shares, maxPrice?, minPrice?}. A buy may name shares instead of spend. Idempotency-Key header honoured.
POST /markets/:id/tradestoken{legs: [1–8 of the above]}, executed atomically; one rate-limit token; the Idempotency-Key covers the whole batch.
GET /metokenYour wallet, open positions, last 50 trades.
GET /players/:name/stats?kind=—Full public profile; kind=pybot for bot-authored seats.

Stream

wss://pachinko-rts.com/betting-api/v1/stream with the same bearer token (or ?token=). Send JSON; receive JSON messages:

NameTypeDescription
{"subscribe": {"all": true}}→ serverEvery live match, including ones that start later. Or {"matches": [ids]} (≤ 10).
{"unsubscribe": {...}}, {"hz": 1}→ serverDrop matches; cap the cadence (feed max 2 Hz).
hello← serverYour bot, the cadence, the matches with a live feed, the SDK floor.
match← serverMatchInfo on subscribe: seats + profiles, map, rules.
observation← serverA BettingObservation, ≤ 2/s per match, with trades (the tape since the previous one). account is null here —
account← server— your balance and positions arrive separately: on subscribe and after each of your fills.
end← serverThe match ended: phase, winner slot, market status.
error← servercode + message (401 token, 404 no feed, 426 SDK too old, 429 too many subscriptions).

One stream per bot: a new connection replaces the old one. Orders still go through POST /markets/:id/trade; the SDK does that for you.

Limits & versions

Reads: 120 requests/min per IP. Trades: 60/min per bot, a batch counting once. Stream: one connection per bot, observations at most twice a second per match. Up to 5 betting bots per account.

The stream checks the SDK version you announce (X-Pachinko-SDK, sent by the SDK) and refuses versions below its floor with code 426 and an upgrade message: pip install -U pachinko-pybind. Hand-written clients that send no version are let through.