Tutorial
How to Build a CS2 Skin Arbitrage Bot Using Skinstrack API
A CS2 arbitrage bot is a price scanner with an alert hook. This guide covers the math, the architecture, and a working bot in JavaScript and Python that pulls cross-market prices from Skinstrack and pings Discord when net ROI clears your threshold.
Read this first
The Skinstrack API does not place trades. It surfaces prices, volume, liquidity and metadata across $34+ marketplaces. The bot in this guide finds spreads and notifies you. You buy and sell on the actual marketplaces yourself. That is on purpose: each marketplace has its own KYC, trade lock and withdrawal rules, and you want a human checking before money moves.
What CS2 Arbitrage Actually Is
Cross-market arbitrage means buying a CS2 skin on one marketplace at a lower price and reselling it on another where it is priced higher. The same item can sit at $11.92 on Buff163, $12.50 on CSFloat, and $13.10 on Skinport at the same moment because each marketplace clears its own supply and demand. A bot that watches all of them simultaneously sees the gap.
The catch: fees, trade locks, and listing time eat into that gap. Most apparent opportunities die in the math. The ones that survive are real, and that is what you want to find.
The Math, Worked Out
Take an item priced at $11.92 on Buff163 and $13.10 on Skinport. Gross spread is $1.18, or roughly 9.9 percent. Apply fees:
buy_price = 11.92 # Buff163 ask
sell_price = 13.10 # Skinport ask we list under
skinport_fee = 0.12 # 12% seller fee
proceeds = sell_price * (1 - skinport_fee) # 11.528
net_profit = proceeds - buy_price # -0.392
net_roi = net_profit / buy_price # -3.3%
# Skip. Try sell on Buff163 instead (2.5% fee):
proceeds = 12.50 * (1 - 0.025) # 12.187
net_profit = proceeds - 11.92 # +0.267
net_roi = +2.2% # still thinThe first apparent spread evaporated. The second is real but tight, and probably not worth the listing time on most items. A bot exists to filter for spreads that clear a real threshold, typically 10 percent net ROI or more, on items liquid enough to actually sell.
Architecture
Four boxes. No queue, no database, no machine learning. Add those when you outgrow this.
Poll prices
Call /paid/items every few minutes to refresh the full marketplace snapshot for your watchlist.
Score opportunities
For each item, find the cheapest buy market and the highest sell market, then compute net ROI after fees.
Filter by ROI and liquidity
Drop anything below your minimum ROI or below a liquidity floor. Most candidates die here, and that is the point.
Alert and execute manually
Push surviving opportunities to Discord, email, or your own dashboard. You buy and sell by hand.
Step 1: Pull Cross-Market Prices
One call returns the entire snapshot. Pick the marketplaces you actually plan to use, because more providers means more bytes per response without changing the shape.
curl -H "X-API-KEY: $SKINSTRACK_KEY" \
"https://api.skinstrack.com/v1/paid/items?providers=csfloat,buff,skinport,waxpeer,dmarket&avg=true"Steps 2 and 3: Fees, ROI, Liquidity
Keep a fee table as data, not as if/else. Compute net ROI per item and gate on both ROI and the Skinstrack liquidity score. A 20 percent spread on an item that trades twice a week is not an opportunity; it is a position you cannot exit.
// Marketplace seller fees (sample, verify on each market).
const SELLER_FEE = {
steam: 0.13, // ~13% combined Steam fee
buff: 0.025, // 2.5%
csfloat: 0.02, // 2%
skinport: 0.12, // 12%
waxpeer: 0.05, // 5%
dmarket: 0.05, // 5%
};
function findArb(item, minRoi = 0.10, minLiquidity = 40) {
if (item.liquidity < minLiquidity) return null;
const buy = item.prices.reduce((a, b) => (a.price < b.price ? a : b));
const sell = item.prices.reduce((a, b) => (a.price > b.price ? a : b));
const sellerFee = SELLER_FEE[sell.provider] ?? 0.10;
const net = sell.price * (1 - sellerFee) - buy.price;
const roi = net / buy.price;
if (roi < minRoi) return null;
return {
name: item.market_hash_name,
buy: { provider: buy.provider, price: buy.price, count: buy.count },
sell: { provider: sell.provider, price: sell.price, count: sell.count },
roi,
liquidity: item.liquidity,
};
}Step 4: Push Alerts to Discord
Discord webhooks are the path of least resistance. Same shape works for Slack or any generic webhook endpoint. Deduplicate before posting so you do not get the same opportunity 12 times.
async function postToDiscord(webhook, opp) {
const body = {
content:
'**' + opp.name + '**\n' +
'Buy on ' + opp.buy.provider + ' @ $' + opp.buy.price.toFixed(2) + '\n' +
'Sell on ' + opp.sell.provider + ' @ $' + opp.sell.price.toFixed(2) + '\n' +
'Net ROI: ' + (opp.roi * 100).toFixed(1) + '% (liquidity ' + opp.liquidity + ')',
};
await fetch(webhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}Full Minimal Bot
Around 60 lines, runs in a single process, no dependencies beyond fetch or requests. Drop it on any cheap VPS or Cloudflare Worker and walk away.
// minimal-arb-bot.js - run: node minimal-arb-bot.js
import 'dotenv/config';
const KEY = process.env.SKINSTRACK_KEY;
const WEBHOOK = process.env.DISCORD_WEBHOOK;
const PROVIDERS = ['csfloat', 'buff', 'skinport', 'waxpeer', 'dmarket'];
const MIN_ROI = 0.10;
const MIN_LIQUIDITY = 40;
const POLL_MS = 5 * 60 * 1000;
const SELLER_FEE = {
steam: 0.13, buff: 0.025, csfloat: 0.02,
skinport: 0.12, waxpeer: 0.05, dmarket: 0.05,
};
const seen = new Set();
async function tick() {
const params = new URLSearchParams({ providers: PROVIDERS.join(',') });
const res = await fetch(
'https://api.skinstrack.com/v1/paid/items?' + params,
{ headers: { 'X-API-KEY': KEY } }
);
if (!res.ok) return console.error('fetch failed', res.status);
const items = await res.json();
for (const item of items) {
if (item.liquidity < MIN_LIQUIDITY) continue;
const buy = item.prices.reduce((a, b) => (a.price < b.price ? a : b));
const sell = item.prices.reduce((a, b) => (a.price > b.price ? a : b));
const fee = SELLER_FEE[sell.provider] ?? 0.10;
const roi = (sell.price * (1 - fee) - buy.price) / buy.price;
if (roi < MIN_ROI) continue;
const key = item.slug + ':' + buy.provider + ':' + sell.provider;
if (seen.has(key)) continue;
seen.add(key);
await fetch(WEBHOOK, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content:
'**' + item.market_hash_name + '**\n' +
'Buy on ' + buy.provider + ' @ $' + buy.price.toFixed(2) +
' -> Sell on ' + sell.provider + ' @ $' + sell.price.toFixed(2) +
' (' + (roi * 100).toFixed(1) + '% net)',
}),
});
}
}
setInterval(tick, POLL_MS);
tick();Want to validate the shape first?
Open the Skinstrack comparison tool to see the same arbitrage opportunities your bot will surface, rendered in a UI. It is a useful sanity check before you ship code that pings you at 4am.
Risks Your Bot Will Not Catch
Trade locks and Steam holds
Items bought via the Steam trade flow are locked for up to 7 days. Prices can move significantly during that window, so always discount your expected ROI for hold time.
Listing time and slippage
Even when a sell offer exists, listing your own item still takes minutes. Cheap items can be sniped by another bot before your listing lands.
Float and pattern mismatches
Cross-marketplace prices can diverge because of float or pattern index. The API gives aggregate prices; verify the actual listing before buying high-value items.
Marketplace withdrawal cooldowns
Some marketplaces hold balances for KYC checks or fraud review. Treat that cash as illiquid until withdrawal clears.
Fee schedule changes
Marketplaces adjust seller fees, deposit fees, and bonuses regularly. Keep your SELLER_FEE table out of source code and refresh it from a config you can update.
Get the API key your bot needs
Standard tier covers a 5 minute poll across the major marketplaces for a full month.
Frequently Asked Questions
Can the Skinstrack API execute trades automatically?
No. The Skinstrack API surfaces prices, volume, liquidity, and metadata across 34+ marketplaces. It does not place buy or sell orders on your behalf. Bots built on top of the API identify opportunities; the user executes the trades manually through each marketplace.
What is CS2 skin arbitrage?
Cross-market arbitrage means buying a CS2 skin on one marketplace at a lower price and reselling it on another where it is priced higher. The spread has to cover marketplace fees, trade hold time, and listing slippage to be worth executing.
How do I avoid false positives?
Filter aggressively. Set a liquidity floor of at least 40 to skip items that rarely trade, require a 10 percent net ROI after fees, and deduplicate alerts by the (item, buy_provider, sell_provider) tuple so you do not re-fire the same opportunity every poll cycle.
Which marketplaces have the lowest fees for arbitrage sells?
CSFloat and Buff163 typically have the lowest seller fees, around 2 to 2.5 percent. Steam and Skinport sit at 12 to 13 percent. Verify the current schedule on each marketplace before locking it into a bot; fees change.
How often should the bot poll?
Every 3 to 10 minutes is a reasonable starting point. Polling more often than that mostly burns your monthly quota for marginal gain, because most arbitrage spreads survive at least a few minutes before they are picked off.
Is CS2 skin arbitrage legal?
Buying and selling CS2 skins on third-party marketplaces is legal in most jurisdictions, but tax treatment varies. Profits from trading digital goods are often taxable. If you are running this at meaningful scale, consult a tax professional in your country.
Related Guides
CS2 Skin Price API Guide
The foundation. Authentication, endpoints, response shape, rate limits.
CS2 Market Data API
Volume, liquidity and time-window stats for backtesting and quant models.
CS2 Buy Bot (WebSocket)
Faster cousin. Subscribe to Waxpeer and Market.CSGO listings and react instantly.
