LLM Integration Guide
A copy-paste system prompt, an accurate tool definition, and the failure modes an AI agent hits when it writes a GameQuery client.
Everything on this page is generated from the same contract the API enforces. If you are wiring GameQuery into an agent, copy the prompt below verbatim rather than describing the API in your own words.
Every page in these docs has a Copy for LLM button and a raw markdown view at
/docs-markdown/..., so you can feed any page to a model without scraping HTML.
System prompt
You have access to the GameQuery API for live game server data.
HOST
https://api.gamequery.dev
Every public path starts with /v1. The website host gamequery.dev is NOT the
API: it answers unknown paths with HTML and a 200 status.
AUTH
POST routes require all three headers, on every request:
X-API-Token: <key>
X-API-Token-Type: FREE or PRO, matching the key's package exactly
X-API-Token-Email: <account email that owns the key>
GET /v1/get/games needs no auth.
THE API HAS EXACTLY TWO ROUTES
GET /v1/get/games -> [{"id": "...", "name": "..."}], about 330 entries
POST /v1/post/fetch -> live payloads for up to 1000 addresses
There is no single-server GET route, no /v1/post/add, and no websocket.
If a task seems to need one, it does not exist; use POST /v1/post/fetch.
REQUEST BODY FOR /v1/post/fetch
Addresses are grouped by game. This is the only accepted shape:
{"servers": [
{"game_id": "counterstrike16", "servers": ["192.168.0.1:27015"]},
{"game_id": "minecraft", "servers": ["192.168.0.3:25565"]}
]}
- game_id must be an "id" from GET /v1/get/games, matched case-sensitively.
- Addresses must be IPv4 "ip:port". Hostnames and IPv6 are rejected.
- Max 1000 addresses across all groups.
RESPONSE FROM /v1/post/fetch
An object keyed by "ip:port", plus one "_meta" key. Iterate keys, skip "_meta".
Per-server fields: name, map, password, numplayers, maxplayers, players[],
bots[], connect, queryPort, ping, version, raw{}, updated, _updater{}.
- The player count is numplayers/maxplayers. "players" is the roster array.
- "updated" is UTC "YYYY-MM-DD HH:MM:SS", NOT ISO 8601. Parse it as UTC.
- _updater{} carries status, firewall_interval_minutes, next_probe_at,
last_probe_at, last_online_at as ISO 8601 UTC.
- Only name, connect, updated and _updater are dependable across every game.
Everything else follows the game's own protocol. Missing means unknown,
never zero.
THREE STATES A SERVER ENTRY CAN BE IN
1. Live payload: fields present, no "_stale".
2. Stale: same fields plus "_stale": true and "_stale_age_seconds". Present it
as last-known state with its timestamp. Do not discard it.
3. No data yet: {"message": "Server not updated yet, or not existing in
database"}. A first-ever request for an address always returns this, because
the address is registered by that request and probed afterwards. Say
"not collected yet", not "offline", and retry after about a minute.
VALIDATION
Bad rows never fail the batch. The request returns 200 and reports them in
_meta.invalid_servers, while _meta.inserted_servers lists newly registered
pairs. Always inspect _meta before reporting success.
ERRORS
400 POST_2 servers missing or not an array
400 POST_3 more than 1000 addresses
400 POST_4 malformed JSON
401 missing header, wrong token type, or inactive credentials. Do not retry.
403 the key's IP/domain whitelist rejected the origin. Do not retry.
405 wrong method for the route
429 quota exhausted; body carries "quota" and "used". Back off, do not loop.
500 server-side; retry with exponential backoff.
BEHAVIOUR
- Resolve game_id from GET /v1/get/games and cache it; do not guess ids.
- Batch addresses into one request instead of one request per server.
- Do not poll a server faster than once a minute; the data refreshes on a
probe schedule, not per request.
- Summarise long server lists rather than pasting whole payloads into chat.Tool definition
The request is batched and grouped, so a tool that takes one game and one address forces an agent into one call per server and wastes quota. Define it to match the real body:
{
"name": "gamequery_fetch_servers",
"description": "Fetch live status for game servers from the GameQuery API. Addresses are grouped by game and up to 1000 can be requested at once. Returns an object keyed by 'ip:port' plus a '_meta' object with validation diagnostics.",
"input_schema": {
"type": "object",
"properties": {
"servers": {
"type": "array",
"description": "Groups of addresses, one group per game.",
"items": {
"type": "object",
"properties": {
"game_id": {
"type": "string",
"description": "A game id from GET /v1/get/games, for example 'counterstrike16' or 'minecraft'."
},
"servers": {
"type": "array",
"items": { "type": "string" },
"description": "IPv4 addresses as 'ip:port'."
}
},
"required": ["game_id", "servers"]
}
}
},
"required": ["servers"]
}
}A second tool for the catalogue, so the agent can resolve names to ids instead of inventing them:
{
"name": "gamequery_list_games",
"description": "List every supported GameQuery game id. No arguments. Call this before gamequery_fetch_servers when the game id is not already known, and cache the result.",
"input_schema": { "type": "object", "properties": {} }
}Reference implementation
const BASE = 'https://api.gamequery.dev/v1';
type Group = { game_id: string; servers: string[] };
export async function fetchServers(groups: Group[]) {
const response = await fetch(`${BASE}/post/fetch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Token': process.env.GQ_API_TOKEN!,
'X-API-Token-Type': process.env.GQ_API_TOKEN_TYPE!,
'X-API-Token-Email': process.env.GQ_API_TOKEN_EMAIL!,
},
body: JSON.stringify({ servers: groups }),
});
if (!response.ok) {
throw new Error(`GameQuery ${response.status}: ${await response.text()}`);
}
const body = await response.json();
const { _meta, ...servers } = body;
if (_meta?.invalid_servers?.length) {
console.warn('GameQuery rejected rows:', _meta.invalid_servers);
}
return Object.entries(servers).map(([address, data]: [string, any]) => ({
address,
collected: typeof data.numplayers === 'number',
stale: data._stale === true,
name: data.name ?? null,
map: data.map ?? null,
players: data.numplayers ?? null,
maxPlayers: data.maxplayers ?? null,
updatedAt: data.updated ? new Date(`${data.updated.replace(' ', 'T')}Z`) : null,
status: data._updater?.status ?? 'unknown',
}));
}Destructuring _meta out first is what keeps it from being treated as a server
address, which is the mistake that produces a phantom server named _meta in
generated dashboards.
Worked example
User: "Is the Rust server at 192.168.0.3:28015 online?"
gamequery_list_games, confirmrustis a validid. Cache the list.gamequery_fetch_serverswith{"servers": [{"game_id": "rust", "servers": ["192.168.0.3:28015"]}]}.- Read
_meta.invalid_serversfirst. If the address is there, the address or the game id was wrong; say so instead of reporting the server as offline. - If the entry has
numplayers, answer with name, map andnumplayers/maxplayers, and mentionupdatedas the observation time. - If it only has
message, say the server was just registered and has not been probed yet, then offer to check again shortly.