tradeit.gg

Architecture · May 15, 2026

Trade Lifecycle

Redis pub/sub → Steam → Bull — how every user trade, deposit, sell listing, and instant sell executes across tradeit-backend and tradeit-tradebot-server

Read on Wiki: /engineering/trade-lifecycle  ·  Linear: DEV-4959
4
Trade sub-types
2
Repos in path
6
Bull queue tasks
20 min
Trade data TTL
15 min
Item lock TTL

Contents


1. At-a-glance

The trade lifecycle covers every way a user exchanges items with tradeit.gg: depositing items to receive balance, purchasing (withdrawing) items by spending balance, listing items for P2P or instant-sell payout, and receiving those payouts. The execution path is always the same: tradeit-backend orchestrates → Redis pub/sub dispatches to a Steam bot → tradeit-tradebot-server sends the Steam trade offer → completion events flow back over Bull queues.

2. Purpose & boundaries

Owns

  • User-initiated item exchange (deposit, purchase, sell listing, instant sell)
  • Bot selection, item availability verification, site item locking
  • Redis pub/sub dispatch to Steam trading bots
  • Post-trade balance settlement (pending_addable / pending_removable ledger)
  • Trade bonus calculation

Does NOT own

  • Steam inventory retrieval (tradeit-inventory-server, sinv/cinv bots)
  • OpenSearch inventory indexing (tradeit-inventory-parser)
  • P2P listing discovery / buyer matching (OpenSearch store queries)
  • Payment / crypto withdrawals (sellService.withdraw flows)
  • Item pricing (pricing-manager / itemPriceRepo)

3. Process / runtime model

tradeit-backend

Single Node.js process (Express + PM2 cluster mode). Handles HTTP requests, database transactions, Redis publish. mainQueue.js runs a Bull consumer for post-trade tasks (6 QueueTask types).

tradeit-tradebot-server

One process per bot (TRADEIT_BOT_ID env var). Each instance: 1 Redis subscriber (bot {botId}) + 1 Redis client + 1 Bull tradeitQueue producer + 1 Bull tradebotQueue:{botId} consumer. Uses node-steam-user + steam-tradeoffer-manager. Internal serial queue (concurrency: 1) — one offer at a time per bot.

Bot intervals (index.ts:3567–3585)

FunctionPeriodPurpose
acceptConfirmations11 sAuto-confirm mobile authenticator
checkTooManyFailedTrades60 sSuspend bot if too many failures
updateUserItems8 minRefresh bot Steam inventory in Redis
refreshInv30 sRe-sync bot inventory if stale
cancelExpiredOffers60 sCancel open offers past expiry
recheckTradeStatuses20 sRe-poll pending offers via Steam API
heartbeat write60 sredis.set(stayingalive:{botId})

4. Architecture (data flow)

Deposit flow (user → bot)

graph LR
  subgraph "tradeit-backend"
    A["POST /trade"] --> B["create :259"]
    B --> C["createTrades :172"]
    C --> D["lockTradeItems :756"]
    C --> E["selectBot :226"]
    C --> F["PUBLISH bot {botId}"]
  end

  subgraph "Redis"
    F --> G["bot {botId} channel"]
    H["tradeitQueue Bull"] --> J["mainQueue consumer"]
    K["tradestate channel"] --> L["Socket.IO → Frontend"]
  end

  subgraph "tradeit-tradebot-server"
    G --> M["redisMessage :810"]
    M --> N["trade() :3064"]
    N --> O["createOffer + send"]
    O --> P["Steam API"]
    P --> Q["accepted: insertBotTrades"]
    Q --> R["PUBLISH tradestate"]
    Q --> S["PUBLISH finishedtrades"]
    Q --> T["tradeItQueue.add"]
  end

  subgraph "MySQL"
    Q --> U["INSERT bot_trades :3737"]
    Q --> V["UPDATE bot_users.balance :1234"]
  end

  R --> K
  T --> H

  classDef ok fill:#0e2a1e,stroke:#10b981,stroke-width:2px,color:#a7f3d0
  classDef hi fill:#1e2438,stroke:#a855f7,stroke-width:2px,color:#e9d5ff
  classDef warn fill:#2a1f0e,stroke:#f59e0b,stroke-width:2px,color:#fde68a
  class F,G,K ok
  class P hi
  class H,J warn

Sell listing flow (user deposits item for payout)

graph LR
  subgraph "tradeit-backend"
    A2["POST /sell/listing"] --> B2["listingSell"]
    B2 --> C2["sendTradeSaleRequest :314"]
    C2 --> D2["PUBLISH bot {botId} sell"]
  end

  subgraph "tradeit-tradebot-server"
    D2 --> E2["redisMessage case sell :816"]
    E2 --> F2["trade() isSale=true :3064"]
    F2 --> G2["addMyItems only"]
    G2 --> H2["Steam offer sent"]
    H2 --> I2["UPDATE sale_offers :1856"]
    I2 --> J2["tradeItQueue.add InstantSellCompleted"]
  end

  J2 --> K2["mainQueue: Amplitude + OAuth2 webhook :146"]

  classDef ok fill:#0e2a1e,stroke:#10b981,stroke-width:2px,color:#a7f3d0
  classDef hi fill:#1e2438,stroke:#a855f7,stroke-width:2px,color:#e9d5ff
  class D2,J2 ok
  class H2 hi

5. Inputs & outputs

HTTP inputs (tradeit-backend)

RouteControllerDescription
POST /tradecreate :259Full trade — deposit userItems, receive siteItems
POST /trade/purchasetradePurchase :700Withdrawal only — spend balance to receive site items
POST /trade/reservedcreateReserved :223Pre-reserve trade-locked items
POST /trade/virtualacceptVirtualTrade :68Accept trade-locked items as virtual hold
POST /trade/continue-resolvedcontinueResolved :547Resend already-validated trades (multi-bot flow)
POST /trade/checkTradecheckTrade :679Price-verify before user commits
POST /sell/listinglistingSellDeposit item(s) with payout type
POST /sell/withdrawwithdrawSaleOfferItemWithdraw sale proceeds (crypto/stripe/store balance)

Outputs

Channel / Queue / TableProducerConsumer
PUBLISH bot {botId}tradeit-backend tradeServicetradeit-tradebot-server subscriber
PUBLISH tradestatetradeit-tradebot-servertradeit-backend → Socket.IO → frontend
PUBLISH finishedtradestradeit-tradebot-servertradeit-backend subscriber
PUBLISH canceledtradestradeit-tradebot-servertradeit-backend subscriber
PUBLISH steamDowntradeit-tradebot-servertradeit-backend (UI warning)
Bull tradeitQueuetradeit-tradebot-servertradeit-backend mainQueue consumer
MySQL bot_trades INSERTtradeit-tradebot-server :3737audit / admin
MySQL sale_offers UPDATEtradeit-tradebot-server :1856sellService payout flows

6. Redis surface

Key patternTTLR/WPurpose
bot {botId} (channel)W:backend R:tradebotCommand channel — trigger trade/sell/withdraw
tradestate (channel)W:tradebot R:backendTrade progress events → frontend
finishedtrades (channel)W:tradebotTrade completion signal
canceledtrades (channel)W:tradebotTrade cancellation signal
steamDown (channel)W:tradebotSteam API down signal
tradeinfo:{infoKey}20 minW:backend R:tradebotTrade payload — key = {steamId}_{tradeHash}
lockTrade:{assetId}15 minW:tradeRedis NXSite item lock (prevents double-booking)
userTradeLock:{steamId}W:tradeRedisHash of user-side asset locks (field=assetId)
sinv:{botId}:compressedW:tradebot R:backendBot site inventory for price verification
stayingalive:{botId}W:tradebot 60sBot heartbeat — tradeRedis monitors all N bots
invsize730:{botId}W:tradebotBot CS2 inventory slot count
balance:{steamId}W:tradebotUser balance cache (populated on balance update)
containers:{botId}W:tradebot 60sCasket/container occupied slot counts

7. MySQL surface

tradeit-backend (Aurora MySQL 8)

TableOperationNotes
bot_usersUPDATE balance / pending ledgersPK: steam_id
balance_transactionsINSERT on every balance changeFK: trade_id
sale_offersINSERT/UPDATE statusStatus: PENDING→LISTED→SOLD/CLAIMABLE/RECLAIMED
saleoffer_tradesINSERT/UPDATE completedFK: offer_id
reserved_itemsSELECT/UPDATE tradeable_atasset_id; gates re-reservation
trade_item_statsSELECT daily_in_amountCross-db via getPricingConnection()

tradeit-tradebot-server (direct MySQL, same Aurora)

TableOperationFile:line
bot_tradesINSERT on completionindex.ts:3737
sell_offersINSERT on sell completionindex.ts:3763
bot_usersUPDATE balance / pending ledgersindex.ts:1234, :1281
saleoffer_tradesINSERT/UPDATEindex.ts:3440, :1817
sale_offersUPDATE statusindex.ts:1856, :1924
bot_trades_sentINSERT on offer sendindex.ts:3776
reserved_itemsSELECT/UPDATE tradeable_atindex.ts:3054

8. OpenSearch surface

OpenSearch is queried at the beginning of trade creation to verify item availability and filter reserved assets. After trade completion, the TradedAssetIds Bull task drives an inventory re-sync which leads to a new OS index via the parser.

9. Async continuations

Shared Bull queue: tradeitQueue

TaskProducer (tradebot)Consumer (mainQueue)Action
tradeBonusindex.ts:996, :1454mainQueue.js:84processTradeBonus + applyFirstTradeBonus
logSoldindex.ts:1467mainQueue.js:49Mark sold item, update botTradeItemLog
logWithdrawindex.ts:1486mainQueue.js:63Record withdrawal in botTradeItemLog
tradedAssetIdsindex.ts:1520mainQueue.js:103Update botTradeItemLog with new assetId
removeReserveindex.ts:1836mainQueue.js:119reservedLootbearRepo cleanup + transaction log
instantSellCompletedindex.ts:1878mainQueue.js:146Amplitude + OAuth2 webhook (Sell event)

Note: tradeitQueue is bidirectional — the tradebot produces for post-trade tasks while the backend consumes. This is the inverse of the typical pattern where the main server produces and workers consume.

10. Configuration flags & guards

KeyPurposeEffect on trade
tradesDisabledGlobal trade kill switchBlock all POST /trade
siteDisabledGlobal site kill switchBlock all traffic
largeTradesDisabledBlock high-value tradesChecked against largeTradeValue
sentTradesValueLimit1/2/3Tiered sent-trade value limitsuserLimitationService.checkSendTradeLimit :200
hourlyLimitHardOutValueHard hourly output ceiling (CSGO)Block withdrawals
hourlyLimitHardOutValueOtherGamesHard hourly ceiling (other games)Block withdrawals
enable24HrTradeSurgeCheck24h surge detectionBlock on surge
enable6HrTradeSurgeCheck6h surge detectionBlock on surge
botWarningPercentageBot inv fill % warning thresholdUI warning
botDangerousPercentageBot inv fill % danger thresholdDeprioritise bot in selection
numBotsCount of trading botsDrives stayingalive / sinv key arrays
haloSkinTradeEnabledHalo marketplace trade toggleBlock Halo deposits
saleWithdrawLimit1/2/3Tiered sale payout limitsThrottle sell proceeds

Per-item config (from item_price table, not configurations): maxDeposit — max concurrent pending deposits per item; dailyMaxStock — daily deposit ceiling (CSGO only).

11. Failure modes

Bot crash mid-offer

Trigger: Bot process dies after offer sent but before accepted  Blast radius: Trade offer open on Steam; items not credited  Recovery: recheckTradeStatuses (20s) re-polls; cancelExpiredOffers (60s) cleans up; tradebot restores pollData from Redis on restart

lockTrade not released

Trigger: Backend crashes after acquiring site item lock  Blast radius: Item unavailable for 15 min  Recovery: Lock expires (EX=900s); or admin DEL lockTrade:{assetId}

Steam error (16) — not responding

Trigger: Steam API timeout during offer send  Blast radius: Trade not sent; frontend shows error  Recovery: Tradebot publishes steamDown; frontend prompts retry; recheckTradeStatuses polls

Steam error (26) — no trade URL

Trigger: Trade send silently fails but may have created an offer  Blast radius: isTradePossibleCreated=true guard prevents double-cancel  Recovery: Logged; cancellation attempted; tradebot marks infoKey used (prevents duplicate send)

tradeitQueue consumer down

Trigger: mainQueue Bull consumer (backend) offline  Blast radius: Bonus/webhook callbacks queued but not processed  Recovery: Jobs persist in Redis; restart mainQueue consumer; backlog drained automatically

12. Observability

13. Code map (file:line)

tradeit-backend

ComponentFile:lineNotes
Trade routesserver/routes/trade.js:18 endpoints
Sell routesserver/routes/sell.js:110 endpoints
create controllerserver/controllers/trade.js:259Full deposit/withdrawal; MySQL transaction
tradePurchaseserver/controllers/trade.js:700Withdrawal-only; balance check
acceptVirtualTradeserver/controllers/trade.js:68Trade-locked item flow
continueResolvedserver/controllers/trade.js:547Multi-bot resend
TradeService classserver/service/tradeService.js:67Core orchestration
createTradesserver/service/tradeService.js:172Bot select, lock, publish
lockTradeItemsserver/service/tradeService.js:756Sets lockTrade:{assetId} NX
Bot selection (CSGO)server/service/tradeService.js:226botsService.getBotForCSGODeposit
BOT_FULL fallbackserver/service/tradeService.js:237Different bot for user items when all bots full
SellService.sendTradeSaleRequestserver/service/sellService.js:314Sell listing dispatch to bot
SaleOfferStatus enumserver/config/enums.js:894PENDING/LISTED/SOLD/RECLAIMED/CANCELED/REVERTED/CLAIMABLE
SaleOfferType enumserver/config/enums.js:1403TRADE_BALANCE/STORE_BALANCE/INSTANT_SELL/P2P_SELL
SellWithdrawType enumserver/config/enums.js:1014crypto/stripe/store_balance/locked_store_balance
QueueTask constantsserver/queue/mainQueue.js:206 task names
mainQueue consumerserver/queue/mainQueue.js:47Processes all 6 task types
publishTradeStateserver/redis/tradeRedis.js:51Publishes to tradestate channel
lockAssetIdsserver/redis/tradeRedis.jsNX lock lockTrade:{assetId} EX 900

tradeit-tradebot-server

ComponentFile:lineNotes
TRADE_DATA_EXPIREDsrc/Constants.ts:1820 min (1200 s)
Redis subscriber initsrc/index.ts:218Subscribes to bot {botId}
Bull tradeitQueuesrc/index.ts:257Shared queue — producer side
Bull tradebotQueue:{botId}src/index.ts:258Per-bot consumer queue
redisMessage handlersrc/index.ts:810Routes trade/sell/withdraw/delayed/sale
trade() functionsrc/index.ts:3064All 4 offer types: build + send Steam offer
Offer expirysrc/index.ts:~3130Sale=10 min, trade=5 min
recheckTradeStatusessrc/index.ts:168620s interval — poll open offers
logTradeOffersrc/index.ts:3776INSERT bot_trades_sent on offer send
insertBotTradesrc/index.ts:3737INSERT bot_trades on completion
movePendingAddable...src/index.ts:1251Atomic balance settlement on deposit
movePendingRemovable...src/index.ts:1204Atomic balance settlement on withdrawal

14. Open questions

Generated 2026-05-15 · tradeit.gg engineering