Files
binance-alpha-farm-agent/agent.user.js

842 lines
30 KiB
JavaScript

// ==UserScript==
// @name Binance Alpha Farm Agent
// @namespace http://baf.thuanle.me
// @version 2025.08.01
// @author TL
// @description Automated trading agent for Binance Alpha Farm
// @match https://www.binance.com/*
// @match https://accounts.binance.com/*
// @run-at document-idle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @grant GM_log
// @connect baf.thuanle.me
// @connect localhost
// @downloadURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js
// @updateURL https://git.thuanle.me/public/binance-alpha-farm-agent/raw/branch/main/agent.user.js
// ==/UserScript==
(async () => {
'use strict';
// ====== CONFIGURATION ======
const CONFIG = {
heartbeat_interval: 10000,
task_poll_interval: 10000,
is_debug: true,
servers: {
local: { label: '🏠 Local', url: 'http://localhost:3000' },
prod: { label: '🌐 Prod', url: 'https://baf.thuanle.me' },
}
};
// ====== STATE MANAGEMENT ======
class AppState {
constructor() {
this.data = {
// Server configuration
server_mode: 'prod',
server: null,
// Page state
current_page: null,
is_logged_in: false,
// Bot status
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
// Task state
current_task: null,
task_data: null,
current_step: null,
step_data: null,
// UI state
is_loading: false,
error_message: null
};
this.observers = new Map();
this.initialized = false;
}
// Initialize state from storage and page detection
async initialize() {
if (this.initialized) return;
// Load from storage
this.data.server_mode = await STORAGE.getServerMode();
this.data.server = CONFIG.servers[this.data.server_mode] || CONFIG.servers.prod;
this.data.bot_status = await STORAGE.getBotStatus();
// Detect current page and login state
this.data.current_page = BINANCE.detectPage();
this.data.is_logged_in = await BINANCE.isLoggedIn();
this.initialized = true;
TL.debug('STATE', 'Initialized', this.data);
}
// Get current state
getData() {
return { ...this.data };
}
// Update state with partial update
async update(updates) {
const oldData = { ...this.data };
// Apply updates
Object.assign(this.data, updates);
// Notify observers
await this.notifyObservers(oldData, this.data);
TL.debug('STATE', 'Updated', updates);
}
// Subscribe to state changes
subscribe(key, callback) {
if (!this.observers.has(key)) {
this.observers.set(key, []);
}
this.observers.get(key).push(callback);
}
// Unsubscribe from state changes
unsubscribe(key, callback) {
const callbacks = this.observers.get(key);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
// Notify all observers
async notifyObservers(oldData, newData) {
for (const [key, callbacks] of this.observers) {
const oldValue = oldData[key];
const newValue = newData[key];
if (oldValue !== newValue) {
for (const callback of callbacks) {
try {
await callback(oldValue, newValue, newData);
} catch (error) {
TL.error('STATE', `Observer error for ${key}:`, error);
}
}
}
}
}
}
// ====== STORAGE MODULE ======
const STORAGE = {
key_token: 'baf-agent-token',
key_server_mode: 'baf-server-mode',
key_bot_status: 'baf-bot-status',
getToken: () => GM_getValue(STORAGE.key_token, ''),
setToken: token => GM_setValue(STORAGE.key_token, String(token || '').trim().replace(/^Bearer\s+/i, '')),
getServerMode: () => GM_getValue(STORAGE.key_server_mode, 'prod'),
setServerMode: (mode) => GM_setValue(STORAGE.key_server_mode, mode),
getBotStatus: () => GM_getValue(STORAGE.key_bot_status, BOT_STATUS.READY_FOR_NEW_TASKS),
setBotStatus: (status) => GM_setValue(STORAGE.key_bot_status, status),
};
// ====== UTILITY MODULE ======
const TL = {
debug: (tag, msg, ...args) => CONFIG.is_debug && GM_log(`[TL] [${tag}]\n${msg}`, ...args),
log: (tag, msg, ...args) => GM_log(`[TL] [${tag}]\n${msg}`, ...args),
error: (tag, msg, ...args) => GM_log(`[TL] [ERROR] [${tag}] ❌\n${msg}`, ...args),
noti: (title, text, timeout = 2500) => {
if (typeof GM_notification === 'function') {
GM_notification({ title, text, timeout });
return true;
}
alert(`${title}\n${text}`);
return false;
},
delay: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
};
// DOM helpers
TL.dom = {
isVisible: (el) => {
if (!el) return false;
const cs = getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
return (el.offsetWidth + el.offsetHeight) > 0;
},
isDisabled: (el) => {
return el?.disabled === true || el?.getAttribute('aria-disabled') === 'true';
},
isInViewport: (el) => {
if (!el) return false;
const rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
},
click: (el) => {
if (el && typeof el.click === 'function') el.click();
},
scrollToView: (el, behavior = 'smooth') => {
el?.scrollIntoView?.({ behavior, block: 'center' });
},
};
// Network helpers
TL.net = {
gmRequest(url, init = {}) {
const headersToObject = (h) => {
const o = {};
(h || new Headers()).forEach((v, k) => { o[k] = v; });
return o;
};
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url,
method: (init.method || 'GET').toUpperCase(),
headers: headersToObject(init.headers),
data: init.body,
onload: (resp) => {
const text = resp.responseText || '';
let data = text;
const isJSON = /content-type:\s*application\/json/i.test(resp.responseHeaders || '');
if (isJSON) { try { data = JSON.parse(text); } catch { } }
TL.debug(`net`,`${init.method} ${resp.status} ${url}`, data);
resolve({
status: resp.status,
ok: resp.status >= 200 && resp.status < 300,
data,
rawText: text,
headers: null
});
},
onerror: reject,
ontimeout: () => reject(new Error('GM_xmlhttpRequest timeout')),
});
});
}
};
// ====== BAF API MODULE ======
const BAF = {
getServer: async () => {
const data = APP_STATE.getData();
return CONFIG.servers[data.server_mode] || CONFIG.servers.prod;
},
getHost: async () => {
const s = await BAF.getServer();
return s.url;
},
request: async (method, path, { params, body, headers } = {}) => {
const base = await BAF.getHost();
const url = new URL(path, base);
if (params) for (const [k, v] of Object.entries(params)) url.searchParams.append(k, v);
const h = new Headers(headers || {});
const token = await STORAGE.getToken();
if (token && !h.has('Authorization')) h.set('Authorization', `Bearer ${token}`);
const init = { method, headers: h };
if (body !== undefined && body !== null) {
if (!(body instanceof FormData) && !h.has('Content-Type')) h.set('Content-Type', 'application/json');
init.body = (h.get('Content-Type') || '').includes('application/json') && typeof body !== 'string'
? JSON.stringify(body)
: body;
}
return TL.net.gmRequest(url.toString(), init);
},
get: (path, params, init = {}) => BAF.request('GET', path, { params, headers: init.headers }),
post: (path, body, init = {}) => BAF.request('POST', path, { body, headers: init.headers }),
ping: (status) => BAF.post('/agent/ping', status),
getTasks: () => BAF.get('/agent/tasks'),
submitTaskResult: (taskId, result) => BAF.post(`/agent/tasks/${taskId}/result`, result),
};
// ====== BINANCE MODULE ======
const BINANCE_PAGES = {
LOGIN: 'login',
ALPHA_SWAP: 'alpha-swap',
ALPHA_ORDER_HISTORY: 'alpha-order-history',
UNKNOWN: 'unknown'
};
const BINANCE = {
detectPage: () => {
const pathname = window.location.pathname;
const hostname = window.location.hostname;
if (hostname === 'accounts.binance.com') {
if (pathname.includes('/login')) {
return BINANCE_PAGES.LOGIN;
}
}
if (hostname === 'www.binance.com') {
if (pathname.includes('/alpha/bsc/')) {
return BINANCE_PAGES.ALPHA_SWAP;
}
if (pathname.includes('/my/orders/alpha/orderhistory')) {
return BINANCE_PAGES.ALPHA_ORDER_HISTORY;
}
}
return BINANCE_PAGES.UNKNOWN;
},
isOnLoginPage: () => BINANCE.detectPage() === BINANCE_PAGES.LOGIN,
isOnAlphaSwapPage: () => BINANCE.detectPage() === BINANCE_PAGES.ALPHA_SWAP,
isOnAlphaOrderHistoryPage: () => BINANCE.detectPage() === BINANCE_PAGES.ALPHA_ORDER_HISTORY,
detectLoginState: () => {
if (BINANCE.isOnLoginPage()) {
return false;
}
const loginBtn = document.querySelector('#toLoginPage');
const regBtn = document.querySelector('#toRegisterPage');
if (TL.dom.isVisible?.(loginBtn) || TL.dom.isVisible?.(regBtn)) return false;
if (!loginBtn && !regBtn) return true;
return null;
},
isLoggedIn: async (timeoutMs = 6000, pollMs = 200) => {
const deadline = Date.now() + timeoutMs;
TL.debug(`BINANCE`,`isLoggedIn: Checking login state...`);
while (Date.now() < deadline) {
const state = BINANCE.detectLoginState();
if (state !== null) {
return state;
}
TL.debug(`BINANCE`,`isLoggedIn: not found. Sleeping...`);
await TL.delay(pollMs);
}
const fallback = BINANCE.detectLoginState() ?? false;
TL.debug(`BINANCE`,`isLoggedIn (timeout, fallback) => ${fallback ? 'true' : 'false'}`);
return fallback;
},
navigateToLogin: () => {
const loginBtn = document.querySelector('#toLoginPage');
if (TL.dom.isVisible?.(loginBtn)) {
TL.dom.click(loginBtn);
}
},
navigateToAlphaSwap: () => {
if (!BINANCE.isOnAlphaSwapPage()) {
window.location.href = 'https://www.binance.com/alpha/bsc/';
}
},
navigateToAlphaOrderHistory: () => {
if (!BINANCE.isOnAlphaOrderHistoryPage()) {
window.location.href = 'https://www.binance.com/en/my/orders/alpha/orderhistory';
}
},
};
// ====== TASK & STEP SYSTEM ======
// Task Types Enum
const TASK_TYPES = {
LOGIN: 'login',
GET_ORDER_HISTORY: 'get_order_history',
GET_BALANCE: 'get_balance',
SWAP: 'swap',
NO_TASK: 'no_task'
};
// Bot Status Enum
const BOT_STATUS = {
READY_FOR_NEW_TASKS: 'ready-for-new-tasks',
PAUSE_AUTOMATION: 'pause-automation',
PERFORMING_TASKS: 'performing-tasks'
};
// Login Method Enum
const LOGIN_METHOD = {
QR_CODE: 'qr_code',
PASSWORD: 'password',
EMAIL: 'email'
};
// Order Type Enum
const ORDER_TYPE = {
SWAP: 'swap',
LIMIT: 'limit',
MARKET: 'market'
};
// Order Status Enum
const ORDER_STATUS = {
COMPLETED: 'completed',
PENDING: 'pending',
CANCELLED: 'cancelled'
};
// Balance Format Enum
const BALANCE_FORMAT = {
JSON: 'json',
CSV: 'csv'
};
// Gas Fee Type Enum
const GAS_FEE_TYPE = {
AUTO: 'auto',
FAST: 'fast',
SLOW: 'slow'
};
const STEP_TYPES = {
// Login task steps
NAVIGATE_TO_LOGIN: 'navigate_to_login',
SELECT_QR_CODE: 'select_qr_code',
SEND_TO_SERVER: 'send_to_server',
WAIT_FOR_LOGIN: 'wait_for_login',
REPORT_RESULT: 'report_result',
// Order history task steps
NAVIGATE_TO_ORDER_HISTORY: 'navigate_to_order_history',
EXTRACT_ORDER_DATA: 'extract_order_data',
SEND_ORDER_DATA: 'send_order_data',
// Balance task steps
NAVIGATE_TO_BALANCE: 'navigate_to_balance',
EXTRACT_BALANCE_DATA: 'extract_balance_data',
SEND_BALANCE_DATA: 'send_balance_data',
// Swap task steps
NAVIGATE_TO_SWAP: 'navigate_to_swap',
FILL_SWAP_FORM: 'fill_swap_form',
CONFIRM_SWAP: 'confirm_swap',
WAIT_FOR_SWAP: 'wait_for_swap',
// Common steps
WAIT: 'wait',
ERROR_HANDLING: 'error_handling'
};
// ====== TASK RUNNER ======
const TaskRunner = {
// Helper function to validate enum values
validateEnum(value, enumObj, defaultValue) {
const validValues = Object.values(enumObj);
if (validValues.includes(value)) {
return value;
}
TL.debug('TASK', `Invalid enum value: ${value}, using default: ${defaultValue}`);
return defaultValue;
},
// Helper function to validate and merge task data with defaults
validateTaskData(taskType, taskData = {}) {
const mergedData = { ...taskData };
// Validate enum fields if present
if (taskType === TASK_TYPES.LOGIN && mergedData.method) {
mergedData.method = this.validateEnum(mergedData.method, LOGIN_METHOD, LOGIN_METHOD.QR_CODE);
}
if (taskType === TASK_TYPES.GET_ORDER_HISTORY) {
if (mergedData.order_types) {
mergedData.order_types = mergedData.order_types.map(type =>
this.validateEnum(type, ORDER_TYPE, ORDER_TYPE.SWAP)
);
}
if (mergedData.status) {
mergedData.status = mergedData.status.map(status =>
this.validateEnum(status, ORDER_STATUS, ORDER_STATUS.COMPLETED)
);
}
}
if (taskType === TASK_TYPES.GET_BALANCE && mergedData.format) {
mergedData.format = this.validateEnum(mergedData.format, BALANCE_FORMAT, BALANCE_FORMAT.JSON);
}
if (taskType === TASK_TYPES.SWAP && mergedData.gas_fee && typeof mergedData.gas_fee === 'string') {
mergedData.gas_fee = this.validateEnum(mergedData.gas_fee, GAS_FEE_TYPE, GAS_FEE_TYPE.AUTO);
}
TL.debug('TASK', `Validated task data for ${taskType}:`, mergedData);
return mergedData;
},
// Helper function to get current task data
getCurrentTaskData() {
const data = APP_STATE.getData();
return data.task_data || {};
},
// Helper function to update task data
async updateTaskData(newData) {
const currentData = this.getCurrentTaskData();
const updatedData = { ...currentData, ...newData };
await APP_STATE.update({ task_data: updatedData });
return updatedData;
},
// Helper function to check if current task is of specific type
isCurrentTaskType(taskType) {
const data = APP_STATE.getData();
return data.current_task?.type === taskType;
},
// Helper function to get current task type
getCurrentTaskType() {
const data = APP_STATE.getData();
return data.current_task?.type || null;
},
async checkForNewTasks() {
try {
const data = APP_STATE.getData();
if (data.bot_status !== BOT_STATUS.READY_FOR_NEW_TASKS) {
TL.debug('TASK', 'Bot not ready for new tasks, status:', data.bot_status);
return;
}
const response = await BAF.getTasks();
if (response.ok && response.data.task) {
const task = response.data.task;
TL.log('TASK', `Received new task: ${task.type}`, task);
// Validate and merge task data with defaults
const validatedTaskData = TaskRunner.validateTaskData(task.type, task.data);
await APP_STATE.update({
current_task: task,
task_data: validatedTaskData,
current_step: task.steps?.[0] || null,
bot_status: BOT_STATUS.PERFORMING_TASKS
});
// Start executing the task
await this.executeTask(task);
} else if (response.ok && response.data.no_task) {
TL.debug('TASK', 'No new tasks available');
// Sleep for interval then check again
setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval);
}
} catch (error) {
TL.error('TASK', 'Failed to check for new tasks:', error);
// Retry after interval
setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval);
}
},
async executeTask(task) {
TL.log('TASK', `Executing task: ${task.type}`, task);
try {
await APP_STATE.update({ is_loading: true });
let result = null;
switch (task.type) {
case TASK_TYPES.LOGIN:
result = await this.executeLoginTask(task);
break;
case TASK_TYPES.GET_ORDER_HISTORY:
result = await this.executeGetOrderHistoryTask(task);
break;
case TASK_TYPES.GET_BALANCE:
result = await this.executeGetBalanceTask(task);
break;
case TASK_TYPES.SWAP:
result = await this.executeSwapTask(task);
break;
default:
throw new Error(`Unknown task type: ${task.type}`);
}
// Submit result to server
await BAF.submitTaskResult(task.id, result);
// Task completed, return to ready state
await APP_STATE.update({
current_task: null,
task_data: null,
current_step: null,
step_data: null,
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
is_loading: false
});
// Check for next task after a short delay
setTimeout(() => this.checkForNewTasks(), 1000);
} catch (error) {
TL.error('TASK', `Task execution failed:`, error);
await APP_STATE.update({
error_message: error.message,
bot_status: BOT_STATUS.READY_FOR_NEW_TASKS,
is_loading: false
});
// Retry after interval
setTimeout(() => this.checkForNewTasks(), CONFIG.task_poll_interval);
}
},
async executeLoginTask(task) {
const taskData = this.getCurrentTaskData();
TL.log('TASK', 'Executing login task', taskData);
// Implementation for login task with steps
// taskData contains: { method: 'qr_code' | 'password', credentials: {...}, timeout: 300000, retry_count: 3 }
// Example: Update task data during execution
await this.updateTaskData({
start_time: Date.now(),
attempts: 0
});
return { success: true, message: 'Login task completed' };
},
async executeGetOrderHistoryTask(task) {
const taskData = this.getCurrentTaskData();
TL.log('TASK', 'Executing get order history task', taskData);
// Implementation for get order history task
// taskData contains: { limit: 100, offset: 0, date_range: {...}, order_types: [...], status: [...] }
return { success: true, order_history: [] };
},
async executeGetBalanceTask(task) {
const taskData = this.getCurrentTaskData();
TL.log('TASK', 'Executing get balance task', taskData);
// Implementation for get balance task
// taskData contains: { currencies: ['USDT', 'BTC'], include_zero: false, include_locked: true, format: 'json' }
return { success: true, balance: {} };
},
async executeSwapTask(task) {
const taskData = this.getCurrentTaskData();
TL.log('TASK', 'Executing swap task', taskData);
// Implementation for swap task
// taskData contains: { from_token: 'USDT', to_token: 'BTC', amount: 100, slippage: 0.5, gas_fee: 'auto', deadline: 300, auto_confirm: true }
return { success: true, swap_result: {} };
}
};
// ====== STATE OBSERVERS ======
const StateWatchers = {
// Storage observer - saves state changes to storage
async onServerModeChange(oldValue, newValue, data) {
TL.debug('OBSERVER', `Server mode changed: ${oldValue} -> ${newValue}`);
await STORAGE.setServerMode(newValue);
// Update server info
await APP_STATE.update({
server: CONFIG.servers[newValue] || CONFIG.servers.prod
});
// Reload page after a short delay
setTimeout(() => location.reload(), 500);
},
async onBotStatusChange(oldValue, newValue, data) {
TL.debug('OBSERVER', `Bot status changed: ${oldValue} -> ${newValue}`);
await STORAGE.setBotStatus(newValue);
// Handle status-specific logic
if (newValue === BOT_STATUS.READY_FOR_NEW_TASKS) {
// Start checking for new tasks
TaskRunner.checkForNewTasks();
}
},
async onCurrentPageChange(oldValue, newValue, data) {
TL.debug('OBSERVER', `Page changed: ${oldValue} -> ${newValue}`);
// Handle page-specific logic for current task
if (data.current_task && data.current_step) {
// Continue with current step based on page change
await TaskRunner.continueCurrentStep();
}
},
async onLoginStateChange(oldValue, newValue, data) {
TL.debug('OBSERVER', `Login state changed: ${oldValue} -> ${newValue}`);
// Handle login state change for current task
if (data.current_task && data.current_step) {
await TaskRunner.continueCurrentStep();
}
}
};
// ====== MENU SYSTEM ======
async function createGM_Menu() {
const data = APP_STATE.getData();
const curSrv = data.server;
GM_registerMenuCommand(`Server: ${curSrv.label} (${curSrv.url})`, async () => {
try {
const next = (data.server_mode === 'local') ? 'prod' : 'local';
await APP_STATE.update({ server_mode: next });
const nsv = CONFIG.servers[next];
const msg = `Switched to ${nsv.label} (${nsv.url})`;
TL.debug(`BAF`, msg);
TL.noti('BAF Server Switched', msg);
} catch (e) {
TL.error('BAF', 'switch server error', e);
TL.noti('BAF Server Switched', `Switch server error: ${e.message}`);
}
});
GM_registerMenuCommand('Token', async () => {
try {
const curToken = await STORAGE.getToken();
const input = prompt('Bearer token:', curToken || '');
if (input !== null && input.trim() && input.trim() !== curToken) {
await STORAGE.setToken(input);
}
const s = await BAF.getServer();
const res = await BAF.ping();
const resStr =
`Server: ${s.label} (${s.url})\n` +
`Status: ${res.ok ? 'Connected ✅' : 'Failed ❌'} (${res.status})`;
TL.debug(`BAF`, resStr);
TL.noti('BAF Server', resStr);
} catch (e) {
const resStr = `ping error: ${e.message}`;
TL.error('BAF', resStr);
TL.noti('BAF Server', resStr);
}
});
GM_registerMenuCommand('Bot Status', async () => {
const data = APP_STATE.getData();
const statusOptions = [
BOT_STATUS.READY_FOR_NEW_TASKS,
BOT_STATUS.PAUSE_AUTOMATION,
BOT_STATUS.PERFORMING_TASKS
];
const currentIndex = statusOptions.indexOf(data.bot_status);
const nextIndex = (currentIndex + 1) % statusOptions.length;
const nextStatus = statusOptions[nextIndex];
await APP_STATE.update({ bot_status: nextStatus });
TL.noti('Bot Status', `Changed to: ${nextStatus}`);
});
GM_registerMenuCommand('Get Tasks', async () => {
await TaskRunner.checkForNewTasks();
});
}
// ====== HEARTBEAT ======
async function heartbeat_report() {
try {
const data = APP_STATE.getData();
const status = {
logged_in: data.is_logged_in,
current_page: data.current_page,
bot_status: data.bot_status,
current_task: data.current_task?.type || null,
task_data: data.task_data || null,
current_step: data.current_step || null,
has_current_task: !!data.current_task
};
TL.debug(`HEARTBEAT`, `${JSON.stringify(status, null, 2)}`);
await BAF.ping(status);
return status;
} catch (e) {
TL.error('HEARTBEAT', e.message);
return null;
}
}
// ====== PAGE MONITOR ======
async function monitorPageChanges() {
let lastPage = APP_STATE.getData().current_page;
setInterval(async () => {
const currentPage = BINANCE.detectPage();
if (currentPage !== lastPage) {
await APP_STATE.update({ current_page: currentPage });
lastPage = currentPage;
}
}, 1000);
}
// ====== INITIALIZATION ======
const APP_STATE = new AppState();
async function initialize() {
// Initialize state
await APP_STATE.initialize();
// Register observers
APP_STATE.subscribe('server_mode', StateWatchers.onServerModeChange);
APP_STATE.subscribe('bot_status', StateWatchers.onBotStatusChange);
APP_STATE.subscribe('current_page', StateWatchers.onCurrentPageChange);
APP_STATE.subscribe('is_logged_in', StateWatchers.onLoginStateChange);
// Create menu
await createGM_Menu();
// Start monitoring
monitorPageChanges();
// Start heartbeat
setInterval(heartbeat_report, CONFIG.heartbeat_interval);
// Welcome message
const ping_res = await BAF.ping();
const data = APP_STATE.getData();
const res =
`Server: ${data.server.label} (${data.server.url})\n` +
`Status: ${ping_res.ok ? 'Connected ✅' : 'Failed ❌'} (${ping_res.status})\n` +
`Page: ${data.current_page || 'unknown'} (${window.location.href})\n` +
`Bot Status: ${data.bot_status}`;
TL.log(`BAF`, res);
// Start initial task checking if ready
if (data.bot_status === BOT_STATUS.READY_FOR_NEW_TASKS) {
TaskRunner.checkForNewTasks();
}
}
// Start the application
await initialize();
})();