---
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
