Network: Somnia Testnet (Chain ID: 50312) Hackathon Track: Native Reactivity Live Dashboard: https://sentinel-nine-chi.vercel.app
Traditional blockchains are passive — dApps must continuously poll for new data, run off-chain bots, or rely on third-party indexers to detect on-chain events. Somnia inverts this model entirely. The Somnia Reactivity Precompile (deployed at address 0x0100) allows smart contracts to subscribe to on-chain events and receive automatic callbacks within the same block the event occurs. When a subscribed event fires, validators invoke the handler contract's onEvent() function directly — no external infrastructure, no cron jobs, no latency. This makes Somnia the first L1 where the blockchain itself acts as the event bus.
This is the most important section for understanding the project. Every component — from the core contracts to the frontend — is built around Somnia's native reactivity. Here is every place reactivity is used:
Contract: SentinelHandler.sol → subscribeToBlockTick()
The handler registers a subscription with the Reactivity Precompile (0x0100) to receive the BlockTick(uint64) system event. This event is emitted by Somnia validators on every single block.
Flow: Somnia Validator → emits BlockTick(uint64) → Precompile (0x0100) invokes handler.onEvent()
When the precompile invokes onEvent(), the internal _onEvent() function checks if the emitter is the precompile address (SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS). If so, it calls _processPriceChecks(), which iterates through all sentinels registered as PRICE_ALERT type and reads the MockPriceOracle for each monitored asset. If the current price exceeds the sentinel's threshold, a PriceThresholdAlert event is emitted and the priceAlertsProcessed counter increments.
Why this matters: Price monitoring runs on every block with zero external triggers. No keeper bot, no cron job, no off-chain infrastructure. The blockchain itself drives the monitoring loop.
Contracts: SentinelHandler.sol → subscribeToEmitter(), SentinelRegistry.sol → registerSentinel()
When a user registers a new sentinel via SentinelRegistry.registerSentinel(), the registry automatically calls handler.subscribeToEmitter(target) if a handler is configured (line 137 in SentinelRegistry.sol). This creates a per-emitter subscription with the precompile — subscribing to all events from that specific contract address using wildcard eventTopics.
Flow: User calls registerSentinel(BRIDGE_WATCH, MockBridge, ...)
→ Registry calls handler.subscribeToEmitter(MockBridge)
→ Handler calls PRECOMPILE.subscribe(...) with emitter = MockBridge
→ Precompile now monitors MockBridge for ANY event
→ When MockBridge emits DepositInitiated, precompile invokes handler.onEvent()
Inside _onEvent(), the handler checks if the emitter matches either the monitoredSentinel address or a target registered in the registry (registry.isTargetRegistered(emitter)). If matched, it increments reactiveCallCount and emits ReactiveActionProcessed with the alert type.
Why this matters: Subscription creation is fully automated — users don't need to manually call the precompile. The registry-to-handler wiring means every new sentinel is immediately reactive.
File: frontend/src/App.tsx → sdk.subscribe()
The dashboard uses @somnia-chain/reactivity SDK to establish a WebSocket connection to the Somnia node. On page load (isWatching defaults to true), the frontend subscribes to events from three system contracts:
SentinelHandler(0xe95d...) — reactive action eventsSentinelRegistry(0xeaf2...) — sentinel creation eventsMockPriceOracle(0xf586...) — price update events
Plus any active user sentinel targets.
const systemSources = [HANDLER_ADDRESS, REGISTRY_ADDRESS, ORACLE_ADDRESS];
const userTargets = mySentinels.filter(s => s.isActive).map(s => s.target);
const allSources = [...new Set([...systemSources, ...userTargets])];
sdk.subscribe({
eventContractSources: allSources,
onData: async (data) => { /* instant UI update */ }
});When a reactive event fires on-chain, the SDK pushes it to the browser instantly. Each event includes the source block number, displayed as ⚡ block #1,234,567 in the log feed. The dashboard also polls the handler's on-chain state (reactiveCallCount, priceAlertsProcessed, blockTickSubId) every 8 seconds to display live reactivity metrics.
Why this matters: No eth_getLogs polling loops, no subgraph, no indexer. The browser receives events the moment the chain reacts — within milliseconds of the transaction landing.
File: frontend/src/App.tsx → sessionClient.sendTransaction()
When a sentinel has an actionTarget configured (a non-zero address), the reactive callback in the SDK subscription triggers an automated transaction using @somnia-chain/viem-session-account. The session account is a disposable key that executes transactions without requiring the user to sign each one manually, and without nonces — preventing stuck transactions during high-throughput reactive execution.
Flow: On-chain event → SDK push → Frontend detects sentinel match
→ sessionClient.sendTransaction({ to: actionTarget, data: actionData })
→ Automated action executes on-chain without user interaction
Why this matters: The full loop — event detection, reactive callback, and automated execution — happens without any human intervention. The user configures a sentinel once, and the system reacts autonomously.
cd frontend && npm install && npm run devOpen http://localhost:5173 — the dashboard begins listening immediately. No wallet connection required.
cast send 0xf586CdD8386e5692b8AB7ef04572700d69eE533C \
"setPrice(address,uint256)" \
0x0000000000000000000000000000000000000001 1500 \
--rpc-url https://api.infra.testnet.somnia.network \
--account somniaDeployercast send 0x7f75521779Ae4CDD3c5eC9fd33221B1E07073dfc \
"deposit(address,uint256,uint256)" \
0x0000000000000000000000000000000000000001 1000000000000000000 1 \
--rpc-url https://api.infra.testnet.somnia.network \
--account somniaDeployerWatch the REACTIVE_INTELLIGENCE feed update in real time — no page refresh, no polling. Events arrive within milliseconds of the transaction landing on-chain, pushed directly by the Somnia Reactivity Precompile at 0x0100.
┌──────────────────────────────────────────────────────────────┐
│ Somnia Validators │
│ ┌───────────────────────────────────────────┐ │
│ │ Reactivity Precompile (0x0100) │ │
│ │ • Manages subscriptions │ │
│ │ • Emits BlockTick(uint64) every block │ │
│ │ • Routes events to handler contracts │ │
│ └──────────┬───────────────┬────────────────┘ │
│ │ │ │
│ BlockTick callback Per-emitter callback │
│ │ │ │
│ ┌──────────▼───────────────▼────────────────┐ │
│ │ SentinelHandler (SomniaEventHandler) │ │
│ │ • _onEvent() routes by emitter address │ │
│ │ • _processPriceChecks() on BlockTick │ │
│ │ • Emits ReactiveActionProcessed │ │
│ └──────────────────────────────────────────┘│ │
│ │ │
│ ┌────────────────────────────────────────────┐ │
│ │ SentinelRegistry │ │
│ │ • Stores sentinel configs │ │
│ │ • Auto-calls subscribeToEmitter() on │ │
│ │ new sentinel registration │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
│ WSS Push
▼
┌──────────────────────────────────────────────────────────────┐
│ Frontend Dashboard │
│ • @somnia-chain/reactivity SDK (WebSocket) │
│ • @somnia-chain/viem-session-account (Nonceless TX) │
│ • Live on-chain stats: reactiveCallCount, priceAlerts │
│ • Real-time block number display │
└──────────────────────────────────────────────────────────────┘
Key architectural decisions:
SentinelHandlerinherits fromSomniaEventHandler— the base class from@somnia-chain/reactivity-contractsthat provides theonEvent()external function and routes to the internal_onEvent()override.- BlockTick vs Per-Emitter subscriptions — BlockTick subscribes to the precompile itself for periodic callbacks; per-emitter subscriptions monitor specific contract addresses for any event they emit. Both types are managed through the same precompile at
0x0100. - Registry → Handler auto-subscription — When
registerSentinel()is called, the registry automatically callshandler.subscribeToEmitter(target), removing any manual subscription step and ensuring every sentinel is immediately reactive.
| Type | Description | How Reactivity Enables It |
|---|---|---|
| PRICE_ALERT | Monitors asset prices against user-defined thresholds | Every block, the BlockTick subscription triggers _processPriceChecks() — no external keeper or bot required |
| BRIDGE_WATCH | Intercepts cross-chain bridge deposit events | Per-emitter subscription on the bridge contract pushes DepositInitiated events to the handler automatically |
| WALLET_WATCH | Monitors specific EOA transfer activity | Per-emitter subscription captures any event from the target address without polling |
| SYSTEM_HEALTH | Monitors system-level health metrics via the core Sentinel alerts | Direct alerts from SentinelCore trigger ReactiveActionProcessed through the precompile callback |
| Contract | Address | Purpose |
|---|---|---|
| SentinelRegistry | 0xeaf2c62c7486c10dac2a1afa31ebcb40759a6ed2 |
Source of truth for all sentinel configurations |
| SentinelHandler | 0xe95d0a5ec446bf84117961d0ae3ccd2452c451d1 |
Reactive handler invoked by the precompile |
| SentinelCore | 0x9FeD00Dc284464e66C996dF0fc3ee24e440ED660 |
Global alert emitter |
| MockPriceOracle | 0xf586CdD8386e5692b8AB7ef04572700d69eE533C |
Simulated price feed for testing |
| MockBridge | 0x7f75521779Ae4CDD3c5eC9fd33221B1E07073dfc |
Simulated bridge for testing |
cd contracts && npm install
forge testDeploy to Somnia Testnet:
forge script script/DeploySomnia.s.sol \
--rpc-url https://api.infra.testnet.somnia.network \
--broadcast \
--account somniaDeployer \
--gas-estimate-multiplier 200After deployment, fund the handler with ≥32 STT and activate the BlockTick subscription:
cast send <HANDLER_ADDRESS> "subscribeToBlockTick()" \
--rpc-url https://api.infra.testnet.somnia.network \
--account somniaDeployerPer-emitter subscriptions are created automatically when sentinels are registered through the registry.
cd frontend && npm install
npm run dev├── contracts/ # Solidity Smart Contracts
│ ├── src/ # Core: Sentinel, Registry, Handler
│ ├── test/ # Foundry test suite (13 tests)
│ └── script/ # Somnia deployment scripts
├── frontend/ # React + Vite Dashboard
│ └── src/ # Reactivity SDK & Viem integration
├── deployments.json # Live contract addresses
└── REACTIVITY_DEMO.md # Quick-start demo guide
- Access Control: Only authorized emitters can trigger global system alerts (
OnlyAuthorizedmodifier). - Input Validation: Action targets must be valid contracts (
InvalidActionTargetcustom error). - Custom Errors: All revert paths use gas-efficient custom errors instead of string reverts.
- Test Coverage: 13 Foundry tests covering authorization, registration, price alerts, bridge events, and access control edge cases.
- Signal Intensity Chart: Real-time Recharts visualization of incoming reactive signals
- Event Distribution: Breakdown of alert types (Price, Wallet, Bridge, System)
- On-Chain Stats: Live polling of
reactiveCallCount,priceAlertsProcessed, andblockTickSubId - Architecture Diagram: Visual flow showing Emitter → Precompile → Handler → Dashboard
- Block Number: Live block counter updating every second
- Copy-to-Clipboard: Interactive contract address pills in the navbar
- Session Management: One-click disposable session key generation for automated execution
Built with ❤️ for the Somnia Ecosystem.