ERC7824
Trivia Royale GuideCore Concepts

The Balance Model

Understanding the 4-layer balance system in Yellow SDK

The Balance Model

The single most important concept to understand when building with Yellow SDK is the 4-layer balance system. This is the architecture that enables instant, gasless transactions while maintaining security.

The Four Balance Types

Your funds exist in four distinct locations, each serving a specific purpose:

Why Four Layers?

This might seem complex at first, but each layer serves a critical purpose:

Layer 1: Wallet (Your Standard Balance)

  • What it is: Normal ERC-20 tokens in your Ethereum address
  • When you use it: This is where you start. All funds begin here.
  • Limitations: Every transaction costs gas and requires block confirmation

Layer 2: Custody Contract (On-Chain Escrow)

  • What it is: Funds locked in Yellow's smart contract
  • When you use it: Intermediate step between wallet and channel
  • Why it exists: Provides on-chain security while preparing for off-chain operations

Layer 3: Channel (State Channel with Broker)

  • What it is: A bilateral state channel between you and Yellow's broker
  • Operations: Open channel, resize (add/remove funds), close channel
  • Why it exists: Provides capacity that backs your off-chain operations (ledger balances and sessions)

Layer 4: Ledger (Off-Chain Net Balance)

  • What it is: Your net position tracked by ClearNode (can go negative up to channel capacity)
  • Operations: Send/receive (p2p transfers), sessions (allocate from channel to session)
  • Why it exists: Enables instant, gasless value transfer without touching on-chain state

Checking All Balances

The getBalances() method returns all four balance types:

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

declare const : ;

const balances = await .();
const balances: {
    custodyContract: bigint;
    channel: bigint;
    ledger: bigint;
    wallet: bigint;
}

Fund Flow Topology

Understanding how funds move between layers is critical:

Forward Flow (Getting Funds Ready to Use)

Example: Preparing 10 USDC for a game

// Step 1: Start with wallet balance
const  = await .();
// { wallet: 100, custody: 0, channel: 0, ledger: 0 }

// Step 2: Deposit moves: wallet → custody → channel
await .deposit(('10'));
deposit: (amount: bigint) => Promise<void>

Deposits funds from the wallet to the channel directly.. if there is balance in the custody contract, it will be also used

@paramamount - amount to deposit
const = await .(); // { wallet: 90, custody: 0, channel: 10, ledger: 0 } // Note: deposit() does BOTH wallet→custody AND custody→channel

Reverse Flow (Withdrawing Funds)

Example: Withdrawing all funds

const  = await .();
// { wallet: 90, custody: 0, channel: 8, ledger: 2 }

const totalAvailable = . + . + .;
const totalAvailable: bigint
// Total we can withdraw: 10 USDC await .withdraw();
withdraw: (amount: bigint) => Promise<void>

Withdraws funds from the custody OR ledger OR channel to the wallet, depending on the amount requested

@paramamount - amount to withdraw
// This does MULTIPLE operations automatically: // 1. Deallocates ledger → channel (if ledger > 0) // 2. Resizes channel → custody (drains channel) // 3. Closes channel (if completely drained) // 4. Withdraws custody → wallet const = await .(); // { wallet: 100, custody: 0, channel: 0, ledger: 0 }

Common Balance Patterns

Pattern 1: Depositing for the First Time

// You have: { wallet: 100, custody: 0, channel: 0, ledger: 0 }
await .(('10'));
// You now have: { wallet: 90, custody: 0, channel: 10, ledger: 0 }

// The deposit() method:
// 1. Approves custody contract to spend USDC
// 2. Calls custody.deposit() → wallet to custody
// 3. Opens OR resizes channel → custody to channel

Pattern 2: Adding More Funds to Existing Channel

// You have: { wallet: 90, custody: 0, channel: 10, ledger: 0 }
await client.deposit(parseUSDC('5'));
// You now have: { wallet: 85, custody: 0, channel: 15, ledger: 0 }

// This time deposit():
// 1. wallet → custody (5 USDC)
// 2. Resizes existing channel with +5 USDC
// 3. custody → channel

Pattern 3: Sending Peer-to-Peer Payment

// You have: { wallet: 90, custody: 0, channel: 10, ledger: 0 }
await .send({ : , : ('1') });
send: (props: {
    to: Address;
    amount: bigint;
}) => Promise<void>

Sends funds offchain from the wallet to another address

@paramprops - props to send funds
// After send, ledger shows -1, but channel unchanged: // { wallet: 90, custody: 0, channel: 10, ledger: -1 } // The recipient's ledger shows +1: // { ledger: 1 } // Important: Ledger can go negative up to your channel capacity! // Your channel (10) backs the ledger balance.

Pattern 4: Partial Withdrawal

// You have: { wallet: 85, custody: 0, channel: 15, ledger: -1 }
await client.withdraw(parseUSDC('5'));

// withdraw() is smart:
// 1. Checks total available: channel(15) + ledger(-1) + custody(0) = 14
// 2. Deallocates ledger → channel: ledger -1 → 0, channel 15 → 14
// 3. Resizes channel by -5: channel 14 → 9, custody 0 → 5
// 4. Withdraws custody: custody 5 → 0, wallet 85 → 90

// Result: { wallet: 90, custody: 0, channel: 9, ledger: 0 }

Key Insights

1. Ledger is Net Balance

Your ledger balance is a net position across all your off-chain activity:

  • Receiving payments increases it (can go to +infinity)
  • Sending payments decreases it (can go negative!)
  • Negative limit = your channel capacity

Understanding negative ledger balances:

// You have a channel with 10 USDC
const balances = { : 10n, : 0n };
const balances: {
    channel: bigint;
    ledger: bigint;
}
// You can send up to 10 USDC (making ledger -10) await .send({ : , : ('7') });
send: (props: {
    to: Address;
    amount: bigint;
}) => Promise<void>

Sends funds offchain from the wallet to another address

@paramprops - props to send funds
// → { channel: 10, ledger: -7 } ✓ Valid // You cannot send more than your channel capacity await .({ : , : ('5') }); // → Would result in { channel: 10, ledger: -12 } ✗ FAILS // Error: Would exceed channel capacity // The invariant: |ledger| ≤ channel // In other words: Math.abs(ledger) <= channel

Why can ledger go negative?

  • Your channel backs your debts
  • Negative ledger = you owe to the network
  • Positive ledger = network owes you
  • Channel capacity secures both directions

2. Channel is Capacity

Your channel balance represents available capacity for off-chain operations:

  • Must be ≥ absolute value of your negative ledger balance
  • Limits how much you can send without rebalancing

3. Total Available = Channel + Ledger + Custody

When withdrawing, all three on-chain/off-chain balances are accessible:

const totalAvailable =
  balances.channel +
  balances.ledger +
  balances.custodyContract;

4. Automatic Operations

The BetterNitroliteClient handles complexity for you:

  • deposit() manages wallet → custody → channel (opens or resizes channel)
  • withdraw() manages ledger → channel → custody → wallet (settles ledger and resizes channel)
  • send() / receive() updates ledger balances off-chain (p2p transfers)
  • Sessions allocate capacity from channel (not from ledger)

Visualizing a Complete Flow

Let's trace funds through a realistic scenario:

// Starting state
// { wallet: 100, custody: 0, channel: 0, ledger: 0 }

// 1. Deposit 10 USDC to play a game
await .deposit(('10'));
deposit: (amount: bigint) => Promise<void>

Deposits funds from the wallet to the channel directly.. if there is balance in the custody contract, it will be also used

@paramamount - amount to deposit
// { wallet: 90, custody: 0, channel: 10, ledger: 0 } // 2. Send 3 USDC to another player await .send({ : , : ('3') });
send: (props: {
    to: Address;
    amount: bigint;
}) => Promise<void>

Sends funds offchain from the wallet to another address

@paramprops - props to send funds
// { wallet: 90, custody: 0, channel: 10, ledger: -3 } // ↑ negative! // 3. Receive 5 USDC from a different player // (They call send() to you, ClearNode updates your ledger) // { wallet: 90, custody: 0, channel: 10, ledger: 2 } // ↑ net position // 4. Withdraw everything const total = . + .; // 10 + 2 = 12
const total: bigint
await .withdraw();
withdraw: (amount: bigint) => Promise<void>

Withdraws funds from the custody OR ledger OR channel to the wallet, depending on the amount requested

@paramamount - amount to withdraw
// { wallet: 102, custody: 0, channel: 0, ledger: 0 } // ↑ gained 2 USDC!

Next Steps

Now that you understand the balance model:

Further Reading