Skip to content

Instantly share code, notes, and snippets.

@Cahl-Dee
Last active January 21, 2026 20:42
Show Gist options
  • Select an option

  • Save Cahl-Dee/3ea7ad93ddb6f29e3ce6db08bc4b6018 to your computer and use it in GitHub Desktop.

Select an option

Save Cahl-Dee/3ea7ad93ddb6f29e3ce6db08bc4b6018 to your computer and use it in GitHub Desktop.
/**
* Grid Trading API - Consumption Balance Maintainer
*
* This script monitors your consumption balance and automatically purchases units
* when the balance drops below the minimum threshold (default: 1 unit).
*
* Prerequisites:
* - Node.js 18+ with ES modules support
* - Environment variables configured in .env file
*
* Environment Configuration:
* The script supports multiple environments (dev, demo, prod, etc.).
* For each environment, use the following naming pattern:
*
* Required:
* - GRID_{ENV}_TRADING_PRIVATE_KEY: Base64-encoded PKCS#8 Ed25519 private key
* - GRID_{ENV}_TRADING_PUBLIC_KEY: Base64-encoded SPKI Ed25519 public key
* - GRID_{ENV}_TRADING_BASE_URL: Trading API base URL
* - GRID_{ENV}_{INSTRUMENT}_MARKET_ID: Market ID for the instrument (e.g., market_b310e860-97cd-45eb-bdc3-5be0b79295d0)
* - GRID_{ENV}_{INSTRUMENT}_INSTRUMENT_ID: Instrument ID (e.g., instrument_e0b04b8a-ce1f-4ccc-a4ca-2a47825d69b0)
*
* Optional (environment-specific):
* - GRID_{ENV}_TRADING_FINGERPRINT: Optional fingerprint
* - GRID_{ENV}_MIN_BALANCE: Minimum units to maintain
* - GRID_{ENV}_BUY_QUANTITY: Units to buy when replenishing
* - GRID_{ENV}_CHECK_INTERVAL: Seconds between balance checks
* - GRID_{ENV}_AUTO_TRANSFER: 'true' or 'false' - whether auto-transfer is enabled (default: 'true')
*
* You can also use non-prefixed variables (GRID_TRADING_PRIVATE_KEY, etc.)
* as fallbacks or for a default environment.
*
* Global optional variables (apply to all environments):
* - RUN_ONCE: If 'true', check once and exit; otherwise run continuously
*
* Usage:
* node maintain-balance.js <instrument-name> [environment]
* node maintain-balance.js chat-fast dev
* node maintain-balance.js chat-prime demo
* RUN_ONCE=true node maintain-balance.js chat-fast dev
* ENV=prod node maintain-balance.js chat-fast
*
* Instrument name must be either "chat-fast" or "chat-prime"
*/
import axios from 'axios';
import nacl from 'tweetnacl';
import util from 'tweetnacl-util';
import crypto from 'crypto';
import dotenv from 'dotenv';
dotenv.config();
/**
* Gets the instrument name from command line args.
* @returns {string} Instrument name ('chat-fast' or 'chat-prime')
*/
function getInstrumentName() {
const instrumentName = process.argv[2];
if (!instrumentName) {
console.error('Error: Instrument name is required');
console.error('Usage: node maintain-balance.js <instrument-name> [environment]');
console.error(' Instrument name must be either "chat-fast" or "chat-prime"');
process.exit(1);
}
const normalized = instrumentName.toLowerCase();
if (normalized !== 'chat-fast' && normalized !== 'chat-prime') {
console.error(`Error: Invalid instrument name "${instrumentName}"`);
console.error(' Instrument name must be either "chat-fast" or "chat-prime"');
process.exit(1);
}
return normalized;
}
/**
* Gets the environment name from command line args or environment variable.
* @returns {string} Environment name (default: 'default')
*/
function getEnvironmentName() {
// Check command line argument (second arg, after instrument name)
const argEnv = process.argv[3];
if (argEnv && !argEnv.startsWith('-')) {
return argEnv.toLowerCase();
}
// Then check environment variable
return (process.env.ENV || process.env.GRID_ENV || 'default').toLowerCase();
}
/**
* Gets an environment variable with environment-specific prefix, falling back to non-prefixed.
* @param {string} envName - Environment name
* @param {string} varName - Variable name (with TRADING_ prefix, e.g., 'TRADING_PRIVATE_KEY')
* @param {string} [defaultValue] - Default value if not found
* @returns {string|undefined} Environment variable value
*/
function getEnvVar(envName, varName, defaultValue = undefined) {
const envPrefix = envName === 'default' ? '' : `GRID_${envName.toUpperCase()}_`;
const prefixedName = envPrefix ? `${envPrefix}${varName}` : `GRID_${varName}`;
const fallbackName = `GRID_${varName}`;
return process.env[prefixedName] || process.env[fallbackName] || defaultValue;
}
/**
* Gets a non-trading environment variable (for settings like MARKET_ID, MIN_BALANCE, etc.)
* @param {string} envName - Environment name
* @param {string} varName - Variable name (without prefix)
* @param {string} [defaultValue] - Default value if not found
* @returns {string|undefined} Environment variable value
*/
function getNonTradingEnvVar(envName, varName, defaultValue = undefined) {
const envPrefix = envName === 'default' ? '' : `GRID_${envName.toUpperCase()}_`;
const prefixedName = envPrefix ? `${envPrefix}${varName}` : `GRID_${varName}`;
const fallbackName = `GRID_${varName}`;
return process.env[prefixedName] || process.env[fallbackName] || defaultValue;
}
/**
* Gets an instrument-specific environment variable (for MARKET_ID and INSTRUMENT_ID).
* Format: GRID_{ENV}_{INSTRUMENT}_VAR_NAME (e.g., GRID_DEV_CHAT_FAST_INSTRUMENT_ID)
* @param {string} envName - Environment name
* @param {string} instrumentName - Instrument name (e.g., 'chat-fast', 'chat-prime')
* @param {string} varName - Variable name (e.g., 'INSTRUMENT_ID', 'MARKET_ID')
* @param {string} [defaultValue] - Default value if not found
* @returns {string|undefined} Environment variable value
*/
function getInstrumentEnvVar(envName, instrumentName, varName, defaultValue = undefined) {
// Convert instrument name to uppercase with underscores (chat-fast -> CHAT_FAST)
const instrumentUpper = instrumentName.toUpperCase().replace(/-/g, '_');
// Try environment-specific instrument variable first
const envPrefix = envName === 'default' ? '' : `GRID_${envName.toUpperCase()}_`;
const prefixedName = envPrefix ? `${envPrefix}${instrumentUpper}_${varName}` : `GRID_${instrumentUpper}_${varName}`;
// Fallback to non-prefixed instrument variable
const fallbackName = `GRID_${instrumentUpper}_${varName}`;
// Final fallback to old format (for backward compatibility)
const oldFormatName = envPrefix ? `${envPrefix}${varName}` : `GRID_${varName}`;
return process.env[prefixedName] || process.env[fallbackName] || process.env[oldFormatName] || defaultValue;
}
/**
* Loads configuration for the specified environment and instrument.
* @param {string} envName - Environment name
* @param {string} instrumentName - Instrument name (e.g., 'chat-fast', 'chat-prime')
* @returns {Object} Configuration object
*/
function loadConfig(envName, instrumentName) {
const autoTransferStr = getNonTradingEnvVar(envName, 'AUTO_TRANSFER', 'true');
const config = {
envName,
instrumentName,
baseUrl: getEnvVar(envName, 'TRADING_BASE_URL'),
privateKey: getEnvVar(envName, 'TRADING_PRIVATE_KEY'),
publicKey: getEnvVar(envName, 'TRADING_PUBLIC_KEY'),
fingerprint: getEnvVar(envName, 'TRADING_FINGERPRINT'),
marketId: getInstrumentEnvVar(envName, instrumentName, 'MARKET_ID'),
instrumentId: getInstrumentEnvVar(envName, instrumentName, 'INSTRUMENT_ID'),
minBalance: parseFloat(getNonTradingEnvVar(envName, 'MIN_BALANCE', '1')),
buyQuantity: parseInt(getNonTradingEnvVar(envName, 'BUY_QUANTITY', '5'), 10),
checkInterval: parseInt(getNonTradingEnvVar(envName, 'CHECK_INTERVAL', '60'), 10) * 1000,
autoTransfer: autoTransferStr.toLowerCase() === 'true',
runOnce: process.env.RUN_ONCE === 'true'
};
return config;
}
/**
* Signature-based authentication handler for Grid Trading API.
*
* Implements Ed25519 signature authentication using DER-encoded keys.
* The signature is computed over: timestamp + HTTP method + path + request body
*/
class SignatureAuth {
/**
* Creates a new SignatureAuth instance.
*
* @param {string} privateKeyBase64 - Base64-encoded PKCS#8 Ed25519 private key
* @param {string} publicKeyBase64 - Base64-encoded SPKI Ed25519 public key
* @param {string} [fingerprint] - Optional fingerprint identifier
*/
constructor(privateKeyBase64, publicKeyBase64, fingerprint) {
// Parse DER-encoded keys (PKCS#8 for private, SubjectPublicKeyInfo for public)
const privateKeyDer = Buffer.from(privateKeyBase64, 'base64');
const publicKeyDer = Buffer.from(publicKeyBase64, 'base64');
// Extract raw Ed25519 keys from DER structure
// For PKCS#8 Ed25519: private key (seed) is the last 32 bytes of the OCTET STRING
// For SPKI Ed25519: public key is the last 32 bytes of the BIT STRING
const rawPrivateKey = privateKeyDer.slice(-32);
const rawPublicKey = publicKeyDer.slice(-32);
// tweetnacl.sign.detached requires a 64-byte key (seed + public key)
// Construct the 64-byte private key: [seed (32 bytes)][public key (32 bytes)]
this.privateKey = new Uint8Array(64);
this.privateKey.set(rawPrivateKey, 0);
this.privateKey.set(rawPublicKey, 32);
// Generate fingerprint from public key if not provided
const publicKeyBuffer = Buffer.from(publicKeyBase64, 'base64');
const publicKeyHash = crypto.createHash('sha256').update(publicKeyBuffer).digest('base64');
this.fingerprint = fingerprint || publicKeyHash.replace(/=+$/, '');
}
/**
* Generates authentication headers for an API request.
*
* @param {string} method - HTTP method (e.g., 'POST', 'GET')
* @param {string} path - API endpoint path
* @param {string} [body=''] - Request body as string
* @returns {Object} Headers object with signature, timestamp, and fingerprint
*/
getHeaders(method, path, body = '') {
const timestamp = Math.floor(Date.now() / 1000).toString();
const message = `${timestamp}${method.toUpperCase()}${path}${body}`;
const messageBytes = util.decodeUTF8(message);
const signatureBytes = nacl.sign.detached(messageBytes, this.privateKey);
return {
'x-thegrid-signature': util.encodeBase64(signatureBytes),
'x-thegrid-timestamp': timestamp,
'x-thegrid-fingerprint': this.fingerprint
};
}
}
/**
* Gets the environment prefix for error messages.
* @param {string} envName - Environment name
* @returns {string} Environment prefix string
*/
function getEnvPrefix(envName) {
return envName === 'default' ? '' : `GRID_${envName.toUpperCase()}_`;
}
/**
* Validates that required configuration is present.
* @param {Object} config - Configuration object
*/
function validateConfig(config) {
const envPrefix = getEnvPrefix(config.envName);
if (!config.privateKey) {
console.error(`Missing required configuration: ${envPrefix}TRADING_PRIVATE_KEY or GRID_TRADING_PRIVATE_KEY`);
process.exit(1);
}
if (!config.publicKey) {
console.error(`Missing required configuration: ${envPrefix}TRADING_PUBLIC_KEY or GRID_TRADING_PUBLIC_KEY`);
process.exit(1);
}
if (!config.instrumentId) {
const instrumentUpper = config.instrumentName.toUpperCase().replace(/-/g, '_');
console.error(`Missing required configuration: ${envPrefix}${instrumentUpper}_INSTRUMENT_ID or GRID_${instrumentUpper}_INSTRUMENT_ID`);
console.error(` Example: GRID_${config.envName.toUpperCase()}_${instrumentUpper}_INSTRUMENT_ID`);
process.exit(1);
}
}
/**
* Gets all instruments and returns a map of instrument_id -> instrument details.
*
* @param {SignatureAuth} auth - Authentication instance
* @param {string} baseUrl - Base URL for the API
* @returns {Promise<Map<string, Object>>} Map of instrument_id to instrument details
*/
async function getAllInstrumentsMap(auth, baseUrl) {
const path = '/api/v1/trading/instruments';
try {
const response = await axios.get(`${baseUrl}${path}`, {
headers: auth.getHeaders('GET', path)
});
const instruments = response.data.data || [];
const instrumentMap = new Map();
for (const instrument of instruments) {
if (instrument.instrument_id) {
instrumentMap.set(instrument.instrument_id, instrument);
}
}
return instrumentMap;
} catch (error) {
if (error.response) {
throw new Error(
`Failed to get instruments: ${error.response.status} - ${JSON.stringify(error.response.data)}`
);
}
throw error;
}
}
/**
* Gets instrument details by instrument ID.
*
* @param {SignatureAuth} auth - Authentication instance
* @param {string} baseUrl - Base URL for the API
* @param {string} instrumentId - Instrument ID to look up
* @returns {Promise<Object|null>} Instrument data object or null if not found
*/
async function getInstrumentDetails(auth, baseUrl, instrumentId) {
const instrumentMap = await getAllInstrumentsMap(auth, baseUrl);
return instrumentMap.get(instrumentId) || null;
}
/**
* Gets the current consumption balance for all instruments.
*
* @param {SignatureAuth} auth - Authentication instance
* @param {string} baseUrl - Base URL for the API
* @returns {Promise<Array>} Array of consumption account objects
*/
async function getConsumptionBalance(auth, baseUrl) {
const path = '/api/v1/trading/consumption-accounts';
try {
const response = await axios.get(`${baseUrl}${path}?order_by=created_at`, {
headers: auth.getHeaders('GET', path)
});
return response.data.data || [];
} catch (error) {
if (error.response) {
throw new Error(
`Failed to get consumption balance: ${error.response.status} - ${JSON.stringify(error.response.data)}`
);
}
throw error;
}
}
/**
* Places a market buy order for units.
*
* @param {SignatureAuth} auth - Authentication instance
* @param {string} baseUrl - Base URL for the API
* @param {string} marketId - Market ID
* @param {number} quantity - Number of units to buy
* @returns {Promise<Object>} Order response data
*/
async function placeBuyOrder(auth, baseUrl, marketId, quantity) {
const orderData = {
market_id: marketId,
side: 'buy',
type: 'market',
quantity: quantity,
time_in_force: 'gtc',
client_order_id: `maintain-balance-${Date.now()}`
};
const path = '/api/v1/trading/orders';
const body = JSON.stringify(orderData);
try {
const response = await axios.post(`${baseUrl}${path}`, body, {
headers: {
'Content-Type': 'application/json',
...auth.getHeaders('POST', path, body)
}
});
return response.data.data;
} catch (error) {
if (error.response) {
throw new Error(
`Failed to place order: ${error.response.status} - ${JSON.stringify(error.response.data)}`
);
}
throw error;
}
}
/**
* Gets the status of an order with retry logic for transient 404s.
*
* @param {SignatureAuth} auth - Authentication instance
* @param {string} baseUrl - Base URL for the API
* @param {string} orderId - Order ID
* @param {number} maxRetries - Maximum number of retries for 404s (default: 3)
* @param {number} retryDelayMs - Initial delay between retries in ms (default: 500)
* @returns {Promise<Object>} Order data with status
*/
async function getOrderStatus(auth, baseUrl, orderId, maxRetries = 3, retryDelayMs = 500) {
const path = `/api/v1/trading/orders/${orderId}`;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await axios.get(`${baseUrl}${path}`, {
headers: auth.getHeaders('GET', path)
});
return response.data.data;
} catch (error) {
// If it's a 404 and we have retries left, wait and retry
if (error.response?.status === 404 && attempt < maxRetries) {
const delay = retryDelayMs * Math.pow(2, attempt); // Exponential backoff
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
// For other errors or if we're out of retries, throw
if (error.response) {
throw new Error(
`Failed to get order status: ${error.response.status} - ${JSON.stringify(error.response.data)}`
);
}
throw error;
}
}
}
/**
* Waits for an order to be filled, polling with retries.
*
* @param {SignatureAuth} auth - Authentication instance
* @param {string} baseUrl - Base URL for the API
* @param {string} orderId - Order ID
* @param {number} maxWaitSeconds - Maximum seconds to wait (default: 15)
* @param {number} pollIntervalSeconds - Seconds between polls (default: 2)
* @param {number} initialDelayMs - Initial delay before first status check in ms (default: 1000)
* @returns {Promise<Object>} Order data when filled
*/
async function waitForOrderFill(auth, baseUrl, orderId, maxWaitSeconds = 15, pollIntervalSeconds = 2, initialDelayMs = 1000) {
// Wait a bit for the order to be available in the API after placement
await new Promise(resolve => setTimeout(resolve, initialDelayMs));
const deadline = Date.now() + (maxWaitSeconds * 1000);
const pollInterval = pollIntervalSeconds * 1000;
let lastStatus = null;
let pollCount = 0;
while (Date.now() < deadline) {
try {
const order = await getOrderStatus(auth, baseUrl, orderId);
const status = order.status?.toLowerCase();
lastStatus = order.status;
// Check if order is filled (status should be "filled")
if (status === 'filled') {
return order;
}
// Check for terminal states that indicate failure
if (status === 'cancelled' || status === 'canceled' || status === 'rejected') {
throw new Error(`Order ${orderId} was ${order.status}`);
}
// Log current status periodically (every 5 polls to avoid spam)
pollCount++;
if (pollCount % 5 === 0) {
console.log(` Order status: ${order.status || 'unknown'}...`);
}
} catch (error) {
// If we get a 404, the order might not be available yet - keep trying
if (error.message.includes('404')) {
if (pollCount % 3 === 0) { // Log every 3rd failed attempt
console.log(` Order not yet available, retrying...`);
}
} else {
// For other errors, rethrow
throw error;
}
}
// Wait before next poll
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
// Try to get final status before timing out
try {
const finalOrder = await getOrderStatus(auth, baseUrl, orderId);
throw new Error(`Timeout waiting for order ${orderId} to fill (waited ${maxWaitSeconds}s, current status: ${finalOrder.status || 'unknown'})`);
} catch (error) {
// If we can't get final status, use last known status
if (error.message.includes('Timeout')) {
throw error; // Re-throw if it's already our timeout error
}
throw new Error(`Timeout waiting for order ${orderId} to fill (waited ${maxWaitSeconds}s, last known status: ${lastStatus || 'unknown'})`);
}
}
/**
* Transfers units from trading account to consumption account.
*
* @param {SignatureAuth} auth - Authentication instance
* @param {string} baseUrl - Base URL for the API
* @param {string} instrumentId - Instrument ID
* @param {number} quantity - Number of units to transfer
* @returns {Promise<Object>} Transfer response data
*/
async function transferToConsumption(auth, baseUrl, instrumentId, quantity) {
const transferData = {
instrument_id: instrumentId,
quantity: quantity
};
const path = '/api/v1/transfers/trading-to-consumption';
const body = JSON.stringify(transferData);
try {
const response = await axios.post(`${baseUrl}${path}`, body, {
headers: {
'Content-Type': 'application/json',
...auth.getHeaders('POST', path, body)
}
});
return response.data.data;
} catch (error) {
if (error.response) {
throw new Error(
`Failed to transfer to consumption: ${error.response.status} - ${JSON.stringify(error.response.data)}`
);
}
throw error;
}
}
/**
* Formats instrument display with friendly name and ID.
*
* @param {string} instrumentId - Instrument ID
* @param {Object|null} instrumentDetails - Instrument details object (optional)
* @returns {string} Formatted string like "Prime Inference Token (instrument_abc123)"
*/
function formatInstrumentDisplay(instrumentId, instrumentDetails) {
if (instrumentDetails) {
const friendlyName = instrumentDetails.name || instrumentDetails.symbol || instrumentId;
return `${friendlyName} (${instrumentId})`;
}
return instrumentId;
}
/**
* Checks balance and replenishes if needed.
*
* @param {SignatureAuth} auth - Authentication instance
* @param {Object} config - Configuration object
* @returns {Promise<boolean>} True if order was placed, false otherwise
*/
async function checkAndReplenish(auth, config) {
try {
const accounts = await getConsumptionBalance(auth, config.baseUrl);
// Fetch all instruments to get friendly names for display
let instrumentMap = new Map();
try {
instrumentMap = await getAllInstrumentsMap(auth, config.baseUrl);
} catch (error) {
// If we can't fetch instruments, continue without friendly names
console.warn(` Warning: Could not fetch instrument details: ${error.message}`);
}
// Display all account balances
const accountDetails = [];
let monitoredBalance = null;
for (const account of accounts) {
const available = parseFloat(account.available_balance || 0);
const isMonitored = account.instrument_id === config.instrumentId;
accountDetails.push({
account_id: account.account_id,
instrument_id: account.instrument_id,
available: available,
isMonitored: isMonitored
});
// Track the balance for the monitored instrument
if (isMonitored) {
monitoredBalance = available;
}
}
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] Consumption Account Balances:`);
if (accountDetails.length > 0) {
accountDetails.forEach(acc => {
const marker = acc.isMonitored ? ' ⭐ (monitored)' : '';
const instrumentDetails = instrumentMap.get(acc.instrument_id);
const instrumentDisplay = formatInstrumentDisplay(acc.instrument_id, instrumentDetails);
console.log(` ${instrumentDisplay}: ${acc.available.toFixed(4)} available${marker}`);
});
} else {
console.log(' No consumption accounts found');
}
// Check if monitored instrument exists
if (monitoredBalance === null) {
const instrumentDisplay = formatInstrumentDisplay(config.instrumentId, config.instrumentDetails);
console.log(`\n ⚠ Warning: Instrument '${instrumentDisplay}' not found in consumption accounts`);
console.log(` Available instruments: ${accountDetails.map(a => a.instrument_id).join(', ') || 'none'}\n`);
return false;
}
// Check balance for the monitored instrument only
const instrumentDisplay = formatInstrumentDisplay(config.instrumentId, config.instrumentDetails);
console.log(`\n Monitoring: ${instrumentDisplay}`);
console.log(` Current consumption balance: ${monitoredBalance.toFixed(4)} units (minimum: ${config.minBalance})`);
if (monitoredBalance >= config.minBalance) {
console.log(` ✓ Balance is sufficient (>= ${config.minBalance}), no action needed\n`);
return false;
}
const deficit = config.minBalance - monitoredBalance;
const buyQuantity = Math.max(config.buyQuantity, Math.ceil(deficit));
console.log(` ⚠ Balance is low (< ${config.minBalance}), placing market buy order for ${buyQuantity} unit(s)...`);
const orderResult = await placeBuyOrder(
auth,
config.baseUrl,
config.marketId,
buyQuantity
);
// Use 'id' as primary field (per OrderSchema), with 'order_id' as fallback
const orderId = orderResult.id || orderResult.order_id || orderResult.orderId;
console.log(` ✓ Order placed: ${orderId} (market, ${buyQuantity} units)`);
// If auto-transfer is disabled, wait for order to fill and then transfer manually
if (!config.autoTransfer) {
console.log(` Auto-transfer is disabled, waiting for order to fill...`);
try {
const filledOrder = await waitForOrderFill(auth, config.baseUrl, orderId, 15, 2);
// Use 'id' as primary field, with 'order_id' as fallback
const filledOrderId = filledOrder.id || filledOrder.order_id || orderId;
console.log(` ✓ Order filled: ${filledOrderId} (status: ${filledOrder.status})`);
console.log(` Transferring ${buyQuantity} unit(s) from trading to consumption account...`);
const transferResult = await transferToConsumption(
auth,
config.baseUrl,
config.instrumentId,
buyQuantity
);
console.log(` ✓ Transfer completed: ${transferResult.transfer_id || transferResult.id || 'success'}`);
} catch (error) {
console.error(` ✗ Error during transfer: ${error.message}`);
// Don't throw - order was placed successfully, transfer can be retried later
}
}
console.log('');
return true;
} catch (error) {
console.error(` ✗ Error: ${error.message}\n`);
throw error;
}
}
/**
* Main function that runs the balance maintenance loop.
*/
async function main() {
const instrumentName = getInstrumentName();
const envName = getEnvironmentName();
const config = loadConfig(envName, instrumentName);
validateConfig(config);
const auth = new SignatureAuth(
config.privateKey,
config.publicKey,
config.fingerprint
);
// Fetch instrument details to get friendly name
let instrumentDetails = null;
try {
instrumentDetails = await getInstrumentDetails(auth, config.baseUrl, config.instrumentId);
if (instrumentDetails) {
config.instrumentDetails = instrumentDetails;
}
} catch (error) {
console.warn(`Warning: Could not fetch instrument details: ${error.message}`);
console.warn(' Continuing with instrument ID only...\n');
}
console.log('Grid Consumption Balance Maintainer');
console.log('====================================');
console.log(`Environment: ${config.envName}`);
console.log(`Instrument: ${config.instrumentName}`);
const instrumentDisplay = formatInstrumentDisplay(config.instrumentId, config.instrumentDetails);
console.log(`Monitoring: ${instrumentDisplay}`);
console.log(`Minimum balance: ${config.minBalance} units`);
console.log(`Buy quantity: ${config.buyQuantity} units`);
console.log(`Order type: market`);
console.log(`Auto-transfer: ${config.autoTransfer ? 'enabled' : 'disabled'}`);
console.log(`Market ID: ${config.marketId}`);
console.log(`Base URL: ${config.baseUrl}`);
console.log(`Run mode: ${config.runOnce ? 'once' : `continuous (check every ${config.checkInterval / 1000}s)`}`);
console.log('');
if (config.runOnce) {
// Run once and exit
try {
await checkAndReplenish(auth, config);
process.exit(0);
} catch (error) {
console.error('Fatal error:', error.message);
process.exit(1);
}
} else {
// Run continuously
console.log('Starting continuous monitoring... Press Ctrl+C to stop.\n');
// Run immediately on start
await checkAndReplenish(auth, config).catch(err => {
console.error('Initial check failed:', err.message);
});
// Then run on interval
const intervalId = setInterval(async () => {
try {
await checkAndReplenish(auth, config);
} catch (error) {
console.error('Check failed:', error.message);
// Continue running even if a check fails
}
}, config.checkInterval);
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\n\nShutting down gracefully...');
clearInterval(intervalId);
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n\nShutting down gracefully...');
clearInterval(intervalId);
process.exit(0);
});
}
}
// Export for use as a module
export { SignatureAuth, getAllInstrumentsMap, getInstrumentDetails, getConsumptionBalance, placeBuyOrder, getOrderStatus, waitForOrderFill, transferToConsumption, checkAndReplenish };
// Execute when run directly
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment