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

View file

@ -0,0 +1,129 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
import path from 'path';
import { logger } from '../util/logger.js';
export interface SavedLink {
url: string;
name: string;
active: boolean;
addedAt: string;
}
export interface SavedSettings {
paused: boolean;
links: SavedLink[];
poe2LogPath: string;
poe2WindowTitle: string;
browserUserDataDir: string;
travelTimeoutMs: number;
stashScanTimeoutMs: number;
waitForMoreItemsMs: number;
betweenTradesDelayMs: number;
dashboardPort: number;
}
const DEFAULTS: SavedSettings = {
paused: false,
links: [],
poe2LogPath: 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Path of Exile 2\\logs\\Client.txt',
poe2WindowTitle: 'Path of Exile 2',
browserUserDataDir: './browser-data',
travelTimeoutMs: 15000,
stashScanTimeoutMs: 10000,
waitForMoreItemsMs: 20000,
betweenTradesDelayMs: 5000,
dashboardPort: 3000,
};
export class ConfigStore {
private filePath: string;
private data: SavedSettings;
constructor(configPath?: string) {
this.filePath = configPath || path.resolve('config.json');
this.data = this.load();
}
private load(): SavedSettings {
if (!existsSync(this.filePath)) {
logger.info({ path: this.filePath }, 'No config.json found, using defaults');
return { ...DEFAULTS };
}
try {
const raw = readFileSync(this.filePath, 'utf-8');
const parsed = JSON.parse(raw) as Partial<SavedSettings>;
const merged = { ...DEFAULTS, ...parsed };
// Migrate old links: add name/active fields, strip /live from URLs
merged.links = merged.links.map((l) => ({
url: l.url.replace(/\/live\/?$/, ''),
name: l.name || '',
active: l.active !== undefined ? l.active : true,
addedAt: l.addedAt || new Date().toISOString(),
}));
logger.info({ path: this.filePath, linkCount: merged.links.length }, 'Loaded config.json');
return merged;
} catch (err) {
logger.warn({ err, path: this.filePath }, 'Failed to read config.json, using defaults');
return { ...DEFAULTS };
}
}
save(): void {
try {
writeFileSync(this.filePath, JSON.stringify(this.data, null, 2), 'utf-8');
} catch (err) {
logger.error({ err, path: this.filePath }, 'Failed to save config.json');
}
}
get settings(): SavedSettings {
return this.data;
}
get links(): SavedLink[] {
return this.data.links;
}
addLink(url: string, name: string = ''): void {
url = url.replace(/\/live\/?$/, '');
if (this.data.links.some((l) => l.url === url)) return;
this.data.links.push({ url, name, active: true, addedAt: new Date().toISOString() });
this.save();
}
removeLink(url: string): void {
this.data.links = this.data.links.filter((l) => l.url !== url);
this.save();
}
removeLinkById(id: string): void {
this.data.links = this.data.links.filter((l) => {
const parts = l.url.split('/');
return parts[parts.length - 1] !== id;
});
this.save();
}
updateLinkById(id: string, updates: { name?: string; active?: boolean }): SavedLink | null {
const link = this.data.links.find((l) => {
const parts = l.url.split('/');
return parts[parts.length - 1] === id;
});
if (!link) return null;
if (updates.name !== undefined) link.name = updates.name;
if (updates.active !== undefined) link.active = updates.active;
this.save();
return link;
}
setPaused(paused: boolean): void {
this.data.paused = paused;
this.save();
}
updateSettings(partial: Partial<SavedSettings>): void {
Object.assign(this.data, partial);
this.save();
}
}