How to Trade on TermMax Programmatically - Part 1

TermMax is a fixed-rate lending protocol that allows users to borrow, lend, and loop at predetermined rates and terms. Unlike traditional DeFi protocols with variable rates, TermMax allows you to lock in your returns or borrowing costs upfront.
In this first tutorial, we'll cover the fundamentals: depositing into vaults and withdrawing funds. These form the foundation for more advanced strategies in future articles.
P.S. All code examples are available in this GitHub repository.
Prerequisites
Before we start, make sure you have:
- Node.js installed - Download here
- A wallet with some gas tokens like ETH or BNB
- Test tokens from testnet faucet if you don’t want to test in production
- Access to the relevant token contracts and market addresses ← you can obtain these by checking the Specs on TermMax or testnet
- An RPC endpoint URL ← you can get one by visiting Chainlist
Environment Setup
1. Create Your Project
mkdir termmax-bot-1
cd termmax-bot-1
npm init -y
Or clone directly from our repository:
git clone https://github.com/weitingattkspring/termmax-bot-1 ./termmax-bot-1
cd termmax-bot-1
2. Install Dependencies
Install the required dependencies:
npm install ethers dotenv
If you’re using our existing repo rather than starting from scratch, use:
npm install
3. Create Environment File
Create a .env file in your project root:
PRIVATE_KEY=your_private_key_here
RPC_URL=your_rpc_endpoint_url
VAULT_ADDRESS=0x... # TermMax Vault contract address
DEBT_TOKEN_ADDRESS=0x... # The debt asset (e.g., USDC, wETH)
SEED_PHRASE="" # Optional. Only needed if you want to run deposits.js (the third operations below)
You can get those values for our testnet if you don’t want to test in production, and there is a faucet for our test tokens.
For those using our repository, please rename the .env.example
file to .env
and fill in the respective values.
4. Create Package Configuration
Add a scripts
section to the package.json
file, as shown below. For those who pull the codes from our repo, please skip this step.
{
"scripts": {
"test": "node ./test.js",
"deposit": "node ./deposit.js",
"withdraw": "node ./withdraw.js",
"deposits": "node ./deposits.js"
},
"dependencies": {
"dotenv": "^17.2.0",
"ethers": "^6.15.0"
}
}
Configuration Setup
Create a config.js
file to handle our contract connections:
import { ethers } from 'ethers';
import dotenv from 'dotenv';
dotenv.config();
// Simplified contract ABIs
const VAULT_ABI = [
"function deposit(uint256 assets, address receiver) external returns (uint256)",
"function withdraw(uint256 assets, address receiver, address owner) external returns (uint256)",
"function redeem(uint256 shares, address receiver, address owner) external returns (uint256)",
"function balanceOf(address account) external view returns (uint256)",
"function totalAssets() external view returns (uint256)",
"function decimals() external view returns (uint8)"
];
const ERC20_ABI = [
"function approve(address spender, uint256 amount) external returns (bool)",
"function transfer(address to, uint256 amount) external returns (bool)",
"function balanceOf(address account) external view returns (uint256)",
"function allowance(address owner, address spender) external view returns (uint256)",
"function decimals() external view returns (uint8)"
];
// Setup provider and wallet
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
// Contract instances
export const vaultContract = new ethers.Contract(
process.env.VAULT_ADDRESS,
VAULT_ABI,
wallet
);
export const debtToken = new ethers.Contract(
process.env.DEBT_TOKEN_ADDRESS,
ERC20_ABI,
wallet
);
export { ethers, wallet, provider };
// Utility Functions
export async function getTokenDecimals(tokenContract) {
try {
return await tokenContract.decimals();
} catch (error) {
console.warn('Could not fetch decimals, defaulting to 18');
return 18;
}
}
export async function parseAmount(humanAmount, tokenContract) {
const decimals = await getTokenDecimals(tokenContract);
return ethers.parseUnits(humanAmount.toString(), decimals);
}
export async function formatAmount(contractAmount, tokenContract) {
const decimals = await getTokenDecimals(tokenContract);
return ethers.formatUnits(contractAmount, decimals);
}
export async function checkBalance(tokenContract, address = wallet.address) {
const balance = await tokenContract.balanceOf(address);
const readable = await formatAmount(balance, tokenContract);
return { raw: balance, formatted: readable };
}
export function floorToDecimals(number, decimals) {
const multiplier = Math.pow(10, decimals);
return Math.floor(number * multiplier) / multiplier;
}
Testing Your Setup
Create a test.js
to verify everything works:
import { vaultContract, debtToken, wallet, checkBalance, formatAmount } from './config.js';
async function testSetup() {
console.log('🔄 Testing TermMax setup...\n');
try {
// Test wallet connection
console.log(`✅ Wallet connected: ${wallet.address}`);
// Test balance check
const balance = await checkBalance(debtToken);
console.log(`✅ Token balance: ${balance.formatted}`);
// Test contract connection
const shareBalance = await vaultContract.balanceOf(wallet.address);
console.log(`✅ Vault shares: ${await formatAmount(shareBalance, vaultContract)}`);
console.log('\n🎉 Setup complete! Ready to start trading.');
} catch (error) {
console.error('❌ Setup failed:', error.message);
console.log('\n🔧 Check your .env file and contract addresses');
}
}
testSetup();
Open your terminal, and runnpm run test
Core Operations
The following codes are already available in the GitHub repository. If you’ve cloned the repo, please check the run command at the end of each operation.
1. Depositing into a Vault
TermMax vaults are managed pools that automatically deploy capital across multiple fixed-term markets. Each vault is managed by a Curator who selects the best opportunities.
Create deposit.js
:
import { vaultContract, debtToken, wallet, parseAmount, formatAmount, checkBalance, ethers } from './config.js';
async function depositToVault(humanAmount) {
try {
console.log(`\n🏦 Depositing ${humanAmount} tokens to vault...`);
// Step 1: Check sufficient balance
const balance = await checkBalance(debtToken);
console.log(`💰 Current balance: ${balance.formatted}`);
if (parseFloat(balance.formatted) < humanAmount) {
throw new Error(`Insufficient balance. You have ${balance.formatted} but need ${humanAmount}`);
}
// Step 2: Parse amount to contract format
const contractAmount = await parseAmount(humanAmount, debtToken);
// Step 3: Check and approve if needed
const allowance = await debtToken.allowance(wallet.address, process.env.VAULT_ADDRESS);
if (allowance < contractAmount) {
console.log('🔓 Approving vault contract...');
const approveTx = await debtToken.approve(
process.env.VAULT_ADDRESS,
contractAmount
);
await approveTx.wait();
console.log('✅ Approval confirmed');
}
// Step 4: Execute deposit
console.log('📥 Executing deposit...');
const depositTx = await vaultContract.deposit(
contractAmount,
wallet.address
);
const receipt = await depositTx.wait();
console.log(`✅ Deposit successful: ${receipt.transactionHash}`);
// Step 5: Show results
const shareBalance = await vaultContract.balanceOf(wallet.address);
console.log(`🎯 Current vault shares: ${await formatAmount(shareBalance, vaultContract)}`);
return receipt;
} catch (error) {
console.error('❌ Deposit failed:', error.message);
throw error;
}
}
// Example usage
const amount = process.argv[2] || '100';
console.log(`\n📋 Single Vault Deposit - Usage Examples:`);
console.log(` node deposit.js 250 - Deposit 250 tokens to vault`);
console.log(` node deposit.js 50.5 - Deposit 50.5 tokens to vault`);
console.log(` node deposit.js 1000 - Deposit 1000 tokens to vault`);
console.log(` node deposit.js - Deposit 100 tokens (default)`);
depositToVault(parseFloat(amount));
Run: npm run deposit
or node deposit.js 50
The former will deposit 100 debt assets (like 100 USDC/wETH) to a vault by default, while the latter will deposit 50 debt assets to a vault.
2. Withdrawing from a Vault
To withdraw your funds from the vault, you can either withdraw a specific amount of assets or redeem a specific number of shares:
import { vaultContract, debtToken, wallet, parseAmount, formatAmount } from './config.js';
async function withdrawFromVault(humanAmount, method = 'assets') {
try {
console.log(`\n🏦 Processing withdrawal...`);
// Check current vault shares
const shareBalance = await vaultContract.balanceOf(wallet.address);
const readableShares = await formatAmount(shareBalance, vaultContract);
console.log(`💎 Current vault shares: ${readableShares}`);
if (shareBalance === 0n) {
throw new Error('No vault shares to withdraw');
}
let withdrawTx;
// Handle zero amount - withdraw all shares regardless of method
if (humanAmount === 0) {
console.log(`🚀 Zero amount detected - withdrawing ALL shares (${readableShares})`);
withdrawTx = await vaultContract.redeem(
shareBalance, // Use the raw BigInt balance directly
wallet.address,
wallet.address
);
} else if (method === 'assets') {
// Withdraw specific amount of underlying assets
const contractAmount = await parseAmount(humanAmount, debtToken);
console.log(`📤 Withdrawing ${humanAmount} assets...`);
withdrawTx = await vaultContract.withdraw(
contractAmount,
wallet.address,
wallet.address
);
} else if (method === 'shares') {
// Redeem specific amount of vault shares
const shareAmount = await parseAmount(humanAmount, vaultContract);
console.log(`🔄 Redeeming ${humanAmount} shares...`);
if (shareBalance < shareAmount) {
throw new Error(`Insufficient shares. You have ${readableShares} but trying to redeem ${humanAmount}`);
}
withdrawTx = await vaultContract.redeem(
shareAmount,
wallet.address,
wallet.address
);
} else {
throw new Error('Invalid method. Use "assets" or "shares"');
}
const receipt = await withdrawTx.wait();
console.log(`✅ Withdrawal successful: ${receipt.hash}`);
// Show updated balances
const newShareBalance = await vaultContract.balanceOf(wallet.address);
const newReadableShares = await formatAmount(newShareBalance, vaultContract);
console.log(`💎 Remaining shares: ${newReadableShares}`);
const tokenBalance = await debtToken.balanceOf(wallet.address);
const readableBalance = await formatAmount(tokenBalance, debtToken);
console.log(`💰 Token balance: ${readableBalance}`);
return receipt;
} catch (error) {
console.error('❌ Withdrawal failed:', error.message);
throw error;
}
}
// Example usage
const amount = process.argv[2] || '50';
const method = process.argv[3] || 'assets';
console.log(`\n📋 Usage examples:`);
console.log(` node withdraw.js 0 - Withdraw ALL shares`);
console.log(` node withdraw.js 50 - Withdraw 50 assets`);
console.log(` node withdraw.js 25 shares - Redeem 25 shares`);
console.log(`\n🎯 Current command: ${amount === '0' ? 'Withdraw ALL shares' : `${method} withdrawal of ${amount}`}`);
withdrawFromVault(parseFloat(amount), method);
Run: npm run withdraw
, node withdraw.js 50 assets
, or node withdraw.js 50 shares
Here are some explanations for the commands above:
- Withdraw 50 assets (default)
- Withdraw 50 debt assets
- Redeem 50 vault shares
3. Using Multiple Addresses
For advanced strategies, you may want to distribute operations across multiple addresses. This is useful for:
- Risk management - Spreading exposure
- Privacy - Avoiding large single-address positions
- Strategy isolation - Different approaches per address
In the deposits.js
below, we will show you how to use multiple accounts derived from the same private key to deposit into the same vault.
As a side note - by default the codes below require you to put the seed phrases in the .env
file, this is because the addresses derived from the seed phrases will be in the same sequence as you see on your wallet. You can also use the createWalletSet
in the codes below instead which uses your private key (but those addresses will NOT be the same as you see on your wallet client like MetaMask/Rabby).
import { parseAmount, formatAmount, provider, ethers, floorToDecimals } from './config.js';
// Helper function to derive multiple wallets
export function createWalletSet(basePrivateKey, count = 1) {
const wallets = [];
// Ensure the private key has the 0x prefix and convert to bytes
const formattedKey = basePrivateKey.startsWith('0x') ? basePrivateKey : `0x${basePrivateKey}`;
const keyBytes = ethers.getBytes(formattedKey);
for (let i = 0; i < count; i++) {
const derivedKey = ethers.keccak256(
ethers.concat([keyBytes, ethers.toBeHex(i, 32)])
);
const derivedWallet = new ethers.Wallet(derivedKey, provider);
wallets.push(derivedWallet);
}
return wallets;
}
// Generate wallets using BIP44 derivation (matches MetaMask/Rabby)
function createWalletSetBIP44(addressCount) {
const mnemonicInstance = ethers.Mnemonic.fromPhrase(process.env.SEED_PHRASE);
const wallets = [];
for (let i = 0; i < addressCount; i++) {
const wallet = ethers.HDNodeWallet.fromMnemonic(mnemonicInstance, `m/44'/60'/0'/0/${i}`);
const connectedWallet = wallet.connect(provider);
wallets.push(connectedWallet);
}
return wallets;
}
async function portfolioVaultDeposits(baseAmount, addressCount, options = {}) {
try {
const {
staggerDelay = 2000, // 2 seconds between operations
amountVariation = 0.1 // 10% variation
} = options;
console.log(`\n🏢 Starting portfolio deposits across ${addressCount} addresses...`);
// const wallets = createWalletSet(process.env.PRIVATE_KEY, addressCount);
const wallets = createWalletSetBIP44(addressCount);
const results = [];
for (let i = 0; i < wallets.length; i++) {
const currentWallet = wallets[i];
console.log(`\n📍 Address ${i + 1}/${addressCount}: ${currentWallet.address}`);
// Add some variation to amounts
const variation = 1 + (Math.random() - 0.5) * 2 * amountVariation;
const rawAmount = baseAmount * variation;
const adjustedAmount = floorToDecimals(rawAmount, 6);
// Create the contract instances for this wallet
const debtToken = new ethers.Contract(
process.env.DEBT_TOKEN_ADDRESS,
['function approve(address,uint256) external returns (bool)', 'function balanceOf(address) external view returns (uint256)', 'function decimals() external view returns (uint8)'],
currentWallet
);
const vaultContract = new ethers.Contract(
process.env.VAULT_ADDRESS,
['function deposit(uint256,address) external returns (uint256)', 'function balanceOf(address) external view returns (uint256)'],
currentWallet
);
try {
// Check balance
const balance = await debtToken.balanceOf(currentWallet.address);
const readableBalance = await formatAmount(balance, debtToken);
if (parseFloat(readableBalance) < adjustedAmount) {
console.log(`⚠️ Insufficient balance (${readableBalance}), skipping`);
continue;
}
const contractAmount = await parseAmount(adjustedAmount, debtToken);
// Approve and deposit
const approveTx = await debtToken.approve(process.env.VAULT_ADDRESS, contractAmount);
await approveTx.wait();
console.log(`📥 Depositing ${adjustedAmount.toFixed(2)} tokens...`);
const depositTx = await vaultContract.deposit(contractAmount, currentWallet.address);
const receipt = await depositTx.wait();
console.log(`✅ Success: ${receipt.hash}`);
results.push({
address: currentWallet.address,
amount: adjustedAmount,
txHash: receipt.hash,
status: 'success'
});
} catch (error) {
console.log(`❌ Error: ${error.message}`);
results.push({
address: currentWallet.address,
amount: adjustedAmount,
error: error.message,
status: 'failed'
});
}
// Wait between operations
if (i < wallets.length - 1) {
await new Promise(resolve => setTimeout(resolve, staggerDelay));
}
}
// Summary
const successful = results.filter(r => r.status === 'success').length;
console.log(`\n📊 Portfolio Summary: ${successful}/${addressCount} successful deposits`);
return results;
} catch (error) {
console.error('Portfolio operation failed:', error);
throw error;
}
}
// Example usage
const baseAmount = parseFloat(process.argv[2]) || 100;
const addressCount = parseInt(process.argv[3]) || 5;
console.log(`\n📋 Portfolio Vault Deposits - Usage Examples:`);
console.log(` node deposits.js 100 3 - Deposit ~100 tokens across 3 addresses`);
console.log(` node deposits.js 50 10 - Deposit ~50 tokens across 10 addresses`);
console.log(` node deposits.js 250 - Deposit ~250 tokens across 5 addresses (default)`);
console.log(` node deposits.js - Deposit ~100 tokens across 5 addresses (defaults)`);
portfolioVaultDeposits(baseAmount, addressCount);
Run: node deposits.js 100 10
, and this will create 10 addresses, each depositing 100 debt assets (such as 100 wETH) to a vault, as specified in the .env
file.
Conclusion
You now have the tools to programmatically interact with TermMax vaults:
- ✅ Deposit funds into a vault
- ✅ Withdraw returns when needed
- ✅ Deposits funds in a vault from various addresses
The programmatic approach gives you precision, consistency, and the ability to implement complex strategies that would be difficult to execute manually.
Coming in Part 2:
- Direct market lending at fixed rates
- Selling FT token
Stay tuned.