Quickstart
Run a complete native v1 Nitrolite flow with @yellow-org/sdk: connect to Nitronode, prepare a home channel, create an app session, deposit into it, operate, withdraw, and close the session.
The runnable source lives in examples/nitrolite-v1-lifecycle. The snippets below explain the stages; the checked-in example is the source of truth.
Prerequisites
You need:
- Node.js 20 or later.
- A disposable user wallet with Sepolia gas and the selected test asset.
- A separate app signer wallet. It can be unfunded for this example.
- A Sepolia RPC URL.
- A v1 Nitronode WebSocket URL.
See Prerequisites & Environment for the full setup checklist.
Step 1: Install and configure
Clone the docs repo, enter the tested example package, and install its isolated dependencies:
git clone https://github.com/layer-3/docs.git
cd docs/examples/nitrolite-v1-lifecycle
cp .env.example .env
npm install
Edit .env:
USER_PRIVATE_KEY=0x...
APP_PRIVATE_KEY=0x...
NITRONODE_WS_URL=<sandbox-url-coming-soon>
RPC_URL=https://ethereum-sepolia-rpc.publicnode.com
CHAIN_ID=11155111
ASSET=yellow
CHANNEL_DEPOSIT_AMOUNT=0.01
APP_DEPOSIT_AMOUNT=0.005
OPERATE_AMOUNT=0.001
WITHDRAW_AMOUNT=0.002
CHANNEL_DEPOSIT_AMOUNT is the amount available in the user's home channel. APP_DEPOSIT_AMOUNT is the smaller amount committed from that home-channel balance into the app session.
Use a dedicated test wallet. Do not reuse a wallet that holds mainnet funds.
Step 2: Connect clients
The example creates separate SDK clients for the user and the app signer:
const userSigners = createSigners(config.userPrivateKey);
const appSigners = createSigners(config.appPrivateKey);
const user = await Client.create(
config.wsURL,
userSigners.stateSigner,
userSigners.txSigner,
withBlockchainRPC(config.chainId, config.rpcURL),
);
createSigners() derives both signer types the SDK needs: off-chain state signing and on-chain transaction signing.
The checked-in Node.js script also applies enableNodeLocalAccountTransactions() after client creation. Public RPC transports need the local account preserved on writeContract() calls, otherwise approveToken() and checkpoint() can fail with viem account errors. Browser wallet apps do not need this shim.
Step 3: Prepare the home channel
The script loads Nitronode config, checks the configured asset, sets the asset home blockchain, and prepares the user home channel.
If the wallet already has a signed funded state, the script continues. Otherwise it runs:
await client.approveToken(chainId, asset, depositAmount);
const depositState = await client.deposit(chainId, asset, depositAmount);
console.log('Deposit state version:', depositState.version);
const txHash = await client.checkpoint(asset);
console.log('Deposit checkpoint tx:', txHash);
deposit() creates a co-signed channel state. checkpoint() submits that state to ChannelHub so the deposit is enforced on-chain.
Step 4: Create the app session
The app session has two participants: the user wallet and the app signer. Both sign the packed session definition.
const definition: AppDefinitionV1 = {
applicationId: appID,
participants: [
{ walletAddress: userAddress, signatureWeight: 1 },
{ walletAddress: appAddress, signatureWeight: 1 },
],
quorum: 2,
nonce: BigInt(Date.now()) * 1_000_000n + BigInt(Math.floor(Math.random() * 1_000_000)),
};
const payload = packCreateAppSessionRequestV1(definition, sessionData);
const userSig = await userSessionSigner.signMessage(payload);
const appSig = await appSessionSigner.signMessage(payload);
const created = await client.createAppSession(definition, sessionData, [userSig, appSig]);
Step 5: Deposit into the app session
The user commits part of the home-channel balance into the app session with a deposit intent:
const { sessions } = await client.getAppSessions({ appSessionId: created.appSessionId });
const session = sessions[0];
const update: AppStateUpdateV1 = {
appSessionId: created.appSessionId,
intent: AppStateUpdateIntent.Deposit,
version: session.version + 1n,
allocations: [
{ participant: userAddress, asset, amount: appDepositAmount },
{ participant: appAddress, asset, amount: new Decimal(0) },
],
sessionData: JSON.stringify({ intent: 'user_deposit' }),
};
const updatePayload = packAppStateUpdateV1(update);
const updateUserSig = await userSessionSigner.signMessage(updatePayload);
const updateAppSig = await appSessionSigner.signMessage(updatePayload);
await client.submitAppSessionDeposit(update, [updateUserSig, updateAppSig], asset, appDepositAmount);
Step 6: Operate, withdraw, and close
After the app session is funded, the remaining stages use submitAppState() with different intents:
await client.submitAppState(operateUpdate, operateSignatures);
await client.submitAppState(withdrawUpdate, withdrawSignatures);
await client.submitAppState(closeUpdate, closeSignatures);
Operatemoves a small purchase amount from the user allocation to the app signer allocation.Withdrawreleases part of the user allocation back to the home channel.Closefinalizes the app session and returns the remaining allocations.
The example leaves the home channel open by default. Set CLOSE_HOME_CHANNEL=true only when you intentionally want to close the example channel after the app-session lifecycle.
Step 7: Run it
Build and run the lifecycle:
npm run build
npm run lifecycle
A successful run prints each stage.
Sanitized sample output
== Connect clients ==
user=0xUSER_WALLET
app=0xAPP_SIGNER
== Inspect Nitronode config and set home blockchain ==
node=0xNITRONODE_SIGNER version=13a90cd
chain=ethereum_sepolia id=11155111
asset=yellow decimals=18
== Prepare user home channel ==
signed state before prepare: version=2 transition=40 homeBalance=0.005 homeChannel=0xHOME_CHANNEL_ID
pending state before prepare: version=4 transition=41 homeBalance=0.009 homeChannel=0xHOME_CHANNEL_ID
acknowledging pending yellow channel state
ack checkpoint tx=0xCHECKPOINT_TX_HASH
home channel ready: version=5 transition=1 homeBalance=0.009 homeChannel=0xHOME_CHANNEL_ID
== Run app-session lifecycle ==
registering app=docs-v1-lifecycle-... owner=0xAPP_SIGNER
app registry is disabled on this node; continuing with app-session creation
created app session=0xAPP_SESSION_ID version=1 status=open
after create: version=1 closed=false user=0 app=0
app-session deposit nodeSig=0xNODE_SIGNATURE
after app deposit: version=2 closed=false user=0.005 app=0
after operate: version=3 closed=false user=0.004 app=0.001
after withdraw: version=4 closed=false user=0.002 app=0.001
after close: version=5 closed=true user=0 app=0
== Final queries ==
final signed channel state: version=6 transition=40 homeBalance=0.004 homeChannel=0xHOME_CHANNEL_ID
final app session: version=5 closed=true user=0 app=0
== Optional cleanup ==
home-channel close skipped; set CLOSE_HOME_CHANNEL=true to run cleanup
Lifecycle complete.
What just happened
Client.create()opened v1 Nitronode WebSocket connections for both signers.setHomeBlockchain()selected the settlement chain for the asset.approveToken(),deposit(), andcheckpoint()prepared and enforced the user's home channel.packCreateAppSessionRequestV1()andpackAppStateUpdateV1()produced the exact hashes participants signed.createAppSession(),submitAppSessionDeposit(), andsubmitAppState()moved through the app-session lifecycle.
Next steps
- Configuration - timeouts, RPC endpoints, application IDs, and error handling.
- Multi-party app sessions - expand the same lifecycle to more participants.
- Key Terms & Mental Models - learn the vocabulary used across the docs.