# Off-chain Reactivity

Off-chain reactivity works through a WebSocket subscription served by the node API. A client subscribes to matching logs and receives pushed events as they land in blocks. Each event notification can optionally include the results of read-only simulations run against the same block, so that an application can react to both the event and its derived state at once. There's no additional latency, and no risk of inconsistency.

The subscription is served by the RPC node the client is connected to. It is not stored on chain, does not cost [SOMI](/developer/network-info/somi-coin.md), and disappears when the client disconnects or unsubscribes.

For reactions that are part of chain execution rather than client-side, see [on-chain reactivity](/developer/reactivity/reactivity-onchain.md).

## Example use cases

* Drive a UI that updates the moment a token is transferred, without polling balances.
* Pipe live swap and liquidity events into an indexer or analytics database.
* Attach a `balanceOf` call to every matching Transfer log, so downstream consumers see both the event and the resulting balance atomically.
* Build a bot that watches a DEX pool and only receives a message when the pool state has actually changed.

## Quick start

1. Open a WebSocket connection to a Somnia node's API endpoint.
2. Call [`eth_subscribe`](/developer/json-rpc-api.md#eth_subscribe) using the method `somnia_watch` and a parameters object. The node returns a subscription ID.
3. Read pushed `eth_subscription` notifications on the same connection. The `result` field of each notification is the event payload.
4. To stop, call [`eth_unsubscribe`](/developer/json-rpc-api.md#eth_unsubscribe) with the subscription ID, or simply close the connection.

Minimal JSON-RPC sequence:

```json
// -> request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_subscribe",
  "params": [
    "somnia_watch",
    {
      "eth_calls": []
    }
  ]
}

// <- response
{ "jsonrpc": "2.0", "id": 1, "result": "0x1234..." }

// <- notifications
{
  "jsonrpc": "2.0",
  "method": "eth_subscription",
  "params": {
    "subscription": "0x1234...",
    "result": {
      "address": "0x...",
      "topics": ["0x...", ...],
      "data": "0x...",
      "simulationResults": []
    }
  }
}
```

## Using from TypeScript

TypeScript and JavaScript apps can use the [`@somnia-chain/reactivity`](https://www.npmjs.com/package/@somnia-chain/reactivity) package. It wraps the WebSocket protocol with Viem and manages subscription/unsubscription, so you don't have to drive `eth_subscribe` directly. (The same package can also create and manage [on-chain reactivity](/developer/reactivity/reactivity-onchain.md#typescript-package) subscriptions owned by an EOA.)

```bash
npm install @somnia-chain/reactivity viem
```

```typescript
import { createPublicClient, defineChain, webSocket } from 'viem';
import { SDK } from '@somnia-chain/reactivity';

const chain = defineChain({ /* see viem docs */ });

const sdk = new SDK({
  public: createPublicClient({ chain, transport: webSocket() }),
});

const subscription = await sdk.watch({
  eventContractSources: ['0x046EDe9564A72571df6F5e44d0405360c0f4dCab'],
  topicOverrides: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'],
  ethCalls: [],
  onData: (data) => {
    const event = data.result;
    console.log(event);
  },
});

if (subscription instanceof Error) {
  throw subscription;
}

// later
await subscription.unsubscribe();
```

The SDK requires the public client to use a WebSocket transport. You don't need a wallet client — off-chain subscriptions don't sign anything.

`sdk.subscribe` accepts a `WebsocketSubscriptionInitParams` object that maps to the [Request parameters](#request-parameters) below:

| SDK field              | RPC field                           |
| ---------------------- | ----------------------------------- |
| `eventContractSources` | `address`                           |
| `topicOverrides`       | `topics`                            |
| `ethCalls`             | `eth_calls`                         |
| `context`              | `context`                           |
| `onlyPushChanges`      | `push_changes_only`                 |
| `onData` / `onError`   | (callback hooks, no RPC equivalent) |

For step-by-step walkthroughs, see the [Wildcard Off-Chain Reactivity Tutorial](/developer/reactivity/tutorials/wildcard-off-chain-reactivity-tutorial.md) and [Off-Chain Reactivity: Filtered Subscriptions tutorial](/developer/reactivity/tutorials/off-chain-reactivity-filtered-subscriptions-tutorial.md).

## Reference

### Subscription method

`somnia_watch`. Subscribed via the standard `eth_subscribe` request over a WebSocket transport. Unsubscribed via `eth_unsubscribe`.

### Request parameters

The parameters object follows [`eth_getLogs`](/developer/json-rpc-api.md#eth_getlogs)-style filter conventions with additions for read-only simulation.

| Field               | Type                         | Purpose                                                                                                                                                                                |
| ------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`           | `address` or `address[]`     | Optional. Matches only logs emitted by one of these addresses. Omit or pass `null` for any address.                                                                                    |
| `topics`            | `(Hash \| Hash[] \| null)[]` | Optional. Positional topic filter. Entry `i` constrains topic `i` of the log. Each entry can be a single hash, an array of hashes (OR), or `null` (any). Omit for any topics.          |
| `eth_calls`         | array                        | Required. A list of read-only calls the node runs for each matching log. Pass `[]` if you only want log notifications with no attached simulation. See [eth\_calls](#eth_calls) below. |
| `context`           | `string` or `string[]`       | Optional. Selectors that tell the node which fields of the matching log to append to each simulated call's calldata. See [context](#context) below.                                    |
| `push_changes_only` | `bool`                       | Optional, default false. If true, and `eth_calls` is non-empty, only events whose simulation results differ from the previous event are pushed.                                        |

#### eth\_calls

Each element is a tuple of `(callRequest, stateOverrides)`, mirroring [`eth_call`](/developer/json-rpc-api.md#eth_call):

* `callRequest` — an `eth_call`-style transaction request object (`to`, `data`, optional `from`, `gas`, etc.).
* `stateOverrides` — an optional `eth_call`-style state override set, applied for the duration of the simulation.

For every matching log the node executes each call as a read-only simulation against the end-of-transaction state. The raw returned bytes are included in `simulationResults`, in order.

The same single-item-or-list shortcut used elsewhere in Ethereum JSON-RPC applies: you can pass just the call request to skip state overrides.

#### context

When `context` is non-empty, for each simulated call the node appends 32-byte chunks drawn from the matching log to the call's calldata, in the order given. This lets a single simulation template be parameterised by the log that triggered it.

Valid selectors:

| Selector  | What is appended                                       |
| --------- | ------------------------------------------------------ |
| `address` | The log's emitter address, left-padded to 32 bytes.    |
| `data`    | The log's data bytes (not padded — appended verbatim). |
| `topic1`  | The log's first topic (`topics[0]`).                   |
| `topic2`  | The log's second topic (`topics[1]`).                  |
| `topic3`  | The log's third topic (`topics[2]`).                   |
| `topic4`  | The log's fourth topic (`topics[3]`).                  |

Note that the 1-based names refer to position within the log, so `topic1` is the first topic (usually the event signature), not the second. Selectors that ask for a topic the log does not have are silently skipped.

If `context` is empty or absent, the node runs each `eth_call` exactly as written.

#### push\_changes\_only

If true and `eth_calls` is non-empty, the node compares the new `simulationResults` against the previous *pushed* event on this subscription. If they are byte-identical, the new event is dropped. The comparison is strictly adjacent — not against any reference value — so a zero result will still be pushed whenever the prior push was non-zero. Use this to avoid re-notifying on a genuinely unchanged view (e.g. a wash transfer that returns a balance to its previous value), not to filter out uninteresting ones.

`push_changes_only` has no effect when `eth_calls` is empty, since there is nothing to compare.

### Event payload

Each pushed notification has a `result` field shaped as:

| Field               | Type      | Meaning                                                                                                                                       |
| ------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`           | `address` | The log's emitter.                                                                                                                            |
| `topics`            | `Hash[]`  | The log's topics, up to four entries.                                                                                                         |
| `data`              | `bytes`   | The log's data payload.                                                                                                                       |
| `simulationResults` | `bytes[]` | One entry per `eth_calls` entry, in order. Empty if no `eth_calls` were configured. Each entry is the raw returndata from the simulated call. |

Topics and data are exactly as they appear in the committed block, so decoding follows the same rules as the event's Solidity definition.

### Filter matching semantics

The `address` and `topics` fields follow the same rules as `eth_getLogs` and `eth_subscribe("logs", ...)`:

* A log matches when its emitter matches the `address` filter (or `address` is omitted).
* For each topic position, a filter entry of `null` or omitted matches anything, a single hash matches exactly, and an array of hashes matches any member (OR).
* The subscription only considers logs from transactions that were included in a block. It does not see pending pool activity.

### Lifetime

The subscription lives on the API connection that created it. If the connection closes (client disconnect, node restart, network blip) the subscription is gone and must be recreated. The node does not buffer missed events across disconnects.

Subscriptions are not authenticated or owner-scoped in any way. Every client creates and sees only its own subscriptions.

### Historical data

Off-chain reactivity only pushes events from the moment the subscription is created onwards. For historical event retrieval, use [`eth_getLogs`](/developer/json-rpc-api.md#eth_getlogs) or another indexing solution.

## Minimal example

Here's how to subscribe to all Wrapped SOMI transfer events on mainnet, checking the token balance of each recipient:

```bash
wscat --connect wss://api.infra.mainnet.somnia.network/ws --wait 300 --execute '
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_subscribe",
  "params": [
    "somnia_watch",
    {
      "address": "0x046EDe9564A72571df6F5e44d0405360c0f4dCab",
      "topics": [
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
      ],
      "eth_calls": [
        {
          "to": "0x046EDe9564A72571df6F5e44d0405360c0f4dCab",
          "data": "0x70a08231"
        }
      ],
      "context": ["topic3"],
      "push_changes_only": false
    }
  ]
}'
```

Walking through each field:

* `address` — restricts matches to logs emitted by this specific ERC-20 token contract (see [Smart contracts](/developer/smart-contracts.md) for the canonical mainnet address).
* `topics` — the single entry is `keccak256("Transfer(address,address,uint256)")`, so that only ERC-20 Transfer logs match. The log's other two topics (`from` and `to`) are left unconstrained.
* `eth_calls` — a single read-only call, back to the token contract. Its `data` is the 4-byte selector `keccak256("balanceOf(address)")`; the 32-byte recipient argument is not written inline, but supplied by `context` below.
* `context` — the `topic3` selector instructs the node to append the log's third topic (the Transfer `to`, already left-padded to 32 bytes) to the simulated call's calldata. The result is a well-formed `balanceOf(recipient)` call, even though the subscription template is fixed.
* `push_changes_only` — enabling this here would discard events when two successive recipients happened to have the same token balance — not very helpful.

On running the command, you'll first receive the subscription ack:

```json
{"jsonrpc":"2.0","id":1,"result":"0x..."}
```

And then one `eth_subscription` notification per WSOMI Transfer, as they happen:

```json
{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x...","result":{"address":"0x046ede9564a72571df6f5e44d0405360c0f4dcab","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x...","0x..."],"data":"0x...","simulationResults":["0x..."]}}}
```

Each `simulationResults[0]` is the recipient's WSOMI balance after the transfer, denoted in wei.

Close the connection (Ctrl-C) to unsubscribe.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.somnia.network/developer/reactivity/reactivity-offchain.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
