Skip to content

Repository files navigation

CS2 Arbitrage Trading Bot

This is a Node.js trading bot I built for buying and selling Counter-Strike items across CSGOEmpire and Clash.gg. It uses pricing and liquidity data from PriceEmpire, with BUFF prices as the main reference point for deciding whether a trade is worth taking.

Note

I have not run this bot in a long time, so please do your own due diligence before connecting it to any accounts. Double-check the marketplace APIs, authentication flows, trading strategy assumptions, disabled execution paths, and account for new Steam mechanics such as reversible trades.

Warning

Using the Steam integration requires your Steam username, password, shared_secret, and identity_secret. In effect, the bot emulates parts of the Steam mobile authenticator so it can log in, create, and confirm trades. These values are exposed to the local process and its dependencies, and anyone who obtains them may be able to take control of your account or inventory. Use or modify this code at your own discretion, keep config.json out of Git, and review the code and dependencies before supplying any real credentials.

A Bit Of Background

Recent changes to Steam trading and the skin market, including reversible trades and expanded trade-up mechanics, have made skin arbitrage more tedious, less profitable, and considerably riskier. It is probably still possible to trade profitably, but I do not see myself returning to it. I have received a few DMs asking about the bot, so I decided to release the code rather than leave it sitting on my machine.

I started building this 5+ years ago, when engineering best practices were not always at the top of my mind. There is rough code, duplicated code, and more logging than I would write today. I would not recommend deploying this repository as-is. If you are serious about building a trading system, use it to understand one possible approach and then build your own around the current behavior of Steam and the marketplaces.

You might also be tempted to rewrite everything in a faster language. That could be worthwhile eventually, but this workload is mostly limited by network calls and the speed of the marketplaces. In practice, hosting close to their infrastructure and getting the trade lifecycle right will probably matter more than shaving some ms off local processing.

What Still Works, And What Does Not

The repository does not currently start a complete trading bot. That is intentional:

  • The initialization in main.js is commented out.
  • Steam login and mobile trade confirmation are disabled.
  • The MongoDB URI has been removed from the code and tests.
  • The marketplace endpoints and payloads have not been checked against their current APIs.
  • The tests need a dedicated MongoDB instance and use helper methods that are currently disabled.

Simply uncommenting the purchasing code is not enough to make this safe. At a minimum, the trade lifecycle needs to be updated for reversible trades, authentication probably needs to be rebuilt, and every marketplace integration needs to be checked against its up-to-date API.

How It Is Structured

I kept each marketplace in its own bot because they behave quite differently. CSGOEmpire exposes a WebSocket feed for listings, auctions, and trade-status events, while the Clash implementation periodically polls its HTTP API for active deposits. Trying to hide both behind one generic marketplace abstraction would not have removed much complexity, so the shared behavior lives in a few smaller services instead.

+----------------------+                              +----------------------+
| CSGOEmpire           |                              | Clash.gg             |
| WebSocket and API    |                              | HTTP API             |
+----------+-----------+                              +-----------+----------+
           |                                                      |
           v                                                      v
+----------------------+  +----------------------+    +----------------------+
| CSGOEmpire bot       |<-| PriceEmpire / BUFF   |    | Clash bot            |
| listings and bids    |  | prices and liquidity |    | sales and polling    |
+----------+-----------+  +----------------------+    +-----------+----------+
           |                                                      |
           +---------------------------+--------------------------+
                                       |
                         +-------------+-------------+
                         |                           |
                         v                           v
              +----------------------+    +----------------------+
              | Steam handler        |    | Shared data handler  |
              | inventory and offers |    | purchase/sale state  |
              +----------------------+    +----------+-----------+
                                                     |
                                                     v
                                          +----------------------+
                                          | MongoDB              |
                                          | transaction records  |
                                          +----------------------+
  • main.js creates the shared services and marketplace bots.
  • empire/empireBot.js listens for listings and auctions and decides when to bid.
  • empire/empireHandler.js handles CSGOEmpire API operations such as bidding and listing.
  • clash/clashBot.js watches Clash sales and coordinates outgoing Steam offers.
  • services/steamHandler.js manages Steam sessions, inventories, and trade offers.
  • services/priceHandler.js caches PriceEmpire price and liquidity data.
  • data/dataHandler.js connects marketplace events with Steam inventory events.
  • data/database.js stores purchase and sale records in MongoDB through Mongoose.
  • filteredItems/ contains the item allowlist used by the buying strategy.

The marketplace bots are meant to share the same Steam and data handler instances. That matters because a purchase begins on one marketplace, appears later as an item in Steam, and may eventually be sold through another marketplace. Those events need to update the same record.

How A Trade Was Intended To Flow

On the buying side, the bot listens for a CSGOEmpire listing or auction, looks up the item's cached BUFF reference price, and checks several conditions before bidding. The checks include the marketplace discount, difference from the BUFF price, liquidity, available balance, PriceEmpire's reliability flag, and whether the item is in the local allowlist.

If a purchase succeeds, it is recorded before the item reaches the Steam inventory. Once Steam receives the item, the bot matches it back to the pending purchase using the item name and purchase timestamp, then stores the new Steam asset ID. This extra step is necessary because asset IDs can change when an item moves between inventories.

On the selling side, the marketplace reports that a buyer is ready, the bot creates a Steam trade offer, and transaction details are attached to that offer. When Steam reports that the offer was accepted, the corresponding database record is marked as sold.

Decisions I Made At The Time

Pricing is cached rather than fetched for every listing. The bot refreshes BUFF pricing and liquidity data through PriceEmpire every 15 minutes. Prices generally did not move quickly enough to justify the added latency and API usage of fetching fresh data for every listing/event; skins tend to be quite illiquid. The bot does not connect to BUFF directly.

No single price signal is trusted. A large marketplace discount can still be a bad trade if the BUFF comparison is poor, the item is illiquid, or the source price is marked unreliable. The strategy therefore applies several gates before it will bid.

I chose MongoDB for schema flexibility, although a relational database would work well too. At the time, I wanted room to change the transaction records as I learned which marketplace and Steam fields were useful. The data has a fairly natural relational shape, though, so PostgreSQL, SQLite, or another relational database would also be a reasonable choice and could provide stronger constraints around trade state.

I would probably run the primary database on the same machine as the bot. Database writes sit on the trade path, so avoiding a network round trip keeps latency low and means a temporary hosted-database outage does not stop local execution. If centralized reporting or multiple bot instances were needed later, the local data could be synced asynchronously to a hosted database without putting that connection in the critical path.

Completed transactions are persisted, but short-lived coordination is not. The database stores purchase and sale records. Auction tracking, duplicate-offer prevention, and pending purchase matching are held in memory because that was simpler at the time. The downside is that restarting the process loses that state. A newer version should persist these workflows and make repeated API calls idempotent.

Marketplace conversions are hardcoded. The Empire-to-USD and Clash-to-USD values in the source are historical values from when I ran the bot. They are not live exchange rates and should not be treated as current. These could be different now.

Configuration

To inspect the project locally:

npm install
cp config_template.json config.json

The template documents the expected Steam credentials, marketplace API keys, polling intervals, and strategy thresholds. Purchasing is disabled by default. The thresholds reflect an old strategy and are examples, not recommendations.

config.json is ignored by Git and should stay that way. The MongoDB URI is currently blank in data/database.js; I would move it, along with the other credentials, into environment variables or a proper secret manager before doing any further work.

Things I Would Change Before Using It Again

The biggest missing piece is a durable trade state machine that understands reversible trades and can recover cleanly after a restart. I would also add idempotency around every bid, listing, and trade action; retries with backoff; periodic reconciliation; account exposure limits; and an emergency/dynamic stop and start when market conditions become unfavourable.

The Empire WebSocket currently disables TLS certificate verification with rejectUnauthorized: false. That should be removed. I would also replace browser cookie scraping with a supported authentication flow if one is available, and make sure logs never include credentials, cookies, authorization headers, trade links, or complete HTTP request objects. I would also check to see if Clash has a WebSocket you can connect to instead of polling.

Finally, all marketplace APIs, authentication flows, rate limits, and terms of service need to be reviewed again. Automated trading can lose money or items through stale prices, software bugs, account compromise, or an API changing underneath the bot, so be careful.

Private Data

Remember to never commit sensitive data or credentials to GitHub. Anyone with access to your API keys, private keys, session cookies, or Steam secrets will be able to act on your behalf, including potentially draining your entire inventory and balances.

That's all, be careful and stay safe. GLHF! 🫡

About

Arbitrage trading bot for CS2 skins

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages