Build a Realtime On-Chain Game

Build a Tap-to-Play Onchain Game

This tutorial shows how to build a Tap-to-Play Onchain Game using Somnia Data Streams, where every player’s tap is written directly to the blockchain and the leaderboard updates in realtime.

Each tap is stored onchain as a structured data record following a schema. The game uses MetaMask for wallet identity and Somnia Streams SDK to:

  • Store tap events onchain using sdk.streams.set()

  • Retrieve and rank all players from onchain data

By the end of this guide, you’ll have: - A working Next.js app - Onchain data storage using Somnia Data Streams - A live leaderboard that reads blockchain state - MetaMask integration for identity and transaction signing


Prerequisites

  • Node.js 20+

  • A funded Somnia Testnet wallet. Kindly get some from the Faucetarrow-up-right

  • Basic familiarity with TypeScript and Next.js


Project Setup

Initialize a new Next.js app and install dependencies. Create the app by creating a directory where the app will live

npx create-next-app@latest somnia-chat --ts --app --no-tailwind
cd somnia-chat

Install the Somnia Streamsarrow-up-right and ViemJS dependencies

Create a .env.local file for storing secrets and environmental variables

triangle-exclamation

Define Tap Schema

Create a file lib/schema.ts:

This schema defines the structure of each tap event:

  • timestamp: when the tap occurred

  • player: who tapped (wallet address)

  • A nonce will be added when the schema is deployed to ensures each record is unique

The Schema ID will be automatically computed by the SDK from this schema.


Setup Clients

Create lib/serverClient.ts for server-side reads:

Create lib/clients.ts for client-side access:


Writing Tap Data Onchain

Each tap is recorded onchain with the sdk.streams.set() method. In this section, we’ll walk through how each part of the sendTap() logic works from wallet connection to writing structured schema data onchain.


Set up state variables

We’ll start by tracking a number of states, such as:

  • the connected wallet address

  • the wallet client (MetaMask connection)

  • and a few helper states for loading, cooldowns, and errors.

These ensure that you can access the connected wallet address (address) and track transaction state (pending). It also prevents spam taps with a 1-second cooldown (cooldownMs)


Connect MetaMask

We use the browser’s window.ethereum API to connect to MetaMask. Once connected, we create a wallet client that Somnia’s SDK can use for signing transactions.

createWalletClient from Viem wraps MetaMask into a signer object that the Somnia SDK can use. This is how the UI and the blockchain are bridged securely.


Initialize the SDK

The Somnia Data Streams SDK provides methods to compute schema IDs, encode structured data, and publish to the blockchain. We initialize it with both the public client (for chain access) and the wallet client (for signing transactions).

This gives you full read/write access to the Somnia Streams contract on Somnia Testnet.


Compute the Schema ID

Schemas define how the onchain data is structured. In this case, the tap schema looks like this:

Before writing data, we must compute its unique Schema ID:

This produces a deterministic ID derived from the schema text, ensuring that any app using the same schema can read or decode your data.


Register the Schema

If this schema wasn’t registered yet, we register it once. It’s safe to call this before sending the first message.

Encode the Data

Somnia Streams stores structured data using its SchemaEncoder class. We create an encoder and provide each field according to the schema definition.

This converts your JavaScript values into the precise binary format that can be stored onchain and later decoded.


Generate a Unique Data ID

Each record needs a unique identifier within the schema. We use the keccak256 hash of the player’s address and timestamp to ensure that it is packed into 32 bits of data.

This ensures no two taps collide, even if the same player taps rapidly.


Store the Tap Onchain

Finally, we push the structured data to the blockchain using:

The set() method writes one or more records (called Data Streams) to the chain. Each record is cryptographically signed by the player’s wallet, and gets stored on Somnia’s decentralized data infrastructure. It can also be retrieved instantly using the same schema


Manage Cooldowns and Feedback

After the tap is sent, we apply a 1-second cooldown to avoid flooding transactions and reset the pending state.

This gives players a smooth UX while maintaining blockchain transaction integrity.


Putting It All Together

Here’s the complete sendTap() method with all steps combined:

Complete `page.tsx` Code

chevron-rightpage.tsxhashtag

Reading Leaderboard Data Onchain

The leaderboard is calculated server-side by reading all tap data stored onchain. Create a lib/store.ts file and add the following code:

The leaderboard logic begins inside the getLeaderboard() function, where we use the SDK to read structured tap data directly from the blockchain. First, the function initializes the SDK with a server-compatible public client, which allows read-only access to the chain without a connected wallet. The next step computes the schemaId by passing our tapSchema to sdk.streams.computeSchemaId(). This produces a deterministic identifier that ensures we’re always referencing the correct data structure.

Once the schemaId is known, the core operation happens through sdk.streams.getAllPublisherDataForSchema(schemaId, publisher). This method queries the blockchain for all records written by the specified publisher under that schema. Each returned record is an array of fields that align with the schema’s definition, in this case [timestamp, player]. The helper function val() is then used to unwrap nested field values (f?.value?.value) from the SDK’s response format, giving us clean, readable values.

getAllPublisherDataForSchema acts like a decentralized “SELECT * FROM” query, fetching all onchain data tied to a schema and publisher, while the rest of the function transforms that raw blockchain data into a structured leaderboard the app can display.


Creat a api route to retrieve Leaderboard score. Create the file app/api/leaderboard/route.ts

This endpoint imports the getLeaderboard() function from lib/store.ts, which handles the heavy lifting of querying Somnia Data Streams, and then exposes that onchain data as a clean, JSON-formatted response for your application. The client simply fetches the leaderboard via /api/leaderboard.

The page.tsx fetches /api/leaderboard every few seconds to stay updated.


Every tap executes a real blockchain transaction:

Field

Description

timestamp

Time of the tap

player

Wallet address of the player

When the set() call succeeds, Somnia Data Streams stores the record and indexes it under your publisher’s address. Any application (including yours) can then read this data and build dashboards, analytics, or game leaderboards.


Run the App

Open http://localhost:3000arrow-up-right and connect your MetaMask wallet. Click 🖱️ Tap to send onchain transactions, and watch your leaderboard update live.


Conclusion

You’ve built a fully onchain game where player interactions are stored via Somnia Data Streams and leaderboard rankings are derived from immutable blockchain data. MetaMask provides secure, user-friendly authentication. This same pattern powers realtime Web3 experiences, from social apps to competitive games, using Somnia’s high-performance onchain data infrastructure.

Last updated