Building Reactive Smart Contracts on Somnia
A Developer Guide with Real Solidity Examples
1. Introduction
As Web3 developers, we often build systems that require automatic reactions.
Examples from real projects like onchain games, PotWar pools, prediction markets:
- Last player wins when timer ends
- Auto reward after chest open
- Pool auto payout
- NFT level up after XP
- DAO proposal auto execute
With traditional smart contracts, this requires:
• Backend server
• Event indexer
• Cron jobs
• Extra user transactions
This increases complexity, cost, and centralization.
Somnia On-Chain Reactivity solves this by allowing smart contracts to automatically react to events fully on-chain.
2. What is Somnia On-Chain Reactivity?
Somnia lets smart contracts execute logic automatically when subscribed events occur.
Traditional Flow :
User Action → Event → Backend Listener → New Tx → State Update
Somnia Reactive Flow :
User Action → Event → Validator Detect → _onEvent() → State Update
No backend.
No extra user gas.
Fully decentralized.
3. Why This Matters for Web3 Devs
For builders working on gaming, DeFi, or automation:
- No server infra
- Less latency
- Cleaner architecture
- Better UX
- Faster hackathon builds
For example, in Last Player Standing, we don’t need a backend timer contract logic can react automatically.
4. Example: Magic Chest Reactive Game
Let’s build a small game.
Game Rules
- Common chest → +10 coins
- Rare chest → +50 coins
- Legendary chest → NFT sword
Player opens chest → reward auto given.
5. Solidity Contract Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
import { SomniaEventHandler }
from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";
contract MagicChestReactiveGame is SomniaEventHandler {
event ChestOpened(address indexed player, uint256 chestType);
mapping(address => uint256) public coins;
mapping(address => bool) public hasSword;
bytes32 constant CHEST_SIG =
keccak256("ChestOpened(address,uint256)");
uint256 constant COMMON = 1;
uint256 constant RARE = 2;
uint256 constant LEGENDARY = 3;
function openChest(uint256 chestType) external {
emit ChestOpened(msg.sender, chestType);
}
function _onEvent(
address,
bytes32[] calldata topics,
bytes calldata data
) internal override {
require(topics[0] == CHEST_SIG, "Wrong event");
address player =
address(uint160(uint256(topics[1])));
uint256 chestType =
abi.decode(data, (uint256));
if (chestType == COMMON)
coins[player] += 10;
else if (chestType == RARE)
coins[player] += 50;
else if (chestType == LEGENDARY)
hasSword[player] = true;
}
}
6. How Reactivity Works Internally
- Player calls openChest()
- Event emitted
- Somnia validators detect subscription
- _onEvent() executes automatically
- State updated
- No extra transaction required.
Reactive tx is executed by validator address:
0x0000000000000000000000000000000000000100
7. Creating a Subscription for the event :
import { SDK } from "@somnia-chain/reactivity";
import { privateKeyToAccount } from "viem/accounts";
import { createPublicClient, createWalletClient, http } from "viem";
import { somniaTestnet } from "viem/chains";
import { keccak256, toBytes } from "viem";
const CONTRACT = "0xYourContractAddress";
async function main() {
const account = privateKeyToAccount(
process.env.PRIVATE_KEY as `0x${string}`
);
const publicClient = createPublicClient({
chain: somniaTestnet,
transport: http(),
});
const walletClient = createWalletClient({
account,
chain: somniaTestnet,
transport: http(),
});
const sdk = new SDK({
public: publicClient,
wallet: walletClient
});
const EVENT_SIG = keccak256(
toBytes("ChestOpened(address,uint256)")
);
const txHash = await sdk.createSoliditySubscription({
handlerContractAddress: CONTRACT,
emitter: CONTRACT,
eventTopics: [EVENT_SIG],
gasLimit: 3_000_000n,
});
const receipt = await publicClient.waitForTransactionReceipt({
hash: txHash,
});
// 👉 Extract subscriptionId
const log = receipt.logs[0];
const subscriptionId = BigInt(log.topics[2]);
console.log("Subscription ID:", subscriptionId.toString());
}
main();
Important Notes
- Need 32 STT balance
- Save subscription ID
- Event signature must match exactly
Now after subscribing event that we have to trigger from reactivity we need to test this ,
Testing Reactivity
After subscription, test contract.
import { ethers } from "hardhat";
const CONTRACT = "0xYourContractAddress";
async function main() {
const [user] = await ethers.getSigners();
const game = await ethers.getContractAt(
"MagicChestReactiveGame",
CONTRACT
);
const before = await game.coins(user.address);
console.log("Coins before:", before.toString());
const tx = await game.openChest(1);
await tx.wait();
console.log("Waiting for Somnia reactivity...");
await new Promise(r => setTimeout(r,15000));
const after = await game.coins(user.address);
console.log("Coins after:", after.toString());
if(after > before)
console.log("✅ Reactivity Working");
else
console.log("❌ No Reactivity");
}
main();
Real Use Cases
-
Gaming
- Last Player Standing
- PotWar auto payout
- Loot rewards
- NFT upgrades
-
Prediction Markets
- Prediction auto reward
- Score updates
-
DeFi
- Liquidations
- Yield distribution
-
DAO
- Proposal auto execute
Conclusion
Somnia On-Chain Reactivity removes backend dependency and enables fully autonomous smart contracts.
For developers building automated gaming logic, prediction markets, or DeFi automation, this unlocks a new level of scalability and simplicity.
The future of Web3 apps is reactive.
Neeraj Choubisa (Nikku.Dev) is a Full-Stack Blockchain Engineer specializing in smart contract development, Web3 integrations, and consumer-focused decentralized applications.
Top comments (0)