Advertisement
Developer docs

Bridge protocol & API

Everything you need to talk to this server from a mod, a script or another client. The bridge is deliberately boring: line-delimited JSON over plain TCP, one reply per line, no framing, no handshake beyond a hello.

TCP

The Minecraft bridge

Default 127.0.0.1:9010. Configurable via bridge_host and bridge_port in config/config.json.

Connection lifecycle

1. connect to 127.0.0.1:9010 2. server sends one hello line ← {"ok":true,"type":"hello", "message":"bridge ready"} 3. you send one JSON object per line → {"type":"ping","source":"mod"} 4. server replies with exactly one line ← {"ok":true,"type":"pong", "time":"2026-08-04T12:00:00+00:00"}

Every line you send gets exactly one reply line. Unknown types are acknowledged rather than rejected, so adding new event types never breaks an old client.

Server replies

You sendYou get
{"type":"ping"}{"ok":true,"type":"pong","time":...}
{"type":"register",...}{"ok":true,"type":"ack","registered":true,"client_id":...}
anything else valid{"ok":true,"type":"ack","time":...}
malformed JSON{"ok":false,"type":"error","error":"invalid json: ..."}
The website pings the bridge every 5 seconds and expects pong. That is what drives the connection pill on the dashboard and the status page.

Events the website sends you

Each of these arrives as one JSON line on a fresh connection, or on your open socket if you registered.

typeactionPayloadWhen
authloginuser_id, username, remote_addrSomeone logs into the website
authsignupuser_id, username, remote_addrA new account is created
authlogoutuser_id, usernameSomeone logs out
presencejoin / leaveuser_id, usernameA browser socket connects or drops
calljoin / leaveroom, user_id, usernameSomeone enters or leaves a call room
callinvitecall_id, room, from, to, videoA direct call is started
callincomingcall_id, room, from, join_urlPushed to a registered client being rung
callaccept / declinecall_id, byThe other side answers or rejects
arcadescoreusername, game, game_title, score, personal_best, rankA logged-in player finishes a run

Receiving pushed events

A client that only wants to fire events at the website can connect, write, and disconnect. To receive pushes — which is what makes web-to-Minecraft calling work — you must register and hold the socket open.

// after the hello line → {"type":"register","username":"Steve"} ← {"ok":true,"type":"ack","registered":true, "client_id":"mc1","username":"Steve"} // keep reading. pushes arrive as extra lines: ← {"type":"call","action":"incoming", "from":"Koula","room":"dm-1-2-a91f3c", "join_url":"/call/dm-1-2-a91f3c"}
Your reader must tolerate unsolicited lines. If it assumes strictly one reply per request it will get out of sync the first time someone rings you.

Username matching is case-insensitive. Send a keepalive ping every 20–30 seconds so the socket is not dropped by an idle timeout.

Minimal Java client

Socket s = new Socket("127.0.0.1", 9010); BufferedReader in = new BufferedReader( new InputStreamReader(s.getInputStream(), UTF_8)); Writer out = new OutputStreamWriter( s.getOutputStream(), UTF_8); in.readLine(); // hello out.write("{\"type\":\"register\"," + "\"username\":\"Steve\"}\n"); out.flush(); String line; while ((line = in.readLine()) != null) { handle(line); // acks AND pushes }

Run the read loop on its own thread. Never block the Minecraft client thread on socket I/O.

HTTP

REST API

All responses are {"ok": true, "data": ...} or {"ok": false, "error": "..."}. Session cookie authentication.

MethodPathAuthDescription
GET/api/arcade/gamesNoneAll 10 games with your personal best and the world best
GET/api/arcade/profileNoneYour profile: XP, level, bests, achievements. Works for guests
POST/api/arcade/scoreNoneSubmit a finished run. Guests are recorded but not published
GET/api/arcade/leaderboard/<game>NoneOne game's board, or global for the XP board
GET/api/arcade/leaderboardsNoneEvery board at once
POST/api/arcade/claimUserMerge a guest id into the logged-in account
POST/api/support/ticketNoneOpen a ticket, returns an id and lookup token
GET/api/support/ticket/<id>?token=NoneRead a ticket with its lookup code
GET/api/support/my-ticketsUserTickets attached to your account
GET/api/bridge/statusNoneCurrent bridge connection state
GET/api/usersUserAll users with online state
GET/api/friendsUserYour friend list
GET/POST/api/dms/<friend_id>UserRead or send direct messages
GET/POST/api/serversUserList, create and join community servers
GET/api/kal/balanceUserYour KAL balance
POST/api/minecraft/linkUserLink a Minecraft username to your account

Submitting a score

POST /api/arcade/score Content-Type: application/json { "game": "neon-snake", "score": 42 } // response { "ok": true, "data": { "score": 42, "previous_best": 31, "personal_best": true, "xp_gained": 47, "rank": 3, "saved_to_leaderboard": true, "new_achievements": [ ... ], "leaderboard": [ ... ] } }

saved_to_leaderboard is false for guests. That is the flag the game overlay uses to decide whether to show the sign-up prompt.

WebSocket

Socket.IO events

Client → server

EventPayload
presence:join
join_call{room, video}
leave_call{room}
webrtc:signal{target, type, sdp|candidate}
call:invite{to_username|to_user_id, video}
call:accept{call_id}
call:decline{call_id, reason}
call:cancel{call_id}
media:state{room, muted, video, screen, speaking}
call:roster{room}

Server → client

EventPayload
call:peers{room, peers[]}
call:peer_joined{room, sid, username, video}
call:peer_left{room, sid}
webrtc:signalrelayed verbatim
call:ringfull invite object
call:accepted{call_id, room, video}
call:declined{call_id, reason}
call:cancelled{call_id, reason}
presence:update{user_id, online}
bridge:update{connected, last_ok, last_error}
bridge:callforwarded from Minecraft
arcade:playing{username, game, at}

Data on disk

FileFormatHolds
data/app.dbSQLiteUsers, friendships, servers, messages, KAL ledger
data/arcade.jsonJSONArcade scores, player profiles, XP, achievements
data/support.jsonJSONSupport tickets and replies
data/admin.jsonJSONAdmin passcode hash
data/secret.keyFernet keyMessage encryption key
data/flask_secret.keyTextSession signing secret
Back up the whole data/ folder together. Losing secret.key makes existing encrypted messages permanently unreadable, and losing flask_secret.key logs everyone out.

JSON writes are atomic — write to a temp file, then replace — and guarded by a re-entrant lock, so a crash mid-write cannot leave a truncated file. If a file is ever found corrupt it is renamed to .corrupt rather than deleted.