Multi-Party Application Sessions Tutorial
Overview
Application sessions in Nitrolite enable multiple participants to interact within a shared off-chain state channel. This is particularly powerful for use cases requiring coordinated actions between parties without on-chain overhead.
This tutorial demonstrates how to create, manage, and close a multi-party application session using the Yellow Network and Nitrolite protocol.
The complete runnable script for this tutorial is available at:
scripts/app_sessions/app_session_two_signers.ts
npx tsx scripts/app_sessions/app_session_two_signers.ts
What is an Application Session?
An application session is a multi-party state channel that allows participants to:
- Execute off-chain logic without blockchain transactions
- Update shared state with cryptographic signatures
- Transfer value between participants instantly
Unlike simple payment channels (1-to-1), application sessions support:
- Multiple participants (2+)
- Complex state logic
- Voting mechanisms (weights and quorum)
- Flexible allocation rules
Prerequisites
Environment Setup
You'll need two wallet seed phrases in your .env file:
WALLET_1_SEED_PHRASE="first wallet 12 or 24 word mnemonic here"
WALLET_2_SEED_PHRASE="second wallet 12 or 24 word mnemonic here"
Funded Wallets
Both wallets should have:
- Funds in Yellow ledger (deposited via custody contract)
Install Dependencies
npm install
Key Concepts
1. App Definition
The application definition specifies the rules of the session:
const appDefinition: RPCAppDefinition = {
protocol: RPCProtocolVersion.NitroRPC_0_5,
participants: [address1, address2],
weights: [50, 50], // Voting power distribution
quorum: 100, // Percentage needed for decisions (100 = unanimous)
challenge: 0, // Challenge period in seconds
nonce: Date.now(), // Unique session ID
application: 'Test app',
};
Key parameters:
| Parameter | Description |
|---|---|
participants | Array of wallet addresses involved |
weights | Voting power for each participant (must sum to 100 or appropriate total) |
quorum | Required percentage of votes for actions (50 = majority, 100 = unanimous) |
challenge | Time window for disputing state changes |
nonce | Unique identifier to prevent replay attacks |
2. Allocations
Allocations define how assets are distributed among participants:
- Sandbox
- Production
const allocations: RPCAppSessionAllocation[] = [
{ participant: address1, asset: 'ytest.usd', amount: '0.01' },
{ participant: address2, asset: 'ytest.usd', amount: '0.00' }
];
const allocations: RPCAppSessionAllocation[] = [
{ participant: address1, asset: 'usdc', amount: '0.01' },
{ participant: address2, asset: 'usdc', amount: '0.00' }
];
Rules:
- Total allocations cannot exceed session funding
- Amounts are strings (to maintain precision)
- Must account for all participants
3. Multi-Party Signatures
For actions requiring consensus (closing, etc.), signatures from multiple participants are collected:
// First participant signs
const closeMessage = await createCloseAppSessionMessage(
messageSigner1,
{ app_session_id: sessionId, allocations: finalAllocations }
);
// Second participant signs
const signature2 = await messageSigner2(closeMessage.req);
// Add second signature
closeMessage.sig.push(signature2);
// Submit with all signatures
await yellow.sendMessage(JSON.stringify(closeMessage));
Step-by-Step Walkthrough
Step 1: Connect to Yellow Network
- Sandbox
- Production
const yellow = new Client({
url: 'wss://clearnet-sandbox.yellow.com/ws',
});
await yellow.connect();
console.log('Connected to Yellow clearnet (Sandbox)');
const yellow = new Client({
url: 'wss://clearnet.yellow.com/ws',
});
await yellow.connect();
console.log('Connected to Yellow clearnet (Production)');
Step 2: Set Up Participant Wallets
// Create wallet clients for both participants
const wallet1Client = createWalletClient({
account: mnemonicToAccount(process.env.WALLET_1_SEED_PHRASE as string),
chain: base,
transport: http(),
});
const wallet2Client = createWalletClient({
account: mnemonicToAccount(process.env.WALLET_2_SEED_PHRASE as string),
chain: base,
transport: http(),
});
Step 3: Authenticate Both Participants
Each participant needs their own session key:
// Authenticate first participant
const sessionKey1 = await authenticateWallet(yellow, wallet1Client);
const messageSigner1 = createECDSAMessageSigner(sessionKey1.privateKey);
// Authenticate second participant
const sessionKey2 = await authenticateWallet(yellow, wallet2Client);
const messageSigner2 = createECDSAMessageSigner(sessionKey2.privateKey);
Step 4: Define Application Configuration
const appDefinition: RPCAppDefinition = {
protocol: RPCProtocolVersion.NitroRPC_0_5,
participants: [wallet1Client.account.address, wallet2Client.account.address],
weights: [50, 50],
quorum: 100,
challenge: 0,
nonce: Date.now(),
application: 'Test app',
};
Step 5: Create Session with Initial Allocations
- Sandbox
- Production
const allocations = [
{ participant: wallet1Client.account.address, asset: 'ytest.usd', amount: '0.01' },
{ participant: wallet2Client.account.address, asset: 'ytest.usd', amount: '0.00' }
];
const sessionMessage = await createAppSessionMessage(
messageSigner1,
{ definition: appDefinition, allocations }
);
const sessionResponse = await yellow.sendMessage(sessionMessage);
const sessionId = sessionResponse.params.appSessionId;
const allocations = [
{ participant: wallet1Client.account.address, asset: 'usdc', amount: '0.01' },
{ participant: wallet2Client.account.address, asset: 'usdc', amount: '0.00' }
];
const sessionMessage = await createAppSessionMessage(
messageSigner1,
{ definition: appDefinition, allocations }
);
const sessionResponse = await yellow.sendMessage(sessionMessage);
const sessionId = sessionResponse.params.appSessionId;
Step 6: Update Session State
You can update allocations to reflect state changes (e.g., a transfer). Since the quorum is 100%, both participants must sign:
- Sandbox
- Production
const newAllocations = [
{ participant: wallet1Client.account.address, asset: 'ytest.usd', amount: '0.00' },
{ participant: wallet2Client.account.address, asset: 'ytest.usd', amount: '0.01' }
];
// Create update message signed by first participant
const updateMessage = await createSubmitAppStateMessage(
messageSigner1,
{ app_session_id: sessionId, allocations: newAllocations }
);
const updateMessageJson = JSON.parse(updateMessage);
// Second participant signs the same state update
const signature2 = await messageSigner2(updateMessageJson.req as RPCData);
// Append second signature to meet quorum requirement
updateMessageJson.sig.push(signature2);
// Submit with all required signatures
await yellow.sendMessage(JSON.stringify(updateMessageJson));
const newAllocations = [
{ participant: wallet1Client.account.address, asset: 'usdc', amount: '0.00' },
{ participant: wallet2Client.account.address, asset: 'usdc', amount: '0.01' }
];
// Create update message signed by first participant
const updateMessage = await createSubmitAppStateMessage(
messageSigner1,
{ app_session_id: sessionId, allocations: newAllocations }
);
const updateMessageJson = JSON.parse(updateMessage);
// Second participant signs the same state update
const signature2 = await messageSigner2(updateMessageJson.req as RPCData);
// Append second signature to meet quorum requirement
updateMessageJson.sig.push(signature2);
// Submit with all required signatures
await yellow.sendMessage(JSON.stringify(updateMessageJson));
Step 7: Close Session with Multi-Party Signatures
// Create close message (signed by participant 1)
const closeMessage = await createCloseAppSessionMessage(
messageSigner1,
{ app_session_id: sessionId, allocations: finalAllocations }
);
const closeMessageJson = JSON.parse(closeMessage);
// Participant 2 signs
const signature2 = await messageSigner2(closeMessageJson.req as RPCData);
closeMessageJson.sig.push(signature2);
// Submit with all signatures
const closeResponse = await yellow.sendMessage(JSON.stringify(closeMessageJson));
Running the Example
npx tsx scripts/app_session_two_signers.ts
Expected Output
Connected to Yellow clearnet
Wallet address: 0x1234...
Wallet address: 0x5678...
Session message created: {...}
Session message sent
Session response: { appSessionId: '0xabc...' }
Submit app state message: {...}
Wallet 2 signed close session message: 0xdef...
Close session message (with all signatures): {...}
Close session message sent
Close session response: { success: true }
Use Cases
The examples below use usdc for production scenarios. When testing on Sandbox, replace usdc with ytest.usd.
1. Peer-to-Peer Escrow
// Buyer and seller agree on terms
const appDefinition = {
participants: [buyer, seller],
weights: [50, 50],
quorum: 100, // Both must agree to release funds
// ...
};
// Buyer funds escrow
const allocations = [
{ participant: buyer, asset: 'usdc', amount: '0' },
{ participant: seller, asset: 'usdc', amount: '100' } // Released to seller
];
2. Multi-Player Gaming
const appDefinition = {
participants: [player1, player2, player3, player4],
weights: [25, 25, 25, 25],
quorum: 75, // 3 out of 4 players must agree
challenge: 3600, // 1 hour challenge period
application: 'poker-game',
};
3. DAO Treasury Management
const appDefinition = {
participants: [member1, member2, member3, member4, member5],
weights: [20, 20, 20, 20, 20],
quorum: 60, // 60% approval needed
application: 'dao-treasury',
};
4. Atomic Swaps
// Party A has USDC, wants ETH
// Party B has ETH, wants USDC
const allocations = [
{ participant: partyA, asset: 'usdc', amount: '100' },
{ participant: partyA, asset: 'eth', amount: '0' },
{ participant: partyB, asset: 'usdc', amount: '0' },
{ participant: partyB, asset: 'eth', amount: '0.05' }
];
// After swap
const finalAllocations = [
{ participant: partyA, asset: 'usdc', amount: '0' },
{ participant: partyA, asset: 'eth', amount: '0.05' },
{ participant: partyB, asset: 'usdc', amount: '100' },
{ participant: partyB, asset: 'eth', amount: '0' }
];
Advanced Topics
Dynamic Participants
For applications requiring flexible participation:
// Start with 2 participants
let participants = [user1, user2];
// Add a third participant (requires re-creating session)
participants.push(user3);
const newAppDefinition = {
participants,
weights: [33, 33, 34],
// ...
};
Weighted Voting
Different participants can have different voting power:
const appDefinition = {
participants: [founder, investor1, investor2],
weights: [50, 30, 20], // Founder has 50% voting power
quorum: 60, // Founder + one investor = 60%
// ...
};
Challenge Periods
Add time for participants to dispute state changes:
const appDefinition = {
// ...
challenge: 86400, // 24 hours in seconds
};
// Participants have 24 hours to challenge a close request before finalization
State Validation
Implement custom logic to validate state transitions:
function validateStateTransition(
oldAllocations: RPCAppSessionAllocation[],
newAllocations: RPCAppSessionAllocation[]
): boolean {
// Ensure total amounts are preserved
const oldTotal = oldAllocations.reduce((sum, a) => sum + parseFloat(a.amount), 0);
const newTotal = newAllocations.reduce((sum, a) => sum + parseFloat(a.amount), 0);
return Math.abs(oldTotal - newTotal) < 0.000001;
}
Troubleshooting
"Authentication failed for participant"
Cause: Session key authentication failed
Solution:
- Ensure both
WALLET_1_SEED_PHRASEandWALLET_2_SEED_PHRASEare set in.env - Verify wallets have been authenticated on Yellow network before
"Unsupported token"
Cause: Using the wrong asset for your environment (e.g., usdc on Sandbox or ytest.usd on Production)
Solution:
- Sandbox (
wss://clearnet-sandbox.yellow.com/ws): Useytest.usd - Production (
wss://clearnet.yellow.com/ws): Useusdc
Ensure the asset in your allocations matches the connected network.
"Insufficient balance"
Cause: Participant doesn't have enough funds in Yellow ledger
Solution:
Deposit sufficient funds into the yellow network account unified balance for each wallet
"Invalid signatures"
Cause: Not all required signatures were collected
Solution:
- Ensure quorum is met (if quorum is 100, need all signatures)
- Check that signatures are added in correct order
- Verify message signers correspond to participants
"Session already closed"
Cause: Trying to update or close an already-finalized session
Solution:
- Create a new session
- Check session status before operations
"Quorum not reached"
Cause: Insufficient voting weight for action
Solution:
// Example: quorum is 60, weights are [30, 30, 40]
// Need at least 2 participants to sign
// Check current signature weight
const signatureWeight = signatures.reduce((sum, sig) => {
const participantIndex = findParticipantIndex(sig);
return sum + weights[participantIndex];
}, 0);
console.log(`Current weight: ${signatureWeight}, Required: ${quorum}`);
Best Practices
- Always validate allocations before submitting state updates
- Store session IDs for future reference and auditing
- Implement timeout handling for multi-party signatures
- Use appropriate quorum settings based on trust model
- Test with small amounts before production use
- Keep participants informed of state changes
- Handle disconnections gracefully (participants may come back)
- Document application logic for all participants
Further Reading
- App Sessions Core Concepts — Understanding app sessions
- App Session Methods — Complete API reference
- Client-Side App Session Signing Guide — Signing implementation details
- Session Keys — Managing session keys