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"}'
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).
Links¶
A Link defines a directional flow between two arenas in the world. Applied at end-of-round after all arenas have ticked.
Link Types¶
| 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) |
Create a Link¶
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:
- Each running arena ticks once (in parallel)
- Link flows are applied (resource transfer, pollution injection)
- World meta-agents observe and emit perturbation specs
- 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 metrics — not
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.