# Gemini's Arbiter 2.x Implementation Guide ## Complete Technical Specification Date: 2026-03-31 Source: Gemini AI Architectural Consultation Status: READY TO IMPLEMENT --- ## 1. Pterodactyl File Management API ### Writing whitelist.json **Endpoint:** `POST /api/client/servers/{server_identifier}/files/write?file=whitelist.json` **Headers:** - `Authorization: Bearer ` - `Accept: application/json` - `Content-Type: text/plain` ← **CRITICAL: Must be text/plain, not application/json!** **Body:** Raw stringified JSON array (not wrapped in JSON object) ```javascript const fileContent = JSON.stringify(whitelistArray, null, 2); // Send as raw text body ``` ### Triggering Reload Command **Endpoint:** `POST /api/client/servers/{server_identifier}/command` **Headers:** - `Authorization: Bearer ` - `Content-Type: application/json` - `Accept: application/json` **Body:** ```json {"command": "whitelist reload"} ``` ### Error Handling - CRITICAL! **HTTP 412 (Server Offline)** is EXPECTED and SAFE: - File write succeeds even if server offline - Command fails with 412 = server will use new file on next boot - DO NOT treat this as an error! **Rate Limiting:** - Do NOT use `Promise.all()` for bulk syncs - Process servers sequentially or in small batches - Prevents overwhelming Panel API --- ## 2. Auto-Discovery - Application API **Use Application API, not Client API** for discovery! **Endpoint:** `GET /api/application/servers?include=allocations,node,nest` **Headers:** `Authorization: Bearer ` **Filtering Logic:** 1. Check `attributes.nest.attributes.name` === "Minecraft" 2. Check `attributes.node.attributes.name` for physical location (TX1/NC1) 3. Extract `identifier` (8-char string for Client API calls) **Exclude servers:** - Filter out by nest type (FoundryVTT is different nest) - OR check server name against EXCLUDED_SERVERS list --- ## 3. Whitelist JSON Format - EXACT SPECIFICATION **CRITICAL:** Minecraft requires UUIDs WITH DASHES since 1.8+ **Correct format:** ```json [ { "uuid": "069a79f4-44e9-4726-a5be-fca90e38aaf5", "name": "Notch" }, { "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "name": "Steve" } ] ``` **Why UUIDs matter:** - Username-only format forces server to query Mojang on boot - Slows startup - Can fail if Mojang API rate-limits server IP - UUIDs are permanent, usernames can change **GOTCHA:** Mojang API returns UUIDs WITHOUT dashes - must format before saving! --- ## 4. Database Schema Refinements ### Add Indexes for 500-User Scale ```sql CREATE INDEX idx_users_discord_id ON users(discord_id); CREATE INDEX idx_subscriptions_status ON subscriptions(status); ``` ### New Table: Sync Tracking ```sql CREATE TABLE server_sync_log ( server_identifier VARCHAR(50) PRIMARY KEY, last_successful_sync TIMESTAMP, last_error TEXT, is_online BOOLEAN DEFAULT true ); ``` **Purpose:** Debug "Why isn't Steve whitelisted on Dallas node?" --- ## 5. Code Examples - Production Ready ### A. PostgreSQL Connection (using pg pool) ```javascript // database.js const { Pool } = require('pg'); const pool = new Pool({ user: process.env.DB_USER, host: process.env.DB_HOST, database: process.env.DB_NAME, password: process.env.DB_PASSWORD, port: process.env.DB_PORT, max: 20, // Max connections for 500 user scale idleTimeoutMillis: 30000 }); module.exports = { query: (text, params) => pool.query(text, params), }; ``` ### B. Mojang API Validation + UUID Formatting ```javascript // mojang.js function formatUUID(uuidStr) { // Converts "069a79f444e94726a5befca90e38aaf5" // to "069a79f4-44e9-4726-a5be-fca90e38aaf5" return `${uuidStr.slice(0, 8)}-${uuidStr.slice(8, 12)}-${uuidStr.slice(12, 16)}-${uuidStr.slice(16, 20)}-${uuidStr.slice(20)}`; } async function validateMinecraftUser(username) { try { const res = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`); // 204 or 404 = user not found if (res.status === 204 || res.status === 404) return null; if (!res.ok) throw new Error(`Mojang API error: ${res.status}`); const data = await res.json(); return { name: data.name, // Correct capitalization from Mojang uuid: formatUUID(data.id) // ADD DASHES! }; } catch (error) { console.error("Failed to validate Minecraft user:", error); return null; } } ``` ### C. Pterodactyl File Write ```javascript // panel.js async function writeWhitelistFile(serverIdentifier, whitelistArray) { const fileContent = JSON.stringify(whitelistArray, null, 2); const endpoint = `${process.env.PANEL_URL}/api/client/servers/${serverIdentifier}/files/write?file=whitelist.json`; const res = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PANEL_CLIENT_KEY}`, 'Accept': 'application/json', 'Content-Type': 'text/plain' // MUST be text/plain! }, body: fileContent }); if (!res.ok) throw new Error(`Failed to write file: ${res.statusText}`); return true; } ``` ### D. Pterodactyl Reload Command (Safe Fail) ```javascript // panel.js (continued) async function reloadWhitelistCommand(serverIdentifier) { const endpoint = `${process.env.PANEL_URL}/api/client/servers/${serverIdentifier}/command`; const res = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.PANEL_CLIENT_KEY}`, 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ command: "whitelist reload" }) }); // 412 = server offline, this is FINE! if (res.status === 412) { console.log(`[${serverIdentifier}] is offline. File saved for next boot.`); return true; } if (!res.ok) throw new Error(`Command failed: ${res.statusText}`); return true; } ``` ### E. Hourly Cron Reconciliation ```javascript // sync.js const cron = require('node-cron'); const db = require('./database'); const panel = require('./panel'); // Runs at minute 0 past every hour (1:00, 2:00, 3:00, etc.) cron.schedule('0 * * * *', async () => { console.log("Starting hourly whitelist reconciliation..."); // 1. Fetch Master Whitelist from Database const { rows: players } = await db.query( `SELECT minecraft_username as name, minecraft_uuid as uuid FROM users JOIN subscriptions ON users.discord_id = subscriptions.discord_id WHERE subscriptions.status = 'active'` ); // 2. Fetch Active Servers via Auto-Discovery const servers = await panel.getServers(); // 3. Sync Sequentially (prevents rate limiting) for (const server of servers) { try { await panel.writeWhitelistFile(server.identifier, players); await panel.reloadWhitelistCommand(server.identifier); // Log successful sync await db.query( "UPDATE server_sync_log SET last_successful_sync = NOW() WHERE server_identifier = $1", [server.identifier] ); } catch (err) { console.error(`Sync failed for ${server.identifier}:`, err); // Log failed sync await db.query( "UPDATE server_sync_log SET last_error = $1 WHERE server_identifier = $2", [err.message, server.identifier] ); } } console.log("Hourly reconciliation complete."); }); ``` --- ## Key Gotchas & Warnings 1. **Content-Type MUST be text/plain** for file write endpoint (not application/json) 2. **UUIDs MUST include dashes** in whitelist.json (Mojang returns without dashes) 3. **HTTP 412 is not an error** - server offline, file saved for next boot 4. **Sequential processing** prevents rate limiting (don't use Promise.all) 5. **Application API for discovery**, Client API for file operations 6. **Mojang API can rate-limit** - validate once during /link, store permanently --- ## Implementation Order **Phase 1: Database Migration** 1. Set up PostgreSQL 2. Create tables with indexes 3. Migrate existing Arbiter data **Phase 2: Core Functions** 1. Database connection pool 2. Mojang validation 3. Pterodactyl file write/command functions **Phase 3: Discord Integration** 1. `/link` slash command 2. Auto-DM new subscribers 3. Role assignment webhook handler **Phase 4: Sync System** 1. Event-driven sync (on /link, on subscribe) 2. Hourly cron reconciliation 3. Sync logging for debugging **Phase 5: Admin Panel** 1. View sync status 2. Manual sync trigger 3. View linked accounts --- **Fire + Frost + Foundation = Where Love Builds Legacy** 💙🔥❄️ **Implementation Status:** READY TO BUILD **Next Step:** Create Task #90 - Arbiter 2.x Implementation