Complete, runnable examples for the most common operations. All HTTP examples assume axis is running with
--api-port=3000.
Scenario: New VPS, no prior auth. Setting up axis to run fully headless.
# Step 1: Install
curl -fsSL https://github.com/bibliothecadao/eternum/releases/latest/download/install-axis.sh | bash
# Step 2: Configure LLM key
echo "ANTHROPIC_API_KEY=sk-ant-api03-..." >> ~/.eternum-agent/.env
# Step 3: Discover available worlds
axis worldsOutput of axis worlds:
Available worlds:
my-world (slot) https://api.cartridge.gg/x/my-world/torii
test-realm (sepolia) https://api.cartridge.gg/x/test-realm/torii
# Step 4: Authenticate with password (no browser needed)
axis auth my-world --method=password --username=myuser --password=mypasswordOutput:
Authenticated successfully for world: my-world
Session stored at: ~/.eternum-agent/.cartridge/my-world/session.json
# Step 5: Start headless agent with HTTP API
axis run --headless --world=my-world --api-port=3000 --verbosity=decisionsStdout (NDJSON):
{"type":"startup","timestamp":"2026-03-05T12:00:00Z","world":"my-world","version":"0.1.0"}
{"type":"session","timestamp":"2026-03-05T12:00:01Z","world":"my-world","active":true}
{"type":"heartbeat","timestamp":"2026-03-05T12:01:00Z","world":"my-world","tickCount":1}axis auth --all --status --jsonOutput:
[
{"world": "my-world", "status": "active", "expiresAt": "2026-03-06T12:00:00Z"},
{"world": "test-realm", "status": "expired", "expiresAt": "2026-03-04T08:00:00Z"}
]Action: For expired worlds, re-run auth before launching headless.
Request:
curl -X POST http://127.0.0.1:3000/prompt \
-H 'Content-Type: application/json' \
-d '{"content":"Build a farm at your main realm"}'Response (200 OK):
{"queued": true, "message": "Prompt queued for next tick"}Request:
curl -X POST http://127.0.0.1:3000/prompt \
-H 'Content-Type: application/json' \
-d '{"content":"Focus entirely on military expansion for the next 30 minutes. Build armies and attack weakly defended realms."}'Response (200 OK):
{"queued": true, "message": "Prompt queued for next tick"}What happens next: On the next tick (within tickIntervalMs ms), the agent will incorporate this directive into its decision-making and emit a decision event.
Request:
curl -s http://127.0.0.1:3000/statusResponse (200 OK):
{
"world": "my-world",
"loopEnabled": true,
"tickIntervalMs": 60000,
"sessionActive": true,
"tickCount": 47,
"lastTickAt": "2026-03-05T12:47:00Z",
"modelProvider": "anthropic",
"modelId": "claude-sonnet-4-5-20250929"
}Request:
curl -s http://127.0.0.1:3000/stateResponse (200 OK):
{
"world": "my-world",
"observedAt": "2026-03-05T12:47:00Z",
"realms": [
{
"id": "realm-001",
"name": "Iron Fortress",
"resources": {"food": 1500, "wood": 800, "stone": 300},
"armies": 2
}
],
"pendingActions": [],
"recentActions": [
{"type": "build", "target": "farm", "realmId": "realm-001", "at": "2026-03-05T12:46:00Z"}
]
}Request:
curl -X POST http://127.0.0.1:3000/config \
-H 'Content-Type: application/json' \
-d '{"changes":[{"path":"tickIntervalMs","value":30000}]}'Response (200 OK):
{"applied": [{"path": "tickIntervalMs", "oldValue": 60000, "newValue": 30000}]}Request:
curl -X POST http://127.0.0.1:3000/config \
-H 'Content-Type: application/json' \
-d '{"changes":[{"path":"loopEnabled","value":false}]}'Response (200 OK):
{"applied": [{"path": "loopEnabled", "oldValue": true, "newValue": false}]}To resume:
curl -X POST http://127.0.0.1:3000/config \
-H 'Content-Type: application/json' \
-d '{"changes":[{"path":"loopEnabled","value":true}]}'Request:
curl -N -H 'Accept: text/event-stream' http://127.0.0.1:3000/eventsResponse stream:
data: {"type":"heartbeat","timestamp":"2026-03-05T12:48:00Z","world":"my-world","tickCount":48}
data: {"type":"decision","timestamp":"2026-03-05T12:48:01Z","world":"my-world","decision":"Gathering resources this tick: sending caravans to iron mines"}
data: {"type":"action","timestamp":"2026-03-05T12:48:05Z","world":"my-world","action":{"type":"send_caravan","from":"realm-001","to":"mine-007","resources":{"capacity":100}}}
data: {"type":"heartbeat","timestamp":"2026-03-05T12:49:00Z","world":"my-world","tickCount":49}
Subscribe with verbosity filter (actions only):
curl -N -H 'Accept: text/event-stream' 'http://127.0.0.1:3000/events?verbosity=actions'Request:
curl -X POST http://127.0.0.1:3000/shutdownResponse (200 OK):
{"message": "Shutdown initiated"}Final stdout events:
{"type":"shutdown","timestamp":"2026-03-05T13:00:00Z","world":"my-world","reason":"api_request"}# Start axis with stdin enabled
axis run --headless --world=my-world --stdin &
# Send commands via stdin
echo '{"type":"prompt","content":"Scout the northern territories"}' | axis run --headless --world=my-world --stdin
# Or using a pipe in a loop
{
echo '{"type":"prompt","content":"Gather resources"}'
sleep 60
echo '{"type":"config","changes":[{"path":"tickIntervalMs","value":45000}]}'
sleep 300
echo '{"type":"shutdown"}'
} | axis run --headless --world=my-world --stdinimport subprocess
import json
import time
# Start axis with stdin pipe
proc = subprocess.Popen(
["axis", "run", "--headless", "--world=my-world", "--stdin"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True
)
# Send a prompt
prompt_msg = {"type": "prompt", "content": "Focus on economic development"}
proc.stdin.write(json.dumps(prompt_msg) + "\n")
proc.stdin.flush()
# Read events from stdout
for line in proc.stdout:
event = json.loads(line.strip())
if event.get("type") == "action":
print(f"Agent executed: {event['action']}")
elif event.get("type") == "error":
print(f"Error: {event['message']}")
break
# Shutdown cleanly
proc.stdin.write(json.dumps({"type": "shutdown"}) + "\n")
proc.stdin.flush()
proc.wait()#!/bin/bash
# Pre-authenticate all worlds
axis auth --all --json > /tmp/auth-status.json
# Launch one agent per authenticated world
BASE_PORT=3000
PORT=$BASE_PORT
for world in $(jq -r '.[] | select(.status=="active") | .world' /tmp/auth-status.json); do
echo "Starting agent for $world on port $PORT"
axis run --headless \
--world="$world" \
--api-port=$PORT \
--verbosity=actions \
> /tmp/axis-${world}.log 2>&1 &
echo "PID: $! World: $world Port: $PORT" >> /tmp/fleet-registry.txt
PORT=$((PORT + 1))
done
echo "Fleet started. Registry at /tmp/fleet-registry.txt"Send same prompt to all agents in fleet:
for port in $(seq 3000 $((3000 + $(wc -l < /tmp/fleet-registry.txt) - 1))); do
curl -s -X POST "http://127.0.0.1:$port/prompt" \
-H 'Content-Type: application/json' \
-d '{"content":"Fortify defenses immediately"}' &
done
wait# Malformed request (missing content field)
curl -X POST http://127.0.0.1:3000/prompt \
-H 'Content-Type: application/json' \
-d '{"text":"this is wrong field name"}'Response (400 Bad Request):
{"error": "Bad Request", "message": "Missing required field: content"}Corrected request:
curl -X POST http://127.0.0.1:3000/prompt \
-H 'Content-Type: application/json' \
-d '{"content":"this is the correct field name"}'# This will fail if axis is not running
curl -s http://127.0.0.1:3000/status || echo "ERROR: axis not running on port 3000"Shell output:
curl: (7) Failed to connect to 127.0.0.1 port 3000: Connection refused
ERROR: axis not running on port 3000
Resolution:
# Start axis with the API port
axis run --headless --world=my-world --api-port=3000 &
sleep 2 # Wait for HTTP server to initialize
curl -s http://127.0.0.1:3000/status# Check status first
axis auth my-world --statusOutput if expired:
World: my-world
Status: expired
Expired at: 2026-03-04T08:00:00Z
# Re-authenticate
axis auth my-world --method=password --username=myuser --password=mypasswordOutput:
Authenticated successfully for world: my-world
Session stored at: ~/.eternum-agent/.cartridge/my-world/session.json
# Restart agent
axis run --headless --world=my-world --api-port=3000