> For the complete documentation index, see [llms.txt](https://docs.somnia.network/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.somnia.network/developer/reactivity/tutorials/cron-subscriptions-via-sdk.md).

# Cron subscriptions via SDK

The `@somnia-chain/reactivity` TypeScript SDK can create on-chain subscriptions owned by an externally-owned account (EOA). This tutorial covers the two system event helpers:

* `scheduleSubscriptionAtBlock`
* `scheduleSubscriptionAtTimestamp`

Use these helpers when a wallet or backend should own and pay for an on-chain callback, while a Solidity handler contract receives the reactive transaction.

For the system-event reference, see [On-chain Reactivity](/developer/reactivity/reactivity-onchain.md#system-events).

## What the Helpers Create

System events are synthetic logs emitted by the reactivity precompile at `0x0100`.

| Helper                            | System event        | Behavior                                                                 |
| --------------------------------- | ------------------- | ------------------------------------------------------------------------ |
| `scheduleSubscriptionAtBlock`     | `BlockTick(uint64)` | Every block if `blockNumber` is omitted, or once at the specified block. |
| `scheduleSubscriptionAtTimestamp` | `Schedule(uint256)` | Once, when block time first reaches the requested millisecond timestamp. |

The handler contract must implement `onEvent(address,bytes32[],bytes)`. The usual way to do that is to inherit from `SomniaEventHandler`, as shown in the [Solidity on-chain tutorial](/developer/reactivity/tutorials/solidity-on-chain-reactivity-tutorial.md).

## Prerequisites

You'll need Node.js 20+.

Install the SDK and Viem:

```bash
npm install @somnia-chain/reactivity viem
npm install --save-dev tsx typescript @types/node
```

To create a subscription, you'll need:

* a deployed handler contract
* an EOA funded with at least 32 SOMI

## Step 1: Prepare the SDK

This example uses Somnia Testnet. For mainnet, use the mainnet chain ID, currency and RPC URL from [Network Info](/developer/network-info.md).

```typescript
import { SDK } from '@somnia-chain/reactivity';
import {
  createPublicClient,
  createWalletClient,
  defineChain,
  http,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  nativeCurrency: {
    decimals: 18,
    name: 'STT',
    symbol: 'STT',
  },
  rpcUrls: {
    default: {
      http: ['https://api.infra.testnet.somnia.network'],
    },
  },
});

const privateKey = process.env.PRIVATE_KEY as `0x${string}` | undefined;

if (!privateKey) {
  throw new Error('Set PRIVATE_KEY=0x... before running this script');
}

const account = privateKeyToAccount(privateKey);

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: http(somniaTestnet.rpcUrls.default.http[0]),
});

const walletClient = createWalletClient({
  account,
  chain: somniaTestnet,
  transport: http(somniaTestnet.rpcUrls.default.http[0]),
});

const sdk = new SDK({
  public: publicClient,
  wallet: walletClient,
});
```

## Step 2: Subscribe to Block Ticks

Omit `blockNumber` for a recurring every-block subscription. Use this carefully: it can invoke your handler every block until the subscription is removed. See the on-chain Reactivity [development advice](/developer/reactivity/reactivity-onchain.md#development-advice) before using a recurring system-event subscription in production.

```typescript
const handlerContractAddress = process.env.HANDLER_ADDRESS as `0x${string}`;

const txHash = await sdk.scheduleSubscriptionAtBlock({
  handlerContractAddress,
  priorityFeePerGas: 1n,
  maxFeePerGas: 0n,
  gasLimit: 2_000_000n,
  isGuaranteed: false,
  isCoalesced: false,
});

if (txHash instanceof Error) {
  throw txHash;
}

console.log('Block tick subscription tx:', txHash);
```

Pass `blockNumber` for a one-shot subscription to a specific future block:

```typescript
const currentBlock = await publicClient.getBlockNumber();
const handlerContractAddress = process.env.HANDLER_ADDRESS as `0x${string}`;

const txHash = await sdk.scheduleSubscriptionAtBlock({
  blockNumber: currentBlock + 20n,
  handlerContractAddress,
  priorityFeePerGas: 1n,
  maxFeePerGas: 0n,
  gasLimit: 2_000_000n,
  isGuaranteed: false,
  isCoalesced: false,
});

if (txHash instanceof Error) {
  throw txHash;
}
```

Internally, the SDK sets:

* `emitter` to the reactivity precompile address
* `eventTopics[0]` to the `BlockTick(uint64)` selector
* `eventTopics[1]` to the block number, or zero for every block
* `handlerFunctionSelector` to `ISomniaEventHandler.onEvent.selector` when you omit it

## Step 3: Schedule a One-Off Callback

`scheduleSubscriptionAtTimestamp` creates a one-shot `Schedule(uint256)` subscription. The timestamp is an absolute Unix timestamp in milliseconds.

```typescript
const timestampMs = Date.now() + 5 * 60 * 1000;
const handlerContractAddress = process.env.HANDLER_ADDRESS as `0x${string}`;

const txHash = await sdk.scheduleSubscriptionAtTimestamp({
  timestampMs,
  handlerContractAddress,
  priorityFeePerGas: 1n,
  maxFeePerGas: 0n,
  gasLimit: 2_000_000n,
  isGuaranteed: false,
  isCoalesced: false,
});

if (txHash instanceof Error) {
  throw txHash;
}

console.log('Scheduled callback tx:', txHash);
```

The SDK returns an `Error` when `timestampMs < Date.now() + 12_000`, so schedule at least 12 seconds ahead. It does not throw for that validation path, so always check `instanceof Error`.

## Full Script

Save this as `cron.ts`:

```typescript
import { SDK } from '@somnia-chain/reactivity';
import {
  createPublicClient,
  createWalletClient,
  defineChain,
  http,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

const handlerContractAddress = process.argv[2] as `0x${string}`;
const privateKey = process.env.PRIVATE_KEY as `0x${string}` | undefined;

if (!handlerContractAddress) {
  throw new Error('Usage: npx tsx cron.ts <handlerContractAddress>');
}

if (!privateKey) {
  throw new Error('Set PRIVATE_KEY=0x... before running this script');
}

const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  nativeCurrency: {
    decimals: 18,
    name: 'STT',
    symbol: 'STT',
  },
  rpcUrls: {
    default: {
      http: ['https://api.infra.testnet.somnia.network'],
    },
  },
});

const account = privateKeyToAccount(privateKey);

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: http(somniaTestnet.rpcUrls.default.http[0]),
});

const walletClient = createWalletClient({
  account,
  chain: somniaTestnet,
  transport: http(somniaTestnet.rpcUrls.default.http[0]),
});

const sdk = new SDK({
  public: publicClient,
  wallet: walletClient,
});

async function main() {
  const currentBlock = await publicClient.getBlockNumber();

  const blockTickTx = await sdk.scheduleSubscriptionAtBlock({
    blockNumber: currentBlock + 20n,
    handlerContractAddress,
    priorityFeePerGas: 1n,
    maxFeePerGas: 0n,
    gasLimit: 2_000_000n,
    isGuaranteed: false,
    isCoalesced: false,
  });

  if (blockTickTx instanceof Error) {
    throw blockTickTx;
  }

  console.log('One-shot block tick subscription tx:', blockTickTx);

  // Wait for the first subscription to land before broadcasting the next one,
  // so the two writes don't race on the wallet's nonce.
  await publicClient.waitForTransactionReceipt({ hash: blockTickTx });

  const scheduleTx = await sdk.scheduleSubscriptionAtTimestamp({
    timestampMs: Date.now() + 5 * 60 * 1000,
    handlerContractAddress,
    priorityFeePerGas: 1n,
    maxFeePerGas: 0n,
    gasLimit: 2_000_000n,
    isGuaranteed: false,
    isCoalesced: false,
  });

  if (scheduleTx instanceof Error) {
    throw scheduleTx;
  }

  console.log('Scheduled callback tx:', scheduleTx);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

Run it:

```bash
PRIVATE_KEY=0x... npx tsx cron.ts <handlerContractAddress>
```

## Cancel from TypeScript

The SDK can also cancel subscriptions owned by the same wallet client. Pass the subscription ID, not the creation transaction hash:

```typescript
const cancelTx = await sdk.cancelSoliditySubscription(subscriptionId);

if (cancelTx instanceof Error) {
  throw cancelTx;
}

console.log('Cancelled subscription tx:', cancelTx);
```

## Equivalent Solidity Helpers

If a Solidity contract owns the subscription, prefer `SomniaExtensions` from inside that contract instead of the TypeScript SDK. The snippets below assume `options` is a `SomniaExtensions.SubscriptionOptions` value.

One-shot block subscription:

```solidity
// Any future block works. This example uses a small offset for easy testing.
uint256 subscriptionId = SomniaExtensions.scheduleSubscriptionAtBlock(
    address(this),
    uint64(block.number + 20),
    options
);
```

One-shot timestamp subscription:

```solidity
uint256 subscriptionId = SomniaExtensions.scheduleSubscriptionAtTimestamp(
    address(this),
    (block.timestamp + 5 minutes) * 1000,
    options
);
```

Recurring every-block subscription:

```solidity
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
    });

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

The `scheduleSubscriptionAt*` Solidity helpers are one-shot. For recurring every-block or every-epoch subscriptions, build the filter and call `SomniaExtensions.subscribe` directly.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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/tutorials/cron-subscriptions-via-sdk.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.
