tradeit.gg

Architecture deep-dive · 2026-05-15

Inventory Search & Indexing

Three-service pipeline: bot aggregation → compressed parsing → OpenSearch blue/green indexing

Read on Wiki: /engineering/backend/inventory · source: inventory-search-indexing-wiki.md
320K
CS2 docs / cycle
~150s
CS2 cycle time
5
game lanes
5 min
index frequency
2
marketplaces

Contents


1. At-a-glance

The inventory pipeline spans three services across two repos. tradeit-inventory-server (sinvbot + cinvbot) aggregates ~400 trade-bot inventories from Redis into a single merged payload and handles on-demand user Steam inventory fetches. tradeit-inventory-parser polls that payload via HTTP, parses its compressed single-letter keys into OpenSearch documents, and performs a blue/green alias swap every 5 minutes per game lane. tradeit-backend (out of scope here) serves the live alias to the site.

2. Purpose & boundaries

Owns

  • Aggregating ~400 bot inventories from Redis into site-inventory payload (sinvbot)
  • HTTP inventory endpoints for internal consumers (sinvbot / cinvbot)
  • On-demand user Steam inventory fetch with IP rotation + ScraperAPI fallback (cinvbot)
  • Parsing compressed inventory payloads into OpenSearch documents (inventory-parser)
  • Blue/green index alias swap — zero-downtime cutover (inventory-parser)
  • Distributed lane-lock per game+marketplace (inventory-parser)
  • HaloSkins and Uuskins marketplace ingestion (inventory-parser)
  • Slack alerts + Cloudflare cache purge post-index (inventory-parser)

Does NOT own

  • Bot inventory writes to Redis — tradeit-tradebot-server
  • OpenSearch cluster management — see OpenSearch Patterns (id 60)
  • Site search API, business filters, unstack UX — tradeit-backend
  • Pricing computation — pricing-manager; prices flow in via MySQL

3. Process / runtime model

tradeit-inventory-server (sinvbot + cinvbot)

AppPM2 modeInstancesPortEC2 host (prod)
tradeit-site-inventory (sinvbot)fork13000ec2-63-33-0-232 (eu-west-1)
tradeit-user-inventory (cinvbot)cluster43001ec2-63-33-0-232 (eu-west-1)

Container: Docker --network host · Max-memory restart: 10 GB · Staging: ec2-63-32-135-112

PM2 instance 0 rule (cinvbot)

NODE_APP_INSTANCE=0 is the sole Bull queue consumer (inventory_server_ip). Other instances only serve HTTP. cinvbot.ts:1551–1557

tradeit-inventory-parser

SettingValue
PM2 modecluster (pinned to 1 instance)
Scriptdist/main.js
Node heap2 600 MB (--max-old-space-size=2600)
Container memory2.9 GB — heap cap chosen so GC fires before Docker OOM-kills
Port3000 (HTTP; TLS at proxy)
TriggerExternal cron server hits HTTP endpoints; service is stateless between calls

4. Architecture (data flow)

graph LR
  classDef gap fill:#2a1218,stroke:#f43f5e,stroke-width:2px,color:#fecdd3
  classDef warn fill:#2a1f0e,stroke:#f59e0b,stroke-width:2px,color:#fde68a
  classDef ok fill:#0e2a1e,stroke:#10b981,stroke-width:2px,color:#a7f3d0
  classDef hi fill:#1e2438,stroke:#a855f7,stroke-width:2px,color:#e9d5ff

  subgraph "tradeit-tradebot-server"
    BOT["~400 bots\nwrite loop"]
  end

  subgraph "tradeit-inventory-server"
    SINV["sinvbot\nport 3000"]
    CINV["cinvbot\nport 3001"]
  end

  subgraph "tradeit-inventory-parser"
    CTRL["InventoryController\nGET /index/:gameId"]
    SVC["InventoryService\nparseData()"]
    OS_SVC["OpenSearchService\nbulk + alias swap"]
  end

  subgraph "External marketplaces"
    HALO["HaloSkins API\ncs2dt.com /v2"]
    UU["Uuskins API\n/v1/market/items"]
  end

  REDIS[("Redis\nElastiCache")]
  MYSQL[("MySQL 8\nAurora")]
  OS[("OpenSearch\nalias: inventory_730...")]
  STEAM["Steam\nCommunity API"]
  SCRAPER["ScraperAPI\nproxy"]
  CF["Cloudflare\ncache purge"]
  SLACK["Slack\n#tradeit-builds"]

  BOT -- "sinv:{n}:compressed" --> REDIS
  REDIS -- "batch mGet sinvKeys" --> SINV
  MYSQL -- "items / item_prices\nsale_offers" --> SINV
  SINV -- "sinv, globalItemInfo\nupdatedPrices, stickerPrices" --> REDIS

  SINV -- "GET /sinv-merged/:gameId" --> CTRL
  HALO --> CTRL
  UU --> CTRL
  MYSQL -- "collections, item_types\nconfigurations, sale_offers" --> SVC
  REDIS -- "indexLaneLock:{gameId}\nstickerPrices, charmPrices" --> CTRL
  CTRL --> SVC
  SVC --> OS_SVC
  OS_SVC -- "bulk index + alias swap" --> OS
  OS_SVC --> CF
  OS_SVC --> SLACK

  STEAM --> CINV
  SCRAPER --> CINV
  MYSQL -- "bot_users, collections\nitem_types, configurations" --> CINV
  REDIS -- "updatedPrices\nrate-limit keys" --> CINV

  class OS_SVC ok
  class HALO,UU hi
  class CINV,STEAM,SCRAPER warn

5. Inputs & outputs

sinvbot

DirSurfaceDescription
inRedis sinv:{botIndex}:compressedCompressed bot inventory from tradebot-server
inMySQL items, item_pricesPrice + stock enrichment
inMySQL sale_offersP2P offer prices and owners
inMySQL configurationsRuntime config
outGET /sinv/:gameidGame-specific inventory; auth-token gated
outGET /sinv-merged/:gameidMerged ctx 2+16 inventory (CS2 protected items)
outGET /compressedstaticFull filtered site inventory
outGET /cached/:gameidCached game inventory
outRedis sinv, sinvItemCountsAggregated site inventory + counts
outRedis globalItemInfo, globalItemInfoByIdItem info maps
outRedis updatedPrices, stickerPrices, charmPricesPrice caches consumed by cinvbot/parser

cinvbot

DirSurfaceDescription
inHTTP user requests/cinv/professional, /cinv/cached, /cinv/search
inSteam Community APIUser inventory JSON (direct + ScraperAPI fallback)
inRedis updatedPricesPolled every 10 s from sinvbot
outBull queue inventory_server_ipIP rotation requests; consumer on instance 0 only
outBull queue tradebotQueue:{botId}Per-bot trade actions (0..MAX_BOT_INDEX)

inventory-parser

DirSurfaceDescription
inGET /index/:gameIdCron-triggered; one per game lane
inGET /index/:gameId/:marketplaceMarketplace-specific (haloskins, uuskins)
insinvbot GET /sinv-merged/:gameIdCompressed site inventory payload
inHaloSkins API v2/items/market/priceMarketplace pricing + item list
inUuskins API /v1/market/itemsUuskins item inventory (RSA-signed)
outOpenSearch bulk indexInventory + group + sticker docs
outOpenSearch alias swapAtomic cutover to new generation
outRedis indexLaneLock:{gameId}:{laneKey}Lock write + renewal every 450 s
outCloudflare cache purgePost-index CDN invalidation
outSlack alertsSlow-run (>3.5 min) and error alerts

6. Redis surface

Key patternTTLWritten byRead byNotes
sinv:{botIndex}:compressednonetradebot-serversinvbotOne key per bot; batch mGet'd
sinvnonesinvbot:305inventory-parser, tradeit-backendFull site inventory
sinvItemCountsnonesinvbot:300tradeit-backendItem count by group
globalItemInfononesinvbot:966tradeit-backendItem info map by name
globalItemInfoByIdnonesinvbot:971tradeit-backendItem info map by id
unStoreInPeriod:{item_id}nonesinvbot:977tradeit-backendPer-item un-store counter
priceupdatenonesinvbot:1103cinvbot:86Epoch ms of last price refresh
updatedPricesnonesinvbot:1108cinvbot:99, parserAll current prices JSON
stickerPricesnonesinvbot:1114cinvbot, parserSticker price map
charmPricesnonesinvbot:1119cinvbot, parserCharm price map
itemsMetanonesinvbot:1205tradeit-backendColors + URL slugs
indexLaneLock:{gameId}:{laneKey}900 sparser controller:135parser controllerUUID value; compare-and-delete EVAL
steamLimitedIp:{ip}implicitcinvbot:242cinvbot:223Per-IP rate-limit flag
userInvRequesting:{steamId}cinvbotcinvbot:505In-flight dedup flag
ud:{token}48 htradeit-backendcinvbot:543Session user data
{gameId}_inventory_{steamId}3 dcinvbotcinvbotCached user inventory
inventory_server_ip:{requestId}10 scinvbot consumer:1539cinvbot producerBull job result handoff

7. MySQL surface

TableOperationsUsed byNotes
configurationsSELECT by keysinvbot:255, cinvbot:269, parserRuntime config via ConfigurationService
itemsSELECT (prices, meta, float ranges)sinvbot:1009–1012, parserLarge table; hot on item_id
item_pricesSELECT (stock, stable_price, bot_price, popularity)sinvbot:934Joined to items
sale_offersSELECT (asset_id, price, owner)sinvbot:823–824, parserP2P offers for price overlay
bot_usersSELECT steam_trade_urlcinvbot:283Bot trade URL lookup
csgo_collectionsSELECT *cinvbot:296, parserCollection name map; loaded on start
item_typesSELECT by app_idcinvbot:302, parserType names by game; loaded on start
items_meta (+ joins)SELECT JOINsinvbot:1173–1178Color + URL slug enrichment; joins items_meta_items, csgo_skin_colors

TypeORM synchronize: false in inventory-parser — migrations are manual. Driver: mysql2 3.12.0.

8. OpenSearch surface

Index naming

Alias familyPatternSource
Site inventoryinventory_{gameId}sinvbot (tradeit bots)
Internal / HaloSkinsinventory_internal_{gameId}HaloSkins marketplace
Group (site)group_{gameId}Derived from site inventory
Group (internal)group_internal_{gameId}Derived from HaloSkins inventory
Stickersstickers_{marketplaceKey}CS2 sticker extraction only

Generation strategy (blue/green)

Each alias points to one of two generations (incremental integer suffix, e.g., _0 / _1, colloquially called "rick/morty"). Per cycle:

  1. Query current alias target → get generation N
  2. Select generation N+1 as write target; create index if missing (openSearch.service.ts:398)
  3. Disable refresh interval on N+1 (setRefreshInterval(-1), line 403)
  4. Bulk-index all docs with exponential backoff
  5. Restore refresh interval (line 408)
  6. Atomically swap alias from N → N+1 (replaceAliasForIndex, line 414)
  7. Delete old generation N index

Bulk pipeline parameters

ParameterValueLocation
MAX_DOCS_PER_BULK2 500openSearch.service.ts:18
MAX_BULK_PAYLOAD_BYTES8 MBopenSearch.service.ts:19
MAX_BULK_RETRIES3openSearch.service.ts:20
Retryable HTTP codes429, 502, 503, 504openSearch.service.ts:21
Retry backoff250 ms × 2^attemptopenSearch.service.ts:335–365
Request timeout120 sopenSearch.service.ts:323
Bulk concurrency1 (hotfix #117, was 2)openSearch.service.ts (getBulkConcurrency)
Halt threshold>30% permanent failuresopenSearch.service.ts:430

Hotfix #117 active (as of 2026-04-15)

getBulkConcurrency() is forced to 1 (sequential) to relieve 100% CPU on OpenSearch data nodes. Code statically reads 2 — runtime behavior differs. Will revert once PR #114 (Lane Scheduler + Backpressure) merges.

Two OpenSearch clients

Search client (coordination node)

OPENSEARCH_COORDINATION_NODE_URL — query path. Mutual TLS (base64 CA/cert/key).

Ingest client (data node)

OPENSEARCH_INGEST_NODE_URL — write path, separate node. keepAlive: false to prevent socket listener buildup on batch writes.

9. Async continuations

Indexing cron (external cron server → HTTP)

EndpointFrequencyOffsetLane
GET /index/730every 5 min:00CS2 site inventory
GET /index/570every 5 min:02Dota 2 (not user-visible)
GET /index/252490every 5 min:03Rust
GET /index/753every 5 min:03Steam
GET /index/440every 5 min:04TF2

sinvbot internal timers

TimerIntervalAction
saveItemInfoToRedis60 000 msRe-aggregate bot inventories; write Redis sinv, globalItemInfo, etc. (sinvbot.ts:230)
loadUpdatedPrices60 000 msReload prices from MySQL; write updatedPrices, stickerPrices, charmPrices (sinvbot.ts:234)

cinvbot internal timers

TimerIntervalAction
Price poll10 000 msRead updatedPrices from Redis (cinvbot.ts:118)
Image loader3 600 000 ms (1 h)Batch-load item images on startup and hourly

cinvbot Bull queues

QueueProducerConsumerPurpose
inventory_server_ipsendToIpQueueAndWait()cinvbot instance 0 only (cinvbot.ts:1533)IP rotation — returns next available outbound IPs for Steam API calls; result stored at {requestId} Redis key with 10 s TTL
tradebotQueue:{botId}cinvbottradeit-tradebot-serverPer-bot trade operations (0..MAX_BOT_INDEX)

Lane lock lifecycle

  1. Acquire: SET indexLaneLock:{gameId}:{laneKey} {uuid} EX 900 (inventory.controller.ts:135)
  2. Renewal: every 450 s (half TTL) while indexing
  3. Release: EVAL compareAndDeleteScript — only deletes if UUID matches (prevents stale release)
  4. Conflict → 409 CONFLICT; cron retries next tick

10. Configuration flags & guards

Database-backed (configurations table — ConfigurationKey enum)

Enum keyDB key stringDescription
TradeLockBaseFeetradeLockBaseFeeBase fee for trade-locked items
TradeLockChangeOverTimetradeLockChangeOverTimeTime-decay factor for trade lock fee
EnableTradeLockedenableTradeLockedFeature flag: include trade-locked items in index
StoreDiscountPercentstoreDiscountPercentDiscount applied to store prices
UserStorePercentuserStorePercentUser cut of store sale
HaloSkinStoreEnabledhaloSkinStoreEnabledHaloSkins store mode toggle
HaloSkinMarkupStorePercenthaloSkinMarkupStorePercentStore markup % for HaloSkins items
HaloSkinMarkupTradePercenthaloSkinMarkupTradePercentTrade markup % for HaloSkins items
HaloSkinMaxPerItemhaloSkinMaxPerItemMax stock per item from HaloSkins
HaloSkinMaxPerBestPriceItemhaloSkinMaxPerBestPriceItemMax stock for best-price items
HaloSkinEnableBestPriceItemhaloSkinEnableBestPriceItemToggle best-price item logic
HaloSkinMinPricehaloSkinMinPriceMin price filter for HaloSkins items
HaloSkinMaxPricehaloSkinMaxPriceMax price filter for HaloSkins items
HaloStablePriceMultiplierCheckhaloStablePriceMultiplierCheckStable-price volatility multiplier guard

Source: src/models/enums/ConfigurationKey.ts

Key env vars (static)

VarServiceDescription
MAX_BOT_INDEXsinvbot, cinvbotHow many bot Redis keys to iterate; mismatched value silently misses bots
TRADEIT_ENVcinvbot, parserstaging / production — affects bot routing
NODE_APP_INSTANCEcinvbotPM2 instance ID; 0 = Bull queue consumer
SCRAPER_API_KEYcinvbotScraperAPI proxy key
HALO_SKIN_APP_KEYparserHaloSkins API auth key
HALO_SKIN_BASE_URLparserHaloSkins API base: https://openapi.cs2dt.com/
OPENSEARCH_INGEST_NODE_URLparserWrite path (separate ingest node)
OPENSEARCH_COORDINATION_NODE_URLparserQuery path (coordination node)

Hardcoded guards

11. Failure modes

Lane already running (non-fatal)

Trigger: Cron fires while previous cycle holds indexLaneLock.
Blast radius: One cycle skipped; inventory ages by one 5-min window.
Recovery: Automatic — next tick wins the lock. Signal: 409 CONFLICT logged; no Slack alert (intentional).

sinvbot unavailable

Trigger: sinvbot crash / OOM / network partition.
Blast radius: GET /sinv-merged/:gameId fails; full lane aborted; OS index not refreshed.
Recovery: PM2 auto-restart; next parser cycle retries. Signal: Slack exception alert (inventory.controller.ts:105).

OpenSearch bulk failures (partial)

Trigger: 429 write rejection or 502–504 from ingest node.
Blast radius: Partial index write; alias swap deferred.
Recovery: Exponential backoff (250 ms × 2^attempt, 3 retries). If >30% docs permanently fail → Slack alert + halt; old alias remains live as safe fallback.

sinvbot concurrent load guard

Guard: isLoadingSiteInventory boolean flag in sinvbot — second concurrent load returns 429 (sinvbot.ts:196, 281).
Blast radius: None — second call dropped cleanly.

cinvbot Steam API rate limiting

Recovery: Round-robin IP rotation via inventory_server_ip Bull queue; ScraperAPI proxy fallback (cinvbot.ts:625).
Signal: Per-IP steamLimitedIp:{ip} Redis flag; viewable via GET /cinv/viewIpCount.

inventory-parser OOM

Trigger: Large inventory cycle exceeds 2.6 GB Node heap.
Guard: GC fires before Docker OOM-kills (2.9 GB container limit).
Recovery: PM2 restarts; next cron cycle re-indexes. No Cronitor monitor wired.

deploy.sh hang — known bug (PR #118 pending)

Trigger: Running deploy.sh from a non-interactive shell (CI, /deploy skill, sh -c).
Root cause: Background SSH subshell has no wait; SIGHUP'd when parent exits.
Workaround: Deploy only from an interactive terminal until PR #118 merges.

12. Observability

13. Code map (file:line)

tradeit-inventory-server

ComponentFile:lineNotes
sinvbot entrysinvbot.ts:1Express app, port 3000
Site inventory load triggersinvbot.ts:188GET /internal/load-site-inventory
Concurrent-load guardsinvbot.ts:196, 281isLoadingSiteInventory boolean
Redis sinv writesinvbot.ts:305Aggregated site inventory
Redis sinvItemCounts writesinvbot.ts:300Item counts
Redis globalItemInfo writesinvbot.ts:966Item info map
Redis priceupdate / updatedPrices / stickerPrices / charmPricessinvbot.ts:1103–1119Price cache writes
Redis itemsMeta writesinvbot.ts:1205Colors + URL slugs
MySQL items querysinvbot.ts:1009–1012Prices, float ranges
MySQL items_meta JOINsinvbot.ts:1173–1178Colors + slugs enrichment
saveItemInfoToRedis timersinvbot.ts:23060 000 ms interval
Compressed property interface (ItemD)util.ts:89–103Single-letter key map (d, p, e, f, etc.)
Asset property interfaceutil.ts:104–115Per-instance data (float, trade lock)
cinvbot entrycinvbot.ts:1Express app, port 3001
Price poll timercinvbot.ts:11810 000 ms interval
userInvRequesting dedupcinvbot.ts:505In-flight dedup flag per steamId
Steam API callcinvbot.ts:~590Direct + ScraperAPI fallback at :625
IP queue producercinvbot.ts:1533inventory_server_ip queue
IP queue consumer guardcinvbot.ts:1551–1557NODE_APP_INSTANCE === 0 only

tradeit-inventory-parser

ComponentFile:lineNotes
Entry pointsrc/main.ts:1NestJS bootstrap, port 3000
Root modulesrc/app.module.tsTypeORM, ConfigModule, InventoryIndexModule
Index controllersrc/modules/inventoryIndex/inventory.controller.ts:56GET /index/:gameId
Marketplace index routesrc/modules/inventoryIndex/inventory.controller.ts:107GET /index/:gameId/:marketplace
Lane lock acquiresrc/modules/inventoryIndex/inventory.controller.ts:135SET ... EX 900 + compare-and-delete EVAL
Lane lock TTL constantsrc/modules/inventoryIndex/inventory.controller.ts:30laneLockTtlSeconds = 900
Slow-run Slack alertsrc/modules/inventoryIndex/inventory.controller.ts:98>3.5 min threshold
Exception Slack alertsrc/modules/inventoryIndex/inventory.controller.ts:105Full stack + instance info
parseData methodsrc/modules/inventoryIndex/inventory.service.tsMain parse pipeline
Compressed key parsingsrc/modules/inventoryIndex/inventory.service.ts:101–119d, e, p, q, hi, ws, cs, vis, etc.
ConfigurationKey enumsrc/models/enums/ConfigurationKey.ts14 DB-backed config keys
GameId enumsrc/models/enums/GameId.ts730, 570, 252490, 440, 753
OpenSearch servicesrc/common/modules/opensearch/openSearch.service.tsBulk + alias swap
Bulk pipelinesrc/common/modules/opensearch/openSearch.service.ts:292–393prep → chunk → submit → retry
Alias swapsrc/common/modules/opensearch/openSearch.service.ts:414replaceAliasForIndex — atomic
Concurrency hotfixsrc/common/modules/opensearch/openSearch.service.tsgetBulkConcurrency() forced to 1
Halt thresholdsrc/common/modules/opensearch/openSearch.service.ts:430>30% failures → halt + Slack
OpenSearch clientssrc/common/modules/opensearch/openSearch.manager.tsCoordination + ingest nodes, mutual TLS
HaloSkins servicesrc/modules/inventoryIndex/haloSkin.service.tscs2dt.com /v2 API integration
Uuskins servicesrc/modules/inventoryIndex/uuskin.service.tsRSA-signed /v1/market/items API
Module registrysrc/modules/inventoryIndex/inventoryIndex.module.tsAll imports + providers

14. Open questions


Generated 2026-05-15 · tradeit.gg engineering · repos: tradeit-inventory-server, tradeit-inventory-parser