API documentation

Step-by-step guide for beginners

What you need

  • A FlipSight account. You can register a bot and download the connector after signing in.
  • One bot photo: a GIF, JPG or PNG file of 5 MB or less, at least 175 px wide and tall.
  • Your own computer to run the bot Your bot's moves are calculated on the computer where the connector runs. Even with the same engine, strength can differ with the computer's speed and the engine settings, and if you change the computer or the settings, the rating can move to match.
  • Python 3, a free program that runs the connector. Version 3.9 or later works; 3.11 or later is recommended. You check it in step 5.
  • Your bot engine. A practice engine for testing is included with the connector.

1. Register your bot

Sign in and open the bot page (othelloclub.com/en/flipsight/bot). In the Register my bot box at the top, enter a name in Bot name (2–24 characters). Leave Supported games with only Standard checked for now. Choose a picture in Upload bot photo, then press the Register my bot button.

Success looks like this: the Connection token (shown only once) box appears with a long text starting with fsbm_.

2. Keep the token

This long text is the token, a password just for your bot. The connector uses it to prove to the server that it speaks for this bot. Copy all of it and paste it somewhere temporary, such as a text editor. Once you close the box, you cannot see the same token again.

Treat the token like a password. Do not show it to anyone or post a screenshot of it. If you lose it or someone sees it, open Manage in the bot list and press Issue connection token. When you get a new token, the old one stops working immediately.

3. Download the connector

Press FlipSight bot connector bundle (.zip) to download the ZIP file and extract it. This guide calls the extracted folder the "bot folder". Put it somewhere easy to find, such as the desktop.

Success looks like this: the bot folder contains the README.md file and the tools folder.

4. Open a terminal in the bot folder

A terminal is a window where you type commands. Open it as follows for your system.

macOS: press Command+Space, type Terminal and open it. Type cd and one space, drag the bot folder into the window, then press Enter.

Linux: in the file manager, right-click the bot folder and choose Open in Terminal. The menu name can differ between distributions. If there is no such menu, open a terminal and use cd as on macOS.

Windows: open the bot folder in File Explorer. Type powershell in the address bar at the top and press Enter. A PowerShell window opens in that folder.

Check: type the command below and press Enter. Success means README.md and tools appear in the list. This command is the same on macOS, Linux and Windows PowerShell.

ls

5. Check Python

In the same window, copy the one command for your system, paste it and press Enter. You can use the copy button at the top right of each code box.

macOS / Linux

python3 --version

Windows PowerShell

py --version

Success looks like this: a version starting with 3, such as Python 3.11.9. Version 3.9 or later works. If the command is not found, install Python as below, close the window and start again from step 4.

macOS: download the macOS installer (.pkg) from python.org and install it. Then run Install Certificates.command once from the Python folder in Applications. This prepares the certificates needed for secure (HTTPS) connections.

Linux: install python3 with your distribution's software manager or package manager.

Windows: install the Python install manager from python.org.

python.org

6. Enter the token

Put the token into an environment variable, a temporary setting that exists only inside this terminal window. Its name is FS_BOT_TOKEN. Copy the one-line command for your system below, paste it and press Enter once. Bot token then appears and waits for your input. Now paste the token you copied in step 2 and press Enter.

macOS / Linux

printf 'Bot token: '; read -r -s FS_BOT_TOKEN; printf '\n'; export FS_BOT_TOKEN

Windows PowerShell

$env:FS_BOT_TOKEN = [Net.NetworkCredential]::new('', (Read-Host 'Bot token' -AsSecureString)).Password

On macOS and Linux the pasted token is not shown, and on Windows you see * instead of its characters. Both are normal. Because the token is not written into the command, it does not stay in the command history. The setting lasts only in this window. If you closed it or opened a new window, repeat steps 4 and 6.

Windows: in PowerShell, paste with Ctrl+V or a right-click. This one-line command uses syntax that works in both Windows PowerShell 5.1, which comes with Windows, and the newer PowerShell 7, but it has not been tested on a real Windows machine yet.

Success looks like this: the prompt for the next command returns without an error.

7. Start the connector

In the same window, copy the command below as it is, paste it and press Enter. Do not change the file names in it. Your bot connects with the practice engine, a very weak engine that plays the legal square with the lowest number.

macOS / Linux

python3 tools/bot_match_client.py --engine 'python3 tools/bot_match_example_engine.py'

Windows PowerShell

py tools/bot_match_client.py --engine 'py tools/bot_match_example_engine.py'

It is normal for this command to produce no text and not return to the prompt while running. To keep your bot online, leave this window open with the connector and engine running, and prevent your computer from sleeping. Closing this window or pressing Ctrl+C stops the connector and engine, and your bot goes offline. Run the command above again to bring it back online. The connector automatically accepts match requests from other bots.

While the bot runs with the practice engine it gets no automatic matches, and bot-vs-bot games it plays, even ones you request, are not rated. Restart it with your own engine (step 10) and it is matched and rated from then on.

Success looks like this: in the Registered bots list on the bot page, the dot to the right of your bot's name is green. Hovering over it shows Online. If it is grey (Offline), press Refresh.

8. Test with a first game

On the bot page, choose your bot in My playing bot. Pick any bot in the FlipSight bots list, press Challenge and confirm in the window that opens. In every game type the server calculates the FlipSight bot's moves, so you do not need to keep the game room open. With auto matching on (the default), the site also sets up bot matches for you while the connector is running.

Success looks like this: the game room shows two boards and a disc is placed on each of your bot's turns. The practice engine is very weak, so losing is normal.

9. Keep it running after closing the window (optional)

Start with the command below instead of step 7, and the bot keeps running after you close the window. Run it in the same window where you entered the token (step 6). It stops if the computer shuts down or goes to sleep. Stopping it during a game loses that game on time or by disconnection.

macOS / Linux

macOS / Linux: after running the start command you can close the window. Messages are written to the bot.log file in the bot folder. To stop it, open a new terminal and run the stop command. Copy only the box you need: pasting both boxes at once stops the bot right after it starts.

Start

nohup python3 tools/bot_match_client.py --engine 'python3 tools/bot_match_example_engine.py' > bot.log 2>&1 &

Stop

pkill -f bot_match_client.py

Windows PowerShell

Windows: a minimized window appears in the taskbar and the bot runs in it. You can close the current PowerShell window. To stop it, open the minimized window and press Ctrl+C, or close that window.

Start

Start-Process py -ArgumentList 'tools/bot_match_client.py --engine "py tools/bot_match_example_engine.py"' -WorkingDirectory $PWD -WindowStyle Minimized

10. Switch to your own engine

After checking the connection with the practice engine, switch to your own. Use the dedicated options below for Edax or Egaroucid, --nboard for other compatible engines, or JSON for your own engine. Keep the same token.

Check a variant game (Anti, 10x10, Octagon, classic Reversi) in Supported games only when your engine knows its rules. Other bots and people can challenge your bot in every checked game. To change it, open Manage in the bot list, edit Supported games and press Save. You cannot change it while a game is running or a challenge is waiting.

Engine request examples by game

Connect an NBoard-compatible engine, such as Edax

NBoard is an Othello graphical application that can be used with NTest. The NBoard protocol is its published format for communicating with engines, and works with engines that implement it, such as NTest and Edax.

The installation and launch examples below use Edax 4.6 and Egaroucid for Console 7.8.2.

Edax 4.6 is not included with the connector. Download the file for your operating system from Assets on Edax 4.6, place it in the bot folder, and extract it with the preparation commands below. The Windows and Linux commands target x86-64 computers.

Windows PowerShell (x86-64): edax-4.6-MS-windows-x86.zip

Prepare (first time only)

Expand-Archive -LiteralPath edax-4.6-MS-windows-x86.zip -DestinationPath edax-4.6

Start

py tools/bot_match_client.py --edax "edax-4.6\wEdax-x86-64.exe"

Linux (x86-64): edax-4.6-linux-x86.tar.gz

Prepare (first time only)

mkdir -p edax-4.6
tar -xzf edax-4.6-linux-x86.tar.gz -C edax-4.6
chmod +x edax-4.6/lEdax-x86-64

Start

python3 tools/bot_match_client.py --edax "edax-4.6/lEdax-x86-64"

macOS: edax-reversi-4.6.tar.gz

There is no official macOS executable for Edax 4.6. Download Source code (tar.gz) and edax-4.6-linux-x86.tar.gz from the same release. Build the Mac executable from source; extract only the shared data/eval.dat file from the Linux archive. Xcode Command Line Tools are required. The preparation commands below compile the source.

Prepare (first time only)

xcode-select --install
tar -xzf edax-reversi-4.6.tar.gz
mv edax-reversi-4.6 edax-4.6
mkdir -p edax-4.6/bin
tar -xzf edax-4.6-linux-x86.tar.gz -C edax-4.6 data/eval.dat
(cd edax-4.6/src && make build ARCH=native OS=osx)

Start

python3 tools/bot_match_client.py --edax "edax-4.6/bin/mEdax-native"

Edax can also be connected with just its executable path using --edax below. No separate adapter is needed; the connector allocates thinking time for each move from the remaining game clock.

Replace the command path with the full path to your executable. Put paths containing spaces in double quotes. If the evaluation file is not found automatically, use --edax-eval to specify eval.dat.

An NBoard connection plays standard 8×8 games only. Keep only Standard checked in Supported games for this bot. If a variant game arrives, the connector does not ask the engine, no move is played, and the game is aborted or lost on time.

Egaroucid: --egaroucid

Prepare Egaroucid for Console. Select the console executable rather than the graphical application, and keep its resources folder in place. On macOS and Linux, use an executable built following the official instructions. The connector sends the position and remaining time with the native setboard, settime and go commands, so no adapter is needed.

Egaroucid for Console

Windows PowerShell
py tools/bot_match_client.py --egaroucid "egaroucid\Egaroucid_for_console.exe"
macOS / Linux
python3 tools/bot_match_client.py --egaroucid "egaroucid/bin/Egaroucid_for_Console.out"

Both modes support standard 8×8 only and run one engine per game when two games run together. --max-think-seconds is the per-move safety cap (10 seconds by default). If Egaroucid reaches that cap, the connector stops that search, plays a legal fallback move and restarts the engine on the next turn.

Self-made engine: connect through JSON

Read this part only if you want to connect an engine you wrote yourself. If you use Edax, Egaroucid or another NBoard engine, you are already done above and can skip it.

Each time it is your bot's turn, the connector runs my_adapter.py once. It hands over the current position as one line (where the discs are, whose turn it is, which squares are legal, how much time is left), my_adapter.py answers with one square as one line, and the connector plays that move on the server. my_adapter.py is a small Python file in between that asks your engine for the move. The connector takes care of the server connection, the token and move submission, so this file never deals with them.

FlipSight→ bot_match_client.py→ my_adapter.py
FlipSight← bot_match_client.py← my_adapter.py

Which kind is your engine?

  • An engine that picks its move in Python code: use example A below.
  • A program file that prints one answer line and exits each time it runs: use example B below.
  • An executable that stays open and keeps exchanging commands: if it supports NBoard, connect it with --nboard above. For another protocol, adapt example B to that protocol and make my_adapter.py keep the engine open and loop, exchanging one line at a time.

Step 1: a file that only checks the connection (my_adapter.py)

In the bot folder (where README.md and tools are), create a file named my_adapter.py, paste the code below as it is and save it. This file uses no engine and simply answers with the first legal square. It is there to confirm the connection works before you attach your engine.

import json
import sys

def choose_move(position):
    legal = position["legal_moves"]
    return legal[0] if legal else -1

line = sys.stdin.readline()
position = json.loads(line)
move = choose_move(position)
print(json.dumps({"move": move}), flush=True)

Check input and output before connecting

The command in box 1 feeds the example position straight into the adapter. It should print one line: {"move": 19}. Once you see that line, the command in box 2 starts the connector with this adapter, in the window that has the token from step 6. Run the two boxes separately. If your engine reads this JSON format itself, you can put its command directly after --engine.

1. Test the adapter alone

macOS / Linux

printf '%s\n' '{"board":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[19,26,37,44],"game_mode":"std","board_size":8,"blocked_squares":[],"placed":{"1":0,"2":0},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}' | python3 my_adapter.py

Windows PowerShell

'{"board":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[19,26,37,44],"game_mode":"std","board_size":8,"blocked_squares":[],"placed":{"1":0,"2":0},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}' | py my_adapter.py

2. Start the connector with the adapter

macOS / Linux

python3 tools/bot_match_client.py --engine 'python3 my_adapter.py'

Windows PowerShell

py tools/bot_match_client.py --engine 'py my_adapter.py'

Step 2: switch to a file that calls your engine

Once step 1 works, replace the whole content of my_adapter.py with example A or B below. Change only the lines marked EDIT and leave the rest as it is. Then run test command 1 above again and check that one answer line appears. Your engine chose the move, so a square other than 19 is normal.

Example A: an engine that picks its move with a Python function

This example assumes a file named my_engine.py with a search function that picks the move. At EDIT 1 change the file name and function name, and at EDIT 2 change the values passed in, to match your engine. The function should return the square number (0 to 63) or a coordinate such as d3.

import json
import sys
from pathlib import Path

from my_engine import search  # EDIT 1: your engine file and function name


def think_ms(position):
    # Time for this move: share the remaining clock over my remaining moves.
    my_moves_left = position["board"].count(0) // 2 + 1
    share = position["remaining_ms"] / my_moves_left
    return int(min(share, position["budget_ms"]) * 0.8)


def choose_move(position):
    if not position["legal_moves"]:
        return -1
    # EDIT 2: pass what your function needs
    return search(
        board=position["board"],
        color=position["color"],
        legal_moves=position["legal_moves"],
        time_limit_ms=think_ms(position),
    )


position = json.loads(sys.stdin.readline())
legal = position["legal_moves"]
try:
    move = choose_move(position)
except Exception as error:
    # The engine failed: note why in my_adapter.log and play the first legal square.
    with open(Path(__file__).with_name("my_adapter.log"), "a") as log:
        log.write(repr(error) + "\n")
    move = legal[0] if legal else -1
print(json.dumps({"move": move}), flush=True)
Example B: an engine that is a program file

This example assumes the program reads one line with 64 board characters, the side to move and the thinking time in milliseconds, and answers with one coordinate line such as D3 (. = empty, X = black, O = white). Every engine has its own format, so change the program file name at EDIT 1, the line sent to the engine at EDIT 2 and the part that reads the answer at EDIT 3 to match your engine.

my_adapter.py → my_engine

...........................OX......XO........................... X 1777

my_engine → my_adapter.py

D3
import json
import subprocess
import sys
from pathlib import Path

# EDIT 1: your engine program file (kept in the same folder as my_adapter.py)
ENGINE = Path(__file__).with_name("my_engine.exe" if sys.platform == "win32" else "my_engine")


def think_ms(position):
    # Time for this move: share the remaining clock over my remaining moves.
    my_moves_left = position["board"].count(0) // 2 + 1
    share = position["remaining_ms"] / my_moves_left
    return int(min(share, position["budget_ms"]) * 0.8)


def choose_move(position):
    if not position["legal_moves"]:
        return -1
    # EDIT 2: build the line your engine reads
    board = "".join(".XO"[cell] for cell in position["board"])
    color = "X" if position["color"] == 1 else "O"
    reply = subprocess.run(
        [str(ENGINE)],
        input=f"{board} {color} {think_ms(position)}\n",
        text=True,
        capture_output=True,
        check=True,
        timeout=position["budget_ms"] / 1000 * 0.9,
    )
    # EDIT 3: take the square out of your engine's answer
    return reply.stdout.strip()


position = json.loads(sys.stdin.readline())
legal = position["legal_moves"]
try:
    move = choose_move(position)
except Exception as error:
    # The engine failed: note why in my_adapter.log and play the first legal square.
    with open(Path(__file__).with_name("my_adapter.log"), "a") as log:
        log.write(repr(error) + "\n")
    move = legal[0] if legal else -1
print(json.dumps({"move": move}), flush=True)

Thinking time: a bot game gives each bot 1 minute per board, so long thinking on one move quickly loses on time. think_ms in the examples divides the remaining time (remaining_ms) by your remaining moves to decide the time for this move. budget_ms is not the time to spend on this move. It is the longest the connector waits for the answer (10 seconds by default). If the answer comes later than that, the bot cannot continue on that board and loses it on time, so pass think_ms to your engine, not budget_ms.

When something breaks: if the engine call fails, the example files write the reason to my_adapter.log and play the first legal square instead. If the moves look odd, open that file first. The connector does not show the adapter's error messages on screen. Use print only for the one answer line and write any debug text to a file.

Speed: the connector keeps my_adapter.py running for each game and sends one line per move. A file that answers one line and exits, like the examples, is started again on the next move, so your engine also starts again each time. If the engine is slow to start or should keep its work from earlier moves, make my_adapter.py loop: read one line, answer one line, repeat. Do not read input to the end, and flush after every answer.

The rest of this part is reference material on what the connector and my_adapter.py exchange. If you use the example files above as they are, you do not need to read it.

Input JSON (connector → adapter)

This is example data sent by the connector, not executable code. Do not save it as a Python file. During a game, the connector sends the current data automatically on each turn.

Below is an example of the standard 8×8 starting position. In real games the connector fills in the current position. board lists squares from a1 to the right, then the next row; 0=empty, 1=black, 2=white. color=1 asks for a black move. legal_moves lists the squares you may play. remaining_ms is the remaining game clock and budget_ms is the longest the connector waits for the answer to this move, both in milliseconds (1000 = 1 second). Fields and examples for variant games are in the variant games section below.

{"board":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[19,26,37,44],"game_mode":"std","board_size":8,"blocked_squares":[],"placed":{"1":0,"2":0},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}

Response JSON (adapter → connector)

Write one of the replies below as a single JSON line ending with a newline. move is a square number or a coordinate, case-insensitive. On 8×8, d3 is (3−1)×8+3 = 19. Always choose from the legal_moves you received. In bot pairs with no legal move, send -1 or pass; the server resolves forced passes in human games. Write only the reply line to stdout, with no other messages.

{"move":19}
{"move":"d3"}
{"move":-1}

Engine request examples by game

Examples of Black's first request that the connector sends to an --engine engine, generated with the rule code. Real games of every type start from a random 12-disc position, so board and legal_moves differ in every game. To keep these examples fixed, their opening moves were chosen near the center of the board. match_id and the clock values also differ in real games.

  • game_mode: One of std, anti, grand, octagon, reversi. XOT in human games arrives as std.
  • board_size: 8, or 10 for grand and octagon.
  • board: Array of length board_size². Read from a1 to the right, then the next row. 0=empty (including Octagon's blocked squares), 1=black, 2=white.
  • blocked_squares: Unusable Octagon squares. An empty array in other games.
  • placed: Discs placed so far per color, including the random opening moves; keys are the strings "1" and "2". Used for the 32-disc limit of classic Reversi.

Engine request fields

Anti (anti)

Input JSON (connector → adapter)

{
    "board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    "color": 1,
    "legal_moves": [2, 3, 12, 25, 42, 44, 45],
    "game_mode": "anti",
    "board_size": 8,
    "blocked_squares": [],
    "placed": {
        "1": 4,
        "2": 4
    },
    "remaining_ms": 60000,
    "match_id": 123,
    "leg": 0,
    "revision": 0,
    "history": [],
    "round_no": null,
    "room_created_at": null,
    "budget_ms": 10000
}

Response JSON (adapter → connector)

{"move":2}
Grand (10x10) (grand)

Input JSON (connector → adapter)

{
    "board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    "color": 1,
    "legal_moves": [13, 14, 25, 42, 63, 65, 66],
    "game_mode": "grand",
    "board_size": 10,
    "blocked_squares": [],
    "placed": {
        "1": 4,
        "2": 4
    },
    "remaining_ms": 60000,
    "match_id": 123,
    "leg": 0,
    "revision": 0,
    "history": [],
    "round_no": null,
    "room_created_at": null,
    "budget_ms": 10000
}

Response JSON (adapter → connector)

{"move":13}
Octagon (octagon)

Input JSON (connector → adapter)

{
    "board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    "color": 1,
    "legal_moves": [13, 14, 25, 42, 63, 65, 66],
    "game_mode": "octagon",
    "board_size": 10,
    "blocked_squares": [0, 1, 10, 8, 9, 19, 80, 90, 91, 89, 98, 99],
    "placed": {
        "1": 4,
        "2": 4
    },
    "remaining_ms": 60000,
    "match_id": 123,
    "leg": 0,
    "revision": 0,
    "history": [],
    "round_no": null,
    "room_created_at": null,
    "budget_ms": 10000
}

Response JSON (adapter → connector)

{"move":13}
Classic Reversi (reversi)

Input JSON (connector → adapter)

{
    "board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 2, 2, 1, 1, 0, 0, 0, 0, 2, 2, 1, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    "color": 1,
    "legal_moves": [18, 25, 33, 41, 42, 50, 51],
    "game_mode": "reversi",
    "board_size": 8,
    "blocked_squares": [],
    "placed": {
        "1": 6,
        "2": 6
    },
    "remaining_ms": 60000,
    "match_id": 123,
    "leg": 0,
    "revision": 0,
    "history": [],
    "round_no": null,
    "room_created_at": null,
    "budget_ms": 10000
}

Response JSON (adapter → connector)

{"move":18}

Common problems

SymptomCause and fix
The bot shows a grey dot (offline).Check that the connector window is still running. It stops when you close the window, press Ctrl+C or the computer sleeps. Repeat steps 6 and 7, then press Refresh.
It says the command was not found.
command not found / is not recognized
Python is missing, or the window was not reopened after installing it. See step 5, open a new window and start again from step 4.
It says the file cannot be opened.
can't open file
You ran it outside the bot folder. Move to the bot folder as in step 4 and check with ls that README.md and tools are listed.
It says the token is missing and exits.
FS_BOT_TOKEN is missing or invalid
This window has no token. If you opened a new window, repeat step 6.
It says the token was rejected and stops.
Bot credential rejected
The token was pasted incorrectly or is an old one. After you issue a new token or disable the bot, the old token no longer works. Start again from step 6 with the latest token.
The same error line keeps appearing.
Connection/state error
Check the internet connection. On macOS, check that you ran Install Certificates.command from step 5. If it appears only during games, the server may be rejecting your engine's move as illegal. The connector sends the same move again, so the engine has to be fixed.
The challenge button does not work, or the challenge is not accepted.Both bots must have a green dot. Check that your bot is selected in My playing bot. A bot already in another game does not take a new challenge. A challenge that is not accepted expires after 120 seconds.
The bot makes no move and then loses on time or the game is aborted.
Engine failed for match
The engine did not answer in time or its answer had the wrong format. Check the engine command and its one-line JSON output. If a bot makes no first move within 30 seconds after the start, the game is aborted.
No variant game challenges arrive.The challenge window lists only games that both bots support. Check that the game is checked in your bot's Supported games and that the other bot supports it too. A bot connected through NBoard should have only Standard checked.

Glossary

TermMeaning
APIAn agreed way for programs to send requests and replies. The connector handles it for you.
TokenA password just for your bot. The connector uses it to prove to the server which bot it is.
TerminalA window where you type commands. On Windows, use PowerShell.
Environment variableA temporary setting passed to programs started from one terminal window. It disappears when the window closes.
JSONA format for writing data as text. Example: {"move":19}
EngineA program that looks at a position and chooses a move.
AdapterA small program that converts data between your engine and the connector.
NBoardNBoard is an Othello graphical application that can be used with NTest. The NBoard protocol is its published format for communicating with engines, and works with engines that implement it, such as NTest and Edax.
Legal moveA square you may play now. The server lists them in legal_moves.

Downloads

FlipSight bot connector bundle (.zip)

OpenAPI 3.1 JSON

README.md

Sign in to download. The first item is a .zip archive containing the README and the connector/example engine in tools/. The API specification (.json) and user guide (.md) are separate downloads.

Detailed specification

The rest of this page is detailed reference for developers who work with an engine or the API directly.

Connector options

Options for tools/bot_match_client.py. Exactly one of --engine and --nboard / --edax / --egaroucid is required. The token is never an option; it is read only from the FS_BOT_TOKEN environment variable. Exit codes: 0 normal stop, 2 token rejected, 3 --max-runtime reached.

ParametersDescription
--engine "COMMAND"Starts this command once per game and exchanges one JSON line per move without closing its input. An engine that answers one line and exits is started again on the next move.
--nboard "COMMAND"Keeps one NBoard engine per leg, two in total. Standard (std) only.
--edax PATHEdax can also be connected with just its executable path using --edax below. No separate adapter is needed; the connector allocates thinking time for each move from the remaining game clock.
--edax-eval PATHPath to Edax eval.dat. If omitted, data/eval.dat is located in the executable directory or its parent.
--egaroucid PATHPrepare Egaroucid for Console. Select the console executable rather than the graphical application, and keep its resources folder in place. On macOS and Linux, use an executable built following the official instructions. The connector sends the position and remaining time with the native setboard, settime and go commands, so no adapter is needed.
--nboard-depth NSends set depth N to the NBoard engine. 0 to 60; 0 keeps the engine default. (--edax: 0 → 60).
--max-think-seconds SMaximum seconds to wait for the engine per move. Default 10, maximum 300. Never longer than the remaining clock minus 0.75 seconds. This is a connector safety cap; the server has no per-move time limit. Set 0 to remove the per-move cap and use the remaining game clock.
--engine-name NAMEEngine name reported in the bot information (120 characters or fewer). Do not include commands or paths.
--cpu-name NAMECPU name to report (120 characters or fewer). Detected automatically when omitted.
--once-match IDHandles only this match and exits when it ends (finished, aborted, expired, rejected). Negative human-game IDs work too. For tests, combine it with --max-runtime.
--max-runtime SCleans up the engines and exits after this many seconds (exit code 3).
--no-streamDoes not use the event stream; polls only.
--api URL, --stream-url URLAPI and stream addresses. Normally keep the defaults. Only HTTPS is accepted, and because the token is sent along, the stream must be on the API host.

Implementing the API yourself

Receive challenges and positions with agent_poll or the authenticated event stream. Send agent_accept for pending challenges where you are bot_b, then calculate only positions where it is your turn in each leg of a running match. Submit at most once per match_id, leg and revision, and drop stale work when the state changes. Human games use a negative match_id and leg=0, and require round_no and room_created_at exactly as received. Normal rooms handle forced passes on the server; only bot pairs submit passes.

agent_poll / bot-stream → pending (bot_b = my_id)
→ agent_accept → running → legs[0] + legs[1]
→ agent_move(match_id, leg, revision, move) → finished / aborted

Variant games

A bot receives only the games checked in Supported games. The game_mode in engine requests and match state tells you the game. The server calculates board size, blocked squares and legal squares, so the engine only has to choose from the legal_moves it receives. Do not generate 10×10 or classic Reversi moves with standard 8×8 move generation.

Gamegame_modeBoardStarting discsClock (per leg, per color)Black's first legal_moves
Standardstd8×8Random 12-disc position60,000 msDepends on the position
Antianti8×8Random 12-disc position60,000 msDepends on the position
Grand (10x10)grand10×10Random 12-disc position60,000 msDepends on the position
Octagonoctagon10×10 (88)Random 12-disc position60,000 msDepends on the position
Classic Reversireversi8×8Random 12-disc position60,000 msDepends on the position

Common rules: Black (1) moves first. Both legs start from the same position with colors swapped and run at the same time. In bot pairs, when you have no legal move, legal_moves arrives as an empty array and the bot must send move=-1 (pass) itself. The server does not pass for you, and your clock keeps running. Passing while a legal move exists is illegal. A leg ends when neither color can move. Empty squares are given to nobody; only actual discs are summed over both legs, and equal totals are a draw. All bot matches start from a random position containing 12 discs. Every game type gives each bot 1 minute per board.

Anti: placing and flipping work as in standard Othello. The side with fewer discs over both legs wins.

10x10: standard rules on a 10×10 board (100 squares). Its initial setup is the four center squares; bot pairs add 8 random moves and start at 12 discs.

Octagon: the same board and starting setup as 10x10, but three squares in each corner, 12 in total, are unusable (88 playable squares). Blocked squares arrive as square numbers in blocked_squares and as 0, the same value as empty, in board. The array is not in numeric order. You cannot play on a blocked square, and lines of discs stop there.

Classic Reversi: by its rules the game starts on an empty board. While any of the four center squares (27, 28, 35, 36) is empty, you place only there and nothing is flipped. Once all four are filled, standard rules apply. Each color places at most 32 discs, including the first four moves. placed counts the discs each color has placed so far; its keys are the strings "1" (black) and "2" (white). Bot pairs start after the server has played 12 random placements, including the four center moves, so in the first request the two placed counts add up to 12 (usually {"1":6,"2":6}). A color that has placed all 32 receives an empty legal_moves and sends -1 on each of its turns.

Engine request examples by game

Examples of Black's first request that the connector sends to an --engine engine, generated with the rule code. Real games of every type start from a random 12-disc position, so board and legal_moves differ in every game. To keep these examples fixed, their opening moves were chosen near the center of the board. match_id and the clock values also differ in real games.

Anti (anti)

{"board":[0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,2,2,0,0,0,0,0,1,2,2,1,0,0,0,0,2,2,2,2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[2,3,12,25,42,44,45],"game_mode":"anti","board_size":8,"blocked_squares":[],"placed":{"1":4,"2":4},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}

Grand (10x10) (grand)

{"board":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[13,14,25,42,63,65,66],"game_mode":"grand","board_size":10,"blocked_squares":[],"placed":{"1":4,"2":4},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}

Octagon (octagon)

{"board":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,1,2,2,1,0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[13,14,25,42,63,65,66],"game_mode":"octagon","board_size":10,"blocked_squares":[0,1,10,8,9,19,80,90,91,89,98,99],"placed":{"1":4,"2":4},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}

Classic Reversi (reversi)

{"board":[0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,2,2,1,1,0,0,0,0,2,2,1,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[18,25,33,41,42,50,51],"game_mode":"reversi","board_size":8,"blocked_squares":[],"placed":{"1":6,"2":6},"remaining_ms":60000,"match_id":123,"leg":0,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}

Engine request fields

FieldTypeDescription
game_modestringOne of std, anti, grand, octagon, reversi. XOT in human games arrives as std.
board_sizeinteger 8 | 108, or 10 for grand and octagon.
boardinteger[board_size²]Array of length board_size². Read from a1 to the right, then the next row. 0=empty (including Octagon's blocked squares), 1=black, 2=white.
blocked_squaresinteger[]Unusable Octagon squares. An empty array in other games.
placed{"1": integer, "2": integer}Discs placed so far per color, including the random opening moves; keys are the strings "1" and "2". Used for the 32-disc limit of classic Reversi.
legal_movesinteger[]Squares you may play now. If empty, answer move=-1.
colorinteger 1 | 2Color to move. 1=black, 2=white.
budget_msintegerTime allowed for this move in milliseconds: the smaller of --max-think-seconds and the remaining clock minus 0.75 seconds. The connector stops the engine when it is exceeded.
remaining_msintegerYour color's remaining clock in this leg, in milliseconds, after subtracting the time already used this turn.
match_idinteger ≠ 0Match id. Negative (-room number) for human games.
leginteger 0 | 1Leg number 0 or 1. Always 0 for human games.
revisionintegerState number of this leg. Send it unchanged with agent_move.
historyarrayMoves played in this leg. Decide from board.
round_nointeger | nullRound number of a human game. null for bot pairs.
room_created_atstring | nullCreation time of the human game room. null for bot pairs.

Challenging another bot to a variant

Add game_mode to agent_challenge; it defaults to std. Both bots must have it in supported_games. Read the opponent's games from bots[].supported_games in the agent_poll response. If either bot lacks the game, the reply is HTTP 409 with the error below.

{"action":"agent_challenge","target_id":2,"game_mode":"anti"}
{"action":"agent_challenge","target_name":"Opponent","game_mode":"grand"}
HTTP 409
{"ok":false,"error":"unsupported game mode","server_now_ms":1800000000000}

Changing supported_games

The registration form starts with Standard checked, and older bots without a saved list support std. You can change the list only in Manage on the bot web page, never with the bot token (API). The change is refused while a match is running or waiting for acceptance. An empty list cannot be saved.

NBoard and variants

An --nboard connection supports standard (std) only. When a variant game arrives, the connector does not ask the engine and gives up that turn (message Engine failed for match). No move is sent, so the game is aborted before the first move, or that leg is lost on time later. Give an NBoard bot standard as its only supported game.

Humans versus registered bots

People can challenge your bot in any game it has registered. If it supports standard, both STD and XOT can arrive. XOT arrives with game_mode std and starts from a board where 8 moves have already been played (they are included in history). The person who challenges chooses the clock, and the game is unrated. A human game has one leg (leg 0) and a negative match_id. The server handles passes when no move is possible, so do not send -1. Online registered bots with automatic matching enabled can also join supported STD/XOT quick matches. Quick matches are rated for members and unrated for guests.

Illegal moves and time

An illegal move or stale revision is rejected with HTTP 409 (illegal_move, stale_revision, not_your_turn) and the board does not change. The clock keeps running meanwhile. The connector resends the same move with the same revision, so unless the engine is fixed, that leg is eventually lost on time. The server has no per-move limit and only checks each color's remaining clock. The 10 seconds is the connector's default --max-think-seconds.

Authentication

Send HTTPS JSON POST requests with Authorization: Bearer. Tokens belong to one bot. Never put them in URLs, public source code or logs.

Content-Type: application/json
Authorization: Bearer <FS_BOT_TOKEN>

POST https://othelloclub.com/flipsight/bot_match_api.php

Match rules

All bot matches start from a random position containing 12 discs. Two games run simultaneously with colors reversed. Combined disc counts decide the winner; fewer discs win in Anti.

All bot matches start from a random position containing 12 discs. Every game type gives each bot 1 minute per board.

If a bot has not made its first move within 30 seconds of the start, the match is aborted and not rated. If one bot's connection is lost for more than 30 seconds during the match while the other is connected, the connected bot wins (finish_reason: disconnect) and the result is rated.

The server calculates the FlipSight bot's moves in every game type. Both web challenges and API challenges (agent_challenge) are accepted, and the server starts the match right away, so you do not need to keep the game room open. If the server calculation stops for more than 30 seconds, the match is aborted (finish_reason: technical) and not rated. Variant moves are expensive to calculate, so at most 2 variant matches against FlipSight bots run at once. When they are full you get HTTP 409 with site bot variant matches are full; try again shortly.

Automatic bot matches: the site keeps two bot match rooms running. While a registered bot with auto matching on is online and not playing, it receives a standard (std) bot match challenge against a bot with a similar rating (another registered bot first, otherwise a FlipSight bot), as bot_b. The connector accepts it automatically and the match starts. If the bot does not accept within 120 seconds or rejects it, it is left out of auto matching for 10 minutes. Automatic matches are rated. Automatic bot matches continue while Arena games are in progress. Auto matching is on by default and can be turned off under Manage on the bot page. A bot started with the practice engine (environment.practice is true) is left out of automatic matches, and bot-vs-bot games it plays meanwhile are not rated.

A direct human challenge plays one game in a normal room with a supported game and clock; the server always makes direct challenges unrated. Online registered bots with automatic matching enabled can also join supported STD/XOT quick matches. Quick matches are rated for members and unrated for guests. A registered bot accepts only as many games as its simultaneous-games setting (default 2). One human game and one bot match (two boards) each count as 1.

pending → running → finished / aborted
pending → rejected / expired
kind=human: id=-room_id, legs[0], round_no, room_created_at
agent_move: move=19 / "d3" / "D3" (8×8)
10×10: "j10" = "J0" = 99
pass: -1 / "pass" / "PA" / "PS" / "--" (paired games only)
index=row*board_size+column (0-based)

Match flow

All bot matches start from a random position containing 12 discs. Two games run simultaneously with colors reversed. Combined disc counts decide the winner; fewer discs win in Anti.

Your participant number is 0 if you are bot_a and 1 if you are bot_b. Participant 0 is black (1) in legs[0] and white (2) in legs[1]; participant 1 is the opposite. It is your turn when leg.status is active and leg.turn equals your color. It can be your turn in both legs at once.

If you have no legal move, you must send move -1 (pass) yourself; the server does not pass for you. In games against a person, the server handles passes as in normal game rooms.

On your turn, remaining time = clocks_ms[your color - 1] - (server_now_ms - turn_started_ms). clocks_ms is in color order (black, white); totals and winner are in participant order (winner 0 or 1, null for a draw).

Square numbers

Square number = row × 8 + column, so a1=0, h1=7, a2=8 and h8=63. For example, 19 is d3 and 37 is f5. The board array uses the same order, with 0=empty, 1=black, 2=white.

    a  b  c  d  e  f  g  h
1   0  1  2  3  4  5  6  7
2   8  9 10 11 12 13 14 15
3  16 17 18 19 20 21 22 23
4  24 25 26 27 28 29 30 31
5  32 33 34 35 36 37 38 39
6  40 41 42 43 44 45 46 47
7  48 49 50 51 52 53 54 55
8  56 57 58 59 60 61 62 63

On 10×10 (grand, octagon), square = row × 10 + column. Columns are a to j, and the tenth row is written as 10 or 0 in coordinates. a1=0, j1=9, a2=10, j10=99 (same as j0). The same coordinate has a different number on each board size: d3 is 19 on 8×8 and 23 on 10×10.

     a  b  c  d  e  f  g  h  i  j
  1   0  1  2  3  4  5  6  7  8  9
  2  10 11 12 13 14 15 16 17 18 19
  3  20 21 22 23 24 25 26 27 28 29
  4  30 31 32 33 34 35 36 37 38 39
  5  40 41 42 43 44 45 46 47 48 49
  6  50 51 52 53 54 55 56 57 58 59
  7  60 61 62 63 64 65 66 67 68 69
  8  70 71 72 73 74 75 76 77 78 79
  9  80 81 82 83 84 85 86 87 88 89
 10  90 91 92 93 94 95 96 97 98 99

Octagon (octagon)

     a  b  c  d  e  f  g  h  i  j
  1  ## ##  2  3  4  5  6  7 ## ##
  2  ## 11 12 13 14 15 16 17 18 ##
  3  20 21 22 23 24 25 26 27 28 29
  4  30 31 32 33 34 35 36 37 38 39
  5  40 41 42 43 44 45 46 47 48 49
  6  50 51 52 53 54 55 56 57 58 59
  7  60 61 62 63 64 65 66 67 68 69
  8  70 71 72 73 74 75 76 77 78 79
  9  ## 81 82 83 84 85 86 87 88 ##
 10  ## ## 92 93 94 95 96 97 ## ##

Event stream

Connect with the same Bearer token to GET https://othelloclub.com/flipsight_api/bot-stream to receive one JSON event per line (NDJSON): hello confirms the connection, match means one of your matches changed (bot-vs-bot events include the full match object), and ping is a keep-alive every 10 seconds. When you receive match, call agent_poll once. If the connection drops, reconnect with a delay growing from 1 to 30 seconds and keep polling meanwhile. While the stream is connected your bot is shown as online; each bot may open up to 3 streams.

Event examples

curl -N 'https://othelloclub.com/flipsight_api/bot-stream' \
  -H "Authorization: Bearer $FS_BOT_TOKEN"

{"type":"hello","bot_id":1,"server_now_ms":1800000000000}
{"type":"match","match":{"id":123,"bot_a":1,"bot_b":2,"status":"running","state":{...}},"server_now_ms":1800000000500}
{"type":"match","match":{"id":-45,"kind":"human","room_id":45},"server_now_ms":1800000003000}
{"type":"ping","server_now_ms":1800000010000}

Engine adapter

The FlipSight bot connector (Python 3.9+, standard library only) runs on Windows, macOS and Linux and connects an engine in two ways. --engine starts the engine once per game, sends one JSON line for every move and reads one {"move": square} line. An engine that answers one line and exits still works because it is started again on the next move, but an engine that reads input to the end (sys.stdin.read() and so on) never answers, so read one line at a time. legal_moves in the request lists the legal squares; if it is empty, answer -1. --nboard keeps an NBoard protocol engine running, one per leg; set its search depth with --nboard-depth.

Edax can also be connected with just its executable path using --edax below. No separate adapter is needed; the connector allocates thinking time for each move from the remaining game clock.

Prepare Egaroucid for Console. Select the console executable rather than the graphical application, and keep its resources folder in place. On macOS and Linux, use an executable built following the official instructions. The connector sends the position and remaining time with the native setboard, settime and go commands, so no adapter is needed.

Input JSON (connector → adapter)

This is example data sent by the connector, not executable code. Do not save it as a Python file. During a game, the connector sends the current data automatically on each turn.

{"board":[0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,2,2,0,0,0,0,0,1,2,2,1,0,0,0,0,2,2,2,2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"color":1,"legal_moves":[2,3,12,25,42,44,45],"game_mode":"std","board_size":8,"blocked_squares":[],"placed":{"1":4,"2":4},"remaining_ms":40000,"match_id":123,"leg":1,"revision":0,"history":[],"round_no":null,"room_created_at":null,"budget_ms":10000}

Response JSON (adapter → connector)

{"move":2}

State synchronization

With the stream connected, call agent_poll when an event arrives and every 10 seconds. When polling without the stream, call it every second during a match and every 5 seconds while idle, and never more often than once per second. Network time counts against your clock. If a move response is lost, poll the new state and resend the same move only if the revision is still the same.

API actions

Response examples show the format. The copy button copies only the request and does not call the API.

POST agent_poll

Read state

ParametersTypeRequiredDescription
actionstring "agent_poll"RequiredName of the action to call
environmentobjectOptionalCPU, OS and engine name of the connector PC (up to 120 characters each, optional) and practice (true or false, optional). practice true means the bot is practising: it is left out of automatic matches, and bot-vs-bot games requested or accepted meanwhile are not rated. The connector sends this automatically when started with the practice engine. The server remembers the last value, so when practice is over, send it without practice or with false.

Request example

curl 'https://othelloclub.com/flipsight/bot_match_api.php' \
  -H "Authorization: Bearer $FS_BOT_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"action":"agent_poll","environment":{"cpu":"Apple M4","engine":"MyEngine","os":"Darwin"}}'

Response example

{
    "ok": true,
    "bots": [
        {
            "id": 1,
            "name": "Opponent",
            "owner_id": "example",
            "owner_name": "Example",
            "owner": {
                "player_id": "example",
                "name": "Example",
                "photo": "/img/no_profile.gif",
                "country_code": "",
                "country_name": "",
                "country_flag_url": "",
                "donor": false,
                "verified_player_id": ""
            },
            "engine_level": null,
            "rating": {
                "rating": 1200,
                "rd": 300,
                "official": false,
                "official_progress": {
                    "kind": "provisional",
                    "current": 0,
                    "required": 50
                },
                "color": "pz-rating-unofficial",
                "high_rating": 1200,
                "wins": 0,
                "losses": 0,
                "draws": 0,
                "streak": 0,
                "best_streak": 0
            },
            "supported_games": ["std"],
            "auto_match": true,
            "bio": "",
            "registered_at": "2026-11-12 18:00:00",
            "registered_at_ts": 1794474000,
            "environment": {
                "cpu": "Apple M4",
                "engine": "MyEngine",
                "os": "Darwin",
                "practice": false
            },
            "is_builtin": false,
            "online": true,
            "disabled": false,
            "deleted": false,
            "photo": "/flipsight/bot_match_api.php?action=photo&bot_id=1&v=1794474000000"
        },
        {
            "id": 2,
            "name": "MyBot",
            "owner_id": "example",
            "owner_name": "Example",
            "owner": {
                "player_id": "example",
                "name": "Example",
                "photo": "/img/no_profile.gif",
                "country_code": "",
                "country_name": "",
                "country_flag_url": "",
                "donor": false,
                "verified_player_id": ""
            },
            "engine_level": null,
            "rating": {
                "rating": 1200,
                "rd": 300,
                "official": false,
                "official_progress": {
                    "kind": "provisional",
                    "current": 0,
                    "required": 50
                },
                "color": "pz-rating-unofficial",
                "high_rating": 1200,
                "wins": 0,
                "losses": 0,
                "draws": 0,
                "streak": 0,
                "best_streak": 0
            },
            "supported_games": ["std", "anti"],
            "auto_match": true,
            "bio": "",
            "registered_at": "2026-11-12 18:00:00",
            "registered_at_ts": 1794474000,
            "environment": {
                "cpu": "Apple M4",
                "engine": "MyEngine",
                "os": "Darwin",
                "practice": false
            },
            "is_builtin": false,
            "online": true,
            "disabled": false,
            "deleted": false,
            "photo": "/flipsight/bot_match_api.php?action=photo&bot_id=2&v=1794474000000"
        }
    ],
    "record_bots": [],
    "matches": [
        {
            "id": 123,
            "bot_a": 1,
            "bot_b": 2,
            "game_mode": "std",
            "time_limit_sec": 60,
            "status": "running",
            "state": {
                "version": 1,
                "game_mode": "std",
                "time_limit_sec": 60,
                "status": "active",
                "revision": 1,
                "started_ms": 1799999980000,
                "last_now_ms": 1799999984200,
                "finished_ms": null,
                "opening_moves": [19, 20, 29, 34, 26, 11, 43, 37],
                "opening_history": [
                    {
                        "color": 1,
                        "move": 19
                    },
                    {
                        "color": 2,
                        "move": 20
                    },
                    {
                        "color": 1,
                        "move": 29
                    },
                    {
                        "color": 2,
                        "move": 34
                    },
                    {
                        "color": 1,
                        "move": 26
                    },
                    {
                        "color": 2,
                        "move": 11
                    },
                    {
                        "color": 1,
                        "move": 43
                    },
                    {
                        "color": 2,
                        "move": 37
                    }
                ],
                "legs": [
                    {
                        "board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 2, 1, 2, 2, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                        "turn": 2,
                        "black_participant": 0,
                        "placed": {
                            "1": 5,
                            "2": 4
                        },
                        "clocks_ms": [55800, 60000],
                        "turn_started_ms": 1799999984200,
                        "revision": 1,
                        "history": [
                            {
                                "color": 1,
                                "participant": 0,
                                "move": 44,
                                "at_ms": 1799999984200
                            }
                        ],
                        "status": "active",
                        "discs": [5, 8],
                        "finished_ms": null,
                        "legal_moves": [18, 21, 22, 25, 30, 33, 38, 42, 50, 51, 52]
                    },
                    {
                        "board": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 1, 2, 2, 1, 0, 0, 0, 0, 2, 2, 2, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
                        "turn": 1,
                        "black_participant": 1,
                        "placed": {
                            "1": 4,
                            "2": 4
                        },
                        "clocks_ms": [60000, 60000],
                        "turn_started_ms": 1799999980000,
                        "revision": 0,
                        "history": [],
                        "status": "active",
                        "discs": [3, 9],
                        "finished_ms": null,
                        "legal_moves": [2, 3, 12, 25, 42, 44, 45]
                    }
                ],
                "totals": [14, 11],
                "winner": null,
                "finish_reason": null,
                "board_size": 8,
                "blocked_squares": []
            },
            "auto": false,
            "expires_ms": 1800000080000,
            "ranked": true,
            "rating_applied": false,
            "rating_before": [null, null],
            "rating_after": [null, null],
            "created_at": "2026-11-12 19:00:00"
        }
    ],
    "me": "example",
    "bot": {
        "id": 2,
        "name": "MyBot",
        "owner_id": "example",
        "owner_name": "Example",
        "owner": {
            "player_id": "example",
            "name": "Example",
            "photo": "/img/no_profile.gif",
            "country_code": "",
            "country_name": "",
            "country_flag_url": "",
            "donor": false,
            "verified_player_id": ""
        },
        "engine_level": null,
        "rating": {
            "rating": 1200,
            "rd": 300,
            "official": false,
            "official_progress": {
                "kind": "provisional",
                "current": 0,
                "required": 50
            },
            "color": "pz-rating-unofficial",
            "high_rating": 1200,
            "wins": 0,
            "losses": 0,
            "draws": 0,
            "streak": 0,
            "best_streak": 0
        },
        "supported_games": ["std", "anti"],
        "auto_match": true,
        "bio": "",
        "registered_at": "2026-11-12 18:00:00",
        "registered_at_ts": 1794474000,
        "environment": {
            "cpu": "Apple M4",
            "engine": "MyEngine",
            "os": "Darwin",
            "practice": false
        },
        "is_builtin": false,
        "online": true,
        "disabled": false,
        "deleted": false,
        "photo": "/flipsight/bot_match_api.php?action=photo&bot_id=2&v=1794474000000"
    },
    "server_now_ms": 1800000000000
}

Match object (matches[])

FieldTypeDescription
idintegerMatch ID (negative for games against a person)
kindstringhuman for games against a person
bot_aintegerBot ID of participant 0 (0 for a person)
bot_bintegerBot ID of participant 1 (0 for a person)
game_modestringGame (std, anti, grand, octagon, reversi)
time_limit_secintegerSeconds given per leg, per color
statusstringpending, running, finished, aborted, rejected, expired
expires_msintegerAcceptance deadline (milliseconds)
rankedbooleanWhether the match is rated
state.statusstringactive, finished or aborted
state.game_modestringGame (std, anti, grand, octagon, reversi)
state.board_sizeinteger 8 | 10Squares per side, 8 or 10
state.blocked_squaresinteger[]Unusable squares (Octagon only, not sorted)
state.legsarrayLegs (2 between bots, 1 against a person)
state.totals[integer, integer]Combined adjudicated scores per participant (use discs if result_discs is absent)
state.winnerinteger | nullWinning participant 0 or 1, null for a draw
state.finish_reasonstring | nulldiscs, timeout, double_timeout, disconnect, no_first_move

Leg object (state.legs[])

FieldTypeDescription
boardinteger[64 | 100]64 / 100 squares, 0=empty 1=black 2=white
turnintegerColor to move, 1 (black) or 2 (white)
legal_movesinteger[]Legal squares for the color to move. Empty for finished legs
placed{"1": integer, "2": integer}Discs placed per color, keys "1" (black) and "2" (white)
black_participantintegerParticipant who plays black in this leg
clocks_ms[number, number]Remaining-time baselines in color order (black, white)
turn_started_msintegerServer time when the current turn started
revisionintegerIncreases by 1 whenever this leg changes
statusstringactive, finished, timeout, stopped
historyarrayMoves played (color, participant, move, at_ms)
discs[integer, integer]Disc counts in color order (black, white)
result_discs[integer, integer] (optional)Optional adjudicated score [black, white] for a leg lost on time; total it instead of discs. Scores for the side that timed out and its opponent: std and reversi 0 to 64, grand 0 to 100, octagon 0 to 88, anti 64 to 0. The other leg continues, and the totals decide the winner after both legs end. Older finished records are not recalculated.

POST agent_challenge

Challenge

Specify target_id (bot ID) or target_name (bot name), available in the bots list returned by agent_poll. Built-in FlipSight bots (is_builtin: true) can be challenged through the API in every game type, and the server starts the match right away. When 2 variant matches against built-in bots are already open, you get HTTP 409 with site bot variant matches are full.

Challenge from the bot page

ParametersTypeRequiredDescription
actionstring "agent_challenge"RequiredName of the action to call
target_idinteger ≥ 1OptionalID of the bot to challenge
target_namestringOptionalName of the bot to challenge (instead of target_id)
game_modestring std | anti | octagon | grand | reversiOptionalGame for a bot pair. Defaults to std. Both bots must support it. Standard / Anti / Octagon / Grand (10x10) / Classic Reversi

Request example

curl 'https://othelloclub.com/flipsight/bot_match_api.php' \
  -H "Authorization: Bearer $FS_BOT_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"action":"agent_challenge","target_id":2,"game_mode":"anti"}'

Response example

{
    "ok": true,
    "match_id": 123,
    "server_now_ms": 1800000000000
}

POST agent_accept

Accept

ParametersTypeRequiredDescription
actionstring "agent_accept"RequiredName of the action to call
match_idinteger ≥ 1RequiredMatch ID (negative for games against a person)

Request example

curl 'https://othelloclub.com/flipsight/bot_match_api.php' \
  -H "Authorization: Bearer $FS_BOT_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"action":"agent_accept","match_id":123}'

Response example

{
    "ok": true,
    "match": {
        "id": 123,
        "status": "running",
        "bot_a": 1,
        "bot_b": 2
    },
    "server_now_ms": 1800000000000
}

POST agent_reject

Decline

ParametersTypeRequiredDescription
actionstring "agent_reject"RequiredName of the action to call
match_idinteger ≥ 1RequiredMatch ID (negative for games against a person)

Request example

curl 'https://othelloclub.com/flipsight/bot_match_api.php' \
  -H "Authorization: Bearer $FS_BOT_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"action":"agent_reject","match_id":123}'

Response example

{
    "ok": true,
    "match": {
        "id": 123,
        "status": "rejected",
        "bot_a": 1,
        "bot_b": 2
    },
    "server_now_ms": 1800000000000
}

POST agent_move

Submit a move

ParametersTypeRequiredDescription
actionstring "agent_move"RequiredName of the action to call
match_idinteger ≠ 0RequiredMatch ID (negative for games against a person)
round_nointeger ≥ 0OptionalGames against a person only: round_no from the match object
room_created_atstringOptionalGames against a person only: room_created_at from the match object
leginteger 0 | 1RequiredLeg 0 or 1 (0 for games against a person)
revisioninteger ≥ 0RequiredRevision of the leg your move was computed for; 409 stale_revision if the leg has changed
moveinteger -1..99 | stringRequiredSquare 0-(board_size²−1), -1 for a pass / d3 / D3 / pass

Request example

curl 'https://othelloclub.com/flipsight/bot_match_api.php' \
  -H "Authorization: Bearer $FS_BOT_TOKEN" \
  -H 'Content-Type: application/json' \
  --data '{"action":"agent_move","match_id":123,"leg":1,"revision":0,"move":2}'

Response example

{
    "ok": true,
    "match": {
        "id": 123,
        "status": "running",
        "bot_a": 1,
        "bot_b": 2
    },
    "server_now_ms": 1800000000000
}

Errors

Errors look like {"ok": false, "error": "…"}. On 401, stop and check the token. On 409, poll the current state and check the turn, revision and legal moves. 429 means too many streams for one bot. Retry connection errors and 503 with growing delays, and never resend an illegal move.

{"ok":false,"error":"stale_revision","server_now_ms":1800000000000}
HTTPerror
400JSON object required / unknown action
401invalid bot credential
403access denied / invalid csrf
405POST required
409stale_revision / not_your_turn / illegal_move / leg_finished / match not running / match timed out / stale room revision / invalid move coordinate / move outside board / unsupported game mode / both bots must be connected / bot already has an active match / bot busy / challenge expired or answered / site bot variant matches are full / target bot not found
413request too large
429too many streams (bot-stream)
503bot match storage unavailable
Copyright © Othelloclubthe.othelloclub.com@gmail.com All rights reserved.