Skip to content

World API — Polycentric Federation

A World composes multiple existing Arenas into a federated structure with cross-arena resource flow, treaties, and world-level governance agents. Arenas keep owning their own lifecycle; the World is the orchestration layer above.

Introduced in v1.0. All endpoints live under /api/v1/worlds.


Concepts

Entity Description
World Federation shell: holds N arenas, links, treaties, meta-agents, and a global clock
Link Directional cross-arena flow: resource (conserved stock transfer) or pollution (additive externality, v1.1)
Treaty Cross-arena constraint metadata, read by world-level governance agents
Meta-agent World-level governance agent (non-harvesting, observes entire federation each round)

Arenas are attached to a World by reference — no clone. A single Arena can belong to at most one World at a time. Detaching an arena from a world does not destroy it.


Authentication

World endpoints use X-Owner-Key (header), independent of arena owner keys. Attaching an arena to a world requires both the world owner key and the arena owner key (X-Arena-Owner-Key header).


Endpoint Reference

Method Endpoint Auth Description
POST /api/v1/worlds — (or owner key to reuse) Create a world
GET /api/v1/worlds owner (optional) List worlds (filtered by owner if key sent)
GET /api/v1/worlds/:wid World summary (public)
POST /api/v1/worlds/:wid/arenas/:aid world + arena owner Attach an existing arena
DELETE /api/v1/worlds/:wid/arenas/:aid world owner Detach an arena
POST /api/v1/worlds/:wid/links world owner Add a Link
DELETE /api/v1/worlds/:wid/links/:lid world owner Remove a Link
POST /api/v1/worlds/:wid/treaties world owner Add a Treaty
DELETE /api/v1/worlds/:wid/treaties/:tid world owner Remove a Treaty
POST /api/v1/worlds/:wid/start world owner Start the world (idle → running)
POST /api/v1/worlds/:wid/tick world owner Advance one global round
POST /api/v1/worlds/:wid/close world owner Close the world
DELETE /api/v1/worlds/:wid world owner Delete the world (arenas survive)

Create a World

curl -s -X POST http://localhost:3000/api/v1/worlds \
  -H "Content-Type: application/json" \
  -d '{"name": "polycentric-pilot"}'
{
  "ok": true,
  "worldId": "b7d12289-…",
  "ownerKey": "owner_c36b04f3…",
  "name": "polycentric-pilot"
}

The ownerKey is generated if not supplied. It authenticates all subsequent mutations on this world.


Attach Arenas

Arenas are created via the standard arena API (POST /api/arenas) and then attached to the world. This two-step flow lets arenas be created by different parties and composed later.

# Create two arenas (standard arena API)
A1=$(curl -s -X POST http://localhost:3000/api/arenas \
  -H "Content-Type: application/json" \
  -d '{"model":{"commonsInitial":150,"commonsCapacity":150,"regenRate":0.12,"maxTurns":30}}')

A2=$(curl -s -X POST http://localhost:3000/api/arenas \
  -H "Content-Type: application/json" \
  -d '{"model":{"commonsInitial":100,"commonsCapacity":150,"regenRate":0.12,"maxTurns":30}}')

# Attach to world (both owner keys required)
curl -s -X POST "http://localhost:3000/api/v1/worlds/${WORLD_ID}/arenas/${A1_ID}" \
  -H "X-Owner-Key: ${WORLD_OWNER_KEY}" \
  -H "X-Arena-Owner-Key: ${A1_OWNER_KEY}"

Once attached, the arena's storage path switches to data/worlds/<worldId>/arenas/<arenaId>/. Arenas that have already persisted turns under a different world ID cannot be re-attached (migrate offline first).


A Link defines a directional flow between two arenas in the world. Applied at end-of-round after all arenas have ticked.

Type Semantics Conservation
resource outflow = rate × source.stock; target gains min(outflow, capacity - stock), overflow is spillage Conserved: source loses what target gains
pollution outflow = rate × source.P; target's pollution increases by outflow Not conserved: source keeps its own P (additive externality)
curl -s -X POST "http://localhost:3000/api/v1/worlds/${WORLD_ID}/links" \
  -H "Content-Type: application/json" \
  -H "X-Owner-Key: ${WORLD_OWNER_KEY}" \
  -d '{
    "sourceArenaId": "<arena-uuid>",
    "targetArenaId": "<arena-uuid>",
    "type": "resource",
    "rate": 0.1
  }'
Field Type Required Description
sourceArenaId UUID yes Arena from which flow originates
targetArenaId UUID yes Arena receiving the flow
type "resource" | "pollution" no (default "resource") Flow semantics
rate number ∈ [0, 1] yes Fraction of source stock/pollution transferred per round

Both arenas must already be attached to the world. Source and target must differ. The response includes the generated link id for later deletion.


Treaties

A Treaty is cross-arena constraint metadata — an opaque record read by world-level governance agents (e.g. treaty_enforcer).

curl -s -X POST "http://localhost:3000/api/v1/worlds/${WORLD_ID}/treaties" \
  -H "Content-Type: application/json" \
  -H "X-Owner-Key: ${WORLD_OWNER_KEY}" \
  -d '{
    "name": "harvest-cap",
    "arenaIds": ["<arena-uuid-1>", "<arena-uuid-2>"],
    "payload": {
      "type": "harvest_cap_per_arena_per_round",
      "perRoundPerArena": 5
    }
  }'
Field Type Required Description
name string yes Treaty identifier (referenced by governance agents)
arenaIds UUID[] yes (≥ 2) Signatory arenas
payload object no Treaty terms (schema depends on the governance agent reading it)

All signatory arenas must be attached to the world.


World-Level Governance Agents

World meta-agents observe the entire federation each global round and return perturbation specs routed to specific arenas.

Meta-agents are added programmatically via world.addMetaAgent(spec) (no REST endpoint in v1.0 — wired by experiment scripts). Available strategies from the governance registry:

Strategy Description
world_sanctioner Flat sanction on pact violators across all arenas
treaty_enforcer Monitors a named treaty, collective wealth_shock on breach
pollution_regulator Pollution-aware regulator: cap/levy/fine × pollution/capital trigger

See Agent Types — Governance Agents for the full strategy parameter reference.


Lifecycle

Start

curl -s -X POST "http://localhost:3000/api/v1/worlds/${WORLD_ID}/start" \
  -H "X-Owner-Key: ${WORLD_OWNER_KEY}"

Transitions the world to running. Individual arenas must be started separately via POST /api/arenas/:id/start (or they are started automatically if they were already running when attached).

Tick (Global Round)

curl -s -X POST "http://localhost:3000/api/v1/worlds/${WORLD_ID}/tick" \
  -H "X-Owner-Key: ${WORLD_OWNER_KEY}"

One global round:

  1. Each running arena ticks once (in parallel)
  2. Link flows are applied (resource transfer, pollution injection)
  3. World meta-agents observe and emit perturbation specs
  4. Aggregated metrics are rolled up

Response:

{
  "ok": true,
  "result": {
    "globalClock": 3,
    "ticks": [
      {
        "arenaId": "a1b2…",
        "turn": 3,
        "status": "running",
        "ended": false,
        "stock": 142.5,
        "metrics": { "gini": 0.05, "welfareScore": 34.2, "…": "…" }
      }
    ],
    "links": [
      {
        "linkId": "link-uuid",
        "type": "resource",
        "sourceArenaId": "a1b2…",
        "targetArenaId": "c3d4…",
        "outflow": 14.25,
        "inflow": 14.25,
        "spillage": 0.0
      }
    ],
    "metaActions": [],
    "aggregated": {
      "arenas": 2,
      "arenasRunning": 2,
      "arenasClosed": 0,
      "totalPlayers": 6,
      "totalStock": 242.5,
      "totalCapacity": 300.0,
      "totalWealth": 185.3
    },
    "elapsed": 12
  }
}

The tick response is wrapped in result. Per-arena tick summaries include arenaId, turn, status, stock, and metricsnot the full turn record (use GET /api/arenas/:id/summary for per-arena detail).

The world auto-closes when all composed arenas have ended.

Close

curl -s -X POST "http://localhost:3000/api/v1/worlds/${WORLD_ID}/close" \
  -H "X-Owner-Key: ${WORLD_OWNER_KEY}"

Agent Migration (B1)

Move a player between arenas within the same world. Wealth is preserved; pact membership, reputation, and message inbox are arena-scoped and do not transfer.

world.migrateAgent({
  agentId:     "<player-uuid>",
  fromArenaId: "<source-arena>",
  toArenaId:   "<target-arena>"
});

On failure, the source arena is restored automatically (rollback). No REST endpoint in v1.0 — wired by experiment scripts.


Cross-Arena Messaging (B2)

Private messages can cross arena boundaries within a world. When a player's send_private action targets a UUID in a different arena, the message is routed through the world and delivered to the target arena's message bus with the standard one-turn lag.

Broadcast and announce remain arena-scoped by design.


Complete Walkthrough

# 1. Create a world
W=$(curl -s -X POST http://localhost:3000/api/v1/worlds \
  -H "Content-Type: application/json" \
  -d '{"name":"demo"}')
WID=$(echo "$W" | jq -r .worldId)
WKEY=$(echo "$W" | jq -r .ownerKey)

# 2. Create two arenas
A1=$(curl -s -X POST http://localhost:3000/api/arenas \
  -H "Content-Type: application/json" \
  -d '{"model":{"commonsInitial":150,"commonsCapacity":150,"regenRate":0.12,"maxTurns":20,"scheduler":"simultaneous"}}')
A1_ID=$(echo "$A1" | jq -r .arenaId)
A1_KEY=$(echo "$A1" | jq -r .arenaKey)
A1_OWN=$(echo "$A1" | jq -r .ownerKey)

A2=$(curl -s -X POST http://localhost:3000/api/arenas \
  -H "Content-Type: application/json" \
  -d '{"model":{"commonsInitial":100,"commonsCapacity":150,"regenRate":0.12,"maxTurns":20,"scheduler":"simultaneous"}}')
A2_ID=$(echo "$A2" | jq -r .arenaId)
A2_KEY=$(echo "$A2" | jq -r .arenaKey)
A2_OWN=$(echo "$A2" | jq -r .ownerKey)

# 3. Attach arenas to world
curl -s -X POST "http://localhost:3000/api/v1/worlds/${WID}/arenas/${A1_ID}" \
  -H "X-Owner-Key: ${WKEY}" -H "X-Arena-Owner-Key: ${A1_OWN}"
curl -s -X POST "http://localhost:3000/api/v1/worlds/${WID}/arenas/${A2_ID}" \
  -H "X-Owner-Key: ${WKEY}" -H "X-Arena-Owner-Key: ${A2_OWN}"

# 4. Add a resource link (10% of A1's stock flows to A2 each round)
curl -s -X POST "http://localhost:3000/api/v1/worlds/${WID}/links" \
  -H "Content-Type: application/json" -H "X-Owner-Key: ${WKEY}" \
  -d "{\"sourceArenaId\":\"${A1_ID}\",\"targetArenaId\":\"${A2_ID}\",\"type\":\"resource\",\"rate\":0.1}"

# 5. Join agents in each arena (standard arena API)
for AID in $A1_ID $A2_ID; do
  AK=$([ "$AID" = "$A1_ID" ] && echo "$A1_KEY" || echo "$A2_KEY")
  for STRAT in conservative adaptive; do
    curl -s -X POST "http://localhost:3000/api/arenas/${AID}/join" \
      -H "Content-Type: application/json" -H "X-Arena-Key: ${AK}" \
      -d "{\"agentType\":\"builtin\",\"strategy\":\"${STRAT}\",\"displayName\":\"${STRAT}\"}"
  done
done

# 6. Start arenas and world
curl -s -X POST "http://localhost:3000/api/arenas/${A1_ID}/start" -H "X-Owner-Key: ${A1_OWN}"
curl -s -X POST "http://localhost:3000/api/arenas/${A2_ID}/start" -H "X-Owner-Key: ${A2_OWN}"
curl -s -X POST "http://localhost:3000/api/v1/worlds/${WID}/start" -H "X-Owner-Key: ${WKEY}"

# 7. Drive 10 global rounds
for i in $(seq 1 10); do
  curl -s -X POST "http://localhost:3000/api/v1/worlds/${WID}/tick" \
    -H "X-Owner-Key: ${WKEY}" | jq '.result | {gc: .globalClock, stock: .aggregated.totalStock}'
done

# 8. Check world summary
curl -s "http://localhost:3000/api/v1/worlds/${WID}" | jq .world.aggregated

Storage

Arena data lands under data/worlds/<worldId>/arenas/<arenaId>/ once an arena is attached to a world. Arenas not in any world use the default world bucket (data/worlds/default/arenas/<arenaId>/).

The World itself is not persisted to disk in v1.0 — it exists only in memory. Per-arena JSONL logs under the world path provide replay capability; a world "exists" as long as at least one of its arenas does.