# On-chain Reactivity

On-chain reactivity lets a smart contract instantly react to events in the same block, without anyone sending an event handling transaction. A user creates a subscription through a call to a precompile, and the subscription persists in chain state. When an event log or system event matches the subscription's filter, validators include a synthetic transaction in the block that calls the subscription's handler contract. The creator of the subscription pays the gas.

This is a powerful feature. As far as we know, no other EVM can do this, and when combined with Somnia's 100 ms blocks, on-chain reactivity enables a class of applications that other chains can only approximate with off-chain infrastructure.

For client-side reactions to events, see [off-chain reactivity](/developer/reactivity/reactivity-offchain.md).

## Example use cases

* Trigger a callback every time a specific ERC-20 Transfer event is emitted, regardless of who sent the transaction.
* Run scheduled upkeep at every epoch boundary.
* Move funds out of escrow at a predetermined time.
* Build an automated liquidation bot as a contract rather than an off-chain service.
* Forward DEX events to a settlement contract without the trader paying the forwarding cost.

## Quick start

1. Install the Solidity reactivity package (see [Solidity package](#solidity-package) below).

   ```bash
   npm install @somnia-chain/reactivity-contracts
   ```
2. Write a handler contract. Inherit from `SomniaEventHandler` and override the protected `_onEvent` hook:

   ```solidity
   import { SomniaEventHandler } from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";

   contract MyHandler is SomniaEventHandler {
       function _onEvent(
           address emitter,
           bytes32[] calldata eventTopics,
           bytes calldata data
       ) internal override {
           // react to the event here
       }
   }
   ```
3. Fill in a `SomniaExtensions.SubscriptionFilter` and a `SomniaExtensions.SubscriptionOptions`. See [Solidity package](#solidity-package) for the struct shapes, and [Filter semantics](#filter-semantics) for what the filter fields mean.
4. Deploy the contract, and ensure the contract account holds at least 32 [SOMI](/developer/network-info/somi-coin.md) (see [Minimum balance](#minimum-balance)).
5. Create the subscription by calling `SomniaExtensions.subscribe(address(this), filter, options)`. The call returns the subscription ID, and the calling contract becomes the subscription owner. For one-shot subscriptions at a specific block, epoch, or timestamp, use `scheduleSubscriptionAtBlock` / `scheduleSubscriptionAtEpoch` / `scheduleSubscriptionAtTimestamp` instead.
6. The subscription runs until you remove it or the owner account runs out of SOMI (see [Automatic removal](#automatic-removal)).
7. Query subscriptions off-chain via the [`somnia_reactivityGetSubscriptionInfo`](#rpc-methods) or [`somnia_reactivityGetSubscriptions`](#rpc-methods) JSON-RPC methods, or on-chain via `SomniaExtensions.getSubscriptionInfo(id)`.
8. Remove the subscription by calling `SomniaExtensions.unsubscribe(id)` from the owning contract.

These steps assume the contract itself is the subscription owner, which is what the `SomniaExtensions` library is designed for. To own the subscription directly from an EOA, see the [TypeScript package](#typescript-package), or for the raw ABI-level surface, see [Precompile](#precompile) and [`SubscriptionData`](#subscriptiondata) in the Reference section.

## How it works

Subscriptions are created by calling the **Somnia reactivity precompile** at address `0x0100`.

When a transaction is executed, every event log it emits is checked against all active subscriptions. Matching subscriptions are placed on a per-block priority queue, ordered by each subscription's `priorityFeePerGas`.

After the block's user transactions have executed, the node drains the queue and executes each pending handler as a synthetic transaction.

These reactive transactions appear in the block's reactivity output alongside the ordinary transactions. They are visible on the block explorer, have normal receipts, emit normal logs, and pay normal gas.

Each reactive transaction has:

* `from` equal to the subscription owner
* `to` equal to the subscription's handler contract
* `msg.sender` inside the call equal to `0x0100`
* Calldata equal to `handlerFunctionSelector` followed by the ABI-encoded tuple `(address emitter, bytes32[] eventTopics, bytes data)` from the matching log
* A block-unique nonce derived from the block number and position in the reactivity queue (so that reactive transactions don't conflict with the owner's regular nonces)

If the handler reverts, runs out of gas, or the owner can't pay, the reactive transaction fails in the ordinary way. A failure doesn't itself remove the subscription, but see [Automatic removal](#automatic-removal) below.

Logs emitted by reactive transactions are checked against subscriptions the same way as user transactions, and any matches are immediately added to the block's reactivity queue. Note that this means that *a subscription can provoke a recursive explosion*, unstoppably draining the owner's balance.

## Reference

### Precompile

* Address: `0x0100`
* Solidity constant: `SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS`

The precompile exposes three functions. The function selectors are keccak256 of the signatures below.

| Function              | Signature                                                                                       |
| --------------------- | ----------------------------------------------------------------------------------------------- |
| `subscribe`           | `subscribe((bytes32[4],address,address,address,address,bytes4,uint64,uint64,uint64,bool,bool))` |
| `unsubscribe`         | `unsubscribe(uint256)`                                                                          |
| `getSubscriptionInfo` | `getSubscriptionInfo(uint256)`                                                                  |

Note that `subscribe` takes a single tuple parameter (the `SubscriptionData` struct), not eleven positional parameters — the outer parentheses are significant.

### `SubscriptionData`

The tuple passed to `subscribe`, in order:

| Field                     | Type         | Purpose                                                                                                                                                       |
| ------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventTopics`             | `bytes32[4]` | Topic filters. A zero value means "match any". (See [Filter semantics](#filter-semantics).)                                                                   |
| `origin`                  | `address`    | Matches logs from transactions sent by this address. Zero means "match any".                                                                                  |
| `caller`                  | `address`    | Reserved. Pass `0x0`.                                                                                                                                         |
| `emitter`                 | `address`    | Matches logs emitted from this contract address. Zero means "match any". Set to `0x0100` to subscribe to system events (see [System events](#system-events)). |
| `handlerContractAddress`  | `address`    | The contract called when a match occurs. Must be non-zero.                                                                                                    |
| `handlerFunctionSelector` | `bytes4`     | The 4-byte function selector invoked on the handler contract.                                                                                                 |
| `priorityFeePerGas`       | `uint64`     | Tip to validators, in wei. Determines order in the reactivity queue (see [Limits](#limits)).                                                                  |
| `maxFeePerGas`            | `uint64`     | Maximum total fee per gas, in wei. If set to zero, the protocol chooses a maximum.                                                                            |
| `gasLimit`                | `uint64`     | Maximum gas the handler may consume per invocation. Must be non-zero, maximum of 200,000,000.                                                                 |
| `isGuaranteed`            | `bool`       | Reserved. Pass `false`.                                                                                                                                       |
| `isCoalesced`             | `bool`       | Reserved. Pass `false`.                                                                                                                                       |

### Validation at subscription creation

`subscribe` reverts if:

* `handlerContractAddress` is zero.
* None of `eventTopics[0..3]`, `origin`, or `emitter` are set. At least one filter is required — wildcard subscriptions that match every log on the chain are not allowed.
* `gasLimit` is zero or above `max_reactivity_handler_gas_limit` (200 million gas).
* `priorityFeePerGas` is above the gas price cap.
* `priorityFeePerGas + baseFee` exceeds `maxFeePerGas`.
* The owner does not hold at least 32 SOMI.

If `maxFeePerGas` is passed as zero, the protocol replaces it with the maximum gas price before storing the subscription. Use this default with caution: if the gas price spikes, your subscription may become unprofitable to execute and fail to fire.

### Filter semantics

A subscription matches an event log when every non-zero filter field equals the corresponding field on the log. Zero acts as a wildcard. The filter fields are: `origin`, `emitter` and `eventTopics`. Topic filters are positional: `eventTopics[0]` matches against the log's first topic (the event signature), `eventTopics[1]` against the second, and so on.

At least one field must be a non-wildcard. An all-zero subscription is rejected at creation.

There is no "OR" support on a single field — a filter value either matches a specific hash or matches everything. If you need disjunctive matching, create multiple subscriptions.

### System events

Instead of an event log, you can subscribe to one of three system events, by setting the following fields in the subscription:

| Event      | `emitter` | `eventTopics[0]`                        | `eventTopics[1]`          | When it fires                                                                                                                   |
| ---------- | --------- | --------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Block tick | `0x100`   | `keccak256("BlockTick(uint64)")`        | block number              | Every block (i.e. ten times a second) if `eventTopics[1]` is zero, or otherwise that specific block                             |
| Epoch tick | `0x100`   | `keccak256("EpochTick(uint64,uint64)")` | epoch number              | At the end of every epoch (i.e. every five minutes) if `eventTopics[1]` is zero, or otherwise at the end of that specific epoch |
| Schedule   | `0x100`   | `keccak256("Schedule(uint256)")`        | timestamp in milliseconds | Once, in the first block whose timestamp is ≥ `eventTopics[1]`                                                                  |

`Schedule`, block-specific `BlockTick`, and epoch-specific `EpochTick` are **one-shot**: the subscription is automatically removed after it fires. Every-block and every-epoch subscriptions are recurring.

Scheduled timestamps must be strictly greater than the current block timestamp at the moment the subscription is created.

### Handler execution context

Inside a handler invocation:

* `msg.sender == 0x0100`
* `tx.origin == subscription owner address`
* `msg.value == 0` — reactive calls never carry value
* Calldata is `handlerFunctionSelector ++ abi.encode(address emitter, bytes32[] eventTopics, bytes data)` where the arguments are from the log that matched

The handler runs with the subscription's `gasLimit`. If it exceeds that limit the transaction reverts and the owner pays for the attempt.

### Minimum balance

The subscription owner must hold at least **32 SOMI** at the moment `subscribe` is called. This is a sybil-resistance barrier against spammy subscription creation.

The 32 SOMI is **not** an escrow and **not** consumed. It sits in the owner's regular balance. Gas for each handler invocation is paid from the owner's balance at the normal per-tx rate.

Note that the 32 SOMI threshold is only enforced at creation. A subscription can continue to fire after its owner's balance has dropped below 32 SOMI, as long as the owner can still pay the gas for each individual handler invocation. See [Automatic removal](#automatic-removal) for what happens when the owner can't pay.

### Gas costs

Creating a subscription (by calling `subscribe` on the reactivity precompile) costs `210,000` gas, charged to the transaction sender as usual.

Handler invocations are charged to the subscription owner. The price per gas is the block's current gas price plus the subscription's `priorityFeePerGas`. The handler can consume at most the subscription's `gasLimit`.

### Limits

There are per-block limits on total execution gas, reactivity-specific execution gas and the number of reactivity transactions. When any of these is hit, the remaining matches stay in the reactivity queue and are attempted again in the next block.

Matches are sorted by `priorityFeePerGas`, so those that pay lower fees may be indefinitely deferred.

There are also limits on the size of the reactivity queue itself. When these are hit, the lowest-priority matches are evicted.

### Automatic removal

A subscription is removed automatically when:

* It's a one-shot scheduled subscription and it has just fired.
* It was evicted from a full reactivity queue and was also one-shot.
* The owner's balance doesn't cover the subscription's `gasLimit` when it fires. i.e. It's less than `(execution price per gas + priorityFeePerGas) * gasLimit`.

A subscription is **not** removed when:

* The handler simply reverts.
* The handler exceeds `gasLimit` during execution.
* The owner's balance drops below 32 SOMI but is still enough to pay for individual handler invocations.
* The handler execution is deferred due to queue limits or low priority fee.

### Emitted events

`subscribe` and `unsubscribe` emit these logs from `0x0100`:

* `SubscriptionCreated(uint256 id, address owner, SubscriptionData data)` — data is the full tuple, ABI-encoded inline.
* `SubscriptionRemoved(uint256 id, address owner)`

These are the logs you should filter against off-chain if you want to track subscription lifecycle.

### RPC methods

The node exposes two reactivity-specific RPC methods:

* [**`somnia_reactivityGetSubscriptionInfo`**](/developer/json-rpc-api.md#somnia_reactivitygetsubscriptioninfo) — Takes a subscription ID (as a quantity string, e.g. `"23"` or `"0x15f019"`) and returns the details of the subscription. Can also take an array of such IDs. Response fields mirror `SubscriptionData` plus `id` and `owner`, with names in `snake_case` (`handler_contract_address`, `handler_function_selector`, `priority_fee_per_gas`, `max_fee_per_gas`, `gas_limit`).
* [**`somnia_reactivityGetSubscriptions`**](/developer/json-rpc-api.md#somnia_reactivitygetsubscriptions) — Takes an owner address, returns an array of subscription objects currently owned by that address. Each entry has the same shape as the `somnia_reactivityGetSubscriptionInfo` response.

Both are `eth_call`-style JSON-RPC methods available on any Somnia node.

### Solidity package

The [`@somnia-chain/reactivity-contracts`](https://www.npmjs.com/package/@somnia-chain/reactivity-contracts) npm package is the idiomatic Solidity wrapper around the precompile. Install it with `npm install @somnia-chain/reactivity-contracts` (or your toolchain's equivalent). It exposes four pieces:

* `ISomniaReactivityPrecompile` — the typed interface for the precompile at `0x0100` — the `SubscriptionData` struct and the `BlockTick`, `EpochTick`, `Schedule` system-event signatures (so topic hashes are `<Event>.selector`), plus the raw `subscribe`/`unsubscribe`/ `getSubscriptionInfo` functions.
* `ISomniaEventHandler` — the callback interface that `SomniaEventHandler` implements. Used as the canonical value for `handlerFunctionSelector` (`ISomniaEventHandler.onEvent.selector`).
* `SomniaEventHandler` — an abstract base contract that implements the "only the precompile may call me" check and the ERC-165 plumbing, leaving you to override a protected `_onEvent`.
* `SomniaExtensions` — the ergonomic helper library most user code will call into. Constants:

  | Constant                               | Value         |
  | -------------------------------------- | ------------- |
  | `SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS` | `0x0100`      |
  | `SUBSCRIPTION_OWNER_MINIMUM_BALANCE`   | `32 ether`    |
  | `MINIMUM_BASE_FEE_PER_GAS`             | `6 gwei`      |
  | `MAXIMUM_HANDLER_GAS_LIMIT`            | `200_000_000` |

  Helper structs:

  ```solidity
  struct SubscriptionFilter {
      bytes32[4] eventTopics;
      address origin;
      address emitter;
  }

  struct SubscriptionOptions {
      uint64 priorityFeePerGas;
      uint64 maxFeePerGas;
      uint64 gasLimit;
  }
  ```

  Functions (all `internal`, i.e. called from a Solidity contract, not an EOA):

  | Function                                                             | Purpose                                                                                                                                                                                                                                                                                                                                                                                              |
  | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `subscribe(handler, filter, options)`                                | Create a subscription with the given filter. Checks `handler != 0`, at least one filter is set, `gasLimit` is within range, `maxFeePerGas` is consistent with `priorityFeePerGas + MINIMUM_BASE_FEE_PER_GAS`, and the caller's balance ≥ `SUBSCRIPTION_OWNER_MINIMUM_BALANCE` — reverts with a typed error if any fails. Hardcodes `handlerFunctionSelector = ISomniaEventHandler.onEvent.selector`. |
  | `scheduleSubscriptionAtTimestamp(handler, timestampMillis, options)` | One-shot Schedule subscription firing when the block timestamp (ms) first reaches `timestampMillis`. Reverts if the timestamp is in the past.                                                                                                                                                                                                                                                        |
  | `scheduleSubscriptionAtBlock(handler, blockNumber, options)`         | One-shot BlockTick subscription firing on the specific block. Reverts if the block number is in the past.                                                                                                                                                                                                                                                                                            |
  | `scheduleSubscriptionAtEpoch(handler, epochNumber, options)`         | One-shot EpochTick subscription firing at the end of the specific epoch.                                                                                                                                                                                                                                                                                                                             |
  | `unsubscribe(subscriptionId)`                                        | Cancel a subscription owned by the caller. (Uses a low-level `.call` because a typed call would revert before reaching the precompile.)                                                                                                                                                                                                                                                              |
  | `getSubscriptionInfo(subscriptionId)`                                | Read a subscription's stored parameters and owner.                                                                                                                                                                                                                                                                                                                                                   |

  The `schedule*` helpers only cover **one-shot** subscriptions at a specific block/epoch/timestamp. For **recurring** every-block or every-epoch subscriptions, build a `SubscriptionFilter` with `eventTopics[1] = 0` and call `subscribe` directly — see the [Example](#example).

### TypeScript package

The [`@somnia-chain/reactivity`](https://www.npmjs.com/package/@somnia-chain/reactivity) package wraps the precompile with Viem for use from TypeScript scripts and Node.js services. It's intended for off-chain orchestration: creating a subscription owned by an externally-owned account, inspecting subscription state, or cancelling a subscription from a script.

| Method                                                      | Purpose                                                                                                                                                                  |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `createSoliditySubscription(data)`                          | Broadcast a `subscribe` transaction with the given `SoliditySubscriptionData`.                                                                                           |
| `cancelSoliditySubscription(id)`                            | Broadcast an `unsubscribe` transaction.                                                                                                                                  |
| `getSubscriptionInfo(id)`                                   | Read a subscription via `eth_call`.                                                                                                                                      |
| `createOnchainBlockTickSubscription({ blockNumber?, ... })` | Convenience wrapper for `BlockTick` subscriptions. Omit `blockNumber` for a recurring every-block subscription; pass it for a one-shot subscription on a specific block. |
| `scheduleOnchainCronJob({ timestampMs, ... })`              | Convenience wrapper for one-shot `Schedule` subscriptions.                                                                                                               |

If the subscription is owned by a Solidity contract — the pattern shown in [Quick start](#quick-start) and [Minimal example](#minimal-example) — use `SomniaExtensions` from the contract directly. The TypeScript SDK is only useful when the subscription owner is an externally-owned account, or when you want to inspect or cancel a subscription from off-chain code. (The same package also drives [off-chain reactivity](/developer/reactivity/reactivity-offchain.md) over WebSocket.)

## Minimal example

This contract subscribes itself to every block and counts them. It's built on the [`@somnia-chain/reactivity-contracts`](https://www.npmjs.com/package/@somnia-chain/reactivity-contracts) package (see [Solidity package](#solidity-package) above).

```solidity
pragma solidity 0.8.30;

import {
    SomniaEventHandler
} from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";
import {
    ISomniaReactivityPrecompile
} from "@somnia-chain/reactivity-contracts/contracts/interfaces/ISomniaReactivityPrecompile.sol";
import {
    SomniaExtensions
} from "@somnia-chain/reactivity-contracts/contracts/interfaces/SomniaExtensions.sol";

/// @notice Counts the blocks it has been invoked on, starting as soon as it
///         is deployed.
contract CounterHandler is SomniaEventHandler {
    uint256 public count;
    uint256 public subscriptionId;

    /// @dev The contract itself owns the subscription and pays gas for each
    ///      invocation, so it must be funded with ≥ 32 SOMI at deploy time.
    constructor(uint64 gasLimit) payable {
        // The scheduleSubscriptionAt* helpers are all one-shot. For a
        // recurring every-block subscription, we build the BlockTick filter
        // ourselves with eventTopics[1] = 0 and call subscribe() directly.
        SomniaExtensions.SubscriptionFilter memory filter =
            SomniaExtensions.SubscriptionFilter({
                eventTopics: [
                    ISomniaReactivityPrecompile.BlockTick.selector,
                    bytes32(0), bytes32(0), bytes32(0)
                ],
                origin: address(0),
                emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS
            });

        SomniaExtensions.SubscriptionOptions memory options =
            SomniaExtensions.SubscriptionOptions({
                priorityFeePerGas: 1,
                maxFeePerGas: 0,
                gasLimit: gasLimit
            });

        subscriptionId = SomniaExtensions.subscribe(
            address(this), filter, options
        );
    }

    /// @dev Called by `SomniaEventHandler.onEvent` after it has verified the
    ///      caller is the precompile. Arguments are the matching log's
    ///      emitter, topics, and non-indexed data.
    function _onEvent(
        address /* emitter */,
        bytes32[] calldata /* eventTopics */,
        bytes calldata /* data */
    ) internal override {
        count += 1;
    }

    function stop() external {
        SomniaExtensions.unsubscribe(subscriptionId);
    }
}
```

Note that **the contract is the subscription owner**, not the deployer — the constructor calls `subscribe` from the contract's own address. If you don't call `stop()`, the subscription will keep running until the contract's balance can't cover `gasLimit`.

### Deploying and driving the example with Foundry

Deploy the handler with [Foundry](/developer/development-frameworks/deploy-with-foundry.md), funding the contract with 33 SOMI (just above the 32 SOMI minimum balance) and passing a 2,000,000 gas limit to the constructor:

```bash
export RPC_URL=<your Somnia RPC endpoint>
export PK=<your funded private key>

forge create \
    --rpc-url "$RPC_URL" \
    --private-key "$PK" \
    --broadcast \
    --value 33ether \
    src/CounterHandler.sol:CounterHandler \
    --constructor-args 2000000
```

`forge create` prints the deployed address. Read the counter — it should be advancing once per block:

```bash
export COUNTER=<address printed by forge create>

cast call --rpc-url "$RPC_URL" "$COUNTER" 'count()(uint256)'
cast call --rpc-url "$RPC_URL" "$COUNTER" 'subscriptionId()(uint256)'
```

Inspect the subscription itself through the precompile's RPC:

```bash
SUB_ID=$(cast call --rpc-url "$RPC_URL" "$COUNTER" 'subscriptionId()(uint256)' \
    | awk '{print $1}')
cast rpc --rpc-url "$RPC_URL" \
    somnia_reactivityGetSubscriptionInfo "$(cast to-hex "$SUB_ID")"
```

Stop the handler by calling `stop()`, which unsubscribes via the library:

```bash
cast send --rpc-url "$RPC_URL" --private-key "$PK" "$COUNTER" 'stop()'
```

After that, `count()` stops advancing and `somnia_reactivityGetSubscriptionInfo` on the old ID returns an empty result.

## Development advice

### Designing

* **Handlers are separate transactions, not callbacks.** A triggering transaction commits first; matching handlers are later executed as synthetic transactions, usually in the same block.
* **System events are just synthetic logs.** `BlockTick`, `EpochTick` and `Schedule` come from fabricated `0x100` logs at block end.
* **Avoid recursive explosions.** Make sure that your subscription's handler can't emit an event which re-triggers your subscription.

### Implementing

* **Use a dedicated account as the subscription owner.** An ill-judged subscription can quietly drain an account. A *recursive* subscription, that reacts to its own handler, can do the same in moments. Consider limiting the blast radius by funding a dedicated account.
* **Budget for gas reserves in subscription gas limits.** Somnia's storage operations require a `1,000,000` gas reserve in case execution requires a disk read or a new storage allocation. Neglecting this reserve is a common cause of out-of-gas errors. See [Somnia gas differences to Ethereum](/developer/deployment-and-production/somnia-gas-differences-to-ethereum.md) for the table of costs.
* **Recreate subscriptions after redeploying.** Subscriptions point at concrete contract addresses, so redeploying a handler requires creating a new subscription.

### Why isn't your handler getting invoked?

Common problems:

1. **Subscription isn't active.** A subscription is automatically removed if the owner's balance can't cover `gasLimit` when it fires. Check `somnia_reactivityGetSubscriptions` or `somnia_reactivityGetSubscriptionInfo`.
2. Implementation of `SomniaEventHandler` interface is invalid. See [Solidity package](#solidity-package) for the expected shape.
3. **Logs aren't matching.** One gotcha: only successful transaction logs trigger reactivity. If a transaction reverts, its logs won't be matched against subscriptions.
4. **Subscription is pointing at wrong handler.** Subscriptions point at concrete contract addresses, so redeploying a handler requires creating a new subscription.
5. **`maxFeePerGas` is too low.** Note that `maxFeePerGas` and `priorityFeePerGas` are denominated in wei, but Somnia's minimum base fee is 6 [nanoSomi](/developer/network-info/somi-coin.md), i.e. 6 gWei, i.e. 6,000,000,000 wei. A common mistake is to specify a fee in wei thinking that the unit is gWei.
6. **`priorityFeePerGas` is too low for the current queue pressure.** This is rare, but when reactivity execution hits its [limits](#limits), low-priority-fee matches are deferred or even evicted. A schedule that fires late or not at all can mean queue pressure rather than a bug.
7. **`gasLimit` is too low.** This is very common: Somnia operates on a different gas model to Ethereum, and some Somnia gas costs, measured in units of gas, are much higher than Ethereum's. Unlike the previous problems, this can be identified in the chain's history — see below.

### Debugging reactive transactions

* **Reactive transactions can be recognised by their nonces.** They're legacy-typed (`type == 0x0`) and their nonces pack the block number in the high bytes and the queue position (0-indexed) in the low three bytes. E.g. the first reactive in block `0xabcdef` has nonce `0xabcdef000000`, the second `0xabcdef000001`, and so on. Wallets and explorers don't yet render this specially; the nonce pattern is the quickest way to spot reactive txs in `eth_getBlockByNumber(..., true)` output.
* **Reactive transactions have hashes and receipts, but fake signatures.** They're encoded into history as legacy-style transactions with placeholder signature fields. They aren't actually signed by their owner.


---

# 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-onchain.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.
