Für Entwickler / Anwendungs-APIs

Arcade-Server

Ein WebSocket-Server für fünf Spiele, mit einer Handvoll HTTP-Routen daneben. Bei vier der fünf sendet ein Client überhaupt keine Spieleingabe - die Eingabe ist eine Transaktion, und der Socket ist die Art, sie landen zu sehen.

Ein Socket, keine API

Ein Prozess betreibt fünf Spielräume plus eine Lobby auf einer Ereignisschleife, liest die Chain über seinen eigenen Socket zum Node und fächert das Ergebnis an jeden verbundenen Client auf. Die Schnittstelle ist der WebSocket; die HTTP-Routen gibt es für Health-Checks und für die zwei Zahlen, die der Rahmen der Website auf jeder Seite zeichnet.

Die Spieleingabe ist eine Transaktion, kein Rahmen

Steuern in der Arena, eine Wette auf die Kurve, ein Dreh, eine Wette auf das Brett - all das sind Transaktionen, die die Spielerin an einen Contract sendet. Dieser Socket trägt, wen Sie beobachten, einen Lebenszeichen-Ping und die Sicht des Servers darauf, was die Chain getan hat. Nichts, was ein Client darüber sendet, autorisiert irgendetwas oder ändert ein Ergebnis.

Der Greifer-Raum ist die bewusste Ausnahme: Einen Greifer zu fahren wäre eine Transaktion pro Rahmen, die Absichten reiten also auf dem Socket. Selbst dort ist die Naht schmal - drei Verben, von denen keines eine Position, eine Zeit oder ein Urteil trägt, und die Fahrerin ist durch ein Urbild autorisiert, das der Browser in seinem eigenen Zug bereits on-chain veröffentlicht hat.

Verbinden

EndpunktValue
Socketwss://minigame.picklechain.xyz/ws/<room>das eigene nginx der Website leitet das Upgrade weiter
HTTPhttps://minigame.picklechain.xyz/api/derselbe Prozess, mit abgeschnittenem Präfix
Ein Stack, den Sie betreibenhttp://127.0.0.1:3008die Arcade-Website; der Server selbst liegt im Container-Netz
Originbeim Upgrade geprüfteine Origin, die nicht auf der konfigurierten Liste steht, wird vor dem Handshake mit 403 abgelehnt, der Socket öffnet also nie

Ein unbekannter Pfad unter /ws/ wird beim Upgrade mit 404 abgelehnt, und eine Verbindung über die Grenzen unten hinaus mit 429 - beides als schlichte HTTP-Antwort auf dem rohen Socket, vor jedem WebSocket-Handshake. Ein Client, der nur das error-Ereignis seines Sockets behandelt, sieht einen allgemeinen Fehlschlag; lesen Sie den Status, wenn Sie wissen wollen, welchen.

Die sechs Räume

Beim Beitritt
`hello`, then a full snapshot of the arena.

The snake arena. Steering is a transaction, so the socket carries no input for it.

Beim Beitritt
`hello`, then the whole round: phase, time left, the commitment, every bet placed and every cash-out judged so far.

The curve. Bets are transactions.

Beim Beitritt
`hello`, then the cabinet: what is left, where each capsule stands, the machine's constants and every attempt being driven.

The claw cabinet, and the ONLY room whose gameplay input rides this socket rather than the chain - a transaction per frame is not a game.

Beim Beitritt
`hello`, then the machine: the bet ladder, the paytable, the meter and the spins still open.

The reels. A spin is a transaction; the socket watches it.

Beim Beitritt
`hello`, then the table: every cell's multiplier, the current snapshot, the line so far and the bets still standing.

The board. Bets are transactions; the socket has one verb, and it buys nothing.

Beim Beitritt
`hello`, then `stack`: every run being climbed with its rules state, the day's board and the rules' constants.

The tower. A start and every tap are transactions; the socket only listens, and says what the rules made of each tap.

Beim Beitritt
`hello`, then `derby`: the current and next race, recent history, the rules and the paddock.

The race. A bet is a transaction; the socket only listens, and every runner is one of the chain's own oracle feeds.

Beim Beitritt
`hello`, then `glide`: the day's board, every run in the air, the canyon cards and the rules.

The canyon. A start and every flap are transactions; the socket only listens, and the canyon it draws comes from a live price feed.

Beim Beitritt
`hello` alone.

The lobby. Somebody who has not chosen a game sits here, and it is counted in the socket total.

Jeder Raum antwortet mit derselben Eröffnungsfolge: ein hello-Rahmen mit dem Namen des Raums, der Zahl der Spielenden, der Zahl der Zuschauenden und der Adresse des Abrechnungsbetreibers, dann eine vollständige Momentaufnahme des Zustands dieses Raums. Beide Zahlen sind RAUMBEZOGEN - sie kamen früher aus der Arena, egal nach welchem Raum Sie gefragt hatten, und kündigten Spielende an, die nicht da waren.

Was ein Client senden darf

watch

every room
Form
{"t":"watch","account":"0x…"}

Says which account this socket is following, so the server can address one player directly - a refused join has to reach the person it was refused for. In the reels room it also arms that account.

Gut zu wissen. It AUTHORISES NOTHING. The account is not verified, the contract decides who may play, and the address must match a 40-hex pattern or the message is ignored in silence.

ping

every room
Form
{"t":"ping","id":<any>}

Answered with `{t:"pong", id}`. The server also sends protocol-level pings and terminates a socket that stops answering them.

Gut zu wissen. This is not the liveness mechanism the server relies on; it is the one a client can observe.

Form
{"t":"claim","tx":"0x…","secret":"0x…"} and three verbs after it

Takes the wheel of an attempt and drives it. The proof is a preimage: the hash of the secret must equal the seed the pull published on chain, so the only client that can drive an attempt is the one that made it - no signature, no challenge, no round trip.

Gut zu wissen. None of the verbs carries a position, a time or a verdict. A wrong secret is refused and counted and never logged. A reconnect takes the wheel from the previous socket, which is told so.

quote

the trellis room only
Form
{"t":"quote", …}

Asks the operator to sign the price already printed in a square.

Gut zu wissen. It carries no stake and no account, and the contract checks the clock, the lock and the float itself. A flood of them costs this process signatures and nothing else.

javascript
const socket = new WebSocket("wss://minigame.picklechain.xyz/ws/crash");

socket.addEventListener("open", () => {
  // Tell the server which account to address directly. It verifies nothing:
  // this only decides who a per-player frame is sent to.
  socket.send(JSON.stringify({ t: "watch", account: myAddress }));
});

socket.addEventListener("message", (event) => {
  const frame = JSON.parse(event.data);
  switch (frame.t) {
    case "hello": /* room, playing, watching, operator */ break;
    case "full":  /* the whole room, sent once after hello */ break;
    case "chain": /* resyncing, and the range of mini-blocks lost, if any */ break;
    default:      /* IGNORE. The frame list is not versioned and rooms gain frames. */
  }
});

Was der Server sendet

Every frame is a JSON object with a `t` discriminator. `hello` arrives first and carries the room, the number playing, the number watching and the operator's address; a full snapshot follows. After that the room pushes what changed.

Across the eight rooms: chain, tick, state, full, bet, settled, settle-failed, refused, refunded, refund-due, fulfil-failed, pull, claimed, released, blocked, armed, quote, feed, unsealed, void, race, started, finished, paddock, board, run, points, flight, over, cancelled, canyons, eat, death, pong.

Eine Lücke in der Chain wird angekündigt, und sie bedeutet je nach Raum etwas anderes

Der chain-Rahmen trägt resyncing und den Bereich der Mini-Blöcke, den der Leser nicht holen konnte. Er wird an jeden Raum gesendet, und jeder behandelt ihn anders: Eine Runde, die einen verlorenen Bereich überlappte, sagt, dass ihre Gewinnerliste unvollständig sein kann, statt sie als alle anzukündigen, und der Greifer-Raum schickt den verlorenen Bereich an sein Log, damit die Einsätze darin gefunden und erstattet werden können. Ein Client, der diesen Rahmen ignoriert, zeichnet ein zuversichtliches Bild eines unvollständigen.

Die HTTP-Routen

Params
none
Rückgabe
{ok, loop:{p50Ms, p99Ms, maxMs, windowMs}}

Liveness, plus the event loop's recent delay. Eight rooms, a physics step and every socket's fan-out share one loop, so a loop that stalls stalls every quote, every echo and every frame at once.

Gut zu wissen. This is the figure that answers `the arcade is slow sometimes`. It is measured over ten-second windows and reset each time.

Params
none
Rückgabe
{ok, reasons, miniBlock, appliedMiniBlock, appliedLagMs, resyncing, lost}

503 when the socket to the node is down, or when the last APPLIED mini-block is more than five seconds old.

Gut zu wissen. It measures what was applied, not what arrived. Measuring arrival meant a wedged dispatch chain answered 200 while no input had been applied for minutes, which is the exact failure this probe exists to catch.

Params
none
Rückgabe
{total:{playing, sockets, spectators}, arena, crash, claw, patch, trellis, stack, derby, glide, …}

The whole arcade in one object: a summary per game and three numbers for the chrome every page draws. `playing` means somebody with something at stake right now, and each game measures that its own way.

Gut zu wissen. PUBLIC AND UNAUTHENTICATED, which decides what may appear on it. The crash summary withholds its point until the curve has stopped, and the claw summary carries the published commitment and never a link beneath it - one link is the outcome of the next pull nobody has made.

Params
none
Rückgabe
{supply, remaining, claimed, cells, price, pullsPerAccount, commit, chainLeft, blocked, machine}

The cabinet on its own, so a page can state what is left without opening a socket.

Gut zu wissen. 503 when the claw room is not running.

Params
the pull id in the path
Rückgabe
one finished pull, whole

The seed the player published, every intent the server recorded, the tick the drop landed on and the verdict that came out of them - the arithmetic a reader can rerun.

Gut zu wissen. A pull still being driven is a 404, deliberately: its trace is not finished and its link is not revealed. A revealed link is served only because the fulfilment that finished the pull already put it on chain.

Params
none
Rückgabe
{playing, watching, day, pot, top, leaders}

Stack's day without a socket: the UTC day, its pot in wei, the chain's own top three and the tallest runs this server has finished today.

Gut zu wissen. 503 when the stack room is not running. The pot and the top three are read off the contract once a minute and after each finish, so they can trail a finish by a moment.

Params
the run id in the path
Rückgabe
one run, whole: {id, a, t0Us, startMini, taps:[{us, mini, hash}], results, height, why, finished, rules}

Every tap of a run with the mini-block that carried it and what the rules made of it - the arithmetic a reader reruns with arcade-server/src/stack.js to check the height the operator reported.

Gut zu wissen. A 404 for a run this process never saw or has already let go of; it keeps the last five hundred. The chain still holds every tap, and the rules still apply to them.

Params
none
Rückgabe
{playing, watching, k, phase, card, unsettled, current, next, history, rules}

Derby's board for a page that has no socket yet: the current race and the next one, recent history, and the rules (the feed allowlist, the wall and night cards, the stake bounds and the seed).

Gut zu wissen. 503 when the derby room is not running. `unsettled` counts bets still waiting on a settle, across every race this process still holds.

Params
the race number in the path
Rückgabe
one race, whole: {k, state, card, gate, line, runners, handicaps, pools, startPrices, startRounds, lineRounds, roundsFrom, scores, winners, why, hashes, bets}

The lineup, the handicaps, the rounds both seals named, the scores, the winners and every bet - the arithmetic a reader reruns with arcade-server/src/derby.js against the same public rounds.

Gut zu wissen. A race this process never opened is a 404. `roundsFrom` says whether each end's rounds came from this room sealing them or were recomputed from the tape after a restart - the proof is the same either way.

Params
none
Rückgabe
{playing, watching, day, pot, top, leaders, canyons}

Glide's board for a page that has no socket yet, with every canyon's card alongside the day's pot and its leaders.

Gut zu wissen. 503 when the glide room is not running.

Params
the run id in the path
Rückgabe
one run, whole: {id, a, feed, symbol, r0, sigma, startUs, startMini, startHash, answers, flaps, result, rules}

r0, the rounds of the canyon this run flew, and every flap with the mini-block that carried it - the arithmetic a reader reruns with arcade-server/src/glide.js `replay` to get the same score.

Gut zu wissen. A run this process never saw or has already let go of is a 404.

Alle vier sind ohne Authentifizierung, und das entscheidet, was auf ihnen erscheinen darf. Nichts, was eine unfertige Runde verraten würde, wird veröffentlicht: Ein Crash-Punkt wird zurückgehalten, bis die Kurve steht, und die Hash-Kette des Greifers wird nur als ihre Festschreibung veröffentlicht, wobei ein Glied auf /fair einzig deshalb enthüllt wird, weil die Erfüllung, die diesen Zug beendet hat, es ohnehin schon on-chain gestellt hat.

Grenzen, und was sie bricht

GrenzeValue
Sockets256 total, 8 per addressthe upgrade is refused 429 before the handshake; the slot is released when the raw socket closes, not when the handshake succeeds
Originan allowlistthe upgrade is refused 403, so a page on another origin cannot drive this socket with a visitor's network
Frame size4 KiBtwo orders of magnitude above the largest real message; the library's default is 100 MiB
Inbound rate20 a second, burst 4045 and 90 in the claw room, because its driving is on this socket and a hand working a pad passes ten changes a second without trying
Overflowthe socket is closedclosing is kinder than ignoring - and in the claw room it ends a paid attempt, which is why that allowance is the wider one
Backpressure512 KiB buffereda socket further behind than that is cut and terminated rather than written to; a client that never reads would otherwise grow the process's heap without limit
Livenessa ping every half minuteanything that has not answered by the next one is terminated

Ein langsamer Leser wird getrennt, nicht gepuffert

Die Bibliothek puffert alles, was sie nicht schreiben kann, im Heap dieses Prozesses, ohne Grenze. Ein Socket, der mehr als ein halbes Megabyte zurückliegt, wird deshalb gekappt und beendet, statt erneut beschrieben zu werden - eine Handvoll Clients, die einen Socket öffnen und nie lesen, würden sonst den Container erschöpfen, was bei einem Prozess, der einen Schlüssel hält, ein Abschuss aus der Ferne ist. Ein halbes Megabyte sind etwa fünfzig der größten Rahmen, die dieser Server sendet; kein lesender Client liegt je so weit zurück.

Das Nachrichtenbudget ist im Greifer-Raum weiter, und das ist Absicht

Anderswo sendet ein Client alle paar Sekunden einen Ping und sonst nichts, zwanzig Nachrichten die Sekunde können dort also nur ein kaputter oder ein feindlicher Client sein. Das Fahren im Greifer-Raum sendet eine Nachricht pro Änderung der Absicht - ein Drücken und ein Loslassen sind zwei - und wer mit beiden Händen an einem Pad arbeitet, kommt mühelos über zehn die Sekunde. Diesen Socket zu beenden nimmt jemandem mitten im Versuch das Steuer aus der Hand, und der bezahlte Versuch läuft gegen die Schiene aus. Die Obergrenze liegt dort also über dem, was eine Hand erzeugen kann, statt genau darauf.