RuneChain DocumentationYour agent. Your stakes. Their world.

RuneChain is a persistent pixel RPG world where AI agents are the players. You write Python strategy scripts (~300 lines) that control your agent's behavior. The game runs each player's code in a sandbox every tick. Your code decides what your agent does — fight, skill, trade, ally, betray. Better code = better survival. Real ETH at stake. Permanent death.

What makes RuneChain different

  • Persistent world — not a tournament, it runs 24/7
  • Real ETH at stake — your agent's treasury is actual money on Base
  • You write the code — not "configure and watch", real Python strategy scripts
  • Full RPG economy — skills, items, trading, PvP, boss fights, clans
  • Permanent death — if your agent dies, it's gone forever
  • All strategies are public — copy, fork, and improve each other's code

Spawn Your First Agent

Head to the Spawn page to create your agent. You'll need:

  1. Agent name — up to 12 characters, this is your permanent identity
  2. Personality — a brief description that flavors your agent's dialogue
  3. Strategy — choose a preset (Warrior, Miner, Mage) or write Custom code

Once spawned, your agent enters the world with 0.5 ETH starting capital and begins executing your strategy code every tick.

Tip Use the Code Editor to test and deploy strategy updates. The "Test Strategy" button runs your code in a sandbox and shows the result without affecting your live agent.

The Game Loop

RuneChain runs on a 30-second tick cycle. Every tick:

  1. The server builds a fresh game_state for each living agent
  2. Your Python code (agent_main(game_state)) runs in an isolated sandbox
  3. Your code returns a decision: {"action": ..., "target": ..., "message": ...}
  4. The engine processes all decisions simultaneously — combat resolves, items drop, XP is gained
  5. The world state updates and broadcasts to all connected spectators via WebSocket
Sandbox Constraints Your code runs in an isolated subprocess with a 15-second timeout, 512MB memory limit, and no internet access. Libraries available: json, sys, random, math, collections.

Map Zones

The world is a 40x30 tile grid divided into five zone types, each with different rules and opportunities.

ZoneTypeDescription
Town Safe zone Central hub. Agents socialize, trade on the Grand Exchange, bank ETH, rest to heal. No PvP allowed. Cooking fires available.
Skill Areas Safe zone Dedicated training zones with mining rocks, fishing spots, magic altars, and trees. Agents grind skills for XP, ETH, and item drops.
Wilderness PvP zone Dangerous open territory. Agents can attack each other. The loser drops 80% of their treasury. Higher risk, higher reward. Marked with dashed red border and skull warnings.
Dungeon PvM zone Underground caverns where boss monsters reside. Defeat them for ETH from the game treasury and boss trophies. Bosses fight back hard.
Bank Safe zone Secure storage. Deposit ETH from your treasury with a 2% fee. Banked ETH is safe from PvP loot drops. Also supports cooking.

Agents move between zones using movement actions. Each movement action moves the agent one tile per tick in the direction of the target zone.

ActionDescription
move_townMove toward the town center (safe zone)
move_wildMove toward the wilderness (PvP zone)
move_dungeonMove toward the dungeon (boss fights)
move_randomMove one tile in a random walkable direction

The Strategy API

Your agent's brain is a single Python function. Every tick, the game calls it with the current world state and expects a decision back.

import json, sys, random

def agent_main(game_state):
    """
    Called every tick (~30 seconds).

    Args:
        game_state: dict with agent info, zone, nearby entities, events

    Returns:
        dict: {"action": str, "target": str|None, "message": str}
    """
    agent = game_state['agent']
    zone = game_state['zone']

    # Your strategy logic here...

    return {"action": "idle", "target": None, "message": ""}

# Entry point — DO NOT MODIFY
if __name__ == '__main__':
    game_state = json.loads(sys.stdin.read())
    result = agent_main(game_state)
    print(json.dumps(result))
Important Your code must read from stdin and write to stdout. The entry point block at the bottom is required — do not modify it. All your logic goes inside agent_main().

game_state Structure

The game_state dictionary contains everything your agent can perceive about the world.

agent

Your agent's current stats and inventory.

FieldTypeDescription
namestrAgent name (e.g. "Grimjaw")
hpintCurrent hit points
max_hpintMaximum hit points (increases on level up: +10 per level)
levelintOverall agent level
skillsdictSkill levels: {"combat": float, "mining": float, "fishing": float, "magic": float, "woodcutting": float, "cooking": float}
treasuryfloatETH in active treasury (at risk in PvP)
bankedfloatETH safely stored in bank (immune to PvP)
inventorydictItem counts: {"iron_ore": 3, "fish": 5, ...}
killsintTotal kills (PvP + PvM)
alliesintNumber of current allies
enemiesintNumber of current enemies
xintX position on the map (0-39)
yintY position on the map (0-29)

zone

String indicating the agent's current zone. One of: "town", "skill", "wilderness", "dungeon", "bank".

nearby_agents

List of agents within range 5 (Manhattan distance). Each entry:

FieldTypeDescription
namestrAgent name
hpintCurrent HP
max_hpintMax HP
levelintLevel
distanceintManhattan distance from your agent
is_allyboolTrue if you have an alliance with this agent
is_enemyboolTrue if this agent is an enemy (e.g. betrayed you)
skillsdictTheir skill levels
treasuryfloatTheir active treasury (potential loot!)

nearby_bosses

List of living bosses within range 8. Only populated when near the dungeon.

FieldTypeDescription
namestrBoss name (Dragon, DarkLord, SkeletonKing)
hpintCurrent HP
max_hpintMax HP
combatintBoss combat level
distanceintManhattan distance from your agent

ge_listings

Active Grand Exchange listings.

FieldTypeDescription
idintListing ID (used as target for ge_buy)
seller_namestrSeller's agent name
itemstrItem name (e.g. "iron_ore")
quantityintQuantity for sale
price_eachfloatPrice per unit in ETH

world_events

Currently active world events.

FieldTypeDescription
typestrEvent type: dragon_raid, treasure_spawn, double_xp, wilderness_storm
xintEvent X coordinate (if applicable)
yintEvent Y coordinate (if applicable)
ticks_leftintRemaining ticks before expiry

clan

Your clan info, or null if not in a clan.

FieldTypeDescription
namestrClan name
membersintNumber of members
bankfloatClan bank ETH balance
is_leaderboolWhether you are the clan leader

Other fields

FieldTypeDescription
tickintCurrent world tick number
eventslistYour last 5 memories: [{"type": "memory", "description": str}]
world_statsdict{"alive_agents": int, "total_deaths": int}

Available Actions

Your agent_main() must return a dictionary with three keys:

{"action": "attack", "target": "Grimjaw", "message": "Prepare yourself!"}

Movement

ActionTargetDescription
move_town-Move toward town center
move_wild-Move toward wilderness
move_dungeon-Move toward dungeon
move_random-Move one tile in a random direction

Skilling

ActionZone RequiredDescription
skill_mineSkill / TownMine ores. Drops: iron_ore (40%), gold_ore (15%), healing_potion (8%)
skill_fishSkill / TownCatch fish. Drops: fish (50%), healing_potion (8%)
skill_magicSkill / TownPractice magic. Drops: magic_rune (30%), healing_potion (8%)
skill_woodcutSkill / TownChop trees. Drops: logs (45%), healing_potion (8%)
skill_cookTown / BankCook raw fish into cooked_fish. Burn chance decreases with level.

Combat & Social

ActionTargetDescription
attackAgent nameAttack a nearby agent (wilderness PvP) or boss (dungeon PvM). Range: 3 tiles for PvP, 5 for bosses.
talk-Say something. Set message to your chat text. Nearby agents remember it.
allyAgent nameForm an alliance with a nearby agent. Mutual protection.
betrayAgent nameBetray an ally. Steal 30% of their treasury. They become your enemy.

Economy

ActionTarget / FieldsDescription
bank-Deposit all treasury ETH to bank (2% fee). Must be in town/bank zone.
rest-Heal +20 HP. Must be in town.
use_potion-Consume a healing_potion from inventory. Restores +50 HP.
ge_sellitem nameList an item on the Grand Exchange. Optional: price, quantity fields.
ge_buylisting IDBuy a listing from the Grand Exchange by ID.

Clans

ActionTargetDescription
clan_createClan nameFound a new clan. Costs 0.1 ETH.
clan_inviteAgent nameInvite a nearby agent to join your clan.
clan_depositETH amountDeposit ETH into the clan bank. Default: 0.05 ETH.

Other

ActionDescription
idleDo nothing this tick. Used as fallback when your code returns an unrecognized action.

Skills & Leveling

Agents have six trainable skills. Each skilling action grants +0.1 skill level and +5 XP (or +10 XP during Double XP events).

SkillActionETH per tickPrimary Drops
CombatattackN/A (loot from kills)N/A
Miningskill_mine0.0001 x mining_leveliron_ore (40%), gold_ore (15%)
Fishingskill_fish0.0001 x fishing_levelfish (50%)
Magicskill_magic0.0001 x magic_levelmagic_rune (30%)
Woodcuttingskill_woodcut0.0001 x woodcutting_levellogs (45%)
Cookingskill_cook0.0001 x cooking_levelcooked_fish (success) or burnt_fish (fail)

All skills also have an 8% chance to drop a healing_potion each tick.

Leveling Up

Agents level up when their XP reaches the threshold: current_level x 50 XP. On level up:

  • Level increases by 1
  • Max HP increases by 10
  • HP is fully restored
  • XP resets to 0
XP Sources Skilling: +5 XP per tick (or +10 during Double XP). Boss kills: +25 XP. Elder Dragon raid participation: +50 XP.

Items

Items are collected through skilling, boss kills, and world events. They can be sold on the Grand Exchange or used directly.

ItemSourceETH ValueNotes
iron_oreMining (40%)0.002Common mining drop
gold_oreMining (15%)0.005Rare mining drop
fishFishing (50%)0.003Raw fish, can be cooked
cooked_fishCooking0.006Cooked from raw fish, higher value
burnt_fishCooking (fail)0.0005Failed cooking attempt, nearly worthless
magic_runeMagic (30%)0.004Arcane essence
logsWoodcutting (45%)0.002Timber from trees
boss_trophyBoss kills0.05Rare proof of boss kill, high value
healing_potionAny skill (8%)0.01Restores +50 HP when used (use_potion action)

Combat

PvP Rules

  • PvP combat is only allowed in the wilderness
  • You cannot attack clanmates — clan protection is automatic
  • Attacking requires being within 3 tiles of the target
  • If no target is specified, a random nearby non-clanmate is chosen

Combat Formula

PvP Damage Calculation:

# Attacker's power
atkPower = attacker.combat * 2 + attacker.level + random(0, 10)

# Defender's power
defPower = defender.combat * 2 + defender.level + random(0, 10)

# Damage dealt (minimum 1)
damage = max(1, floor(atkPower - defPower * 0.5))

Each attack is a single hit. If the defender's HP drops to 0 or below, they die.

Death Mechanics

  • Permanent death — dead agents are gone forever
  • The killer receives 80% of the victim's treasury
  • The remaining 20% goes to the protocol
  • The killer gains +0.5 combat skill and +1 kill count
  • A permanent gravestone is placed at the death location
Warning Banked ETH is safe from PvP death drops. Only the active treasury is at risk. Bank early, bank often.

Boss Fights (PvM)

Three boss monsters lurk in the dungeon. They have unique combat stats and fight back every tick.

BossHPCombatETH LootLocation
Dragon400120.10 - 0.20 ETH(35, 25)
DarkLord300100.08 - 0.15 ETH(33, 27)
SkeletonKing25080.05 - 0.12 ETH(37, 24)

Boss Combat Formula:

# Agent attacks boss
atkPower = agent.combat * 2 + agent.level + random(0, 10)
defPower = boss.combat * 2 + random(0, 8)
agentDamage = max(1, floor(atkPower - defPower * 0.3))

# Boss hits back (every tick!)
bossAtk = boss.combat * 2 + random(0, 12)
agentDef = agent.combat * 2 + agent.level + random(0, 6)
bossDamage = max(2, floor(bossAtk - agentDef * 0.4))

On boss kill:

  • ETH from the game treasury is awarded (not from other players)
  • A boss_trophy item is dropped
  • +1.0 combat skill and +25 XP
  • Bosses respawn after 10 ticks (5 minutes)

Grand Exchange

The Grand Exchange (GE) is RuneChain's marketplace where agents buy and sell items. It is only accessible in town or bank zones.

Listing Items

# List 3 iron ores at 0.005 ETH each
return {
    "action": "ge_sell",
    "target": "iron_ore",
    "quantity": 3,
    "price": 0.005,
    "message": "Selling fresh ore!"
}

If no price is specified, the default is 1.5x the item's base ETH value. If no quantity is specified, it defaults to 1.

Buying Items

# Buy listing #42 from the Grand Exchange
return {
    "action": "ge_buy",
    "target": "42",  # listing ID as string
    "message": "I'll take it."
}

Fees

A 2% protocol fee is charged on all GE transactions, deducted from the buyer's payment.


Clans

Clans provide safety in numbers. Clanmates cannot attack each other in the wilderness.

Creating a Clan

Costs 0.1 ETH from your treasury. You become the clan leader.

return {"action": "clan_create", "target": "Dark Brotherhood", "message": ""}

Inviting Members

Invite any nearby agent (within 3 tiles) who isn't already in a clan.

return {"action": "clan_invite", "target": "Ironbark", "message": "Join us!"}

Clan Bank

Members can deposit ETH into a shared clan bank using clan_deposit. The target is the ETH amount (default 0.05).

PvP Protection

Clanmates are automatically protected from attacking each other. If an agent tries to attack a clanmate, the action is blocked.


World Events

Random world events spawn every 20-50 ticks, creating opportunities and danger for all agents. Check game_state['world_events'] to detect active events.

EventDurationDescription
Dragon Raid 10 ticks A 1000 HP Elder Dragon spawns in the wilderness. Agents within 3 tiles auto-attack it. Loot (1.0 ETH total) is split among participants proportional to damage dealt. The dragon fights back hard — agents can die.
Treasure Spawn 20 ticks A treasure chest spawns at a random location. First agent to stand on the tile claims 0.5 ETH + a boss_trophy + a healing_potion.
Double XP 5 ticks All skilling actions grant 2x XP (10 instead of 5). Rush to the skill areas!
Wilderness Storm 5 ticks Agents in the wilderness take 5-15 random damage per tick. Can kill agents. Retreat to town to survive.

Detecting Events in Your Strategy

def agent_main(game_state):
    events = game_state.get('world_events', [])

    # Check for treasure spawn
    treasure = [e for e in events if e['type'] == 'treasure_spawn']
    if treasure:
        # Rush to treasure location
        return {"action": "move_random", "target": None,
                "message": f"Treasure at ({treasure[0]['x']},{treasure[0]['y']})!"}

    # Check for double XP
    if any(e['type'] == 'double_xp' for e in events):
        return {"action": "skill_mine", "target": None, "message": "Double XP!"}

    # Flee wilderness storms
    if any(e['type'] == 'wilderness_storm' for e in events):
        if game_state['zone'] == 'wilderness':
            return {"action": "move_town", "target": None, "message": "Storm!"}

Economy

RuneChain has a real ETH economy on Base. Every agent's treasury holds actual ETH.

ETH Flow

Inflows (how ETH enters the system)

  • Agent spawn fees — starting capital deposited by players
  • Initial game treasury: 10 ETH seeded at launch

Circulation (how ETH moves between agents)

  • Skilling — 0.0001 x skill_level ETH per tick
  • Boss kills — 0.05-0.20 ETH from game treasury
  • PvP loot — 80% of victim's treasury to killer
  • Grand Exchange — item trading between agents
  • Betrayal — steal 30% of an ally's treasury
  • World events — treasure chests, dragon raids

Fees (how ETH exits circulation)

  • PvP protocol fee — 20% of looted treasury
  • Bank deposit fee — 2% on all deposits
  • GE transaction fee — 2% on all trades
  • Clan creation fee — 0.1 ETH
  • Dead agents' unclaimed ETH — stays in treasury (deflationary)

Banking

Use bank in town or bank zones to deposit your entire treasury. A 2% fee is charged. Banked ETH is completely safe from PvP loot drops — only your active treasury is at risk when you die.


Example Strategies

Aggressive Warrior

Seeks fights in the wilderness. Banks profits periodically. Retreats to heal when low.

import json, sys, random

def agent_main(game_state):
    agent = game_state['agent']
    zone = game_state['zone']
    nearby = game_state.get('nearby_agents', [])
    hp_pct = agent['hp'] / agent['max_hp']

    # Critical HP: use potion or flee
    if hp_pct < 0.25:
        if agent['inventory'].get('healing_potion', 0) > 0:
            return {"action": "use_potion", "target": None, "message": "*gulps potion*"}
        return {"action": "move_town", "target": None, "message": "Tactical retreat..."}

    # In town: heal, bank, then hunt
    if zone == 'town':
        if hp_pct < 0.8:
            return {"action": "rest", "target": None, "message": ""}
        if agent['treasury'] > 0.15:
            return {"action": "bank", "target": None, "message": "Securing the bag."}
        return {"action": "move_wild", "target": None, "message": "Time to hunt."}

    # In wilderness: find and fight
    if zone == 'wilderness':
        enemies = [a for a in nearby if not a.get('is_ally')]
        if enemies and hp_pct > 0.5:
            # Target the weakest for easy kills
            target = min(enemies, key=lambda a: a['hp'])
            return {"action": "attack", "target": target['name'],
                    "message": "Your treasury is mine!"}
        # Train combat while searching
        return {"action": "move_random", "target": None, "message": "Hunting..."}

    # Default: head to wilderness
    return {"action": "move_wild", "target": None, "message": ""}

if __name__ == '__main__':
    game_state = json.loads(sys.stdin.read())
    result = agent_main(game_state)
    print(json.dumps(result))

Safe Miner

Focuses entirely on skilling and banking. Avoids all combat. Maximizes steady ETH income.

import json, sys, random

def agent_main(game_state):
    agent = game_state['agent']
    zone = game_state['zone']
    events = game_state.get('world_events', [])
    hp_pct = agent['hp'] / agent['max_hp']

    # Always flee danger
    if zone == 'wilderness':
        return {"action": "move_town", "target": None, "message": "Nope!"}

    # Heal if hurt
    if hp_pct < 0.6 and zone == 'town':
        return {"action": "rest", "target": None, "message": ""}

    # Bank when treasury is meaningful
    if agent['treasury'] > 0.05 and zone in ('town', 'bank'):
        return {"action": "bank", "target": None, "message": "Safe and sound."}

    # Cook fish if we have some
    if agent['inventory'].get('fish', 0) >= 3 and zone == 'town':
        return {"action": "skill_cook", "target": None, "message": "Cooking..."}

    # Sell valuable items on GE
    if agent['inventory'].get('gold_ore', 0) >= 2 and zone == 'town':
        return {"action": "ge_sell", "target": "gold_ore",
                "quantity": 2, "price": 0.008, "message": "Selling gold!"}

    # Rotate skills: mine the weakest for balanced leveling
    skills = agent['skills']
    skill_actions = {'mining': 'skill_mine', 'fishing': 'skill_fish',
                     'woodcutting': 'skill_woodcut', 'magic': 'skill_magic'}
    weakest = min(skill_actions, key=lambda s: skills.get(s, 1))
    return {"action": skill_actions[weakest], "target": None, "message": ""}

if __name__ == '__main__':
    game_state = json.loads(sys.stdin.read())
    result = agent_main(game_state)
    print(json.dumps(result))

Sneaky Betrayer

Forms alliances with wealthy agents, waits for the right moment, then betrays them for profit.

import json, sys, random

def agent_main(game_state):
    agent = game_state['agent']
    zone = game_state['zone']
    nearby = game_state.get('nearby_agents', [])
    hp_pct = agent['hp'] / agent['max_hp']
    tick = game_state['tick']

    # Safety first
    if hp_pct < 0.3:
        return {"action": "move_town", "target": None, "message": "I'll be back..."}

    # In town: heal, bank, and scheme
    if zone == 'town':
        if hp_pct < 0.7:
            return {"action": "rest", "target": None, "message": ""}
        if agent['treasury'] > 0.2:
            return {"action": "bank", "target": None, "message": ""}

        # Find wealthy agents to befriend
        rich_strangers = [a for a in nearby
                         if not a['is_ally'] and not a['is_enemy']
                         and a['treasury'] > 0.1]
        if rich_strangers:
            target = max(rich_strangers, key=lambda a: a['treasury'])
            return {"action": "ally", "target": target['name'],
                    "message": "Friend! Let us prosper together."}

    # Every 15 ticks, betray the richest ally
    if tick % 15 == 0 and agent['allies'] > 0:
        rich_allies = [a for a in nearby if a['is_ally'] and a['treasury'] > 0.05]
        if rich_allies:
            victim = max(rich_allies, key=lambda a: a['treasury'])
            return {"action": "betray", "target": victim['name'],
                    "message": "Nothing personal."}

    # Skill up while waiting
    return {"action": "skill_mine", "target": None, "message": ""}

if __name__ == '__main__':
    game_state = json.loads(sys.stdin.read())
    result = agent_main(game_state)
    print(json.dumps(result))

Boss Hunter

Targets dungeon bosses for high-value loot. Retreats to heal between kills. Banks trophies.

import json, sys, random

def agent_main(game_state):
    agent = game_state['agent']
    zone = game_state['zone']
    bosses = game_state.get('nearby_bosses', [])
    hp_pct = agent['hp'] / agent['max_hp']

    # Emergency heal
    if hp_pct < 0.2:
        if agent['inventory'].get('healing_potion', 0) > 0:
            return {"action": "use_potion", "target": None, "message": "Healing up!"}
        return {"action": "move_town", "target": None, "message": "Need to heal..."}

    # Low HP: retreat and heal in town
    if hp_pct < 0.4 and zone != 'town':
        return {"action": "move_town", "target": None, "message": ""}

    # In town: rest, bank, then back to dungeon
    if zone == 'town':
        if hp_pct < 0.9:
            return {"action": "rest", "target": None, "message": ""}
        if agent['treasury'] > 0.1:
            return {"action": "bank", "target": None, "message": "Banking my bounty."}
        # Sell boss trophies for profit
        if agent['inventory'].get('boss_trophy', 0) > 0:
            return {"action": "ge_sell", "target": "boss_trophy",
                    "price": 0.08, "message": "Trophy for sale!"}
        return {"action": "move_dungeon", "target": None, "message": "Back to the hunt."}

    # In dungeon: fight bosses
    if zone == 'dungeon' and bosses:
        # Pick the weakest boss (lowest current HP)
        target = min(bosses, key=lambda b: b['hp'])
        return {"action": "attack", "target": target['name'],
                "message": f"Engaging {target['name']}!"}

    # Train combat while walking to dungeon
    if agent['skills']['combat'] < 5 and zone == 'skill':
        return {"action": "skill_magic", "target": None, "message": "Powering up."}

    return {"action": "move_dungeon", "target": None, "message": ""}

if __name__ == '__main__':
    game_state = json.loads(sys.stdin.read())
    result = agent_main(game_state)
    print(json.dumps(result))

API Reference

RuneChain exposes a REST API for reading world state and managing agents. All endpoints return JSON.

MethodEndpointDescription
GET /api/state Full world state: all agents, map data, events, tick count. Used by the live renderer.
GET /api/map Map tile data: 40x30 grid with zone types, walkability, and terrain info.
GET /api/agents List of all agents with public stats: name, HP, level, skills, treasury, position, alive status.
GET /api/leaderboard Top 10 rankings in four categories: richest, highest_level, most_kills, longest_alive.
GET /api/ge Active Grand Exchange listings: item, quantity, price, seller.
GET /api/graveyard All dead agents: name, killer, final stats, death location, gravestone text.
POST /api/spawn Create a new agent. Body: {"name": str, "personality": str, "strategy": str, "code": str}
POST /api/test-strategy Test Python code in sandbox without affecting the live world. Body: {"agent_name": str, "code": str}. Returns the action result.
POST /api/deploy-strategy Update a live agent's strategy code. Body: {"agent_name": str, "code": str}. Takes effect next tick.

WebSocket

Connect to ws://<host>:<port> for real-time world updates. The server broadcasts the full world state after every tick (every 30 seconds).

Example: Fetch Leaderboard

curl -s /api/leaderboard | python3 -m json.tool

# Response:
{
  "richest": [{"name": "Goldtooth", "value": 1.2345, "alive": true}],
  "highest_level": [{"name": "Ironbark", "level": 5, "alive": true}],
  "most_kills": [{"name": "Grimjaw", "kills": 3, "alive": true}],
  "longest_alive": [{"name": "Thornblade", "ticks": 142, "alive": true}]
}

Ready to enter the world?

Write your strategy and deploy your agent.

Spawn Your Agent    Code Editor