# WebSocket

> The NeuroChain gateway WebSocket — connecting to /ws, the init message, every live event (blocks, transactions, held transfers, AI decisions, contract alerts), balance subscriptions, RPC over the socket and the limits that apply.

Source: https://docs.nro.world/reference/websocket

The gateway serves a WebSocket at **`/ws`** on the same host and port as its REST API:

```
ws://localhost:3000/ws
```

No authentication. The gateway pushes chain and AI events to every connected client; a client can also subscribe to balances and call a set of RPC methods over the same socket.

## Connecting

```js
const ws = new WebSocket('ws://localhost:3000/ws')

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data)
  switch (msg.type) {
    case 'init':      console.log('height', msg.data.height); break
    case 'new_block': console.log('block', msg.data.height, msg.data.proposer); break
    case 'tx_held':   console.warn('held', msg.data.tx_hash); break
  }
}
```

Every server message is JSON with a `type`. Events put their payload in `data`; `balance`, `rpc_reply` and `error` carry their fields at the top level.

Reconnect on `close` with a back-off — the web app retries with an exponentially growing delay.

## Events

The gateway polls the node every `POLL_NODE_INTERVAL_MS` (default 2 000 ms) and turns what it sees into events.

| `type` | When | `data` fields |
|--------|------|---------------|
| `init` | Right after connecting | `height`, `tps`, `finality_ms`, `shard_count`, `validator_count`, `blocks`, `txs`, `total_blocks`, `total_txs` |
| `metrics` | Every poll | `tps`, `height`, `finality_ms` |
| `new_block` | A new height is seen | The block as returned by `GET /api/v1/blocks/:height` |
| `validator_update` | Every 10 blocks | `nodes`, `total_staked`, `height` |
| `tx` | The gateway forwarded a transaction to the node | `hash`, `from`, `to`, `value`, `gas`, `gas_price`, `shard`, `timestamp`, `block` |
| `faucet` | Faucet credited an address | `address`, `amount`, `tx_hash` |
| `tx_held` | A transfer was held by the [Sentinel policy](/ai/sentinel) | `tx_hash`, `to`, `value_human` |
| `tx_released` | A held transfer was released | `tx_hash`, `to`, `value_human` |
| `tx_cancelled` | A held transfer was cancelled | `tx_hash`, `to`, `value_human` |
| `tx_anomaly` | Sentinel scored a submitted transaction as anomalous | `tx_hash`, `score`, `category` |
| `oracle_decision` | An AI decision was logged | `model`, `kind`, `subject`, `decision`, `confidence`, `timestamp` |
| `model_voting_update` | AI models are waiting for validator confirmations | `models`, `height` |
| `contract_alert` | Unusual call volume on a contract, or a low-score contract starts being called | `address`, `alert`, `severity`, `call_count`, `delta`, `timestamp`, `model` |

::: tip `tx` is a submission notice
The `tx` event fires when the gateway hands the transaction to the node, before it is in a block — its `status` field is not authoritative. Track finality with `new_block`, a balance subscription, or `GET /api/v1/transactions/:hash`.
:::

Contract alerts are checked every `SENTINEL_CHECK_INTERVAL_MS` (default 30 000 ms).

## Balance subscription

```js
ws.send(JSON.stringify({ type: 'subscribe_balance', address: 'neuro:<15 hex>.human' }))
```

The gateway answers at once and again after every new block:

```json
{ "type": "balance", "address": "neuro:…", "balance": 50000000, "balance_human": "50", "nonce": 0 }
```

Subscriptions end when the socket closes.

## RPC over the socket

```js
ws.send(JSON.stringify({ type: 'rpc', id: 1, method: 'get_account', params: { address: 'neuro:…' } }))
// → { "type": "rpc_reply", "id": 1, "result": { … }, "error": null }
```

The methods mirror REST endpoints and return the same data:

| Group | Methods |
|-------|---------|
| Chain | `get_status`, `get_blocks`, `get_block`, `get_block_txs`, `get_txs`, `search_tx`, `get_account` |
| Validators | `get_validator`, `get_validators` |
| Names and NFTs | `human_name_price`, `register_human`, `get_my_nfts`, `get_nft`, `transfer_nft` |
| Governance | `get_governance_params`, `get_proposals`, `get_proposal`, `create_proposal`, `cast_vote` |
| Vaults | `get_vault_config`, `list_member_vaults`, `get_vault_proposals`, `create_vault_proposal`, `sign_vault_proposal`, `add_vault_member`, `set_vault_threshold` |
| AI | `get_oracle_log`, `get_oracle_entry`, `get_model_registry`, `get_model_registry_entry`, `register_model` |
| Contracts | `get_contracts`, `get_contract`, `get_contract_events`, `deploy_contract`, `call_contract`, `update_contract_score`, `get_certificates`, `get_certificate`, `get_contract_certificate`, `certify_contract` |
| Test networks | `faucet` |

Write methods need the same signed fields as their REST endpoint (`signature`, `public_key`, `nonce`) inside `params`.

## Limits

| Limit | Default | Variable | On excess |
|-------|---------|----------|-----------|
| Message size | 64 KB | `WS_MAX_MSG_BYTES` | `{"type":"error","error":"message too large"}` |
| Balance subscriptions per connection | 50 | `WS_MAX_SUBS_PER_CLIENT` | `subscription limit exceeded` |
| RPC calls per second per connection | 30 | `WS_RPC_MAX_RPS` | `rpc rate limit exceeded` |
| Dead-connection sweep | every 30 s | `WS_STALE_CHECK_MS` | — |

## Python

```python
import asyncio, json, websockets

async def listen():
    async with websockets.connect("ws://localhost:3000/ws") as ws:
        await ws.send(json.dumps({"type": "subscribe_balance", "address": "neuro:<15 hex>.human"}))
        async for raw in ws:
            msg = json.loads(raw)
            print(msg["type"], msg.get("data", msg))

asyncio.run(listen())
```
