The relay behind duels and shares in AKAddons RuneLite plugins (Trappy Chinchompa first). A Cloudflare Worker with one Durable Object. It knows no game: it holds who is online by display name, forwards challenges, broadcasts inside rooms, and passes player-to-player payloads.
Client to relay:
| Message | Fields | Relay does |
|---|---|---|
hello |
name, v, game, account (the client's SHA-256 of its RuneLite account hash under the akaddons: prefix), hidden, dev |
registers the socket under the name (newest wins), binds the name to the account (4001 if another account holds it, 4002 if banned), replies welcome then sync{duels, history} for the game |
challenge |
to, game, data |
forwards {challenge, from, game, data}; replies sent or offline |
accept / decline |
to, game, data |
forwards to the challenger |
join / leave |
room |
membership; replies joined{members}; others get peer{name, online} |
env |
room, body |
broadcasts {env, room, from, body} to every member, sender included |
send |
to, game, kind, data |
forwards any plugin payload to a named player (share primitive) |
who |
names[] |
replies who{online[]} |
Errors come back as {t:"error", msg}. Messages over 64KB or more
than 30 per second per socket are refused. Names compare
case-insensitively with non-breaking spaces and underscores as spaces.
Plugins put a game id on everything and derive room ids themselves
(tc1:<hash>); the relay never reads data or body.
An accept no longer just forwards: the relay creates a duel record
with ten seeds and a five-minute idle deadline and pushes duel{record}
to both players. From there:
| Message | Fields | Relay does |
|---|---|---|
report |
duel, game, score, flaps[], ticks |
replays the run against the seed (src/sim.js); a mismatch or an impossible time forfeits the reporter (CHEAT); otherwise stores it, refreshes the deadline, pushes the record to both; settles when both sides are in |
start |
duel, game |
reveals that game's seed to you (one at a time); a report for an unstarted game is refused |
touch |
duel |
"still playing": refreshes the deadline (send once a minute mid-game) |
concede |
duel |
forfeit |
compete / uncompete |
game, rules |
queue at these rules; the next arrival with another name is paired into a compete duel |
board / ranks |
game, key |
verified compete scores / Elo ratings for a difficulty (e, n, h) |
history |
game |
wins, losses, draws, per-opponent record, compete record, ratings, last 20 duels |
Settlement: best round compares each side's highest score, average the
sums; a tie adds a sudden-death game from the next seed (ten seeds, then
a draw). At the deadline a side with its stretch complete wins by
timeout; if neither finished, the side that never started loses; if
nobody did anything, a challenge's acceptor loses (accept and vanish is
a concede) and a compete pairing is no contest. A hello that names a game gets
sync{duels, history} back. Only compete duels move Elo ratings and
feed the score board, and only runs the relay itself replayed.
| Data | Why | For how long |
|---|---|---|
| display name | who is online, who to challenge | until the operator releases or forgets it |
keyed HMAC of the account id (HASH_SALT) |
the name stays with one account | same |
| duel records: rules, seeds, each run's score, flap trace and tick count | replay and settlement | finished duels drop after a day; no duel outlives 45 minutes |
| results, ratings, last 20 duels, compete record per difficulty | the player's Record and the boards | until forgotten; hidden keeps a player off every board |
| up to 5 recent addresses per player | compete never pairs one machine with itself; abuse handling | rolling, operator-only |
| telemetry counters, per-day buckets, an audit trail of security events and operator actions | usage, defects, abuse | 400 days of buckets, latest 200 audit entries |
Nothing in a duel payload is read for anything but the duel. Bans are
shadow bans and only the operator applies them. A player is erased with
admin.mjs forget <name> on request.
- The relay replays every run (
src/sim.js, a line-for-line port of the Java game withjava.util.Randomreproduced exactly;test/golden.jsonholds runs the Java game recorded and the port must score identically). A report that does not replay to its score forfeits the reporter as CHEAT. No client's verdict is ever taken. - Seeds are revealed one at a time with
start{duel, game}, and a report is refused if it arrives sooner than its tick count could have been played, or claims more traps than the physics allow in that time. - Names are bound to accounts.
hellomust carryaccount(a SHA-256 the client makes of its RuneLite account hash, never the number itself); the relay stores a keyed HMAC of it (HASH_SALT) and refuses a different account on a claimed name (close 4001). Bans are shadow bans: a banned account or address connects and looks normal, but the compete queue never pairs it. - Farming limits. The compete queue never pairs the same account, the same IP, or two players paired within the hour; Elo gains against a repeat opponent in a day go full, half, quarter, then nothing; no account gains more than 100 rating a day; a sender may challenge one player three times in five minutes.
- No duel outlives 45 minutes, touches or not; finished records are dropped after a day (the player records stay).
- Developer mode.
hello{dev: true}(RuneLite's--developer-mode) lets two dev clients pair from one machine. Everything they play is taggeddevand feeds a shadow board and shadow ratings (dev:<game>) that only dev clients are shown; the real board never sees them. - Privacy.
hello{hidden: true}keeps a player off boards and rankings. The operator (admin{key, op, name}withADMIN_KEY) canforgeta player,releasea name,ban/unbanan account, andremoveScore{game, key}.
Telemetry (node scripts/admin.mjs stats [--days N]), all-time totals and
a bucket per day for 400 days (the readout prints the last N days, 14 by
default, and a per-month roll-up):
- Usage. Hellos, players seen and active today, session length, challenges sent / accepted / declined / offline, shares, duels by kind and rules, games by difficulty with a score histogram and average length, settlements by reason, sudden deaths, compete queues / pairings / leaves / wait time, board and record views.
- Health. Oversized, unparseable and malformed messages, unknown types, rate-limit hits, refused starts and reports by reason, replaced sockets, sweeps, expired duels and records, storage failures, and gauges for connections, queue length and open duels.
- Abuse. Hellos without an account, name-taken and banned-account attempts, bad admin keys, challenge floods, cheat forfeits split by replay mismatch vs physics cap, too-fast reports, pairings blocked by same account / machine / recent pair, Elo damping and daily caps hit, names claimed, accounts holding several names, a suspects list, and an audit trail (latest 200) of security events and operator actions. Security events are also written to the Worker's logs.
- Recommendations, never automation. Players who abandon duels
(accept and never play, let a timeout happen, start games and never
report), fail replay, or flood challenges are listed with the evidence
and their recent addresses. The operator decides:
admin.mjs ban <name>(account) oradmin.mjs banip <ip>(address);unbanandunbanipreverse them. Bans are shadow bans: the player connects as usual and simply never finds an opponent. No ban is ever applied by the relay itself.
Addresses are used for pairing fairness and abuse control and are seen by the operator only.
Secrets (set once with npx wrangler secret put NAME): HASH_SALT
(never rotate: bindings would all break) and ADMIN_KEY.
npm test # node --test, no account needed
npx wrangler dev # local, ws://localhost:8787/ws
npx wrangler login # once, opens the browser
npx wrangler deploy # -> https://akaddons-relay.<account>.workers.devThe plugin README carries the full text (Trappy Chinchompa's "Duels" section is the reference). The short form:
Duels connect to the AKAddons relay (akaddons-relay.workers.dev) while you are logged in and only after you turn duels on. It receives your display name, a hash of your RuneLite account id (made on your client and keyed again by the relay, so the name stays yours), the duels you play (scores and runs, kept a day) and sees your address like any server. It keeps your results, ratings and last 20 duels; hide-from-boards is a setting and the operator erases a player on request. The relay's source is this repository.
- When it connects. Only with the plugin's
Enable duelssetting on (default off) and a player logged in. Off, the plugin opens no socket. The endpoint is a config value (relayUrl), so a reviewer can point the plugin atnpx wrangler dev(ws://localhost:8787/ws) and watch every frame. - What the plugin sends. Exactly the message types in the two tables
above:
hello,challenge,accept,decline,start,report,touch,concede,compete,uncompete,board,ranks,history,send,who. Payloads carry duel rules, scores and flap traces of the minigame; nothing about the game client, the world, the bank or other players. - Outages. The client reconnects with backoff and the duel dock says "relay offline"; the minigame itself never needs the relay.
- Abuse. 64KB frames, 30 messages a second per socket, one name per account, challenge floods refused, every run replayed server-side.
- Operator. One person (the plugin author) holds
ADMIN_KEY; the telemetry and ban tooling is inscripts/admin.mjsin this repository.