ERC7824
Trivia Royale GuideBuilding Blocks

Fund Management

Complete deposit, withdraw, and resize flows with practical examples

Fund Management

Fund management is one of the most important - and initially confusing - aspects of Yellow SDK. This guide walks through every operation with diagrams, code examples, and balance checks at each step.

Overview

Fund management involves moving value through the 4-layer balance system:

WALLET ←→ CUSTODY ←→ CHANNEL ←→ LEDGER

The BetterNitroliteClient provides three high-level methods:

  • deposit(amount) - Move funds from wallet into the channel
  • withdraw(amount) - Move funds from channel back to wallet
  • send({ to, amount }) - Transfer value via ledger (peer-to-peer)

Under the hood, these use low-level resize and allocate operations.

Deposit Flow

Implementation: client.ts:597-699 (deposit)

First Deposit (No Channel Exists)

When you have no channel, deposit() creates one:

Code path: client.ts:607-639 (channel creation branch)

// Starting state
const  = await .();
// { wallet: 100, custody: 0, channel: 0, ledger: 0 }

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 }

What happened:

  1. Approve: ERC-20 approval for custody contract to spend USDC
  2. Deposit: Transfer from wallet → custody contract
  3. Create Channel: On-chain transaction creates channel, moves custody → channel

Under the hood:

// Check if channel exists
const channelId = await getChannelWithBroker(ws, wallet, BROKER_ADDRESS);

if (!channelId) {
  // No channel - must create one
  // Note: Custody funds can't be used for initial channel creation
  if (balances.wallet < amount) {
    throw new Error('Insufficient wallet balance for channel creation');
  }

  // This calls createChannelViaRPC which:
  // 1. Approves custody contract
  // 2. Calls depositAndCreateChannel()
  // 3. Waits for ChannelCreated event
  const newChannelId = await createChannelViaRPC(ws, wallet, formatUSDC(amount));
  console.log(`Channel created: ${newChannelId}`);
}

Why wallet balance only? The initial depositAndCreateChannel() transaction atomically deposits and creates in one step. Custody funds from previous operations can't be used here - they require the channel to already exist for resize operations.

Subsequent Deposits (Channel Exists)

When you already have a channel, deposit() resizes it:

Code path: client.ts:643-698 (resize branch)

// Starting state (channel exists)
const  = await .();
// { wallet: 90, custody: 0, channel: 10, ledger: 0 }

await .deposit(('5'));
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: 85, custody: 0, channel: 15, ledger: 0 }

What happened:

  1. Deposit: Transfer from wallet → custody (5 USDC)
  2. Resize: Channel resized +5 (from 10 to 15), custody drained back to 0

Under the hood (custody optimization):

// Channel exists - resize it
let remainingToDeposit = amount;
let custodyToUse = 0n;
let walletToUse = 0n;

// Step 1: Use existing custody funds first (if any)
if (balances.custodyContract > 0n) {
  custodyToUse = balances.custodyContract >= remainingToDeposit
    ? remainingToDeposit
    : balances.custodyContract;
  remainingToDeposit -= custodyToUse;

  console.log(`Using ${formatUSDC(custodyToUse)} from custody balance`);
}

// Step 2: Pull from wallet for remaining amount
if (remainingToDeposit > 0n) {
  if (balances.wallet < remainingToDeposit) {
    throw new Error(`Insufficient wallet balance. Need ${formatUSDC(remainingToDeposit)} more`);
  }

  walletToUse = remainingToDeposit;

  // Approve and deposit to custody
  await ensureAllowance(wallet, CUSTODY_CONTRACT, walletToUse);
  const depositTxHash = await client.deposit(tokenAddress, walletToUse);
  await client.publicClient.waitForTransactionReceipt({ hash: depositTxHash });

  console.log(`Deposited ${formatUSDC(walletToUse)} from wallet to custody`);
}

// Step 3: Resize channel to include all custody funds
const totalResizeAmount = custodyToUse + walletToUse;
if (totalResizeAmount > 0n) {
  await resizeChannelWithCustodyFunds(channelId, totalResizeAmount);
  console.log(`Resized channel +${formatUSDC(totalResizeAmount)}`);
}

Custody optimization: If you previously withdrew funds that left a custody balance, deposit() automatically uses those funds first before touching your wallet. This saves gas by avoiding unnecessary ERC-20 transfers.

Example:

// You withdrew 7 from a 10 channel
// Result: wallet: 107, custody: 0, channel: 3, ledger: 0

// Later you deposit 10
// Expected: Use 10 from wallet
// Actual result: wallet: 97, custody: 0, channel: 13

// If you had leftover custody balance:
// Before: wallet: 100, custody: 3, channel: 10
// deposit(10): Uses 3 from custody + 7 from wallet
// After: wallet: 93, custody: 0, channel: 20

How Channel Creation Works (Low-Level)

When you call deposit() for the first time, the BetterNitroliteClient creates a state channel with ClearNode as the broker. Here's what happens under the hood:

The Flow

Implementation: createChannelViaRPC in rpc/channels.ts

Step-by-Step Breakdown

Step 1: Approve ERC-20 Allowance

// Ensure custody contract can spend USDC
await ensureAllowance(wallet, CUSTODY_CONTRACT, amountWei);

This is a standard ERC-20 approval allowing the custody contract to transfer tokens on your behalf.

Step 2: Create RPC Request

const params: CreateChannelRequestParams = {
  chain_id: SEPOLIA_CONFIG.chainId,
  token: SEPOLIA_CONFIG.contracts.tokenAddress,
  amount: amountWei,
  session_key: wallet.sessionSigner.address  // ← Critical for channel ownership
};

const message = await createCreateChannelMessage(
  wallet.sessionSigner.sign,
  params
);
ws.send(message);

The session key is crucial - it determines who can sign future channel operations (resize, close). If you lose this key, you lose control of the channel.

Step 3: ClearNode Signs and Returns

ClearNode validates your request and returns:

  • channel: Channel parameters (participants, chain ID, challenge duration)
  • state: Initial state with allocations
  • serverSignature: Broker's signature on the initial state
const response = parseCreateChannelResponse(event.data);
const { channel, state, serverSignature } = response.params;

Step 4: Submit On-Chain Transaction

const { channelId, txHash, initialState } = await nitroliteClient.depositAndCreateChannel(
  tokenAddress,
  amountWei,
  {
    channel: convertRPCToClientChannel(channel),
    unsignedInitialState: convertRPCToClientState(state, serverSignature),
    serverSignature,
  }
);

This calls the custody contract's depositAndCreate() function, which:

  1. Transfers USDC from your wallet to custody
  2. Creates the channel on-chain
  3. Sets initial allocations
  4. Emits a ChannelCreated event

Step 5: Wait for Confirmation

await publicClient.waitForTransactionReceipt({ hash: txHash });

The transaction must be mined before the channel is usable.

Step 6: ClearNode Updates Ledger

ClearNode watches for the ChannelCreated event and automatically:

  • Registers the channel in its database
  • Creates ledger balances for both participants (you + broker)
  • Makes the channel available for app sessions

Channel State Structure

Every channel has a state with these fields:

{
  channelId: Hex,              // Unique channel identifier
  intent: Hex,                 // Contract address (adjudicator)
  version: bigint,             // Starts at 0, increments on each update
  data: Hex,                   // Encoded state data
  allocations: [               // How funds are distributed
    {
      destination: Address,    // Participant 1 (you)
      amount: bigint,         // Your balance
      allocationType: 0,
      metadata: Hex
    },
    {
      destination: Address,    // Participant 2 (broker)
      amount: bigint,         // Broker balance (usually 0)
      allocationType: 0,
      metadata: Hex
    }
  ],
  serverSignature: Hex        // Broker's signature
}

The version field is critical - it prevents replay attacks by ensuring only newer states are accepted.

Session Key Ownership

The session key you use to create the channel must be used for all future operations:

// ✓ Good: Same session key
const wallet = createWallet({ sessionKeyManager: keyManager });
await client.deposit(parseUSDC('10'));  // Creates channel
await client.deposit(parseUSDC('5'));   // Resizes (works!)

// ✗ Bad: Different session key
const wallet2 = createWallet({ sessionKeyManager: differentKeyManager });
await client2.deposit(parseUSDC('5'));  // Error: Can't resize other's channel

Always persist your session keys using createFileSystemKeyManager() or createLocalStorageKeyManager().

How Channel Resizing Works (Low-Level)

When a channel already exists, deposit() and withdraw() operations resize it by moving funds between custody and channel. This is a two-party signed operation between you and ClearNode.

The Resize Flow

Implementation: client.ts:374-441 (resizeChannelWithCustodyFunds)

Understanding resize_amount vs allocate_amount

Resize operations have two parameters that control fund movement:

{
  resize_amount: bigint,    // Custody ↔ Channel
  allocate_amount: bigint   // Channel ↔ Ledger
}

resize_amount moves funds between custody and channel:

  • Positive: Custody → Channel (adding funds)
  • Negative: Channel → Custody (removing funds)
  • Zero: No custody ↔ channel movement

allocate_amount moves funds between channel and ledger:

  • Positive: Channel → Ledger (allocating for app sessions)
  • Negative: Ledger → Channel (deallocating from sessions)
  • Zero: No channel ↔ ledger movement

Example: Deposit (Resize +5 USDC)

// Starting: custody = 5, channel = 10
const message = await createResizeChannelMessage(wallet.sessionSigner.sign, {
  channel_id: channelId,
  resize_amount: 5n * 10n**6n,   // +5 USDC: custody → channel
  allocate_amount: 0n,            // No ledger movement
  funds_destination: wallet.address,
});

ws.send(message);
// Ending: custody = 0, channel = 15

Example: Withdraw (Resize -5 USDC)

// Starting: custody = 0, channel = 15
const message = await createResizeChannelMessage(wallet.sessionSigner.sign, {
  channel_id: channelId,
  resize_amount: -5n * 10n**6n,  // -5 USDC: channel → custody
  allocate_amount: 0n,            // No ledger movement
  funds_destination: wallet.address,
});

ws.send(message);
// Ending: custody = 5, channel = 10

Example: Deallocate Ledger Balance

// Starting: channel = 10, ledger = 5
const message = await createResizeChannelMessage(wallet.sessionSigner.sign, {
  channel_id: channelId,
  resize_amount: 0n,              // No custody movement
  allocate_amount: -5n * 10n**6n, // -5 USDC: ledger → channel
  funds_destination: wallet.address,
});

ws.send(message);
// Ending: channel = 15, ledger = 0

Proof States: Why Only lastValidState?

Every resize requires proof states - evidence that the new state is valid. Unlike other channel protocols that require full history, Yellow SDK only needs the most recent on-chain state:

const channelData = await client.getChannelData(channelId);
const proofStates = [channelData.lastValidState];  // Just one state!

await client.resizeChannel({
  resizeState: newState,
  proofStates,
});

This works because:

  1. Brokerless trust model: ClearNode signs all state transitions, so disputes are rare
  2. Version incrementing: Each state has a version number that must increase
  3. On-chain finality: The contract only accepts states with valid signatures and higher versions

The contract verifies:

  • ✓ New version > last on-chain version
  • ✓ Both signatures are valid (wallet + broker)
  • ✓ State hash matches signed data

State Version Management

Every channel operation increments the version:

// Initial state after creation
{ version: 0n, allocations: [{ amount: 10n }] }

// After first resize (+5)
{ version: 1n, allocations: [{ amount: 15n }] }

// After second resize (-3)
{ version: 2n, allocations: [{ amount: 12n }] }

// After close
{ version: 3n, allocations: [{ amount: 0n }], isFinal: true }

If you try to submit an old version, the contract rejects it:

// Current on-chain version: 2

// ✗ Try to submit version 1 (old state)
await client.resizeChannel({ version: 1n, ... });
// Error: Version must be greater than current version

// ✓ Submit version 3 (new state)
await client.resizeChannel({ version: 3n, ... });
// Success!

The Complete Resize Implementation

Here's what happens in resizeChannelWithCustodyFunds:

const resizeChannelWithCustodyFunds = async (
  channelId: Hex,
  amount: bigint,  // Positive = add, negative = remove
): Promise<void> => {
  // 1. Create signed RPC message
  const message = await createResizeChannelMessage(wallet.sessionSigner.sign, {
    channel_id: channelId,
    resize_amount: amount,
    allocate_amount: 0n,
    funds_destination: wallet.address,
  });

  // 2. Set up WebSocket listener for response
  return new Promise((resolve, reject) => {
    const handleMessage = async (event: MessageEvent) => {
      const response = parseAnyRPCResponse(event.data);

      if (response.method === RPCMethod.ResizeChannel) {
        ws.removeEventListener('message', handleMessage);

        // 3. Get proof state (last valid on-chain state)
        const channelData = await client.getChannelData(channelId);
        const proofStates = [channelData.lastValidState];

        // 4. Parse broker's signature
        const { state, serverSignature } = parseResizeChannelResponse(event.data).params;

        // 5. Submit transaction with both signatures
        const txHash = await client.resizeChannel({
          resizeState: {
            channelId,
            intent: state.intent,
            version: BigInt(state.version),  // Incremented by ClearNode
            data: state.stateData,
            allocations: state.allocations,
            serverSignature,                  // Broker's signature
          },
          proofStates,                        // Proof of last state
        });

        // 6. Wait for confirmation
        await client.publicClient.waitForTransactionReceipt({ hash: txHash });
        resolve();
      }
    };

    ws.addEventListener('message', handleMessage);
    ws.send(message);
  });
};

The SDK handles signing with your session key automatically inside client.resizeChannel().

For more details on WebSocket RPC patterns and how ClearNode communication works, see ClearNode Communication.

Withdrawal Flow

Implementation: client.ts:513-595 (withdraw)

Withdrawal is more complex because it must handle ledger, channel, and custody balances.

The withdraw() function only withdraws the requested amount and keeps the channel open. It optimizes by:

  1. Using custody funds first (no resize needed)
  2. Then pulling from channel/ledger only if necessary
  3. Leaving remaining balance in the channel for future use

The Withdrawal Strategy

const withdraw = async (amount: bigint): Promise<void> => {
  // Step 1: Get current balances across all layers
  const balances = await getBalances();
  // { wallet, custody, channel, ledger }

  // Step 2: Calculate available funds
  const totalAvailable = balances.channel + balances.ledger + balances.custodyContract;

  if (amount > totalAvailable) {
    throw new Error(`Insufficient funds. Requested ${formatUSDC(amount)}, available ${formatUSDC(totalAvailable)}`);
  }

  // Step 3: Optimize withdrawal sources
  let remainingToWithdraw = amount;

  // Strategy A: Use custody first (cheapest - already on-chain, no gas)
  const fromCustody = balances.custodyContract >= remainingToWithdraw
    ? remainingToWithdraw
    : balances.custodyContract;

  remainingToWithdraw -= fromCustody;

  // Strategy B: If need more, pull from channel/ledger via resize
  if (remainingToWithdraw > 0n) {
    const channelId = await getChannelWithBroker(ws, wallet, BROKER_ADDRESS);

    if (channelId) {
      // Calculate how much from ledger vs channel
      const fromLedger = balances.ledger >= remainingToWithdraw
        ? remainingToWithdraw
        : balances.ledger;

      const fromChannel = remainingToWithdraw - fromLedger;

      // Resize channel to move funds → custody
      const resizeAmount = -(fromLedger + fromChannel);  // Negative = channel → custody
      const allocateAmount = fromLedger;                  // Deallocate ledger → channel first

      await resizeChannelWithAmounts(channelId, resizeAmount, allocateAmount);
    }
  }

  // Step 4: Withdraw from custody to wallet
  const totalInCustody = await client.getAccountBalance(tokenAddress);
  if (totalInCustody > 0n) {
    const withdrawAmount = amount > totalInCustody ? totalInCustody : amount;

    const txHash = await client.withdrawal(tokenAddress, withdrawAmount);
    await client.publicClient.waitForTransactionReceipt({ hash: txHash });
  }
};

Why This Order?

1. Custody First: Funds already in custody can be withdrawn immediately with one transaction. No need for channel resize coordination with ClearNode.

2. Ledger Deallocation: If you have ledger balance (from receiving payments), it must be moved back to the channel before you can access it. This uses allocate_amount in the resize.

3. Channel Resize: Finally, pull from the channel itself if needed.

4. Keep Channel Open: Unlike a full channel close, withdraw only removes what you request and leaves the channel ready for future deposits.

Simple Withdrawal (No Ledger Balance)

const  = await .();
// { wallet: 85, custody: 0, channel: 15, ledger: 0 }

await .withdraw(('5'));
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
const = await .(); // { wallet: 90, custody: 0, channel: 10, ledger: 0 } // Note: Channel still open with 10 USDC remaining

What happened:

  1. Resize: Channel reduced by 5 (15 → 10), custody increased by 5 (0 → 5)
  2. Withdraw: Custody transferred to wallet (5), custody back to 0

Complex Withdrawal (With Ledger Balance)

When you have a ledger balance, it must be deallocated first:

const  = await .();
// { wallet: 85, custody: 0, channel: 10, ledger: 5 }
// Note: Total available = 10 + 5 = 15 USDC

await .withdraw(('10'));
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
const = await .(); // { wallet: 95, custody: 0, channel: 5, ledger: 0 } // Note: Channel still open with 5 USDC remaining

What happened:

  1. Allocate: Ledger balance moved to channel (ledger 5 → 0, channel 10 → 15)
  2. Resize: Channel reduced by 10 (15 → 5), custody increased (0 → 10)
  3. Withdraw: Custody to wallet (10 USDC)

Withdrawing All Funds

You can withdraw all available funds while keeping the channel open:

const before = await .();
const before: {
    custodyContract: bigint;
    channel: bigint;
    ledger: bigint;
    wallet: bigint;
}
// { wallet: 85, custody: 0, channel: 10, ledger: 5 } const = . + . + .; // total = 15 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
const = await .(); // { wallet: 100, custody: 0, channel: 0, ledger: 0 } // Note: Channel still open, ready for future deposits

What happened:

  1. Deallocate: Ledger balance moved to channel (5 → channel)
  2. Resize: Channel drained to custody (15 → custody)
  3. Withdraw: Custody transferred to wallet (15 USDC)
  4. Channel remains open with 0 balance, ready for future deposits

Resize Operations

Implementation: client.ts:374-511 (resizeChannelWithCustodyFunds, resizeChannelWithAmounts)

Resize operations move funds between custody and channel.

Understanding resize_amount

The resize_amount parameter controls custody ↔ channel movement:

// Positive resize_amount: custody → channel (add funds)
resize_amount: 5n  // Add 5 to channel from custody

// Negative resize_amount: channel → custody (remove funds)
resize_amount: -5n // Remove 5 from channel to custody

Understanding allocate_amount

The allocate_amount parameter controls channel ↔ ledger movement:

// Negative allocate_amount: ledger → channel (deallocate)
allocate_amount: -5n  // Move 5 from ledger back to channel

// Positive allocate_amount: channel → ledger (allocate)
allocate_amount: 5n   // Move 5 from channel to ledger

Example: Adding Funds with Existing Custody Balance

const before = await client.getBalances();
// { wallet: 90, custody: 3, channel: 10, ledger: 0 }
// Note: We have leftover custody balance from previous operations

// We want to add the 3 USDC in custody to our channel
await client.deposit(0n); // Deposit 0 from wallet, but use custody funds

const after = await client.getBalances();
// { wallet: 90, custody: 0, channel: 13, ledger: 0 }

Actually, let's correct this - you'd call:

// The deposit() method automatically uses custody funds if available
await client.deposit(parseUSDC('7'));
// This will:
// 1. Use 3 USDC from custody
// 2. Pull 4 USDC from wallet → custody
// 3. Resize channel +7

const after = await client.getBalances();
// { wallet: 86, custody: 0, channel: 17, ledger: 0 }

Example: Draining Ledger to Channel

When you have a negative ledger balance (you sent money):

const before = await client.getBalances();
// { wallet: 85, custody: 0, channel: 10, ledger: -3 }
// Note: Negative ledger means you owe 3 to the channel

// To withdraw, must first deallocate ledger → channel
// This happens automatically in withdraw(), but manually:
await resizeChannelWithAmounts(channelId, 0n, -3n);
// allocate_amount: -3 → move ledger balance into channel

const after = await client.getBalances();
// { wallet: 85, custody: 0, channel: 7, ledger: 0 }
// Note: Channel reduced from 10 to 7 because ledger debt was settled

For details on channel state structure, proof states, and session key management, see ClearNode Communication.

Peer-to-Peer Payments

Implementation: client.ts:701-716 (send), rpc/ledger.ts:582-632 (transferViaLedger)

The send() method transfers value via ledger balances:

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

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
const = await .(); // { wallet: 90, custody: 0, channel: 10, ledger: -3 } // ↑ negative!

Key insight: Ledger balances are net positions:

  • Sending decreases your ledger (can go negative)
  • Receiving increases your ledger (can go positive)
  • Your channel capacity backs negative balances

Recipient's Perspective

// Before (recipient)
const before = await recipientClient.getBalances();
// { wallet: 50, custody: 0, channel: 5, ledger: 0 }

// After sender.send() completes
const after = await recipientClient.getBalances();
// { wallet: 50, custody: 0, channel: 5, ledger: 3 }
//                                             ↑ increased!

The recipient's ledger balance increased by 3 USDC, backed by the sender's channel.

Checking Available Balance

Implementation: client.ts:315-372 (getBalances)

Always check total available before operations:

const balances = await .();
const balances: {
    custodyContract: bigint;
    channel: bigint;
    ledger: bigint;
    wallet: bigint;
}
const = . + // Can deposit from here . + // Already in escrow . + // In channel .; // Off-chain balance (can be negative!) .(`Total funds: ${()}`);

For withdrawals, exclude wallet:

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

if (totalWithdrawable > 0) {
  await client.withdraw(totalWithdrawable);
}

Error Handling

Insufficient Wallet Balance

try {
  await client.deposit(parseUSDC('1000'));
} catch (error) {
  // Error: Insufficient funds. Need 1000.00 USDC, have 100.00 USDC
  console.error(error.message);
}

Insufficient Channel Capacity

try {
  await client.send({ to: recipient, amount: parseUSDC('50') });
} catch (error) {
  // Error: Would exceed channel capacity
  // Current capacity: 10 USDC, ledger: -5, requested: 50
  console.error(error.message);
}

Not Connected to ClearNode

try {
  await client.withdraw(parseUSDC('5'));
} catch (error) {
  // Error: Not connected to ClearNode
  console.error('Please connect first');
  await client.connect();
}

Best Practices

1. Always Check Balances First

const balances = await client.getBalances();
console.log('Before operation:', {
  wallet: formatUSDC(balances.wallet),
  channel: formatUSDC(balances.channel),
  ledger: formatUSDC(balances.ledger),
});

await client.deposit(parseUSDC('10'));

const after = await client.getBalances();
console.log('After operation:', {
  wallet: formatUSDC(after.wallet),
  channel: formatUSDC(after.channel),
  ledger: formatUSDC(after.ledger),
});

2. Handle Partial Failures

try {
  await client.withdraw(totalAvailable);
} catch (error) {
  // Withdrawal might partially succeed (e.g., ledger deallocated but custody withdrawal failed)
  // Always re-check balances after errors
  const balances = await client.getBalances();
  console.log('Current state after error:', balances);
}

3. Wait for State to Settle

After deposits/withdrawals, give state time to propagate:

await client.deposit(parseUSDC('10'));

// Wait a moment before checking balances
await new Promise(resolve => setTimeout(resolve, 1000));

const balances = await client.getBalances();

4. Keep Channel Funded

Maintain enough channel capacity for expected activity:

const MIN_CHANNEL_BALANCE = parseUSDC('10');

const balances = await client.getBalances();
if (balances.channel < MIN_CHANNEL_BALANCE) {
  const needed = MIN_CHANNEL_BALANCE - balances.channel;
  await client.deposit(needed);
}

Complete Example

Here's a complete flow from start to finish:

async function () {
  const  = ({  });
  await .connect();
connect: () => Promise<void>

Connects to the clearnode

.('=== Initial State ==='); let = await client.();
const client: BetterNitroliteClient
.(`Wallet: ${(.)}`); .(`Channel: ${(.)}`); .(`Ledger: ${(.)}`); .('\n=== Depositing 10 USDC ==='); 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
= await .(); .(`Channel: ${(.)}`); .('\n=== Sending 3 USDC ==='); 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
= await .(); .(`Ledger: ${(.)}`); .('\n=== Withdrawing All ==='); const = . + . + .; 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
= await .(); .(`Wallet: ${(.)}`); .(`Channel: ${(.)}`); .(`Ledger: ${(.)}`); await .(); }

Next Steps