ERC7824
Trivia Royale GuideCore Concepts

Application Sessions

Understanding sessions - the foundation of state channel applications

Application Sessions

An application session is an isolated instance of your game, contract, or application running off-chain. Think of it like a "room" where specific participants can interact with instant, gasless transactions.

What is a Session?

A session is:

  • A container for application-specific state and messages
  • Multi-party - can include 2 or more participants
  • Isolated - sessions don't interfere with each other
  • Temporary - created for a specific interaction, then closed
  • Off-chain - all activity happens via the ClearNode, not on-chain

Real-World Analogy

Think of a session like a poker table at a casino:

  • The table has specific players (participants)
  • Each player puts chips on the table (allocations)
  • Players interact following game rules (message handlers)
  • When the game ends, chips are distributed (final allocations)
  • The casino facilitates but doesn't play (ClearNode/broker)

Session Lifecycle

1. Prepare

The session initiator (usually a server) prepares the session request:

const  = .({
  : [, , ],
  : [
    { : , : 'USDC', : '0.01' },
    { : , : 'USDC', : '0.01' },
    { : , : 'USDC', : '0' },
  ],
});

2. Sign

All participants sign the request (can happen in parallel):

const sig1 = await client1.signSessionRequest(request);
const sig2 = await client2.signSessionRequest(request);
const sigServer = await serverClient.signSessionRequest(request);

3. Create

The initiator sends all signatures to ClearNode to create the session:

const  = await .(, [
  ,  // Server signature first
  ,       // Then players in allocation order
  ,
]);

4. Active & Messaging

Participants exchange typed messages within the session:

declare const sessionId: `0x${string}`;
// Server broadcasts a question
await serverClient.sendMessage(sessionId, 'question', {
  text: 'What is 2+2?',
  round: 1
});

// Players auto-respond via their handlers
onAppMessage: (type, sessionId, data) => {
  if (type === 'question') {
    await client.sendMessage(sessionId, 'answer', {
      answer: '4',
      from: player.address
    });
  }
}

5. Close & Settle

The initiator closes the session with final fund allocations:

await .closeSession(, [
closeSession: (sessionId: Hex, finalAllocations: Array<{
    participant: Address;
    asset: string;
    amount: string;
}>) => Promise<void>

Closes an active app session

@paramsessionId - The session to close@paramfinalAllocations - Final allocations for all participants
{ : , : 'USDC', : '0.015' }, // Winner gets 75% { : , : 'USDC', : '0.005' }, // Loser gets 25% { : , : 'USDC', : '0' }, ]);

Session Components

Participants

An array of Ethereum addresses that can interact in the session:

participants: [
  '0xPlayer1Address',
  '0xPlayer2Address',
  '0xServerAddress'
]

Important: Order matters for signature collection and allocations.

Allocations

Specifies how much each participant commits to the session:

allocations: [
  { participant: player1, asset: 'USDC', amount: '1.00' },  // Commits 1 USDC
  { participant: player2, asset: 'USDC', amount: '1.00' },  // Commits 1 USDC
  { participant: server, asset: 'USDC', amount: '0' },      // Commits nothing
]

Allocations represent:

  • Entry fees or stakes for games
  • Collateral for contracts
  • Zero for observers or facilitators (like a game server)

Weights & Quorum

Control who can make decisions:

const request = client.prepareSession({
  participants: [player1, player2, server],
  // Weights define voting power (must sum to quorum)
  weights: [0, 0, 100],        // Server has full control
  quorum: 100,                 // 100% needed to make decisions
  // ...
});

Common patterns:

PatternWeightsQuorumUse Case
Server-controlled[0, 0, 100]100Games with server authority
Equal peers[50, 50]100Peer-to-peer negotiations
Majority[33, 33, 34]51Multi-party voting

Nonce

A unique identifier to prevent replay attacks:

nonce: Date.now()  // Timestamp is common

Common Application Patterns

Now that you understand session components, let's explore different patterns for real-world applications. The weights and quorum determine who controls the session and how decisions are made.

Pattern 1: Server-Controlled Applications

Use when: A trusted server needs to make all decisions while players participate.

Typical participants: N players + 1 server/coordinator

Configuration:

participants: [player1, player2, player3, server],
weights: [0, 0, 0, 100],  // Only server can make decisions
quorum: 100,              // Server has 100% voting power

Real-world use cases:

  • Trivia games: Server asks questions, validates answers, distributes prizes
  • Casino games: House controls game logic, players place bets
  • Matchmaking: Coordinator pairs players and manages game sessions
  • Tournaments: Organizer controls brackets and progression

Example:

// Trivia game with 3 players
const  = .({
  : [, , , ],
  : [
    { : , : 'USDC', : '0.01' },
    { : , : 'USDC', : '0.01' },
    { : , : 'USDC', : '0.01' },
    { : , : 'USDC', : '0' },
  ],
  // Server has full control over session outcomes
  : [0, 0, 0, 100],
  : 100,
});

Pattern 2: Escrow & Marketplace

Use when: Two parties transact with a neutral mediator for dispute resolution.

Typical participants: Buyer + Seller + Mediator

Configuration:

participants: [buyer, seller, mediator],
weights: [33, 33, 34],    // Equal power, mediator breaks ties
quorum: 67,               // Requires 2 of 3 signatures

Real-world use cases:

  • E-commerce: Buyer pays, seller delivers, mediator resolves disputes
  • Freelance platforms: Client pays upfront, freelancer delivers work
  • P2P marketplaces: Secure item exchanges with escrow protection
  • Service agreements: Payment released when conditions met

Example:

// Escrow for a $100 purchase
const  = .({
  : [, , ],
  : [
    { : , : 'USDC', : '100' },    // Buyer pays upfront
    { : , : 'USDC', : '0' },     // Seller receives on delivery
    { : , : 'USDC', : '0' },   // Mediator doesn't stake
  ],
  // Requires 2 of 3 to close session (e.g., buyer + seller agree, or mediator decides)
  : [33, 33, 34],
  : 67,
});

// Later: Close with successful delivery
// await closeSession(sessionId, [
//   { participant: buyer, asset: 'USDC', amount: '0' },      // Buyer gets nothing back
//   { participant: seller, asset: 'USDC', amount: '100' },   // Seller receives payment
//   { participant: mediator, asset: 'USDC', amount: '0' },
// ]);

Pattern 3: Peer-to-Peer Contracts

Use when: Two equal parties negotiate without a third party.

Typical participants: Party A + Party B

Configuration:

participants: [partyA, partyB],
weights: [50, 50],        // Both parties equal
quorum: 100,              // Both must agree to close

Real-world use cases:

  • Bilateral trades: Swap assets with mutual consent
  • Agreements: Both parties must sign off on terms
  • Joint ventures: Equal partners making shared decisions
  • Splitting bills: Friends settling expenses fairly

Example:

// Two friends betting on a sports game
const  = .({
  : [, ],
  : [
    { : , : 'USDC', : '10' },
    { : , : 'USDC', : '10' },
  ],
  // Both must agree on the outcome
  : [50, 50],
  : 100,
});

// Later: Both agree on winner
// Requires both signatures to close

Pattern 4: Multi-Player Tournaments

Use when: Many players compete with a coordinator managing the event.

Typical participants: N players (4-100+) + 1 tournament coordinator

Configuration:

participants: [player1, player2, ..., playerN, coordinator],
weights: [0, 0, ..., 0, 100],  // Coordinator controls progression
quorum: 100,

Real-world use cases:

  • Battle royale: Large player count, elimination rounds
  • Poker tournaments: Multiple tables, blinds increase over time
  • Bracket competitions: March Madness style eliminations
  • Leaderboard games: Async competition with final rankings

Example:

// Tournament with 16 players, $5 entry fee
const  = '5';
const  = 16;

const  = .({
  : [..., ],
  : [
    ....( => ({
      : ,
      : 'USDC',
      : 
    })),
    { : , : 'USDC', : '0' },
  ],
  // Coordinator controls all game progression
  : [...().(0), 100],
  : 100,
});

// Prize pool: 16 × $5 = $80
// Distribution: 1st: $40, 2nd: $24, 3rd: $16

Pattern 5: Governance & Voting

Use when: Multiple stakeholders make decisions based on voting power.

Typical participants: Stakeholders with varying influence

Configuration:

participants: [admin, mod1, mod2, user1, user2],
weights: [40, 20, 20, 10, 10],  // Admin has most power
quorum: 60,                      // Requires 60% consensus

Real-world use cases:

  • DAOs: Token-weighted voting on proposals
  • Multi-sig wallets: M-of-N signature requirements
  • Committee decisions: Board members with different voting shares
  • Treasury management: Stakeholder approval for spending

Example:

// DAO treasury management session
const  = .({
  : [, , , , ],
  : [
    { : , : 'USDC', : '0' },
    { : , : 'USDC', : '0' },
    { : , : 'USDC', : '0' },
    { : , : 'USDC', : '0' },
    { : , : 'USDC', : '0' },
  ],
  // Admin: 40%, Mods: 20% each, Members: 10% each
  : [40, 20, 20, 10, 10],
  // Need 60% to pass (e.g., admin + one mod, or all 3 mods + 1 member)
  : 60,
});

// Examples of valid signatures to close:
// - Admin (40%) + Mod1 (20%) = 60% ✓
// - Mod1 (20%) + Mod2 (20%) + Member1 (10%) + Member2 (10%) = 60% ✓
// - Admin (40%) alone = 40% ✗

Choosing the Right Pattern

PatternControl ModelTypical ParticipantsBest For
Server-ControlledCentralizedPlayers + ServerGames, competitions, matchmaking
EscrowMediatedBuyer + Seller + MediatorMarketplaces, freelancing, trades
Peer-to-PeerEqual2 partiesBets, swaps, bilateral agreements
TournamentCentralizedMany players + CoordinatorLarge competitions, brackets
GovernanceWeighted votingMultiple stakeholdersDAOs, multi-sig, committees

Pro tip: You can combine patterns! For example:

  • Escrow with governance (community votes on disputes)
  • Tournament with peer-to-peer (players negotiate side bets)
  • Server-controlled with governance (players vote on rule changes)

Session IDs

When a session is created, you receive a unique session ID:

const sessionId = await client.createSession(request, signatures);
// sessionId: "0x8f3b2c1d..."

This ID is used for all subsequent operations:

  • Sending messages: client.sendMessage(sessionId, ...)
  • Closing the session: client.closeSession(sessionId, ...)
  • Tracking active sessions: client.getActiveSessions() → includes this ID

Active Session Management

The BetterNitroliteClient automatically tracks active sessions:

// After creating or joining
const activeSessions = .();
const activeSessions: `0x${string}`[]
// → ['0x8f3b2c1d...', '0x7a2b3c4d...'] // After closing await .closeSession(, );
closeSession: (sessionId: Hex, finalAllocations: Array<{
    participant: Address;
    asset: string;
    amount: string;
}>) => Promise<void>

Closes an active app session

@paramsessionId - The session to close@paramfinalAllocations - Final allocations for all participants
// → Session removed from active list

Message Broadcasting

When you send a message to a session, it broadcasts to all participants:

Important: You also receive your own messages! Filter in your handler:

onAppMessage: (type, sessionId, data) => {
  // Server sent 'start_game', all players receive it
  if (type === 'start_game') {
    console.log('Game started!');
  }

  // Players send 'answer', server wants to collect them
  if (type === 'answer') {
    // Make sure we're not processing our own answer
    if (data.from !== myAddress) {
      collectAnswer(data);
    }
  }
}

Session Allowances

When connecting to ClearNode, you can specify a session allowance - the maximum amount you're willing to commit to any single session:

const client = createBetterNitroliteClient({
  wallet,
  sessionAllowance: '0.01',  // Max 0.01 USDC per session
  // ...
});

This prevents:

  • Accidentally joining expensive sessions
  • Malicious sessions draining your channel
  • Overspending in games

When someone tries to create a session with you requiring more than your allowance, you won't auto-join.

Session Closure & Settlement

When closing a session, you must specify final allocations for all participants:

await client.closeSession(sessionId, [
  { participant: winner, asset: 'USDC', amount: '1.80' },   // Winner gets 90%
  { participant: loser, asset: 'USDC', amount: '0.20' },    // Loser gets 10%
  { participant: server, asset: 'USDC', amount: '0' },
]);

Critical: The sum of final allocations should equal the sum of initial allocations (fund conservation):

// Initial allocations
// player1: 1.00, player2: 1.00, server: 0 → Total: 2.00

// Final allocations must also sum to 2.00
// winner: 1.80, loser: 0.20, server: 0 → Total: 2.00 ✓

After closure:

  • Funds are returned to participants' ledger balances
  • Session is removed from active session list
  • No more messages can be sent to this session

Example: Complete Session Flow

// Server creates a simple 2-player game
const  = ({ :  });
await .();

// 1. Prepare session
const request = .({
const request: NitroliteRPCMessage
: [., ., .], : [ { : ., : 'USDC', : '1' }, { : ., : 'USDC', : '1' }, { : ., : 'USDC', : '0' }, ], }); // 2. Collect signatures (in real app, via HTTP/WebSocket) const [, , ] = await .([ .(), .(), .(), ]); // 3. Create session const sessionId = await .(, [, , ]);
const sessionId: `0x${string}`
// 4. Play the game await .sendMessage(, 'start', { : .() });
sendMessage: <"start">(sessionId: Hex, type: "start", data: any) => Promise<void>

Sends a message within an active session

@paramsessionId - The session to send the message to@paramtype - Message type@paramdata - Message data
// ... game logic happens via message handlers ... // 5. Close with results await .closeSession(, [
closeSession: (sessionId: Hex, finalAllocations: Array<{
    participant: Address;
    asset: string;
    amount: string;
}>) => Promise<void>

Closes an active app session

@paramsessionId - The session to close@paramfinalAllocations - Final allocations for all participants
{ : ., : 'USDC', : '1.5' }, // Player 1 won { : ., : 'USDC', : '0.5' }, // Player 2 lost { : ., : 'USDC', : '0' }, ]); .('Session closed, prizes distributed to ledger balances');

Next Steps

Now that you understand sessions: