WebSocket
The gateway serves a WebSocket at /ws on the same host and port as its REST API:
ws://localhost:3000/wsNo 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
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 | 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 |
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
ws.send(JSON.stringify({ type: 'subscribe_balance', address: 'neuro:<15 hex>.human' }))The gateway answers at once and again after every new block:
{ "type": "balance", "address": "neuro:…", "balance": 50000000, "balance_human": "50", "nonce": 0 }Subscriptions end when the socket closes.
RPC over the socket
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
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())