A complete TypeScript SDK for Lighter Protocol - trade perpetual futures with built-in stop-loss and take-profit orders, position management, and comprehensive error handling.
This SDK uses a WASM signer for cryptographic operations. The signer is compiled during the build process.
Key Features: - ✅ Uses a WASM signer for transaction signing - ✅ Automatic error recovery and nonce management - ✅ Support for all transaction types - ✅ Multiple API key support - ✅ Production-ready and battle-tested
npm install lighter-ts-sdk
# or
yarn add lighter-ts-sdk
The package ships ESM, CommonJS, and a browser UMD bundle, resolved automatically via package.json's exports field. Both of the following work out of the box:
// ESM (import)
import { SignerClient, ApiClient, OrderType } from 'lighter-ts-sdk';
// CommonJS (require)
const { SignerClient, ApiClient, OrderType } = require('lighter-ts-sdk');
<script src="https://unpkg.com/lighter-ts-sdk/dist/umd/lighter-ts-sdk.js"></script>
<script>
const { SignerClient } = window.LighterSDK;
</script>
The Lighter TypeScript SDK provides everything you need to: - Trade perpetual futures and spot markets on Lighter Protocol - Create orders (Market, Limit, TWAP) with automatic SL/TP - Build grouped orders (OTO, OCO, OTOCO) and self-trade-prevention orders - Manage positions (open, close, update leverage and margin mode) - Manage sub-accounts and transfer funds between accounts (including spot↔perp) - Approve and route fees to integrators (frontends, bots, affiliate platforms) - Manage Unified Trading Account (UTA) and per-asset margin settings - Use public pools, staking, and RFQ where available on your account - Monitor transactions with built-in status tracking - Handle errors automatically with retry logic
See examples/ for a runnable script covering every feature above.
Create a .env file in your project root:
# Required credentials
API_PRIVATE_KEY=your_private_key_here
ACCOUNT_INDEX=0
API_KEY_INDEX=0
BASE_URL=https://mainnet.zklighter.elliot.ai
# Optional: for specific examples
MARKET_ID=0
SUB_ACCOUNT_INDEX=1
DEPOSIT_AMOUNT=1
npm install lighter-ts-sdk
import { SignerClient, OrderType } from 'lighter-ts-sdk';
import dotenv from 'dotenv';
dotenv.config();
async function placeOrder() {
// Initialize the client
const signerClient = new SignerClient({
url: process.env.BASE_URL!,
privateKey: process.env.API_PRIVATE_KEY!,
accountIndex: parseInt(process.env.ACCOUNT_INDEX!),
apiKeyIndex: parseInt(process.env.API_KEY_INDEX!)
});
// Initialize WASM signer (required)
await signerClient.initialize();
await signerClient.ensureWasmClient();
// Create a market order with SL/TP using OTOCO
const result = await signerClient.createOtocoOrder({
mainOrder: {
marketIndex: 0, // ETH market
clientOrderIndex: Date.now(), // Unique ID
baseAmount: 10000, // 0.01 ETH (scaled: 1 ETH = 1,000,000)
isAsk: false, // BUY (true = SELL)
orderType: OrderType.MARKET,
// Slippage protection
idealPrice: 400000, // Ideal price ($4000)
maxSlippage: 0.001 // Max 0.1% slippage
},
// Automatic stop-loss
stopLoss: {
triggerPrice: 380000, // Stop loss at $3800
isLimit: false // Market SL
},
// Automatic take-profit
takeProfit: {
triggerPrice: 420000, // Take profit at $4200
isLimit: false // Market TP
}
});
// Check if order succeeded
if (result.error || !result.hash) {
console.error('❌ Order failed:', result.error);
return;
}
console.log('✅ OTOCO order created!');
console.log('Grouped order hash:', result.hash);
// Wait for transaction confirmation
await signerClient.waitForTransaction(result.hash, 30000);
await signerClient.close();
}
placeOrder().catch(console.error);
Lighter uses fixed decimal scaling: - ETH amounts: 1 ETH = 1,000,000 units - Prices: $1 = 100 units
// To buy 0.01 ETH at $4000:
baseAmount: 10000 // 0.01 ETH (10,000 / 1,000,000)
price: 400000 // $4000 (400,000 / 100)
OrderType.MARKET // Executes immediately at market price
OrderType.LIMIT // Executes at your specified price
OrderType.TWAP // Executes gradually over time
isAsk: false // BUY - You're buying ETH
isAsk: true // SELL - You're selling ETH
SL/TP orders are automatically reduce-only - they only close positions:
stopLoss: {
triggerPrice: 380000, // When price hits this, close position
isLimit: false // false = market SL, true = limit SL
},
takeProfit: {
triggerPrice: 420000, // When price hits this, take profit
isLimit: false // false = market TP, true = limit TP
}
Important: SL/TP orders require an existing position. For Market orders, this works immediately. For Limit orders, SL/TP are created in the same batch.
Note for TWAP orders: TWAP orders execute over time, creating positions gradually. SL/TP cannot be created in the same batch as TWAP orders. You should create SL/TP orders separately after the TWAP has started creating positions.
Lighter Protocol transactions go through multiple network states. Use waitForTransaction() to monitor status changes with automatic error handling and recovery:
// Transaction status constants
enum TransactionStatus {
PENDING = 0, // Initial state, not yet queued
QUEUED = 1, // Queued for processing
COMMITTED = 2, // Committed to block
EXECUTED = 3, // Successfully executed
FAILED = 4, // Execution failed
REJECTED = 5 // Transaction rejected
}
// Monitor transaction with built-in retry logic
try {
const txResult = await signerClient.waitForTransaction(
txHash,
30000, // maxWaitTime: wait up to 30 seconds
2000 // pollInterval: check status every 2 seconds
);
console.log('Status:', txResult.status); // Will be EXECUTED (3) if successful
console.log('✅ Transaction confirmed on-chain');
} catch (error) {
// Error handling is centralized here
if (error.message.includes('timeout')) {
console.error('❌ Transaction didn\'t confirm within timeout');
} else if (error.message.includes('FAILED') || error.message.includes('REJECTED')) {
console.error('❌ Transaction failed:', error.message);
} else {
console.error('❌ Unexpected error:', error.message);
}
}
// Advanced: Manual status polling for fine-grained control
const checkStatus = async (hash: string) => {
const txResult = await signerClient.getTransaction(hash);
switch (txResult.status) {
case SignerClient.TX_STATUS_PENDING:
console.log('⏳ Still pending...');
break;
case SignerClient.TX_STATUS_QUEUED:
console.log('📋 Queued for processing');
break;
case SignerClient.TX_STATUS_COMMITTED:
console.log('✍️ Committed to block');
break;
case SignerClient.TX_STATUS_EXECUTED:
console.log('✅ Transaction executed successfully');
break;
case SignerClient.TX_STATUS_FAILED:
console.error('❌ Transaction execution failed');
break;
case SignerClient.TX_STATUS_REJECTED:
console.error('❌ Transaction was rejected');
break;
}
return txResult;
};
// Pattern: Fire-and-forget with error handling
const result = await signerClient.createOrder(orderParams);
if (result.hash) {
// Schedule async confirmation check (don't block)
signerClient.waitForTransaction(result.hash, 60000).catch(err => {
console.error('Transaction confirmation failed:', err);
// Handle error (alert user, retry, etc.)
});
}
Key Points:
- waitForTransaction() centralizes error handling and automatic retries
- No need to parse transaction status manually after wait
- Throw errors are detected and propagated with context
- Use timeout to prevent indefinite waits in production systems
Lighter Protocol supports both cross-margin and isolated-margin modes. Margin direction constants determine whether you're adding or removing collateral:
// Margin mode
SignerClient.CROSS_MARGIN_MODE = 0 // Shared collateral across markets
SignerClient.ISOLATED_MARGIN_MODE = 1 // Per-market collateral
// Margin direction (when updating isolated margin)
SignerClient.ISOLATED_MARGIN_REMOVE_COLLATERAL = 0 // Remove collateral from position
SignerClient.ISOLATED_MARGIN_ADD_COLLATERAL = 1 // Add collateral to position
// Add 100 USDC collateral to ETH position in isolated mode
const [marginInfo, txHash, error] = await signerClient.updateMargin(
0, // marketIndex: 0 = ETH/USDC
100, // usdcAmount: 100 USDC to add
SignerClient.ISOLATED_MARGIN_ADD_COLLATERAL // direction: 1
);
if (error) {
console.error('Failed to add margin:', error);
} else {
console.log('✅ Margin added:', txHash);
// Wait for confirmation
await signerClient.waitForTransaction(txHash, 30000);
console.log('✅ Margin update confirmed');
}
// Remove 50 USDC collateral from ETH position
const [marginInfo, txHash, error] = await signerClient.updateMargin(
0, // marketIndex: 0 = ETH/USDC
50, // usdcAmount: 50 USDC to remove
SignerClient.ISOLATED_MARGIN_REMOVE_COLLATERAL // direction: 0
);
if (error) {
console.error('Failed to remove margin:', error);
} else {
console.log('✅ Margin removed:', txHash);
// Wait for confirmation
await signerClient.waitForTransaction(txHash, 30000);
console.log('✅ Margin removal confirmed');
}
// === CROSS-MARGIN MODE ===
// Step 1: Update leverage to cross-margin
const [crossInfo1, crossHash1, crossErr1] = await signerClient.updateLeverage(
0, // marketIndex
SignerClient.CROSS_MARGIN_MODE, // mode: 0
5 // leverage: 5x
);
// Step 2: Create order (no margin updates needed - uses account balance)
const result = await signerClient.createMarketOrder({
marketIndex: 0,
baseAmount: 10000,
isAsk: false
});
// === ISOLATED-MARGIN MODE ===
// Step 1: Update leverage to isolated-margin
const [isolatedInfo1, isolatedHash1, isolatedErr1] = await signerClient.updateLeverage(
0, // marketIndex
SignerClient.ISOLATED_MARGIN_MODE, // mode: 1
20 // leverage: 20x (IMF = 10000/20 = 500)
);
// Step 2: Create order
const result = await signerClient.createMarketOrder({
marketIndex: 0,
baseAmount: 10000,
isAsk: false
});
// Step 3: Manage collateral dynamically
// Add more collateral if position grows risky
await signerClient.updateMargin(
0,
200, // Add 200 USDC
SignerClient.ISOLATED_MARGIN_ADD_COLLATERAL
);
// Remove collateral if position has buffer
await signerClient.updateMargin(
0,
100, // Remove 100 USDC
SignerClient.ISOLATED_MARGIN_REMOVE_COLLATERAL
);
Important:
- Cross-margin pools collateral across all markets
- Isolated margin requires explicit collateral management per-market
- direction parameter is only used for isolated-margin positions
- Always verify sufficient collateral before removing it
const [tx, hash, error] = await signerClient.createMarketOrder({
marketIndex: 0,
clientOrderIndex: Date.now(),
baseAmount: 10000, // Amount (0.01 ETH)
avgExecutionPrice: 400000, // Max execution price ($4000)
isAsk: false // BUY
});
if (error || !hash) {
console.error('Failed:', error);
return;
}
const [tx, hash, error] = await signerClient.createOrder({
marketIndex: 0,
clientOrderIndex: Date.now(),
baseAmount: 10000, // Amount (0.01 ETH)
price: 400000, // Limit price ($4000)
isAsk: false, // BUY
orderType: OrderType.LIMIT,
orderExpiry: Date.now() + (60 * 60 * 1000) // Expires in 1 hour
});
if (error || !hash) {
console.error('Failed:', error);
return;
}
// Wait for it to fill
await signerClient.waitForTransaction(hash);
const [tx, hash, error] = await signerClient.cancelOrder({
marketIndex: 0,
orderIndex: 12345 // Your order's index
});
if (error) {
console.error('Cancel failed:', error);
return;
}
await signerClient.waitForTransaction(hash);
console.log('✅ Order cancelled');
const [tx, hash, error] = await signerClient.createMarketOrder({
marketIndex: 0,
clientOrderIndex: Date.now(),
baseAmount: 10000, // Position size to close
avgExecutionPrice: 400000,
isAsk: false, // Opposite of position
reduceOnly: true // IMPORTANT: Only closes, doesn't open new
});
if (error) {
console.error('Close failed:', error);
return;
}
await signerClient.waitForTransaction(hash);
console.log('✅ Position closed');
const status = await signerClient.getTransaction(txHash);
console.log('Status:', status.status); // 0=pending, 1=queued, 2=committed, 3=executed
```t
$ claude mcp add lighter-ts \
-- python -m otcore.mcp_server <graph>