Structs Guild Stack

The Guild Stack is a Docker Compose application that runs a full guild node with sync-state indexing, GRASS real-time events, a webapp, and an optional transaction signing agent. It provides sub-second database queries for game state that would take 1-60 seconds via CLI.

This is an advanced/optional upgrade. CLI commands work for basic gameplay. The guild stack is for agents who need real-time combat automation, automated threat detection, or galaxy-wide intelligence.

Repository: https://github.com/playstructs/docker-structs-guild


Safety

The Guild Stack runs persistent services on your machine and (if exposed) on your network. See SAFETY.md for the trust contract; in this skill:


When to Use the Guild Stack

Situation CLI Guild Stack (PG)
Simple single-object query 1-5s (fine) <1s
Galaxy-wide scouting (all players, all planets) 30-60s (too slow) <1s
Real-time threat detection (poll every block) Impossible (query > block time) Trivial
Combat targeting (weapon/defense matching) Minutes to gather data <1s
Submitting transactions CLI required CLI required

Rule: Use PG for reads, CLI for writes.


Prerequisites


Setup Procedure

1. Clone the Repository and Pin a Release

git clone https://github.com/playstructs/docker-structs-guild
cd docker-structs-guild
git fetch --tags
git checkout <latest-tag>     # do NOT track `main` unless you are actively developing against upstream

Pinning a tag makes the Compose file you are about to run reviewable. Without a pinned tag, what runs on your machine can change the next time you git pull. See Lifecycle & Trust below for the longer rationale.

2. Review the Compose File

less compose.yaml

You are about to launch a fleet of containers including a chain node, sync-state indexer, PostgreSQL, GRASS/NATS, webapp, and an optional transaction signing agent. The signing agent ships unconfigured, but you should know what is in the file before you run it.

3. Configure Environment

Copy or create .env with at minimum:

MONIKER=MyAgentNode
NETWORK_VERSION=113b
NETWORK_CHAIN_ID=structstestnet-111
STRUCTS_PG_BRANCH=main

For PG-driven game-state queries, you need four services. structs-sync-state is required — without it, PG has no indexed game state:

docker compose up -d structsd structs-pg structs-sync-state structs-grass

The webapp, TSA, crawler, and NATS (if not needed for GRASS) stay stopped unless you enable them. Only enable services when you specifically need them (see Enabling Additional Services below).

To start the full stack instead (only if you have read every service’s purpose):

docker compose up -d

5. Wait for Chain Sync and Indexer Catch-Up

The blockchain node must sync from genesis or a snapshot. This takes hours on first run. Monitor progress:

docker compose logs -f structsd --tail 20

Check sync-state indexer progress:

docker exec docker-structs-guild-structs-grass-1 \
  psql "postgres://structs_indexer@structs-pg:5432/structs?sslmode=require" \
  -t -A -c "SELECT chain_id, last_height, status, lag_blocks FROM sync_state.sync_cursor;"

The node is synced when the structsd health check passes. All services should show healthy or running:

docker compose ps

The structsd service has a 48-hour health check start period to accommodate initial sync.

6. Verify PG Access

Run a test query (see “Connecting to PostgreSQL” below):

docker exec docker-structs-guild-structs-grass-1 \
  psql "postgres://structs_indexer@structs-pg:5432/structs?sslmode=require" \
  -t -A -c "SELECT count(*) FROM structs.player;"

If this returns a number, the stack is working.


Connecting to PostgreSQL

Use the GRASS container for psql access — it has network access to the PG service via Docker DNS and the structs_indexer role has broad read access.

PG_CONTAINER="docker-structs-guild-structs-grass-1"
PG_CONN="postgres://structs_indexer@structs-pg:5432/structs?sslmode=require"

docker exec "$PG_CONTAINER" psql "$PG_CONN" -t -A -c "SELECT ..."

For JSON output:

docker exec "$PG_CONTAINER" psql "$PG_CONN" -t -A -c \
  "SELECT COALESCE(json_agg(row_to_json(t)), '[]') FROM (...) t;"

The container name may vary by installation. Find it with docker compose ps and look for the structs-grass service.


The Grid Table Gotcha

The structs.grid table is a key-value store, not a columnar table. Each row is one attribute for one object.

-- WRONG: There is no 'ore' column
SELECT ore FROM structs.grid WHERE object_id = '1-142';

-- CORRECT: Filter by attribute_type
SELECT val FROM structs.grid WHERE object_id = '1-142' AND attribute_type = 'ore';

For multiple attributes on the same object, use multiple JOINs:

SELECT p.id,
    COALESCE(g_ore.val, 0) as ore,
    COALESCE(g_load.val, 0) as structs_load
FROM structs.player p
LEFT JOIN structs.grid g_ore ON g_ore.object_id = p.id AND g_ore.attribute_type = 'ore'
LEFT JOIN structs.grid g_load ON g_load.object_id = p.id AND g_load.attribute_type = 'structsLoad'
WHERE p.id = '1-142';

Common Queries

Player Resources (with UGC)

SELECT p.id, p.username, p.pfp, p.guild_id, p.planet_id, p.fleet_id,
    COALESCE(g_ore.val, 0) as ore,
    COALESCE(g_load.val, 0) as structs_load
FROM structs.player p
LEFT JOIN structs.grid g_ore ON g_ore.object_id = p.id AND g_ore.attribute_type = 'ore'
LEFT JOIN structs.grid g_load ON g_load.object_id = p.id AND g_load.attribute_type = 'structsLoad'
WHERE p.id = '1-142';

Fleet Composition with Weapon Stats

SELECT s.id, st.class_abbreviation, s.operating_ambit,
    st.primary_weapon_control, st.primary_weapon_damage,
    st.primary_weapon_ambits_array, st.unit_defenses,
    st.counter_attack_same_ambit
FROM structs.struct s
JOIN structs.struct_type st ON st.id = s.type
WHERE s.owner = '1-142' AND s.location_type = 'fleet'
    AND s.is_destroyed = false
ORDER BY s.operating_ambit, s.slot;

Raid Target Scouting

SELECT pl.id as planet, pl.name, pl.owner, g_ore.val as ore,
    COALESCE(pa_shield.val, 0) as shield,
    COALESCE(g_load.val, 0) as structs_load
FROM structs.planet pl
JOIN structs.grid g_ore ON g_ore.object_id = pl.owner AND g_ore.attribute_type = 'ore'
LEFT JOIN structs.planet_attribute pa_shield ON pa_shield.object_id = pl.id
    AND pa_shield.attribute_type = 'planetaryShield'
LEFT JOIN structs.grid g_load ON g_load.object_id = pl.owner
    AND g_load.attribute_type = 'structsLoad'
WHERE g_ore.val > 0
ORDER BY g_ore.val DESC, shield ASC;

A high ore balance and low shield are only half the picture: in v0.18.0 a raid can only complete while the owner’s Command Ship is offline or destroyed (shieldsVulnerable). Join the owner’s Command Ship struct status before committing PoW — see structs-combat.

Enemy Structs at a Planet

SELECT s.id, st.class_abbreviation, s.operating_ambit,
    st.primary_weapon_control, st.primary_weapon_damage,
    st.unit_defenses
FROM structs.struct s
JOIN structs.struct_type st ON st.id = s.type
JOIN structs.fleet f ON f.id = s.location_id
WHERE f.location_id = '2-105' AND s.is_destroyed = false
    AND s.location_type = 'fleet'
ORDER BY s.operating_ambit;

Real-Time Threat Detection (Poll Pattern)

-- Set high-water mark on startup
SELECT COALESCE(MAX(seq), 0) FROM structs.planet_activity
WHERE planet_id IN ('2-105');

-- Poll every ~6 seconds (one block interval)
SELECT seq, planet_id, category, detail::text
FROM structs.planet_activity
WHERE planet_id IN ('2-105', '2-127')
    AND seq > $LAST_SEQ
ORDER BY seq ASC;

Watch for fleet_arrive, raid_status, and struct_attack categories.

Sync-State Health

SELECT chain_id, last_height, status, lag_blocks, tip_height, updated_at
FROM sync_state.sync_cursor;

SELECT chain_id, height, num_handler_errors, ingested_at
FROM sync_state.block_log
ORDER BY height DESC
LIMIT 5;

Struct Health and Defense Assignments

SELECT sa.object_id as struct_id, sa.attribute_type, sa.val
FROM structs.struct_attribute sa
WHERE sa.object_id = '5-1165';

SELECT defending_struct_id, protected_struct_id
FROM structs.struct_defender
WHERE protected_struct_id = '5-100';

Stack Management

# Start the read-only profile (recommended default)
docker compose up -d structsd structs-pg structs-sync-state structs-grass

# Start all services (only if you have reviewed every one)
docker compose up -d

# Check service status
docker compose ps

# View blockchain sync progress
docker compose logs -f structsd --tail 20

# View sync-state indexer progress
docker compose logs -f structs-sync-state --tail 20

# Stop everything (preserves all data)
docker compose down

# Destroy all data (start fresh)
docker compose down -v

Enabling Additional Services

The setup procedure above starts only structsd, structs-pg, structs-sync-state, and structs-grass. Enable the rest one at a time, only when you have a reason.

Service What it is When to enable
structs-nats NATS messaging + GRASS WebSocket on port 1443 Required for GRASS (included in read-only profile when using structs-grass)
structs-webapp Browser/API dashboard on port 8080 When you need the HTTP guild API locally
structs-tsa Transaction signing daemon Only after reviewing its source. See Signing-Agent Caveat below.
structs-crawler Guild metadata crawler When running a guild node that publishes guild config
structs-mcp (separate repo) MCP server on port 3000 Deploy from structs-mcp; bind to 127.0.0.1

To enable a service, add it to the docker compose up -d argument list:

docker compose up -d structsd structs-pg structs-sync-state structs-grass structs-webapp

Lifecycle & Trust

The stack is a persistent local fleet of services. Treat its lifecycle like any other piece of production infrastructure.

Why pin a tag

Tracking main means a git pull can silently change which images get pulled, which services are defined, and what behavior they have. Pinning a tag turns “what runs on my machine” into a reviewable artifact. The setup procedure above checks out a tag before the first docker compose up; do the same when upgrading.

Signing-Agent Caveat

The structs-tsa service is designed to sign transactions on behalf of a stored key. The default Compose ships it unconfigured (no key), but if you ever wire a real key into it:

For read-only intelligence work this service is unnecessary. Keep it stopped or remove it from the Compose override.

Teardown

# Stop, preserve volumes (state survives)
docker compose down

# Stop and destroy volumes (full reset, frees ~10 GB)
docker compose down -v

# Confirm nothing is left
docker compose ps
docker volume ls | grep structs

If you spun the stack up to investigate something, tear it down when you’re done. Running services consume CPU/memory and are an attack surface.


Port Summary

Port Service Purpose
26656 structsd P2P blockchain networking
26657 structsd CometBFT RPC (transactions + queries)
1317 structsd Cosmos SDK REST API
5432 structs-pg PostgreSQL database
8080 structs-webapp Webapp HTTP API
443 structs-webapp Webapp HTTPS
4222 structs-nats NATS client connections
1443 structs-nats NATS WebSocket (GRASS events)

Error Handling

Error Cause Fix
“connection refused” on PG Stack not started or PG not healthy yet docker compose ps to check; wait for PG healthy
Query returns 0 rows Chain sync or sync-state catch-up incomplete Check sync_state.sync_cursor; wait for status to leave catching_up
Container name not found Container naming varies by installation Run docker compose ps to find actual container names
“role does not exist” Wrong PG role in connection string Use structs_indexer role via the GRASS container
Slow PoW with guild stack Multiple agents running concurrent PoW CPU contention; stagger PoW operations or reduce parallelism
Second sync-state instance fails Writer lock — only one indexer allowed Never scale structs-sync-state

See Also