structs-streaming
Connects to the GRASS real-time event system via NATS WebSocket. Use when you need real-time game updates, want to react to events as they happen, need to monitor raids or attacks,…
Josh McDougall
@abstrct
Install
$ openclaw skills install @abstrct/structs-streamingStructs Streaming (GRASS)
GRASS (Game Real-time Application Streaming Service) delivers real-time game events over NATS. Instead of polling queries repeatedly, subscribe to GRASS and react to events the moment they happen.
When to Use GRASS
| Situation | Use GRASS | Use Polling |
|---|---|---|
| Detect incoming raid | Yes — instant alert | Too slow |
| Wait for player creation after guild signup | Yes — listen for address_register | Polling every 10s works too |
| Monitor fleet arriving at your planet | Yes — fleet_arrive event | Might miss it |
| Track struct health during combat | Yes — planet_activity with struct_health | Too slow |
| Check your own resource balance | No | Yes — one-off query |
| Read struct type stats | No | Yes — static data |
Rule of thumb: If you need to react to something, use GRASS. If you need to read something, use a query.
Finding Your GRASS Endpoint
The GRASS WebSocket URL is not hardcoded — it comes from the guild configuration.
- Query the guild list:
curl https://public.testnet.structs.network/structs/guild - Follow the guild's
endpointURL to get its config - Look for
services.grass_nats_websocket
Example (Orbital Hydro guild):
{
"services": {
"grass_nats_websocket": "ws://crew.oh.energy:1443",
"guild_api": "http://crew.oh.energy/api/",
"reactor_api": "https://public.testnet.structs.network/"
}
}
The grass_nats_websocket value is your NATS WebSocket endpoint. Not all guilds provide this service — check before relying on it.
A reliable reference endpoint: ws://crew.oh.energy:1443 (Orbital Hydro / Slow Ninja). GRASS is hosted by individual guilds and is currently HTTP only — do not rewrite to wss://.
Tendermint WebSocket vs GRASS
These are two different streams. GRASS at ws://crew.oh.energy:1443 carries game-level events. Tendermint's own WebSocket at wss://public.testnet.structs.network:26657/websocket carries chain events (txs, blocks, validator updates). Use Tendermint subscriptions when you need to audit ugc_moderated or other untyped chain events that GRASS does not republish.
Discovery First
Before subscribing to specific subjects, subscribe to the > wildcard to see all traffic flowing through the GRASS server. This reveals the actual subject patterns in use, which may differ from documentation.
const sub = nc.subscribe(">");
for await (const msg of sub) {
console.log(`[${msg.subject}]`, new TextDecoder().decode(msg.data));
}
Watch the output for 30-60 seconds. You will see subjects like structs.planet.2-1.1-11, consensus, healthcheck, etc. Once you know what subjects carry the events you need, narrow your subscriptions to those specific subjects.
Important: Struct events (attacks, builds, status changes) often arrive on the planet subject rather than the struct subject. If you are not receiving expected struct events, subscribe to the struct's planet subject instead.
Subject Patterns
Subscribe to subjects matching the entities you care about:
| Entity | Wildcard | Specific | Example |
|---|---|---|---|
| Player | structs.player.> | structs.player.{guild_id}.{player_id} | structs.player.0-1.1-11 |
| Planet | structs.planet.> | structs.planet.{planet_id}.{player_id} | structs.planet.2-1.1-11 |
| Guild | structs.guild.* | structs.guild.{guild_id} | structs.guild.0-1 |
| Struct | structs.struct.* | structs.struct.{struct_id} | structs.struct.5-1 |
| Fleet | structs.fleet.* | structs.fleet.{fleet_id} | structs.fleet.9-1 |
| Address | structs.address.register.* | structs.address.register.{code} | -- |
| Inventory | structs.inventory.> | structs.inventory.{denom}.{guild_id}.{player_id}.{address} | Token movements |
| Grid | structs.grid.> | structs.grid.{object_type}.{object_id}.{player_id} | Attribute changes (ore, power, load, etc.) |
| Global | structs.global | structs.global | Block updates |
| Consensus | consensus | consensus | Chain consensus events |
| Healthcheck | healthcheck | healthcheck | Node health status |
Grid and planet subjects end with the owning player_id (added 2026-07-07): grid is structs.grid.{object_type}.{object_id}.{player_id} and planet is structs.planet.{planet_id}.{player_id}. When the owner can't be resolved the segment is the literal noPlayer. This lets you filter by owner from the subject alone — but mind the NATS wildcard rule: * matches exactly one token, > matches one or more trailing tokens. So structs.planet.* no longer matches (that's only two tokens); use structs.planet.{planet_id}.* for one planet (any owner) or structs.planet.> for all. The player subject (structs.player.{guild_id}.{player_id}) is likewise three tokens — use structs.player.>, not structs.player.*.
Use wildcards to discover what events exist. Narrow to specific subjects once you know what you need. Use > to see everything (see "Discovery First" above).
Event Types
Planet Events
| Event | Description | React By |
|---|---|---|
raid_status | Raid lifecycle on planet — status values include shieldsVulnerable (defender's shields down, raid can now complete), ongoing, and completed | Restore shields (Command Ship online, fleet on station); activate defenses, alert |
shield_change | Planetary shield value changed (planetary_shield / planetary_shield_old in detail) | Recompute raid feasibility against the target |
block_raid_start | The planet's raid vulnerability clock (blockStartRaid) armed | Note the raid window opened (yours: defend; theirs: a raid may be incoming) |
struct_health | A struct's HP changed (health / health_old) | Track combat damage live |
struct_status | A struct went online / offline / destroyed | Rebuild, reallocate power |
fleet_arrive | Fleet arrived at planet | Prepare defense or welcome |
fleet_depart | Fleet left planet | Update threat assessment |
All of the above (and the struct categories below) are planet_activity rows that arrive on the planet subject structs.planet.{planet_id}.{player_id} (subscribe with structs.planet.{planet_id}.*). struct_health, struct_status, shield_change, raid_status, and fleet_arrive/fleet_depart are the effect events that fire during combat — see the stub note under Combat Event Payloads for why these, not struct_attack detail, are what you reliably get live.
Struct Events
Note: Struct events frequently arrive on the planet subject (structs.planet.{planet_id}.{player_id}, i.e. subscribe structs.planet.{planet_id}.*) rather than the struct subject. Subscribe to both if you need complete coverage.
| Event | Description | React By |
|---|---|---|
struct_attack | Struct was attacked | Counter-attack, repair |
struct_status | Struct status changed (online/offline/destroyed) | Rebuild, reallocate power |
struct_defense_add / struct_defense_remove | Defense assignments changed | Update defense map |
struct_defender_clear | All defense relationships cleared | Re-assign defenders |
struct_block_build_start | Build operation initiated | Track in job list |
struct_block_ore_mine_start | Mine operation initiated | Track in job list |
struct_block_ore_refine_start | Refine operation initiated | Track in job list |
Player Events
| Event | Description | React By |
|---|---|---|
player_consensus | Player state updated (including username/pfp on structs.player after chain UGC) | Update intel |
player_address | An address was added to / changed on a player | Track multi-address / delegate setup |
player_address_pending | A pending address registration appeared (awaiting confirmation) | Watch for registration completion |
Guild Events
| Event | Description | React By |
|---|---|---|
guild_consensus | Guild chain data updated | Update guild status |
guild_meta | Off-chain guild metadata changed (description, tag, logo, services on structs.guild_meta) | Update intel |
guild_membership | Member joined/left guild | Update relationship map |
UGC Moderation Events
UGC name/pfp updates emit two distinct streams:
- GRASS DB-trigger events (the table above):
player_consensusfires when sync-state commits player UGC (username/pfponstructs.player).guild_metafires for off-chain guild config updates. Chain UGCname/pfpon guilds live onstructs.guild. Planet and substation UGC reach observers via chain events andplanet_activityentries. - Cosmos chain event
ugc_moderated— emitted by the keeper directly (untypedsdk.Event, not GRASS). Fires only when the actor of the update is not the target object's owner (i.e. only on guild-moderation overrides, never on self-service updates).
Subscribe to chain events via Tendermint's tx.events or block_events subscription (separate from GRASS) when you want a complete audit trail of moderation activity. Schema:
| Attribute | Description |
|---|---|
actor_player_id | Player ID of the moderator who performed the override |
actor_address | Signing address that authored the tx |
target_object_id | Player / planet / substation / guild ID being moderated |
target_owner_player_id | Owner player ID at the time of the update |
field | name or pfp |
old_value | Field value before the update |
new_value | Field value after the update |
Use this stream as a moderation audit log — see knowledge/mechanics/ugc-moderation.md for context.
Inventory Events
Subject: structs.inventory.{denom}.{guild_id}.{player_id}.{address}
Track token movements — Alpha Matter, guild tokens, ore, etc.
| Category | Description | React By |
|---|---|---|
sent | Tokens sent from this player | Update balance tracking |
received | Tokens received by this player | Update balance tracking |
seized | Tokens seized via raid | Trigger counter-raid or refine alert |
mined | Ore mined | Start refining immediately |
refined | Ore refined into Alpha | Update wealth tracking |
minted | Guild tokens minted | Track guild economy |
infused | Alpha infused into reactor/generator | Update capacity tracking |
forfeited | Tokens lost (penalties, etc.) | Investigate cause |
Grid Events
Subject: structs.grid.{object_type}.{object_id}.{player_id}
Track attribute changes on any game object (players, structs, planets). Every grid payload also carries a top-level player_id field (the resolved owner, or noPlayer).
| Category | Description | React By |
|---|---|---|
capacity | Power capacity changed | Check if approaching offline |
connectionCapacity | Connection capacity changed | Update power routing |
connectionCount | Connection count changed | Update power routing |
fuel | Fuel level changed | Monitor generator/reactor |
lastAction | Last action timestamp updated | Track activity |
load | Power load changed | Check if approaching offline |
nonce | Player nonce incremented | Detect activity (useful for scouting) |
ore | Ore balance changed | Refine immediately if yours; raid target if theirs |
player_consensus | Player consensus data updated | Update intel |
power | Power level changed | Monitor energy infrastructure |
proxyNonce | Proxy nonce changed | Detect proxy activity |
structsLoad | Structs load changed | Assess fleet strength changes |
allocationPointerStart / allocationPointerEnd | Energy allocation range pointers changed | Track allocation/substation routing |
ready | Object readiness flag changed | Track object availability |
checkpointBlock | Checkpoint block updated | Track grid bookkeeping |
Grid categories are the attribute name itself (e.g. ore, load), carried on structs.grid.{object_type}.{object_id}.{player_id} — not grass_category values. For an object_type of player the owner is the object itself, so the subject reads structs.grid.player.{id}.{id}.
Combat Event Payloads
The stub: why combat looks like effects, not attacks.
struct_attackis a published category, but the NATS NOTIFY payload has an ~8000-byte ceiling. When aplanet_activityrow's full payload (e.g. astruct_attackdetailwith itseventAttackShotDetail[]shot log) exceeds 7995 bytes — which any multi-shot, multi-defender fight does — the stream sends a stub instead. The stub keeps the routing/identity fields and drops the heavydetail:{ "subject": "structs.planet.{planet_id}.{player_id}", "planet_id": "...", "player_id": "...", "seq": ..., "category": "struct_attack", "time": "...", "stub": "true" }(notestubis the string"true"). So for real combat you cannot rely on the livestruct_attackpayload for the blow-by-blow. Detect combat from the effect events that always stream in full (struct_health,struct_status,shield_change,raid_status,fleet_arrive/fleet_depart), then pull the full shot detail from the Guild APIplanet-activityfeed (or the chain) keyed by the stub'sseq/planet_id. Small attacks ship fulldetailinline; large ones arrive stubbed. The canonicalstruct_attackdetailschema is in api/integration-notes.md — struct_attack event detail schema.
struct_attack events (when not stubbed) include shot-by-shot resolution. Example payload (observed on planet subject; the full planet payload also carries subject and player_id alongside the row fields):
{
"category": "struct_attack",
"player_id": "1-11",
"attackingStructId": "5-100",
"targetStructId": "5-200",
"weaponSystem": "primary",
"eventAttackShotDetail": [
{
"shotIndex": 0,
"damage": 2,
"evaded": false,
"blocked": false,
"blockerStructId": "",
"counterAttackDamage": 1,
"counterAttackerStructId": "5-200"
}
],
"attackerHealthRemaining": 2,
"targetHealthRemaining": 1,
"targetDestroyed": false,
"attackerDestroyed": false
}
Key fields in eventAttackShotDetail:
evaded-- true if the shot missed (defense type interaction)blocked-- true if a defender interceptedblockerStructId-- which struct blocked (if any)counterAttackDamage/counterAttackerStructId-- counter-attack info per shot
struct_health events track HP changes:
{
"category": "struct_health",
"structId": "5-200",
"health": 1,
"maxHealth": 3,
"destroyed": false
}
Noise Filtering
The consensus and healthcheck subjects fire constantly (every few seconds). When using the > wildcard for discovery, filter these out to see actual game events:
const sub = nc.subscribe(">");
for await (const msg of sub) {
if (msg.subject === "consensus" || msg.subject === "healthcheck") continue;
console.log(`[${msg.subject}]`, new TextDecoder().decode(msg.data));
}
Global Events
| Event | Description | React By |
|---|---|---|
block | New block produced | Tick game loop, update charge calculations |
Building Event Listener Tools
Agents should build custom tools that connect to GRASS when they need event-driven behavior. Here are patterns to follow.
Minimal Node.js Listener
Install the NATS WebSocket client:
npm install nats.ws
import { connect } from "nats.ws";
const nc = await connect({ servers: "ws://crew.oh.energy:1443" });
// Subjects end with the owner player_id, so match any owner with a trailing *
const sub = nc.subscribe("structs.planet.2-1.*");
for await (const msg of sub) {
const event = JSON.parse(new TextDecoder().decode(msg.data));
console.log(JSON.stringify(event));
}
Minimal Python Listener
Install the NATS client:
pip install nats-py
import asyncio, json, nats
async def main():
nc = await nats.connect("ws://crew.oh.energy:1443")
sub = await nc.subscribe("structs.planet.2-1.*") # trailing * = any owner player_id
async for msg in sub.messages:
event = json.loads(msg.data.decode())
print(json.dumps(event))
asyncio.run(main())
Raid Alert Tool (example pattern)
A tool that watches for raids on your planet and outputs an alert:
import { connect } from "nats.ws";
const PLANET_ID = process.argv[2]; // e.g. "2-1"
const nc = await connect({ servers: "ws://crew.oh.energy:1443" });
const sub = nc.subscribe(`structs.planet.${PLANET_ID}.*`); // trailing * = any owner player_id
for await (const msg of sub) {
const event = JSON.parse(new TextDecoder().decode(msg.data));
if (event.category === "raid_status") {
console.log(JSON.stringify({ alert: "RAID", planet: PLANET_ID, data: event }));
}
if (event.category === "fleet_arrive") {
console.log(JSON.stringify({ alert: "FLEET_ARRIVAL", planet: PLANET_ID, data: event }));
}
}
Player Creation Watcher (example pattern)
Instead of polling structsd query structs address after guild signup, watch for the address registration event:
import { connect } from "nats.ws";
const nc = await connect({ servers: "ws://crew.oh.energy:1443" });
const sub = nc.subscribe("structs.address.register.*");
for await (const msg of sub) {
const event = JSON.parse(new TextDecoder().decode(msg.data));
console.log(JSON.stringify(event));
break; // exit after first match
}
await nc.close();
When to Build a Custom Tool
Build a GRASS listener tool when:
- You need to wait for an event — guild signup completion, fleet arrival, raid detection
- You need continuous monitoring — threat detection during vulnerable ore window, combat tracking
- You want an event-driven game loop — react to block events instead of polling on a timer
- You're managing multiple players — one GRASS connection can monitor all your entities simultaneously
Store custom tools in your workspace (e.g., scripts/ or alongside the relevant skill).
Connection Best Practices
- Use specific subjects once you know what you need. Wildcards are for discovery.
- Limit to 10-20 subscriptions per connection to avoid overwhelming the client.
- Implement reconnection with exponential backoff — NATS connections can drop.
- Parse JSON defensively — not all messages may match expected schema.
- Close connections when done. Don't leave idle GRASS connections open.
Procedure
Quick Setup
- Get the GRASS endpoint from your guild config (or use
ws://crew.oh.energy:1443). For chain queries you can use the SSL endpointhttps://public.testnet.structs.network/structs/guild. - Record the endpoint in TOOLS.md under Servers
- Choose your language (Node.js or Python)
- Install the NATS client library (
nats.wsfor Node,nats-pyfor Python) - Write a listener script for your specific use case
- Run it in a background terminal
For Ongoing Monitoring
- Subscribe to your planet(s):
structs.planet.{id}.*— raid alerts, fleet arrivals (trailing*matches any ownerplayer_id) - Subscribe to your structs:
structs.struct.{id}— attack/status alerts - Subscribe to global:
structs.global— block tick for game loop timing - Log events to memory/ for cross-session awareness
Automation Patterns (Defence Contractor)
The Structs permission system and GRASS event stream were designed for AI agents to automate game responses. The design docs call this the "Defence Contractor" pattern — an agent that monitors events and acts on behalf of players within scoped permissions.
Common Automation Triggers
| Event | Action | Permission Needed |
|---|---|---|
struct_ore_mine_complete on your extractor | Immediately start struct-ore-refine-compute | Signer key for the player |
struct_ore_refine_complete on your refinery | Immediately start next struct-ore-mine-compute | Signer key for the player |
planet_raid_start on your planet | Alert, activate stealth, reposition defenders | Signer key or delegated permission |
struct_attack targeting your struct | Log attacker, assess threat, counter-attack if able | Signer key or delegated permission |
struct_health showing HP drop | Prioritize defense, consider fleet retreat | Signer key for fleet-move |
fleet_move to your planet from unknown fleet | Identify incoming player, assess threat level | Read-only (query) |
Permission Scoping for Automated Agents
When delegating actions to an automation agent (separate key or service):
- Grant minimal permissions: Use
permission-grant-on-objectto allow specific actions on specific structs, not blanket access. - Separate keys: The automation agent should use its own signing key, registered as a secondary address on the player via
address-register. - Scope by struct: Grant mine/refine permissions on extractors and refineries only. Grant defense permissions on fleet structs only.
- Revoke when not needed: Use
permission-revoke-on-objectto remove automation access during sensitive operations.
Example: Refine-on-Mine-Complete Loop
Subscribe to: structs.struct.{extractor-id}
On event: struct_ore_mine_complete
→ Run: structsd tx structs struct-ore-refine-compute -D 1 --from [key] --gas auto -y -- [refinery-id]
Subscribe to: structs.struct.{refinery-id}
On event: struct_ore_refine_complete
→ Run: structsd tx structs struct-ore-mine-compute -D 1 --from [key] --gas auto -y -- [extractor-id]
This creates a continuous mine-refine loop that runs unattended. Ore is never left unrefined.
Example: Defend-on-Raid-Detected
Subscribe to: structs.planet.{planet-id}.*
On event: planet_raid_start
→ Activate stealth on vulnerable structs
→ Set defenders on high-value structs
→ Log raid to memory/intel/threats.md
→ Alert commander if available
Safe Boundaries
- Never auto-spend Alpha without commander approval (infusion, guild-bank operations)
- Never auto-move fleets away from defended planets without threat assessment
- Always log actions to
memory/for cross-session audit trail - Rate-limit reactions — one transaction per ~6 seconds per key (sequence number constraint)
See Also
- protocols/streaming — Full GRASS/NATS protocol specification
- api/streaming/event-types — Complete event type catalog
- api/streaming/event-schemas — JSON schema definitions for event payloads
- api/streaming/subscription-patterns — Subscription patterns and examples
- awareness/async-operations — Background operations and pipeline strategy
- awareness/threat-detection — Using GRASS for early warning
Top skills in this category
Dashboard
@cellcogAI dashboard and web app generation powered by CellCog. Interactive dashboards, KPI trackers, data visualization, charts, analytics apps, data explorers, calculators, games. Responsive HTML apps with real-time filters.
Instagram Skill via cyberdrk/gram CLI
@areinInstagram CLI for viewing feeds, posts, profiles, and engagement via cookies.
Section 11: Endurance Training Coach (Intervals.icu)
@crankaddictEvidence-based endurance coaching protocol (v11.35). Use when analyzing training data, reviewing sessions, generating pre/post-workout reports, planning work...
Skill Vetter
@spclaudehomeSecurity-first skill vetting for AI agents. Use before installing any skill from ClawdHub, GitHub, or other sources. Checks for red flags, permission scope, and suspicious patterns.
Github
@steipeteInteract with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.