Somnia Reactivity / Tutorials
Cron Subscriptions via SDK
Create block-tick and scheduled subscriptions using 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:
scheduleSubscriptionAtBlockscheduleSubscriptionAtTimestamp
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.
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.
Prerequisites
You'll need Node.js 20+.
Install the SDK and Viem:
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.
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 before using
a recurring system-event subscription in production.
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:
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:
emitterto the reactivity precompile addresseventTopics[0]to theBlockTick(uint64)selectoreventTopics[1]to the block number, or zero for every blockhandlerFunctionSelectortoISomniaEventHandler.onEvent.selectorwhen 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.
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:
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:
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:
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:
// 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:
uint256 subscriptionId = SomniaExtensions.scheduleSubscriptionAtTimestamp(
address(this),
(block.timestamp + 5 minutes) * 1000,
options
);
Recurring every-block subscription:
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.
Copied from docs.somnia.network (somnia-main-docs) by scripts/import-gitbook.mts, until it finds a home in a product's docs.Edit this page