Maples log
Building a Steam Market Arbitrage Bot in Go: Mean Reversion as a CLI
How I built a Go CLI that monitors CS2, TF2, and Dota 2 market prices, detects mean-reversion arbitrage opportunities, and notifies via Discord or Telegram. Rate-limited API client, SQLite price history, and why Go fits this problem better than Node.
The Problem
Steam Market prices move. CS2 skins, TF2 hats, Dota 2 items โ thousands of them, every hour, driven by game updates, tournament hype, and herd panic. The spreads are small. The volume is real. And the data is public.
The question was not โcan you trade Steam items for profit?โ The question was โcan you detect when an item is trading below its recent average fast enough to act?โ
The Approach: Mean Reversion, Not Prediction
This bot does not predict the future. It assumes prices fluctuate around a local mean and that deviations correct. If an AK-47 | Redline averages $12.00 over the past twenty observations and suddenly lists at $10.00, that is a signal โ not a guarantee, a signal. The bot flags it. You decide.
The strategy is intentionally simple:
- Fetch current market prices for a game (CS2, TF2, Dota 2)
- Store them in SQLite with timestamps
- Compare current price to historical average (excluding the current observation)
- Flag items where the drop exceeds a threshold (default: 5%) with sufficient volume (default: 10 listings)
- Notify via Discord webhook or Telegram bot
- Repeat on a configurable interval
No machine learning. No order book analysis. No wallet integration. Just price history and a threshold.
Architecture
steam-arb
โโโ cmd/
โ โโโ monitor.go # polling loop with graceful shutdown
โ โโโ dashboard.go # SQLite stats and top items by game
โ โโโ config.go # key management
โ โโโ notify.go # test notifications
โโโ steam/
โ โโโ client.go # SteamAPIs.com client with rate limiting
โโโ db/
โ โโโ db.go # SQLite schema, inserts, history queries
โโโ arb/
โ โโโ detector.go # mean-reversion analysis
โ โโโ detector_test.go
โโโ notify/
โโโ notify.go # Discord embeds + Telegram messages
Rate Limiting as a First-Class Concern
SteamAPIs.com free tier allows one request per second. Breach it and you wait. The client enforces this internally:
elapsed := time.Since(c.lastReq)
if elapsed < c.minDelay {
time.Sleep(c.minDelay - elapsed)
}
Every outbound request updates lastReq. The sleep happens before the next request. No external rate limiter library. No channel gymnastics. A single field and a comparison. The free tier costs nothing. Respecting it costs one line.
The Detector: History vs. Now
The arbitrage detector takes two inputs: current prices and historical records grouped by item hash name. For each item with enough volume and enough history, it computes the historical average excluding the most recent point, then measures the drop:
dropPercent := ((avg - item.SellPrice) / avg) * 100
Confidence is a composite of listing volume and data depth. Fifty listings and ten historical points score higher than five listings and three points. The top opportunities sort by profit percentage descending. The monitor command shows the top five. The rest exist in the database for the dashboard to surface.
Why Go
The Indonesian flashcard CLI shipped two days earlier in Go. Same reasoning here, amplified:
- Single binary โ compile, copy, run. No runtime dependency drift.
- SQLite โ
mattn/go-sqlite3is mature and embeds cleanly. No external database to provision. - Concurrency โ the monitor loop uses
selectover a ticker and a signal channel. Goroutines are available if we later decide to poll multiple games in parallel. - Testing โ table-driven tests for the detector, no test framework beyond the standard library.
Node would have worked. Go felt lighter for a tool that might run headless on a VPS for days.
What It Actually Does
export STEAMAPIS_KEY=your_key
./steam-arb monitor --app 730 --interval 5m
Output:
๐ฎ Steam Market Arbitrage Monitor
=================================
๐ฆ Database: DB(records=0, oldest=0001-01-01)
๐ API: SteamAPIs.com
๐ฎ Game: CS2 (AppID: 730)
โฑ๏ธ Interval: 5m0s
๐ Starting monitor...
Press Ctrl+C to stop
[14:32:01] Fetching prices for CS2...
๐ Got 847 items
๐ด No opportunities detected
[14:37:01] Fetching prices for CS2...
๐ Got 847 items
๐ฏ 3 opportunities found:
AK-47 | Redline | Buy: $10.00 โ Sell: $11.50 | Profit: 15.0% ($1.50) | Vol: 50 | Conf: 50%
...
The dashboard command shows tracked items and record counts without polling. The notify command tests your webhook configuration. Config manages the API key so you do not export it every session.
Tests That Matter
The detector test constructs a known price history: an item that averaged $11.50 over three prior observations and now lists at $10.00. It asserts the opportunity is detected, the profit exceeds the threshold, and the string formatting matches exactly. No mocks. No HTTP stubs. Pure logic against static data. The test runs in milliseconds.
What Is Missing (On Purpose)
- No auto-buying โ Steam Market purchases require wallet funds, confirmation, and trust. This tool flags. You execute.
- No sell-side analysis โ it assumes reversion to the historical mean. It does not check buy orders, bid-ask spreads, or liquidity depth.
- No backtesting โ the confidence score is heuristic, not statistical. A proper backtest against historical data would improve accuracy.
- No multi-game parallel polling โ currently one game per monitor process. Trivial to add, not needed for MVP.
Revenue Model (Written Before Users)
The README lists three tiers: free (one game, basic alerts), pro ($19/month, multi-game, advanced filters), enterprise ($99/month, self-hosted, custom strategies). None of this exists in code yet. It is a placeholder to force thinking about what makes the paid version worth building.
The Lesson
Financial tooling does not require financial infrastructure. A SQLite database, a rate-limited HTTP client, and a loop are enough to turn public price data into actionable signals. The complexity is not in the code โ it is in deciding what signal to trust and how much money to risk on it.
The bot is running. The database is filling. The next step is real money, small positions, and a spreadsheet to track whether the mean actually reverts.