Initial commit: POE2 automated trade bot

Monitors pathofexile.com/trade2 for new listings, travels to seller
hideouts, buys items from public stash tabs, and stores them.

Includes persistent C# OCR daemon for fast screen capture + Windows
native OCR, web dashboard for managing trade links and settings,
and full game automation via Win32 SendInput.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Boki 2026-02-10 14:03:47 -05:00
commit 41d174195e
28 changed files with 6449 additions and 0 deletions

190
src/index.ts Normal file
View file

@ -0,0 +1,190 @@
import { Command } from 'commander';
import { loadConfig } from './config.js';
import { TradeMonitor } from './trade/TradeMonitor.js';
import { GameController } from './game/GameController.js';
import { ScreenReader } from './game/ScreenReader.js';
import { ClientLogWatcher } from './log/ClientLogWatcher.js';
import { TradeExecutor } from './executor/TradeExecutor.js';
import { TradeQueue } from './executor/TradeQueue.js';
import { BotController } from './dashboard/BotController.js';
import { DashboardServer } from './dashboard/DashboardServer.js';
import { ConfigStore } from './dashboard/ConfigStore.js';
import { logger } from './util/logger.js';
import type { Page } from 'playwright';
const program = new Command();
program
.name('poe2trade')
.description('POE2 automated trade bot')
.option('-u, --url <urls...>', 'Trade search URLs to monitor')
.option('--log-path <path>', 'Path to POE2 Client.txt')
.option('-p, --port <number>', 'Dashboard port')
.option('-c, --config <path>', 'Path to config.json', 'config.json')
.action(async (options) => {
// Load persisted config
const store = new ConfigStore(options.config);
const saved = store.settings;
// CLI/env overrides persisted values
const envConfig = loadConfig(options.url);
if (options.logPath) envConfig.poe2LogPath = options.logPath;
// Merge: CLI args > .env > config.json defaults
const config = {
...envConfig,
poe2LogPath: options.logPath || saved.poe2LogPath,
poe2WindowTitle: saved.poe2WindowTitle,
browserUserDataDir: saved.browserUserDataDir,
travelTimeoutMs: saved.travelTimeoutMs,
stashScanTimeoutMs: saved.stashScanTimeoutMs,
waitForMoreItemsMs: saved.waitForMoreItemsMs,
betweenTradesDelayMs: saved.betweenTradesDelayMs,
};
const port = parseInt(options.port, 10) || saved.dashboardPort;
// Collect all URLs: CLI args + saved links (deduped)
const allUrls = new Set<string>([
...config.tradeUrls,
...saved.links.map((l) => l.url),
]);
// Initialize bot controller with config store
const bot = new BotController(store);
// Start dashboard
const dashboard = new DashboardServer(bot, port);
await dashboard.start();
// Initialize game components
const screenReader = new ScreenReader();
const gameController = new GameController(config);
dashboard.setDebugDeps({ screenReader, gameController });
const logWatcher = new ClientLogWatcher(config.poe2LogPath);
await logWatcher.start();
dashboard.broadcastLog('info', 'Watching Client.txt for game events');
const tradeMonitor = new TradeMonitor(config);
await tradeMonitor.start(`http://localhost:${port}`);
dashboard.broadcastLog('info', 'Browser launched');
const executor = new TradeExecutor(
gameController,
screenReader,
logWatcher,
tradeMonitor,
config,
);
const tradeQueue = new TradeQueue(executor, config);
// Helper to add a trade search
const activateLink = async (url: string) => {
try {
await tradeMonitor.addSearch(url);
dashboard.broadcastLog('info', `Monitoring: ${url}`);
dashboard.broadcastStatus();
} catch (err) {
logger.error({ err, url }, 'Failed to add trade search');
dashboard.broadcastLog('error', `Failed to add: ${url}`);
}
};
// Load all saved + CLI links (only activate ones marked active)
for (const url of allUrls) {
const link = bot.addLink(url);
if (link.active) {
await activateLink(url);
} else {
dashboard.broadcastLog('info', `Loaded (inactive): ${link.name || link.label}`);
}
}
dashboard.broadcastLog('info', `Loaded ${allUrls.size} trade link(s) from config`);
// When dashboard adds a link, activate it in the trade monitor
bot.on('link-added', async (link) => {
if (link.active) {
await activateLink(link.url);
}
});
// When dashboard removes a link, deactivate it
bot.on('link-removed', async (id: string) => {
await tradeMonitor.removeSearch(id);
dashboard.broadcastLog('info', `Removed search: ${id}`);
dashboard.broadcastStatus();
});
// When dashboard toggles a link active/inactive
bot.on('link-toggled', async (data: { id: string; active: boolean; link: { url: string; name: string } }) => {
if (data.active) {
await activateLink(data.link.url);
dashboard.broadcastLog('info', `Activated: ${data.link.name || data.id}`);
} else {
await tradeMonitor.pauseSearch(data.id);
dashboard.broadcastLog('info', `Deactivated: ${data.link.name || data.id}`);
}
});
// Wire up events: when new listings appear, queue them for trading
tradeMonitor.on('new-listings', (data: { searchId: string; itemIds: string[]; page: Page }) => {
if (bot.isPaused) {
dashboard.broadcastLog('warn', `New listings (${data.itemIds.length}) skipped - bot paused`);
return;
}
// Check if this specific link is active
if (!bot.isLinkActive(data.searchId)) {
return;
}
logger.info(
{ searchId: data.searchId, itemCount: data.itemIds.length },
'New listings received, queuing trade...',
);
dashboard.broadcastLog('info', `New listings: ${data.itemIds.length} items from ${data.searchId}`);
tradeQueue.enqueue({
searchId: data.searchId,
itemIds: data.itemIds,
whisperText: '',
timestamp: Date.now(),
tradeUrl: '',
page: data.page,
});
});
// Forward executor state changes to dashboard
const stateInterval = setInterval(() => {
const execState = executor.getState();
if (bot.state !== execState) {
bot.state = execState;
dashboard.broadcastStatus();
}
}, 500);
// Graceful shutdown
const shutdown = async () => {
logger.info('Shutting down...');
clearInterval(stateInterval);
await screenReader.dispose();
await dashboard.stop();
await tradeMonitor.stop();
await logWatcher.stop();
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
logger.info(`Dashboard: http://localhost:${port}`);
logger.info(
`Monitoring ${allUrls.size} trade search(es). Press Ctrl+C to stop.`,
);
});
program.parse();