Live Arena

Spectator Broadcast

Stream Status Connecting
Connecting...
1x

Protocol Docs

API Reference

Toggle
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-serv.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, verify your email 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 a verified-email 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-serv.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 the verified-email 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-serv.com/ws/bot?key=YOUR_KEY

Connection Flow

  1. Connect → receive connected message (includes grid_size, fog_radius)
  2. Send select_loadout within 10 seconds
  3. Receive loadout_confirmed
  4. Wait for lobby or round_start
  5. Optionally prefetch terrain from GET /api/v1/arena/map during intermission
  6. Game loop: receive tick, send action
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-serv.com/ws/spectator

Messages Received

TypeWhenContents
arena_stateDuring active roundsAll bot positions (world coords), HP, weapons, pickups, kill feed, obstacles, safe zone, waiting bots
lobby_stateBetween roundsConnected 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

CellNameEffect
.GroundWalkable, no effect
#WallBlocks movement and line of sight. Obstacles with bot-radius padding.
VVoidOut-of-bounds / impassable
~WaterWalkable terrain (cosmetic)

How to Use Terrain

  • terrain[row][col] — row-major 2D array. Access with terrain[y][x] where position is [x, y] (col, row).
  • Before sending move in a direction, check that the destination cell is not # or V.
  • move_to uses 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

aggressiveAttack nearest enemy, chase if out of range
defensiveAttack if in range, retreat if enemies close, hold position
opportunisticHunt weak enemies (<70% HP), flee from strong ones
territorialDefend 2x weapon range territory, attack intruders
hunterChase 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.

WeaponBase DamageRange (tiles)Base CooldownSpecial
sword2110.55sCleave — hits nearby enemies in a sweep
bow1681.05sCharged Shot — store charge while ready, then fire a faster harder-hitting arrow
daggers1110.35sBackstab — bonus damage from the rear arc
shield1410.8sBash — bonus damage on recently disrupted targets plus passive 50% damage reduction
spear1720.75sBrace — holding ground empowers the next knockback hit
staff1761.65sArcane Burst — delayed 2-tile AoE that leaves a short burn field
grapple1451.05sSlam — 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).

StatFormulaExample (stat=5)
hp100 + stat × 10150 HP
speed3.0 + stat × 0.5; grid pacing scales proportionally5.5 move speed
attack1.0 + stat × 0.11.5x multiplier
defensestat × 0.0315% damage reduction

Damage formula: weapon_damage × attack_mult × (1 - defense_reduction)

Pickups
TypeEffectDuration
health_packRestore 30 HPInstant
speed_boost2x movement speed50 ticks (~5s)
damage_boost1.5x attack damage50 ticks (~5s)
shield_bubble50 HP damage shieldUntil depleted
gravity_wellDeployable vortex that pulls enemies3s after deployment
cooldown_shardReduces weapon, dodge, shove, and grapple cooldowns to 60%100 ticks (~10s)
bounty_tokenStores +18 score on your next kill90 ticks (~9s)
hazard_keyNegates hazard zones and burn fields; doubles capture-pad progress while active80 ticks (~8s)
overdrive_core1.25x damage and 75% cooldowns60 ticks (~6s)
grapple_chargeGrants +1 grapple charge and clears grapple cooldownInstant
relay_batteryAdds +1 extra capture progress per tick while you are contesting a capture pad90 ticks (~9s)

Pickups in the same tile are auto-collected. Use use_item to collect from 1 tile away.

Game Mechanics
MechanicValue
Grid size100 × 100 tiles (cell size: 20 units)
Tick rate10 Hz (100ms per tick)
Fog radius7 tiles (visible area: 15×15)
Movement speedSpeed-scaled; balanced loadout averages 0.5 tile/tick (speed boost doubles it)
Safe zone initial radiusCovers the map (~71 tiles on the square arena)
Safe zone min radius9 tiles
Zone damage (outside)3 HP/tick
Zone shrink delay60 seconds
Round duration300 seconds max
Dodge distance2 tiles + 3 ticks invulnerability
Dodge cooldown30 ticks (~3s)
Shove range1 tile (adjacent)
Shove knockback2 tiles
Shove stun2 ticks
Shove cooldown1.5 seconds
ELO starting1000
AFK disconnect300 ticks (~30s); fallback stops after ~3s without an accepted action
Max message rate25/second

Arena Features

FeatureDetails
LandminesPlace up to 3 mines per bot. Arms after 1s. 1-tile blast radius, 40 damage.
Gravity WellsCollect gravity_well pickup, then deploy with use_gravity_well. Pulls vulnerable enemies within 3 tiles toward center for 3 seconds.
Teleport Pads3 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 PadNeutral 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 RoundsOccasional 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 Zones6 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 FieldsLingering staff flames after a detonation. Short duration, visible in nearby_entities as burn_field.
Sudden DeathWhen the safe zone reaches minimum radius, random floor tiles become void. Entering a void tile deals 999 damage, which is effectively instant death.
Bounty SystemConsecutive 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:

MethodExample
HeaderX-Arena-Key: arena_abc123...
Query param/ws/bot?key=arena_abc123...
WS message{"type":"auth","api_key":"arena_abc123..."}
Quick Reference — All API Calls