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 ←→ LEDGERThe BetterNitroliteClient provides three high-level methods:
deposit(amount)- Move funds from wallet into the channelwithdraw(amount)- Move funds from channel back to walletsend({ 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 before = await client.getBalances();
// { wallet: 100, custody: 0, channel: 0, ledger: 0 }
await client.deposit(parseUSDC('10'));
const after = await client.getBalances();
// { wallet: 90, custody: 0, channel: 10, ledger: 0 }What happened:
- Approve: ERC-20 approval for custody contract to spend USDC
- Deposit: Transfer from wallet → custody contract
- 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 before = await client.getBalances();
// { wallet: 90, custody: 0, channel: 10, ledger: 0 }
await client.deposit(parseUSDC('5'));
const after = await client.getBalances();
// { wallet: 85, custody: 0, channel: 15, ledger: 0 }What happened:
- Deposit: Transfer from wallet → custody (5 USDC)
- 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: 20How 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 allocationsserverSignature: 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:
- Transfers USDC from your wallet to custody
- Creates the channel on-chain
- Sets initial allocations
- Emits a
ChannelCreatedevent
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 channelAlways 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 = 15Example: 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 = 10Example: 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 = 0Proof 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:
- Brokerless trust model: ClearNode signs all state transitions, so disputes are rare
- Version incrementing: Each state has a version number that must increase
- 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:
- Using custody funds first (no resize needed)
- Then pulling from channel/ledger only if necessary
- 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 before = await client.getBalances();
// { wallet: 85, custody: 0, channel: 15, ledger: 0 }
await client.withdraw(parseUSDC('5'));
const after = await client.getBalances();
// { wallet: 90, custody: 0, channel: 10, ledger: 0 }
// Note: Channel still open with 10 USDC remainingWhat happened:
- Resize: Channel reduced by 5 (15 → 10), custody increased by 5 (0 → 5)
- 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 before = await client.getBalances();
// { wallet: 85, custody: 0, channel: 10, ledger: 5 }
// Note: Total available = 10 + 5 = 15 USDC
await client.withdraw(parseUSDC('10'));
const after = await client.getBalances();
// { wallet: 95, custody: 0, channel: 5, ledger: 0 }
// Note: Channel still open with 5 USDC remainingWhat happened:
- Allocate: Ledger balance moved to channel (ledger 5 → 0, channel 10 → 15)
- Resize: Channel reduced by 10 (15 → 5), custody increased (0 → 10)
- Withdraw: Custody to wallet (10 USDC)
Withdrawing All Funds
You can withdraw all available funds while keeping the channel open:
const before = await client.getBalances();// { wallet: 85, custody: 0, channel: 10, ledger: 5 }
const total = before.channel + before.ledger + before.custodyContract;
// total = 15 USDC
await client.withdraw(total);
const after = await client.getBalances();
// { wallet: 100, custody: 0, channel: 0, ledger: 0 }
// Note: Channel still open, ready for future depositsWhat happened:
- Deallocate: Ledger balance moved to channel (5 → channel)
- Resize: Channel drained to custody (15 → custody)
- Withdraw: Custody transferred to wallet (15 USDC)
- 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 custodyUnderstanding 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 ledgerExample: 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 settledFor 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 before = await client.getBalances();
// { wallet: 90, custody: 0, channel: 10, ledger: 0 }
await client.send({ to: recipientAddress, amount: parseUSDC('3') });
const after = await client.getBalances();
// { 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 client.getBalances();
const totalAvailable =
balances.wallet + // Can deposit from here
balances.custodyContract + // Already in escrow
balances.channel + // In channel
balances.ledger; // Off-chain balance (can be negative!)
console.log(`Total funds: ${formatUSDC(totalAvailable)}`);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 completeFlow() {
const client = createBetterNitroliteClient({ wallet });
await client.connect();
console.log('=== Initial State ===');
let balances = await client.getBalances(); console.log(`Wallet: ${formatUSDC(balances.wallet)}`);
console.log(`Channel: ${formatUSDC(balances.channel)}`);
console.log(`Ledger: ${formatUSDC(balances.ledger)}`);
console.log('\n=== Depositing 10 USDC ===');
await client.deposit(parseUSDC('10')); balances = await client.getBalances();
console.log(`Channel: ${formatUSDC(balances.channel)}`);
console.log('\n=== Sending 3 USDC ===');
await client.send({ to: recipientAddress, amount: parseUSDC('3') }); balances = await client.getBalances();
console.log(`Ledger: ${formatUSDC(balances.ledger)}`);
console.log('\n=== Withdrawing All ===');
const total = balances.channel + balances.ledger + balances.custodyContract;
await client.withdraw(total); balances = await client.getBalances();
console.log(`Wallet: ${formatUSDC(balances.wallet)}`);
console.log(`Channel: ${formatUSDC(balances.channel)}`);
console.log(`Ledger: ${formatUSDC(balances.ledger)}`);
await client.disconnect();
}Next Steps
- ClearNode Communication: Deep dive into WebSocket RPC patterns, state management, and session keys
- Distributed Sessions: Coordinate multi-party session creation
- Session Lifecycle: Manage active sessions and cleanup
- Complete Game: Fund flows in a real application