ERC7824
Trivia Royale GuideCore Concepts

Message Flow

How clients exchange messages in real-time through the ClearNode broker

Message Flow

All real-time communication in Trivia Royale happens through typed messages broadcast via the ClearNode broker. This guide shows you how to define, send, and handle messages using the actual trivia game as an example.

The Pattern

// 1. Define your message types
interface TriviaGameSchema extends MessageSchema {
  question: { data: { text: string; round: number } };
  answer: { data: { answer: string; from: Address; timestamp: number } };
}

// 2. Handle incoming messages
onAppMessage: async (type, sessionId, data) => {
  if (type === 'question') {
    // Respond to question
    await client.sendMessage(sessionId, 'answer', { ... });
  }
}

// 3. Send messages
await client.sendMessage(sessionId, 'question', {
  text: 'What is 2+2?',
  round: 1,
});

Key principle: Messages are broadcast to all participants including the sender. This means:

  • Server sends question → Server, Player 1, Player 2, Player 3 all receive it
  • Player 1 sends answer → Server, Player 1, Player 2, Player 3 all receive it

Step 1: Define Your Message Schema

Use TypeScript to define all message types your game will use:

import type { MessageSchema } from '@trivia-royale/game';
import type {  } from 'viem';

interface TriviaGameSchema extends MessageSchema {
  : {
    : { : number; : string };
  };
  : {
    : { : string; : number };
  };
  : {
    : { : string; : number; : ; : number };
  };
  : {
    : { : ; : string; : number };
  };
  : {
    : { : ; : <string, number> };
  };
}

TypeScript automatically narrows the data type based on the message type in your handlers.

Step 2: Handle Incoming Messages

Set up onAppMessage to receive and process messages:

const  = <TriviaGameSchema>({
  ,
  : async (, , ) => {
    if ( === 'question') {
      // TypeScript knows data is { text: string; round: number }
      .(`Question ${.round}: ${.text}`);

      // Respond with answer
      await .(, 'answer', {
        : (.text),
        : .round,
        : .,
        : .(),
      });
    }

    if ( === 'answer') {
      // Only process answers from OTHER players
      if (.from !== .) {
        .(`${.from} answered: ${.answer}`);
      }
    }
  },
});

Pattern: Check message type → Process data → Optionally send response

When to filter: Since you receive your own messages, filter them out when collecting responses from others (like answer messages). Don't filter when everyone should react the same way (like game_start).

Step 3: Send Messages

Use sendMessage() to broadcast to all session participants:

// Server broadcasts question to all players
await .(, 'question', {
  : 'What is the capital of France?',
  : 2,
});

This broadcasts to:

  • All other participants in the session
  • Yourself (arrives in your onAppMessage)

Real Example: Trivia Game Round

Here's how a complete round flows in the trivia game (view source):

// Server broadcasts question
await serverClient.sendMessage(sessionId, 'question', {
  text: 'What is 2+2?',
  round: 1,
});

// All players receive it and auto-respond via their handlers:
// Player 1's handler:
onAppMessage: async (type, sessionId, data) => {
  if (type === 'question') {
    await client.sendMessage(sessionId, 'answer', {
      answer: '4',
      round: data.round,
      from: player1.address,
      timestamp: Date.now(),
    });
  }
}

// Server receives all answers and determines winner
onAppMessage: async (type, sessionId, data) => {
  if (type === 'answer') {
    answerSubmissions.push(data);

    if (answerSubmissions.length === totalPlayers) {
      // Find fastest correct answer
      const correctAnswers = answerSubmissions
        .filter(a => a.answer === '4')
        .sort((a, b) => a.timestamp - b.timestamp);

      const winner = correctAnswers[0].from;

      // Announce winner
      await serverClient.sendMessage(sessionId, 'round_result', {
        winner,
        correctAnswer: '4',
        round: 1,
      });
    }
  }
}

Flow:

  1. Server: broadcasts question → Everyone receives it (including server)
  2. Players: each sends answer → Everyone receives all answers (including the player who sent it)
  3. Server: collects answers, determines winner, broadcasts round_result → Everyone receives it

Total messages: 1 question + 3 answers + 1 result = 5 messages, all broadcast to all 4 participants

Important Details

Always Include Sender Address

Since messages broadcast to everyone, include the sender so recipients can identify who sent it:

interface AnswerMessage {
  data: {
    answer: string;
    from: Address;  // ← Always include!
  };
}

Add Timestamps for Timing

When speed matters (like trivia), include timestamps:

interface AnswerMessage {
  data: {
    answer: string;
    from: Address;
    timestamp: number;  // ← Enables "fastest correct answer" logic
  };
}

Then sort by timestamp to determine order:

const sorted = answers.sort((a, b) => a.timestamp - b.timestamp);
const fastest = sorted[0];

Message Ordering

Per-sender ordering is guaranteed:

// Server sends 3 questions
await server.sendMessage(sessionId, 'question', { round: 1, ... });
await server.sendMessage(sessionId, 'question', { round: 2, ... });
await server.sendMessage(sessionId, 'question', { round: 3, ... });

// All players receive them in order: round 1, 2, 3 ✓

Cross-sender ordering is NOT guaranteed:

// Player 1 sends answer at 10:00:00.100
await player1.sendMessage(sessionId, 'answer', { ... });

// Player 2 sends answer at 10:00:00.080 (earlier!)
await player2.sendMessage(sessionId, 'answer', { ... });

// Server might receive Player 2's first, even though it was sent later

Solution: Use timestamps to determine actual order when it matters.

Next Steps

Now that you understand message flow: