Architecture · May 15, 2026
Redis pub/sub → Steam → Bull — how every user trade, deposit, sell listing, and instant sell executes across tradeit-backend and tradeit-tradebot-server
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.
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).
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.
| Function | Period | Purpose |
|---|---|---|
acceptConfirmations | 11 s | Auto-confirm mobile authenticator |
checkTooManyFailedTrades | 60 s | Suspend bot if too many failures |
updateUserItems | 8 min | Refresh bot Steam inventory in Redis |
refreshInv | 30 s | Re-sync bot inventory if stale |
cancelExpiredOffers | 60 s | Cancel open offers past expiry |
recheckTradeStatuses | 20 s | Re-poll pending offers via Steam API |
| heartbeat write | 60 s | redis.set(stayingalive:{botId}) |
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
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
| Route | Controller | Description |
|---|---|---|
POST /trade | create :259 | Full trade — deposit userItems, receive siteItems |
POST /trade/purchase | tradePurchase :700 | Withdrawal only — spend balance to receive site items |
POST /trade/reserved | createReserved :223 | Pre-reserve trade-locked items |
POST /trade/virtual | acceptVirtualTrade :68 | Accept trade-locked items as virtual hold |
POST /trade/continue-resolved | continueResolved :547 | Resend already-validated trades (multi-bot flow) |
POST /trade/checkTrade | checkTrade :679 | Price-verify before user commits |
POST /sell/listing | listingSell | Deposit item(s) with payout type |
POST /sell/withdraw | withdrawSaleOfferItem | Withdraw sale proceeds (crypto/stripe/store balance) |
| Channel / Queue / Table | Producer | Consumer |
|---|---|---|
PUBLISH bot {botId} | tradeit-backend tradeService | tradeit-tradebot-server subscriber |
PUBLISH tradestate | tradeit-tradebot-server | tradeit-backend → Socket.IO → frontend |
PUBLISH finishedtrades | tradeit-tradebot-server | tradeit-backend subscriber |
PUBLISH canceledtrades | tradeit-tradebot-server | tradeit-backend subscriber |
PUBLISH steamDown | tradeit-tradebot-server | tradeit-backend (UI warning) |
Bull tradeitQueue | tradeit-tradebot-server | tradeit-backend mainQueue consumer |
MySQL bot_trades INSERT | tradeit-tradebot-server :3737 | audit / admin |
MySQL sale_offers UPDATE | tradeit-tradebot-server :1856 | sellService payout flows |
| Key pattern | TTL | R/W | Purpose |
|---|---|---|---|
bot {botId} (channel) | — | W:backend R:tradebot | Command channel — trigger trade/sell/withdraw |
tradestate (channel) | — | W:tradebot R:backend | Trade progress events → frontend |
finishedtrades (channel) | — | W:tradebot | Trade completion signal |
canceledtrades (channel) | — | W:tradebot | Trade cancellation signal |
steamDown (channel) | — | W:tradebot | Steam API down signal |
tradeinfo:{infoKey} | 20 min | W:backend R:tradebot | Trade payload — key = {steamId}_{tradeHash} |
lockTrade:{assetId} | 15 min | W:tradeRedis NX | Site item lock (prevents double-booking) |
userTradeLock:{steamId} | — | W:tradeRedis | Hash of user-side asset locks (field=assetId) |
sinv:{botId}:compressed | — | W:tradebot R:backend | Bot site inventory for price verification |
stayingalive:{botId} | — | W:tradebot 60s | Bot heartbeat — tradeRedis monitors all N bots |
invsize730:{botId} | — | W:tradebot | Bot CS2 inventory slot count |
balance:{steamId} | — | W:tradebot | User balance cache (populated on balance update) |
containers:{botId} | — | W:tradebot 60s | Casket/container occupied slot counts |
| Table | Operation | Notes |
|---|---|---|
bot_users | UPDATE balance / pending ledgers | PK: steam_id |
balance_transactions | INSERT on every balance change | FK: trade_id |
sale_offers | INSERT/UPDATE status | Status: PENDING→LISTED→SOLD/CLAIMABLE/RECLAIMED |
saleoffer_trades | INSERT/UPDATE completed | FK: offer_id |
reserved_items | SELECT/UPDATE tradeable_at | asset_id; gates re-reservation |
trade_item_stats | SELECT daily_in_amount | Cross-db via getPricingConnection() |
| Table | Operation | File:line |
|---|---|---|
bot_trades | INSERT on completion | index.ts:3737 |
sell_offers | INSERT on sell completion | index.ts:3763 |
bot_users | UPDATE balance / pending ledgers | index.ts:1234, :1281 |
saleoffer_trades | INSERT/UPDATE | index.ts:3440, :1817 |
sale_offers | UPDATE status | index.ts:1856, :1924 |
bot_trades_sent | INSERT on offer send | index.ts:3776 |
reserved_items | SELECT/UPDATE tradeable_at | index.ts:3054 |
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.
openSearchService.queryByAssetIds(assetIds, context, appId) — verifies site availability, applies reserved-asset must_not filterappId scopes query to one game's aliastradeitQueue| Task | Producer (tradebot) | Consumer (mainQueue) | Action |
|---|---|---|---|
tradeBonus | index.ts:996, :1454 | mainQueue.js:84 | processTradeBonus + applyFirstTradeBonus |
logSold | index.ts:1467 | mainQueue.js:49 | Mark sold item, update botTradeItemLog |
logWithdraw | index.ts:1486 | mainQueue.js:63 | Record withdrawal in botTradeItemLog |
tradedAssetIds | index.ts:1520 | mainQueue.js:103 | Update botTradeItemLog with new assetId |
removeReserve | index.ts:1836 | mainQueue.js:119 | reservedLootbearRepo cleanup + transaction log |
instantSellCompleted | index.ts:1878 | mainQueue.js:146 | Amplitude + 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.
| Key | Purpose | Effect on trade |
|---|---|---|
tradesDisabled | Global trade kill switch | Block all POST /trade |
siteDisabled | Global site kill switch | Block all traffic |
largeTradesDisabled | Block high-value trades | Checked against largeTradeValue |
sentTradesValueLimit1/2/3 | Tiered sent-trade value limits | userLimitationService.checkSendTradeLimit :200 |
hourlyLimitHardOutValue | Hard hourly output ceiling (CSGO) | Block withdrawals |
hourlyLimitHardOutValueOtherGames | Hard hourly ceiling (other games) | Block withdrawals |
enable24HrTradeSurgeCheck | 24h surge detection | Block on surge |
enable6HrTradeSurgeCheck | 6h surge detection | Block on surge |
botWarningPercentage | Bot inv fill % warning threshold | UI warning |
botDangerousPercentage | Bot inv fill % danger threshold | Deprioritise bot in selection |
numBots | Count of trading bots | Drives stayingalive / sinv key arrays |
haloSkinTradeEnabled | Halo marketplace trade toggle | Block Halo deposits |
saleWithdrawLimit1/2/3 | Tiered sale payout limits | Throttle sell proceeds |
Per-item config (from item_price table, not configurations): maxDeposit — max concurrent pending deposits per item; dailyMaxStock — daily deposit ceiling (CSGO only).
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
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}
Trigger: Steam API timeout during offer send Blast radius: Trade not sent; frontend shows error Recovery: Tradebot publishes steamDown; frontend prompts retry; recheckTradeStatuses polls
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)
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
trade-service, main-queue, trade-redis, controllers-selltrade -> create failed (includes steamId + stack)TI_SLACK_BLOCKED_ITEMS_WEBHOOK — deposit limit breaches, x_content_parse_exceptionstayingalive:{i} missing — monitored by tradeRedis initnotifySlack('Send trade offer error') — Steam error codes (16), (26)| Component | File:line | Notes |
|---|---|---|
| Trade routes | server/routes/trade.js:1 | 8 endpoints |
| Sell routes | server/routes/sell.js:1 | 10 endpoints |
create controller | server/controllers/trade.js:259 | Full deposit/withdrawal; MySQL transaction |
tradePurchase | server/controllers/trade.js:700 | Withdrawal-only; balance check |
acceptVirtualTrade | server/controllers/trade.js:68 | Trade-locked item flow |
continueResolved | server/controllers/trade.js:547 | Multi-bot resend |
TradeService class | server/service/tradeService.js:67 | Core orchestration |
createTrades | server/service/tradeService.js:172 | Bot select, lock, publish |
lockTradeItems | server/service/tradeService.js:756 | Sets lockTrade:{assetId} NX |
| Bot selection (CSGO) | server/service/tradeService.js:226 | botsService.getBotForCSGODeposit |
| BOT_FULL fallback | server/service/tradeService.js:237 | Different bot for user items when all bots full |
SellService.sendTradeSaleRequest | server/service/sellService.js:314 | Sell listing dispatch to bot |
SaleOfferStatus enum | server/config/enums.js:894 | PENDING/LISTED/SOLD/RECLAIMED/CANCELED/REVERTED/CLAIMABLE |
SaleOfferType enum | server/config/enums.js:1403 | TRADE_BALANCE/STORE_BALANCE/INSTANT_SELL/P2P_SELL |
SellWithdrawType enum | server/config/enums.js:1014 | crypto/stripe/store_balance/locked_store_balance |
QueueTask constants | server/queue/mainQueue.js:20 | 6 task names |
| mainQueue consumer | server/queue/mainQueue.js:47 | Processes all 6 task types |
publishTradeState | server/redis/tradeRedis.js:51 | Publishes to tradestate channel |
lockAssetIds | server/redis/tradeRedis.js | NX lock lockTrade:{assetId} EX 900 |
| Component | File:line | Notes |
|---|---|---|
TRADE_DATA_EXPIRED | src/Constants.ts:18 | 20 min (1200 s) |
| Redis subscriber init | src/index.ts:218 | Subscribes to bot {botId} |
Bull tradeitQueue | src/index.ts:257 | Shared queue — producer side |
Bull tradebotQueue:{botId} | src/index.ts:258 | Per-bot consumer queue |
redisMessage handler | src/index.ts:810 | Routes trade/sell/withdraw/delayed/sale |
trade() function | src/index.ts:3064 | All 4 offer types: build + send Steam offer |
| Offer expiry | src/index.ts:~3130 | Sale=10 min, trade=5 min |
recheckTradeStatuses | src/index.ts:1686 | 20s interval — poll open offers |
logTradeOffer | src/index.ts:3776 | INSERT bot_trades_sent on offer send |
insertBotTrade | src/index.ts:3737 | INSERT bot_trades on completion |
movePendingAddable... | src/index.ts:1251 | Atomic balance settlement on deposit |
movePendingRemovable... | src/index.ts:1204 | Atomic balance settlement on withdrawal |
[OPEN: DEV-4959-1] Who calls POST /trade/purchase (withdrawal) — frontend directly, or another service?[OPEN: DEV-4959-2] delayed operation in redisMessage — what triggers a delayed trade dispatch? Is delayed_trades table still active?[OPEN: DEV-4959-3] sale operation (case in redisMessage) — distinct from sell? Not seen dispatched from backend.[OPEN: DEV-4959-4] distribution_queue table — what writes to it? Tradebot reads+deletes every 30–60s.[OPEN: DEV-4959-5] Datadog trade funnel dashboard URL — add to §12.[OPEN: DEV-4959-6] tradebotQueueTask.checkRevertTrade handled but the setInterval is commented out — is revert checking disabled?[OPEN: DEV-4959-7] P2PSell (SaleOfferType.P2PSell) defined in enum but not in the supported types of listingSell controller. Where is it initiated?Generated 2026-05-15 · tradeit.gg engineering