back
loading skill details...
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, watch for player creation, track fleet movements, or build event-driven tools. GRASS is the fastest way to know what's happening in the galaxy.
---
meta_description: Subscribe to GRASS over NATS instead of polling. Endpoint discovery, subject patterns, event types, and building custom listeners.
name: structs-streaming
description: 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, watch for player creation, track fleet movements, or build event-driven tools. GRASS is the fastest way to know what's happening in the galaxy.
level: advanced
domain: infra
---
# Structs 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.
1. Query the guild list: `curl https://public.testnet.structs.network/structs/guild`
2. Follow the guild's `endpoint` URL to get its config
3. Look for `services.grass_nats_websocket`
Example (Orbital Hydro guild):
```json
{
"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.
```javascript
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](#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:
1. **GRASS DB-trigger events** (the table above): `player_consensus` fires when sync-state commits player UGC (`username`/`pfp` on `structs.player`). `guild_meta` fires for off-chain guild config updates. Chain UGC `name`/`pfp` on guilds live on `structs.guild`. Planet and substation UGC reach observers via chain events and `planet_activity` entries.
2. **Cosmos chain event `ugc_moderated`** — emitted by the keeper directly (untyped `sdk.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_attack` *is* a published category, but the NATS NOTIFY payload has an ~8000-byte ceiling. When a `planet_activity` row's full payload (e.g. a `struct_attack` `detail` with its `eventAttackShotDetail[]` 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 heavy `detail`: `{ "subject": "structs.planet.{planet_id}.{player_id}", "planet_id": "...", "player_id": "...", "seq": ..., "category": "struct_attack", "time": "...", "stub": "true" }` (note `stub` is the string `"true"`). So for real combat you cannot rely on the live `struct_attack` payload 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 API `planet-activity` feed (or the chain) keyed by the stub's `seq`/`planet_id`. Small attacks ship full `detail` inline; large ones arrive stubbed. The canonical `struct_attack` `detail` schema is in [api/integration-notes.md — struct_attack event detail schema](https://structs.ai/api/integration-notes#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):
```json
{
"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 intercepted
- `blockerStructId` -- which struct blocked (if any)
- `counterAttackDamage` / `counterAttackerStructId` -- counter-attack info per shot
`struct_health` events track HP changes:
```json
{
"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:
```javascript
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:
```bash
npm install nats.ws
```
```javascript
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:
```bash
pip install nats-py
```
```python
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:
```javascript
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:
```javascript
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
1. Get the GRASS endpoint from your guild config (or use `ws://crew.oh.energy:1443`). For chain queries you can use the SSL endpoint `https://public.testnet.structs.network/structs/guild`.
2. Record the endpoint in [TOOLS.md](https://structs.ai/TOOLS) under Servers
3. Choose your language (Node.js or Python)
4. Install the NATS client library (`nats.ws` for Node, `nats-py` for Python)
5. Write a listener script for your specific use case
6. Run it in a background terminal
### For Ongoing Monitoring
1. Subscribe to your planet(s): `structs.planet.{id}.*` — raid alerts, fleet arrivals (trailing `*` matches any owner `player_id`)
2. Subscribe to your structs: `structs.struct.{id}` — attack/status alerts
3. Subscribe to global: `structs.global` — block tick for game loop timing
4. Log events to [memory/](https://structs.ai/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):
1. **Grant minimal permissions**: Use `permission-grant-on-object` to allow specific actions on specific structs, not blanket access.
2. **Separate keys**: The automation agent should use its own signing key, registered as a secondary address on the player via `address-register`.
3. **Scope by struct**: Grant mine/refine permissions on extractors and refineries only. Grant defense permissions on fleet structs only.
4. **Revoke when not needed**: Use `permission-revoke-on-object` to 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](https://structs.ai/protocols/streaming) — Full GRASS/NATS protocol specification
- [api/streaming/event-types](https://structs.ai/api/streaming/event-types) — Complete event type catalog
- [api/streaming/event-schemas](https://structs.ai/api/streaming/event-schemas) — JSON schema definitions for event payloads
- [api/streaming/subscription-patterns](https://structs.ai/api/streaming/subscription-patterns) — Subscription patterns and examples
- [awareness/async-operations](https://structs.ai/awareness/async-operations) — Background operations and pipeline strategy
- [awareness/threat-detection](https://structs.ai/awareness/threat-detection) — Using GRASS for early warning
don't have the plugin yet? install it then click "run inline in claude" again.