Agent protocol overview

How an agent enrols, signs requests and sends batches to /ingest/v1. For people writing their own agent or reading the wire format.

For plugin authorsUpdated

This is an overview of protocol 1.2, which is language-neutral and additive over 1.1. The full contract, with signing test vectors, is maintained alongside the agents; write to [email protected] for a copy. Base URL: https://serverstats.cc/ingest/v1, HTTPS only. Every response carries X-SS-Protocol and X-SS-Time (server time in Unix milliseconds); errors are {"error":{"code","message","details"?}} and agents branch on code.

Identity

An agent has one identity: an Ed25519 keypair it generates itself. The private key never leaves the machine; the server stores only the public key.

  1. The owner creates a claim code (SS-XXXX-XXXX-XXXX, 60 random bits, single use, 30 minutes).
  2. POST /enrol with {claim, public_key, game, agent_version, platform}. public_key is the raw 32 bytes in standard base64 with padding. The answer holds server_id, action_public_key and the action key set. A new claim code with a new key replaces the old key at once.

Roblox cannot hold a signing key, so experiences use an experience key (ssx_) to register a per-instance key at POST /instances.

Signed requests

Every request except /enrol, /instances, /action-keys and /health carries:

HeaderValue
X-SS-Serverthe server_id
X-SS-Tscurrent time in Unix milliseconds, corrected by the last X-SS-Time (must be within 5 minutes)
X-SS-Nonce16 to 128 characters of [A-Za-z0-9_-], fresh for every request including retries
X-SS-Sigbase64 of Ed25519(private_key, "ss1." + ts + "." + nonce + "." + hex(sha256(body)))

body is the bytes as sent (the gzip bytes if compressed, none for a GET). A nonce cannot repeat within 10 minutes. The rate limit is per server across all signed requests: a bucket of 30 refilling 1 per 2 seconds.

Sending data

POST /batch (gzip recommended, 1 MiB on the wire, 16 MiB decompressed) with {agent, samples, events, sessions, players, player_props, entities, secrets}, all arrays optional. Typical agents send every 10 seconds.

json
{"agent":{"game":"minecraft","version":"serverstats-paper/1.1.2","platform":"paper","server_name":"Survival EU"},
 "samples":[{"m":"tick_rate","t":1767225600000,"v":19.8,"d":{"window":"1m"}}],
 "events":[{"type":"player_death","t":1767225598000,"pid_raw":"069a79f4-44e9-4726-a5be-fca90e38aaf5","props":{"world":"world"}}],
 "sessions":[{"sid":"a1b2c3","pid_raw":"069a79f4-44e9-4726-a5be-fca90e38aaf5","join":1767225000000,"leave":1767225590000,"country":"GB"}]}

Limits: 10,000 samples, 5,000 events and 2,000 sessions per batch; timestamps are Unix ms within 7 days back and 5 minutes forward. Agents reduce a player's IP to a country locally and never send IPs. Ingest hashes pid_raw with the organisation's key and stores the original encrypted. The 200 answer reports accepted and dropped counts and config_rev. A drop is still success: do not resend. 400 invalid_batch will never succeed, so drop it. A paused server is answered 200 with status: "paused" and nothing stored. Retries back off, and offline agents buffer to disk (50 MB, 7 days).

Names: plain snake_case names are stored as custom.<name> unless they match a catalogue key for the game.

Config, inventory and actions

  • POST /inventory declares detected plugins and currencies. GET /config returns a signed config document (source selection, thresholds, running experiments) when the X-SS-Config-Rev header says it changed. Agents verify its Ed25519 signature before applying.
  • GET /config may carry an unsigned update_available hint (status outdated, unsupported or known_bad). Agents may show it to admins, must never download or install from it, and must not trust it.
  • Moderation actions are opt-in per server. GET /actions?wait=N long-polls; each action is signed with the action key (a different key from the agent's), and the agent must verify it and the server's pinned keys before doing anything, then POST /actions/ack. Restart and automation actions use the same channel.
  • GET /health is unauthenticated and returns the protocol version.

Error codes agents see

bad_auth_headers, stale_timestamp (use details.server_time), unknown_server, not_enrolled, bad_signature, replayed_nonce (all 401), wrong_game (400), payload_too_large (413), unsupported_encoding (415), rate_limited (429, honour Retry-After) and internal (500, retry with backoff). Enrolment errors (claim_invalid, claim_used, claim_expired, claim_revoked) are permanent; never retry them automatically.

Capabilities

Agents announce agent.caps such as config.v1, domains.v1 (sends typed domain events with src, amount, cur, subj) and experiments.v1. The server never needs them to accept data. Event types, channels and sources are kept in a machine-readable catalogue (metrics, events, sources and enums) with JSON Schemas.