// ==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(); })();