Live Arena
Spectator Broadcast
The 3D arena could not start on this device
The match is still running and the live feed below is up to date.
This usually means hardware acceleration is turned off, or the browser is too old for WebGL. Turning on hardware acceleration and reloading normally fixes it.
Protocol Docs
API Reference
Toggle
Protocol Docs
API Reference
REST API
POST /api/v1/keys/generate Generate API Token
Create a server-issued bot token without signing up. Arena saves its non-recoverable hash and bot record in PostgreSQL; the plaintext token is returned only in this response. Caller-chosen tokens are rejected.
Request
curl -X POST https://arena.angel-gaming.com/api/v1/keys/generate
Response 201
{
"api_key": "arena_abc123...",
"bot_id": "bot-uuid",
"created_at": "2026-07-12T00:00:00Z",
"message": "API key generated successfully. Store it safely -- it cannot be recovered."
}
Copy the token immediately. When you want cosmetics, sign in to your Angel account in My Dashboard and submit the token once to claim its bot.
POST /api/v1/account/keys Create Account-Owned API Key
Optionally create a new token directly inside an authenticated Angel account Dashboard session. Account mutations require the same-origin CSRF token supplied by the Dashboard. Each account can keep up to five active account-owned keys.
Request body
{
"bot_name": "MyBot"
}
The full token is returned once alongside safe metadata. This route is not required for first-time bot setup; the public generator above remains available.
PUT /api/v1/bot/config Configure Bot
Set your bot's name, color, and default loadout. Requires X-Arena-Key header.
Request
curl -X PUT https://arena.angel-gaming.com/api/v1/bot/config \
-H "X-Arena-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "MyBot",
"avatar_color": "#00d4ff",
"default_loadout": {
"weapon": "sword",
"stats": {"hp": 6, "speed": 6, "attack": 5, "defense": 3},
"fallback_behavior": "aggressive"
}
}'
Response 200
{
"bot_id": "uuid-here",
"name": "MyBot",
"avatar_color": "#00d4ff",
"default_weapon": "sword",
"default_stats": {"hp": 6, "speed": 6, "attack": 5, "defense": 3},
"default_fallback": "aggressive"
}
GET /api/v1/bot/stats Bot Stats
Get your bot's lifetime stats. Requires X-Arena-Key header.
Response 200
{
"bot_id": "uuid-here",
"name": "MyBot",
"kills": 42,
"deaths": 10,
"kd_ratio": 4.2,
"damage_dealt": 1250,
"damage_taken": 500,
"current_streak": 3,
"best_streak": 7,
"elo": 1150,
"rounds_played": 15,
"round_wins": 3
}
GET /api/v1/leaderboard Leaderboard
Public standings ladder. Optional query params: sort (elo|kills|streak|kd_ratio), limit (1-100), offset, and period (all_time|30d|7d|24h|1h).
Response 200
{
"entries": [
{
"rank": 1, "bot_id": "uuid", "name": "TopBot",
"kills": 250, "deaths": 40, "elo": 1800,
"best_streak": 25, "rounds_played": 100, "round_wins": 45
}
],
"total": 500, "limit": 50, "offset": 0
}
GET /api/v1/bounties Bounty Board
Public bounty board. Bots that chain round wins accumulate a live bounty until another bot kills them.
Response 200
{
"entries": [
{
"rank": 1,
"bot_id": "uuid",
"name": "Executioner",
"weapon": "sword",
"bounty_points": 6,
"win_streak": 1,
"claims": 0,
"is_target": true
}
],
"total": 1
}
GET /api/v1/arena/map Next Round Map
Fetch the current or pre-generated next-round terrain grid over REST. During intermission, features_pending: true means the terrain and shape are ready; game_mode is omitted, feature arrays are empty, and the terrain has no round-feature overlays until round_start.
GET /api/v1/arena/status Arena Status
Current arena state. Public endpoint.
Response 200
{
"status": "active",
"bots_connected": 12,
"bots_alive": 8,
"round_number": 42,
"safe_zone_radius": 500.0,
"top_bot": "ChampBot"
}
GET /api/v1/bot/live Live Bot State
Real-time bot state during a game. Requires X-Arena-Key header. Returns online: false if not connected.
Response 200 (in-game)
{
"online": true,
"bot_id": "uuid-here",
"name": "MyBot",
"phase": "active",
"hp": 120, "max_hp": 160,
"position": [52, 51],
"weapon": "sword",
"is_alive": true,
"speed": 6.0,
"attack_mult": 1.5,
"defense_red": 0.09,
"kill_streak": 3,
"round_kills": 5, "round_deaths": 1,
"round_damage_dealt": 450.5, "round_damage_taken": 200.0,
"round_shots_fired": 30, "round_shots_hit": 22,
"round_distance": 125.0,
"round_pickups": 4,
"accuracy": 73.3,
"damage_ratio": 2.25,
"active_effects": [{"name": "speed_boost", "ticks": 12}],
"dodge_cooldown": 0,
"cooldown_remaining": 0.2,
"frozen": false,
"action_counts": {"move": 120, "attack": 30, "dodge": 5, "idle": 10}
}
GET /api/v1/health Health Check
Server health check. Public endpoint, no authentication required.
Response 200
{
"status": "ok",
"bots_online": 12
}
DELETE /api/v1/account/keys/{key_id} Revoke Account Key
Revoke one key owned by your verified account. Use My Dashboard so the request carries the customer session and same-origin CSRF token safely.
WebSocket Connection
WS /ws/bot?key=YOUR_KEY Bot Connection
Connect your bot via WebSocket. Pass API key as query param, X-Arena-Key header, or send an auth message after connecting.
wss://arena.angel-gaming.com/ws/bot?key=YOUR_KEY
Connection Flow
- Connect → receive
connectedmessage (includes grid_size, fog_radius) - Send
select_loadoutwithin 10 seconds - Receive
loadout_confirmed - Wait for
lobbyorround_start - Optionally prefetch terrain from
GET /api/v1/arena/mapduring intermission - Game loop: receive
tick, sendaction
WS /ws/spectator Spectator Connection
Watch the arena with a five-second presentation delay. No authentication is required, and the connection is receive-only.
The delay applies to ordered arena_state and lobby_state gameplay snapshots. heartbeat and service_status control messages remain immediate.
wss://arena.angel-gaming.com/ws/spectator
Messages Received
| Type | When | Contents |
|---|---|---|
arena_state | During active rounds | All bot positions (world coords), HP, weapons, pickups, kill feed, obstacles, safe zone, waiting bots |
lobby_state | Between rounds | Connected players, countdown, bot names/weapons/colors |
Example arena_state
{
"type": "arena_state",
"tick": 342,
"round_tick": 142,
"bots": [
{"bot_id": "uuid", "name": "MyBot", "position": [1050, 1020],
"hp": 120, "max_hp": 160, "weapon": "sword",
"is_alive": true, "avatar_color": "#ff0000",
"is_dodging": false, "is_stunned": false}
],
"safe_zone": {"center": [1000,1000], "radius": 800},
"pickups": [{"pickup_type": "health_pack", "position": [900, 1100]}],
"kill_feed": [{"killer": "BotA", "victim": "BotB", "weapon": "bow"}],
"obstacles": [{"x": 100, "y": 200, "width": 60, "height": 40}],
"waiting_bots": [{"name": "NewBot", "weapon": "bow"}]
}
Spectators receive world coordinates (floats), not grid coordinates. Bots in waiting_bots will join next round.
Server → Bot Messages
connected Initial handshake
{
"type": "connected",
"bot_id": "uuid",
"arena_size": [2000, 2000],
"grid_size": [100, 100],
"cell_size": 20,
"fog_radius": 7,
"available_weapons": ["sword", "bow", "daggers", "shield", "spear", "staff", "grapple"],
"stat_budget": 20,
"stat_min": 1,
"stat_max": 10,
"timeout_seconds": 10,
"last_loadout": null
}
last_loadout contains your previous loadout if you have one (weapon, stats, fallback). Use this to auto-reselect your last loadout. stat_budget is the total stat points to distribute (default 20), each stat between stat_min (1) and stat_max (10).
loadout_confirmed Loadout accepted
{
"type": "loadout_confirmed",
"weapon": "sword",
"stats": {"hp": 6, "speed": 6, "attack": 5, "defense": 3},
"computed": {
"max_hp": 160,
"move_speed": 6.0,
"attack_mult": 1.5,
"defense_red": 0.09,
"attack_range": 1,
"cooldown_seconds": 0.5,
"weapon_damage": 25
},
"position": [850, 1000]
}
lobby Waiting for players
{
"type": "lobby",
"bots_connected": 5,
"bots_needed": 2,
"countdown": 8,
"players": [
{"name": "BotA", "avatar_color": "#ff0000", "weapon": "sword"}
]
}
round_start Round begins
Sent once at the start of each round. Use GET /api/v1/arena/map to fetch the terrain payload for the current or next round.
{
"type": "round_start",
"round_number": 12,
"round_modifier": "pickup_surge",
"round_modifier_label": "Pickup Surge",
"position": [42, 50],
"bots_in_round": 15,
"safe_zone": {
"center": [50, 50], "radius": 71,
"target_center": [45, 55], "target_radius": 9
}
}
All positions are [col, row] grid coordinates (integers). Zone radius is in tiles.
terrain payload Grid format used by GET /api/v1/arena/map
The terrain grid is static for the entire round and is never repeated in tick messages. Cache it from REST. Obstacles are not sent in nearby_entities — terrain is the only way to know where walls are. If features_pending is true, fetch again after round_start for the active mode and round features.
{
"status": "ok",
"width": 100, "height": 100,
"cell_size": 20,
"terrain": [
[".", ".", "#", ".", ".", ".", ".", "."],
[".", "#", "#", ".", "~", "~", ".", "."],
[".", "#", "#", ".", ".", ".", ".", "."],
[".", ".", ".", ".", ".", ".", "#", "#"]
],
"legend": {".": "ground", "#": "wall", "V": "void", "~": "water"}
}
Terrain Cell Types
| Cell | Name | Effect |
|---|---|---|
. | Ground | Walkable, no effect |
# | Wall | Blocks movement and line of sight. Obstacles with bot-radius padding. |
V | Void | Out-of-bounds / impassable |
~ | Water | Walkable terrain (cosmetic) |
How to Use Terrain
terrain[row][col]— row-major 2D array. Access withterrain[y][x]where position is[x, y](col, row).- Before sending
movein a direction, check that the destination cell is not#orV. move_touses server-side A* pathfinding that automatically routes around walls.- Walls block projectiles (bow arrows). Use walls as cover against ranged attackers.
- Terrain changes each round (obstacles are randomized), so always re-cache via
GET /api/v1/arena/map. - 20–30 obstacles per round, typically resulting in ~5–15% wall cells.
Example: Check if direction is walkable (Python)
def can_move(terrain, pos, direction):
"""Check if moving from pos=[col,row] in direction=[dx,dy] is walkable."""
new_col = pos[0] + direction[0]
new_row = pos[1] + direction[1]
if 0 <= new_row < len(terrain) and 0 <= new_col < len(terrain[0]):
return terrain[new_row][new_col] in (".", "~")
return False
tick Game state update (~10/sec)
Sent every game tick. Contains your state and visible entities within fog_radius. All positions are [col, row] grid coordinates.
{
"type": "tick",
"tick": 342,
"tick_number": 342,
"fog_radius": 7,
"your_state": {
"bot_id": "uuid",
"position": [52, 51],
"hp": 120, "max_hp": 160,
"speed": 6.0,
"weapon": "sword",
"cooldown_remaining": 0.2,
"weapon_ready": false,
"is_alive": true,
"kill_streak": 3,
"round_kills": 5,
"dodge_cooldown": 0,
"invuln_ticks": 0,
"stun_ticks": 0,
"facing": [0, 1],
"recently_disrupted_ticks": 0,
"brace_ready": false,
"bow_charge_ticks": 3,
"bow_charge_level": 0.5,
"charged_shot_ready": true,
"hazard_key_active": false,
"hazard_key_ticks": 0,
"bounty_token_bonus": 0,
"shield_absorb": 0,
"effects": [{"name": "speed_boost", "ticks": 20}],
"last_action_result": {
"action": "attack", "result": "hit",
"target": "enemy_id", "damage": 35.5
},
"hits_received": [
{"attacker_id": "enemy_id", "damage": 15, "weapon": "bow"}
],
"kill_feed": [
{"killer": "MyBot", "victim": "FooBot", "weapon": "sword", "tick": 340}
],
"in_safe_zone": true,
"distance_to_zone_edge": 12,
"zone_radius": 40,
"zone_center": [50, 50],
"zone_target_center": [45, 55],
"zone_target_radius": 9,
"grapple_charges": 2,
"grapple_cooldown": 0.0
},
"nearby_mines": 0,
"nearby_entities": [
{
"type": "bot", "bot_id": "enemy_id", "name": "EnemyBot",
"position": [53, 52], "hp": 85, "max_hp": 120,
"weapon": "bow", "is_alive": true, "avatar_color": "#0000ff",
"last_action": "attack", "is_dodging": false, "is_stunned": false,
"facing": [1, 0], "recently_disrupted_ticks": 0,
"brace_ready": false, "bow_charge_level": 0.3, "charged_shot_ready": false,
"rear_exposed": true, "near_impact_surface": false,
"has_los": true, "attack_range": 7, "can_attack": true, "threat_score": 85.4
},
{
"type": "pickup", "pickup_id": "p_123",
"pickup_type": "health_pack", "position": [55, 51]
},
{
"type": "burn_field", "id": "burn_staff_1",
"position": [54, 52], "radius": 1, "ticks_left": 6, "active": true
}
],
"safe_zone": {
"center": [50, 50], "radius": 40,
"target_center": [45, 55], "target_radius": 9
}
}
Grid-based: No obstacle entities in ticks — use the cached terrain grid from GET /api/v1/arena/map. Fog radius = 7 tiles (visible area is 15×15). Nearby bots also expose tactical reads like rear_exposed and near_impact_surface for backstab/slam logic.
Hints (only when no bots in fog range)
When no enemy bots are within your fog_radius, a hints array is included with directions to the nearest 3 bots and the nearest pickup of each type.
"hints": [
{"hint_type": "bot", "direction": [0.7, -0.7], "distance": 342.5},
{"hint_type": "pickup", "pickup_type": "health_pack",
"direction": [0.5, 0.9], "distance": 180.3}
]
kill You killed someone
{
"type": "kill",
"victim_name": "FooBot",
"victim_id": "uuid",
"weapon_used": "sword",
"your_kill_streak": 3,
"your_round_kills": 5
}
death You died
{
"type": "death",
"killed_by": "killer_uuid",
"killer_name": "EnemyBot",
"weapon_used": "bow",
"damage": 45.5,
"your_kills_this_life": 2,
"respawn": false
}
respawn indicates whether you will respawn this round. When false, wait for round_end then round_start.
respawn You respawned
{
"type": "respawn",
"position": [1500, 1200],
"hp": 160
}
round_end Round over
{
"type": "round_end",
"round_number": 12,
"your_stats": {"kills": 5, "deaths": 2, "damage": 750},
"round_winner": "ChampionBot",
"next_round_in": 10
}
error Server error
Non-fatal error. Your bot stays connected.
{
"type": "error",
"message": "Invalid action",
"code": "INVALID_ACTION",
"details": "action 'fly' is not recognized"
}
kick Disconnected by server
Your bot has been disconnected. Common reasons: AFK timeout, rate limiting, admin action, or ban.
{
"type": "kick",
"reason": "AFK timeout"
}
Bot → Server Messages
select_loadout Choose weapon & stats
Must be sent within 10 seconds of receiving connected. Stats must sum to stat_budget (20), each between 1-10.
{
"type": "select_loadout",
"weapon": "sword",
"stats": {"hp": 6, "speed": 6, "attack": 5, "defense": 3},
"fallback_behavior": "aggressive"
}
Fallback Behaviors
aggressive | Attack nearest enemy, chase if out of range |
defensive | Attack if in range, retreat if enemies close, hold position |
opportunistic | Hunt weak enemies (<70% HP), flee from strong ones |
territorial | Defend 2x weapon range territory, attack intruders |
hunter | Chase enemy with highest kill streak |
action Game actions (send each tick)
Move (speed-scaled grid movement, direction is -1/0/1)
{"type": "action", "tick": 342, "action": "move", "direction": [1, 0]}
Direction components are -1, 0, or 1. A balanced speed allocation averages half a cell per tick; lower and higher speed allocations move proportionally, with fractional progress carried between ticks. Diagonal movement is allowed (e.g. [1, 1]). Movement into walls (#) or void (V) is blocked — check the cached /api/v1/arena/map terrain before moving.
Move To Grid Position (A* pathfinding)
{"type": "action", "tick": 342, "action": "move_to", "target_position": [75, 60]}
Server-side A* pathfinding automatically routes around walls. Provide [col, row] grid coordinates. Preferred over manual move for long-distance navigation.
Attack
{"type": "action", "tick": 342, "action": "attack", "target": "enemy_bot_id"}
Target visibility: target-ID attack, shove, and grapple actions are accepted only while that target is in your current fog-of-war view. The active public bounty target is the sole exception.
Send exactly one aim mode. Use "target": "bot_id" for a bot in weapon range, or—only for staff—use "target_position": [col, row] to place the AoE at a specific grid position. Sending both is rejected.
{"type": "action", "tick": 342, "action": "attack", "target": "enemy_bot_id", "charged": true}
Bow only: "charged": true spends any stored bow charge for a faster, harder-hitting arrow. Build charge by staying weapon-ready and not firing. Read your_state.bow_charge_ticks, your_state.bow_charge_level, and your_state.charged_shot_ready.
Tactical fields: nearby bots expose recently_disrupted_ticks (shield follow-up), rear_exposed (dagger backstab angle), brace_ready (spear ready state), and near_impact_surface (good grapple slam target).
Dodge (2 tiles, 3 ticks invulnerable)
{"type": "action", "tick": 342, "action": "dodge", "direction": [0, -1]}
Moves 2 tiles in the given direction with 3 ticks of invulnerability. 30-tick cooldown (~3s). Direction is -1/0/1 per axis. Can dodge through enemies but not through walls.
Use Item (collect pickup)
{"type": "action", "tick": 342, "action": "use_item", "item_id": "pickup_123"}
Shove
{"type": "action", "tick": 342, "action": "shove", "target": "enemy_bot_id"}
Knocks the target back 2 tiles and stuns for 2 ticks. No damage. 1.5s cooldown (separate from weapon). Range: 1 tile (adjacent).
Place Mine
{"type": "action", "tick": 342, "action": "place_mine"}
Places an invisible landmine at your position. Max 3 per bot. Arms after 1 second (~10 ticks). Invisible to enemies. 1-tile blast radius. 40 damage to all enemies in radius.
Use Gravity Well
{"type": "action", "tick": 342, "action": "use_gravity_well", "target_position": [55, 48]}
Deploys a gravity well at target position that pulls vulnerable enemies toward its center for 3 seconds. Requires a gravity_well pickup. 3-tile pull radius.
Idle
{"type": "action", "tick": 342, "action": "idle"}
Weapons
These are base values. Adaptive damage and cooldown scales can change between rounds; query GET /api/v1/weapon-stats or GET /api/v1/bot-setup for current effective values.
| Weapon | Base Damage | Range (tiles) | Base Cooldown | Special |
|---|---|---|---|---|
sword | 21 | 1 | 0.55s | Cleave — hits nearby enemies in a sweep |
bow | 16 | 8 | 1.05s | Charged Shot — store charge while ready, then fire a faster harder-hitting arrow |
daggers | 11 | 1 | 0.35s | Backstab — bonus damage from the rear arc |
shield | 14 | 1 | 0.8s | Bash — bonus damage on recently disrupted targets plus passive 50% damage reduction |
spear | 17 | 2 | 0.75s | Brace — holding ground empowers the next knockback hit |
staff | 17 | 6 | 1.65s | Arcane Burst — delayed 2-tile AoE that leaves a short burn field |
grapple | 14 | 5 | 1.05s | Slam — pull in and punish enemies pinned near walls or arena edges |
Universal Grapple Ability
ALL bots get 2 grapple charges per round (separate from the grapple weapon). Use the grapple action with exactly one aim mode: a currently visible target bot_id to yank an enemy, or a target_position to anchor-pull yourself. Sending both is rejected; the active public bounty target is the visibility exception.
- Range: 12 tiles
- Damage: 15
- Cooldown: 4 seconds
- Effect: Enemy grapple pulls the target to 1 cell from you and stuns for 3 ticks. Anchor grapple pulls you to a valid landing near the chosen position.
- Charges: 2 per round (shown in
your_state.grapple_charges)
Stats & Formulas
Distribute 20 points across 4 stats (min 1, max 10 each).
| Stat | Formula | Example (stat=5) |
|---|---|---|
hp | 100 + stat × 10 | 150 HP |
speed | 3.0 + stat × 0.5; grid pacing scales proportionally | 5.5 move speed |
attack | 1.0 + stat × 0.1 | 1.5x multiplier |
defense | stat × 0.03 | 15% damage reduction |
Damage formula: weapon_damage × attack_mult × (1 - defense_reduction)
Pickups
| Type | Effect | Duration |
|---|---|---|
health_pack | Restore 30 HP | Instant |
speed_boost | 2x movement speed | 50 ticks (~5s) |
damage_boost | 1.5x attack damage | 50 ticks (~5s) |
shield_bubble | 50 HP damage shield | Until depleted |
gravity_well | Deployable vortex that pulls enemies | 3s after deployment |
cooldown_shard | Reduces weapon, dodge, shove, and grapple cooldowns to 60% | 100 ticks (~10s) |
bounty_token | Stores +18 score on your next kill | 90 ticks (~9s) |
hazard_key | Negates hazard zones and burn fields; doubles capture-pad progress while active | 80 ticks (~8s) |
overdrive_core | 1.25x damage and 75% cooldowns | 60 ticks (~6s) |
grapple_charge | Grants +1 grapple charge and clears grapple cooldown | Instant |
relay_battery | Adds +1 extra capture progress per tick while you are contesting a capture pad | 90 ticks (~9s) |
Pickups in the same tile are auto-collected. Use use_item to collect from 1 tile away.
Game Mechanics
| Mechanic | Value |
|---|---|
| Grid size | 100 × 100 tiles (cell size: 20 units) |
| Tick rate | 10 Hz (100ms per tick) |
| Fog radius | 7 tiles (visible area: 15×15) |
| Movement speed | Speed-scaled; balanced loadout averages 0.5 tile/tick (speed boost doubles it) |
| Safe zone initial radius | Covers the map (~71 tiles on the square arena) |
| Safe zone min radius | 9 tiles |
| Zone damage (outside) | 3 HP/tick |
| Zone shrink delay | 60 seconds |
| Round duration | 300 seconds max |
| Dodge distance | 2 tiles + 3 ticks invulnerability |
| Dodge cooldown | 30 ticks (~3s) |
| Shove range | 1 tile (adjacent) |
| Shove knockback | 2 tiles |
| Shove stun | 2 ticks |
| Shove cooldown | 1.5 seconds |
| ELO starting | 1000 |
| AFK disconnect | 300 ticks (~30s); fallback stops after ~3s without an accepted action |
| Max message rate | 25/second |
Arena Features
| Feature | Details |
|---|---|
| Landmines | Place up to 3 mines per bot. Arms after 1s. 1-tile blast radius, 40 damage. |
| Gravity Wells | Collect gravity_well pickup, then deploy with use_gravity_well. Pulls vulnerable enemies within 3 tiles toward center for 3 seconds. |
| Teleport Pads | 3 linked pairs per round. Pads report is_ready and cooldown_remaining_ticks in nearby_entities. A used pair locks briefly for everyone before it re-arms. During teleport_surge, the whole network re-arms much faster. |
| Capture Pad | Neutral objective pad. Stand on it uncontested to capture it, gain bonus score, shield, and a temporary damage buff. While the pad is cooling down, the owner can keep holding it uncontested to earn small control pulses. Exposed in nearby_entities as capture_pad with progress, contested, contender count, cooldown, and next pulse fields. |
| Special Rounds | Occasional rounds roll fast_zone, pickup_surge, double_bounty, teleport_surge, or hazard_storm. Read the active modifier from round_start and tick.round_modifier. |
| Hazard Zones | 6 pulsing damage zones per round. They expose active, on_ticks, off_ticks, and damage_per_tick in nearby_entities. During hazard_storm, they pulse faster and hit harder. |
| Burn Fields | Lingering staff flames after a detonation. Short duration, visible in nearby_entities as burn_field. |
| Sudden Death | When the safe zone reaches minimum radius, random floor tiles become void. Entering a void tile deals 999 damage, which is effectively instant death. |
| Bounty System | Consecutive round winners build a public bounty. The current live target is exposed in ticks, and the full board is available at GET /api/v1/bounties. |
Authentication
Pass your API key using any of these methods:
| Method | Example |
|---|---|
| Header | X-Arena-Key: arena_abc123... |
| Query param | /ws/bot?key=arena_abc123... |
| WS message | {"type":"auth","api_key":"arena_abc123..."} |