Skip to content

Sistema de Importacao de Itens

Documentacao completa do sistema de importacao, sincronizacao e atualizacao de itens CS2 no banco de dados. Cobre todos os scripts, servicos, formulas de conversao, parsing de nomes, deteccao de raridade e tratamento de erros.


Indice

  1. Visao Geral
  2. Fonte de Dados: Waxpeer API
  3. Schema Prisma: Model Item
  4. Enums do Prisma
  5. Conversao de Precos e Moeda
  6. Parsing de Nomes de Itens
  7. Deteccao de Categoria de Arma
  8. Deteccao de Raridade
  9. Construcao de URL de Imagem
  10. Scripts de Seed e Sincronizacao
  11. Use Cases (NestJS)
  12. Controller Publico de Itens
  13. Servico de Cotacao (ExchangeRateService)
  14. Scripts Utilitarios
  15. Tratamento de Erros
  16. Regras e Decisoes Criticas

Visao Geral

O sistema de importacao de itens possui tres camadas:

  1. Scripts CLI (scripts/): Scripts TypeScript executados manualmente via npx ts-node para seed inicial, reset e sincronizacao em massa.
  2. Servicos de Infraestrutura (src/infrastructure/external-apis/): Services NestJS que encapsulam a comunicacao com a API da Waxpeer, parsing de nomes, cotacao de moeda e deteccao de raridade.
  3. Use Cases (src/application/use-cases/admin/): Logica de negocio para sincronizacao via endpoint HTTP (admin).

Fluxo Principal

Waxpeer API (/v1/prices ou /v1/get-items-list)
    |
    v
WaxpeerSnapshotService / WaxpeerItemsListService / WaxpeerService
    |
    v
ItemsPublicController (GET /api/items) -- aplica margem de 20% e cotacao BRL
    |
    v
Scripts de Seed (seed-all-items-v3.ts, etc.) -- consomem GET /api/items
    |
    v
Prisma createMany / upsert --> Tabela "items" no PostgreSQL

Alternativamente, scripts de sincronizacao direta (sync-all-items.ts, oficial-sync-items.ts) acessam a Waxpeer API sem intermediario.


Fonte de Dados: Waxpeer API

Endpoints Utilizados

EndpointServicoUso
GET /v1/pricesWaxpeerSnapshotServiceSnapshot de todos os itens com precos minimos. Retorna itens unicos. Endpoint principal para seed via controller.
GET /v1/get-items-listWaxpeerService, WaxpeerItemsListServiceLista paginada de itens a venda. Suporta filtros de preco, exterior, sort, search. Usado por scripts de sincronizacao direta e pelo use case SyncWaxpeerItemsUseCase.
GET /v1/prices/snapshotsync-all-items.ts (script)Snapshot completo (~13k itens). Usado pelo script de sincronizacao direta.

Parametros do /get-items-list

Definidos na interface GetItemsListParams em waxpeer.service.ts:

typescript
export interface GetItemsListParams {
  game?: 'csgo' | 'dota2' | 'tf2';
  minified?: number; // 0, 1, or 2
  exterior?: 'FN' | 'MW' | 'FT' | 'WW' | 'BS';
  brand?: string;
  sort?: 'asc' | 'desc' | 'profit' | 'best_deals';
  skip?: number;
  limit?: number;
  search?: string;
  max_price?: number;
  min_price?: number;
  discount?: number;
}

Autenticacao

Todos os endpoints requerem a query parameter api com a chave da Waxpeer:

typescript
const WAXPEER_API_KEY = process.env.WAXPEER_API_KEY || '';
const url = `https://api.waxpeer.com/v1/prices?api=${WAXPEER_API_KEY}&game=csgo`;

Estrutura de Resposta da Waxpeer

Endpoint /v1/prices:

typescript
interface WaxpeerPriceItem {
  name: string;       // "StatTrak(TM) AK-47 | Redline (Field-Tested)"
  min: number;        // Preco minimo em coins (1000 coins = $1.00)
  img: string;        // URL da imagem
  rarity_color?: string; // Cor hex da raridade (ex: "#eb4b4b")
  type?: string;
}

Endpoint /v1/get-items-list:

typescript
interface WaxpeerItem {
  item_id: string;
  brand: string;      // "rifle", "knife", "pistol", etc.
  image: string;
  type: string;
  price: number;      // Preco em coins
  name: string;       // Market hash name
  float?: number;
  best_deals: number;
  discount: number;
  steam_price: number;
  phase: string | null;
  partner_price: number;
}

Conversao de Coins para Centavos

A Waxpeer usa "coins" como unidade monetaria. A conversao e:

1000 coins = $1.00 USD = 100 centavos USD

Portanto:

typescript
const priceCentsUsd = Math.round(priceInCoins / 10);

Implementado em WaxpeerMapper.convertPriceToCents():

typescript
static convertPriceToCents(price: number): bigint {
  const cents = Math.round(price / 10);
  return BigInt(cents);
}

Schema Prisma: Model Item

Arquivo: prisma/schema.prisma

prisma
model Item {
  id                  BigInt          @id
  name                String
  marketHashName      String          @unique @map("market_hash_name")
  description         String?
  imageUrl            String          @map("image_url")
  rarity              Rarity
  valueCents          BigInt          @map("value_cents")
  valueCentsBrl       BigInt          @default(0) @map("value_cents_brl")
  originalPriceCents  BigInt          @default(0) @map("original_price_cents")
  marginPercent       Float           @default(20) @map("margin_percent")
  weaponCategory      WeaponCategory  @map("weapon_category")
  collection          String?
  exterior            ItemExterior?
  isStatTrak          Boolean         @default(false) @map("is_stattrak")
  createdAt           DateTime        @default(now()) @map("created_at")
  updatedAt           DateTime        @updatedAt @map("updated_at")

  caseItems          CaseItem[]
  inventory          UserInventory[]
  openings           CaseOpening[]
  upgradesTarget     Upgrade[]
  upgradeItems       UpgradeItem[]
  battleRounds       BattleRound[]
  battleSettlements  BattleSettlement[]
  rafflePrizes       RafflePrize[]
  eventLevelRewards  EventLevel[]
  siteSwaps          SiteSwap[]
  siteSwapItems      SiteSwapItem[]
  waxpeerWithdrawals WaxpeerWithdrawal[]
  eventShootItems    EventShootItem[]
  eventShootWon      EventShootOpening[] @relation("EventShootWonItem")
  eventShootAlt1     EventShootOpening[] @relation("EventShootAlt1Item")
  eventShootAlt2     EventShootOpening[] @relation("EventShootAlt2Item")

  @@map("items")
  @@index([rarity])
  @@index([valueCents])
  @@index([valueCentsBrl])
  @@index([weaponCategory])
}

Descricao de Cada Campo

CampoTipoObrigatorioDescricao
idBigIntSimSnowflake ID gerado como BigInt(Date.now()) * BigInt(100000) + BigInt(counter)
nameStringSimNome da arma + skin sem prefixos. Ex: "FAMAS | Sundown", "Bowie Knife | Fade"
marketHashNameString (unique)SimNome completo original da Waxpeer/Steam. Ex: "StatTrak(TM) AK-47 | Redline (Field-Tested)"
descriptionString?NaoDescricao opcional (raramente preenchido)
imageUrlStringSimURL da imagem do item (da Waxpeer ou construida)
rarityRarity (enum)SimRaridade do item (CONSUMER a EXTRAORDINARY)
valueCentsBigIntSimPreco em centavos USD com margem de 20% aplicada
valueCentsBrlBigIntSim (default 0)Preco em centavos BRL com margem de 20% aplicada
originalPriceCentsBigIntSim (default 0)Preco original em centavos USD sem margem (usado como teto para saques)
marginPercentFloatSim (default 20)Percentual de margem aplicado (default: 20%)
weaponCategoryWeaponCategory (enum)SimCategoria da arma (PISTOL, RIFLE, KNIFE, etc.)
collectionString?NaoColecao do item (raramente preenchido)
exteriorItemExterior? (enum)NaoCondicao de desgaste (FACTORY_NEW a BATTLE_SCARRED)
isStatTrakBooleanSim (default false)Se o item e StatTrak
createdAtDateTimeAutoData de criacao
updatedAtDateTimeAutoData de ultima atualizacao

Indices do Banco

sql
CREATE INDEX idx_items_rarity ON items (rarity);
CREATE INDEX idx_items_value_cents ON items (value_cents);
CREATE INDEX idx_items_value_cents_brl ON items (value_cents_brl);
CREATE INDEX idx_items_weapon_category ON items (weapon_category);
CREATE UNIQUE INDEX idx_items_market_hash_name ON items (market_hash_name);

Enums do Prisma

Rarity

prisma
enum Rarity {
  CONSUMER        // Cinza - grau de consumidor
  INDUSTRIAL      // Azul claro - grau industrial
  MIL_SPEC        // Azul - grau mil-spec
  RESTRICTED      // Roxo - restrito
  CLASSIFIED      // Rosa - classificado
  COVERT          // Vermelho - encoberto
  EXTRAORDINARY   // Dourado - extraordinario (facas, luvas)
}

WeaponCategory

prisma
enum WeaponCategory {
  PISTOL    // Glock, USP, Deagle, P250, Tec-9, Five-SeveN, CZ75, R8, Dual Berettas
  RIFLE     // AK-47, M4A4, M4A1-S, FAMAS, Galil AR, AUG, SG 553
  SMG       // MP9, MAC-10, MP7, UMP-45, P90, PP-Bizon
  SNIPER    // AWP, SSG 08, SCAR-20, G3SG1
  SHOTGUN   // Nova, XM1014, Sawed-Off, MAG-7
  HEAVY     // M249, Negev
  KNIFE     // Bayonet, Karambit, M9 Bayonet, Butterfly, Flip, Gut, etc.
  GLOVE     // Sport Gloves, Specialist Gloves, Driver Gloves, Hand Wraps, etc.
  OTHER     // Stickers, agents, patches, music kits, graffiti, containers, keys
}

ItemExterior

prisma
enum ItemExterior {
  FACTORY_NEW      // (Factory New)
  MINIMAL_WEAR     // (Minimal Wear)
  FIELD_TESTED     // (Field-Tested)
  WELL_WORN        // (Well-Worn)
  BATTLE_SCARRED   // (Battle-Scarred)
  NOT_PAINTED      // Sem pintura
}

Conversao de Precos e Moeda

Campos de Preco no Item

O item possui tres campos de preco:

CampoMoedaConteudo
originalPriceCentsUSD (centavos)Preco bruto da Waxpeer SEM margem
valueCentsUSD (centavos)originalPriceCents * 1.20 (com 20% de margem)
valueCentsBrlBRL (centavos)valueCents * cotacaoUSD/BRL

Formula Completa

1. Waxpeer retorna preco em "coins"
2. originalPriceCents = Math.round(coins / 10)
3. valueCents = Math.round(originalPriceCents * PRICE_MARGIN)
4. valueCentsBrl = Math.round((valueCents / 100) * exchangeRate * 100)

Onde:

  • PRICE_MARGIN = 1.2 (20% de margem)
  • exchangeRate = cotacao USD -> BRL da AwesomeAPI

Constante de Margem

typescript
private readonly PRICE_MARGIN = 1.2; // 20% margem

Usada em:

  • ItemsPublicController (aplica margem ao servir itens via GET /api/items)
  • SyncItemsFromWaxpeerUseCase (aplica margem ao salvar no banco)
  • sync-all-items.ts (script de sincronizacao direta)
  • oficial-sync-items.ts (script de sincronizacao direta)

Exemplo Numerico Completo

Item: "AK-47 | Redline (Field-Tested)"

Waxpeer retorna: price = 3500 coins

1. originalPriceCents = Math.round(3500 / 10) = 350 centavos USD ($3.50)
2. valueCents = Math.round(350 * 1.2) = 420 centavos USD ($4.20)
3. exchangeRate = 5.25 (cotacao do dia)
4. valueCentsBrl = Math.round((420 / 100) * 5.25 * 100) = Math.round(2205) = 2205 centavos BRL (R$22.05)

Resultado no banco:

originalPriceCents = 350n
marginPercent = 20
valueCents = 420n
valueCentsBrl = 2205n

Servico de Cotacao (ExchangeRateService)

Arquivo: src/infrastructure/external-apis/exchange-rate.service.ts

typescript
@Injectable()
export class ExchangeRateService {
  private readonly apiUrl = 'https://economia.awesomeapi.com.br/json/last/USD-BRL';
  private cachedRate: number | null = null;
  private cacheTime: Date | null = null;
  private readonly cacheTTL = 30 * 60 * 1000; // 30 minutos

  async getUsdToBrlRate(): Promise<number> {
    if (this.cachedRate && this.cacheTime) {
      const age = new Date().getTime() - this.cacheTime.getTime();
      if (age < this.cacheTTL) {
        return this.cachedRate;
      }
    }

    try {
      const response = await firstValueFrom(
        this.httpService.get<ExchangeRateResponse>(this.apiUrl, { timeout: 10000 }),
      );
      const rate = parseFloat(response.data.USDBRL.bid);
      if (isNaN(rate) || rate <= 0) throw new Error('Cotacao invalida');
      this.cachedRate = rate;
      this.cacheTime = new Date();
      return rate;
    } catch (error) {
      if (this.cachedRate) return this.cachedRate; // cache expirado
      return 5.00; // fallback
    }
  }
}

Regras do servico de cotacao:

  • Cache de 30 minutos
  • Se a API falha e existe cache expirado, retorna o cache expirado
  • Se nao existe cache nenhum, retorna fallback de 5.00
  • Fonte: AwesomeAPI (economia.awesomeapi.com.br)

Nos scripts CLI que acessam a Waxpeer diretamente, a cotacao tambem e buscada da AwesomeAPI:

typescript
async function getExchangeRate(): Promise<number> {
  try {
    const res = await axios.get('https://economia.awesomeapi.com.br/json/last/USD-BRL', { timeout: 10000 });
    exchangeRate = parseFloat(res.data.USDBRL.bid);
    return exchangeRate;
  } catch {
    return 5.00; // fallback
  }
}

Parsing de Nomes de Itens

Os nomes dos itens CS2 seguem o formato:

[StatTrak(TM) ][Souvenir ][*estrela* ]Arma | Skin (Exterior)

Exemplos:

  • "AK-47 | Redline (Field-Tested)"
  • "StatTrak(TM) M4A4 | Poly Mag (Factory New)"
  • "Souvenir M4A4 | Poly Mag (Field-Tested)"
  • "*estrela* Karambit | Fade (Factory New)"
  • "*estrela* StatTrak(TM) M9 Bayonet | Fade (Factory New)"
  • "Glock-18" (sem skin)

Implementacao Principal: ItemNameParserService

Arquivo: src/infrastructure/external-apis/item-name-parser.service.ts

typescript
@Injectable()
export class ItemNameParserService {
  parse(marketHashName: string): ParsedItemName {
    let workingName = marketHashName;
    let isStatTrak = false;
    let isSouvenir = false;

    // 1. Verificar e remover StatTrak
    if (workingName.includes('StatTrak\u2122')) {
      isStatTrak = true;
      workingName = workingName.replace('StatTrak\u2122 ', '');
    }

    // 2. Verificar e remover Souvenir
    if (workingName.includes('Souvenir')) {
      isSouvenir = true;
      workingName = workingName.replace('Souvenir ', '');
    }

    // 3. Extrair exterior dos parenteses no final
    let exterior: ItemExterior | undefined;
    const exteriorMatch = workingName.match(/\s*\(([^)]+)\)\s*$/);
    if (exteriorMatch) {
      const exteriorText = exteriorMatch[1].trim();
      workingName = workingName.replace(/\s*\([^)]+\)\s*$/, '').trim();
      switch (exteriorText) {
        case 'Factory New':    exterior = ItemExterior.FACTORY_NEW; break;
        case 'Minimal Wear':   exterior = ItemExterior.MINIMAL_WEAR; break;
        case 'Field-Tested':   exterior = ItemExterior.FIELD_TESTED; break;
        case 'Well-Worn':      exterior = ItemExterior.WELL_WORN; break;
        case 'Battle-Scarred': exterior = ItemExterior.BATTLE_SCARRED; break;
      }
    }

    // 4. Separar weapon name e skin name pelo "|"
    const parts = workingName.split('|').map(p => p.trim());
    const weaponName = parts[0];
    const skinName = parts[1] || '';

    return { fullName: marketHashName, weaponName, skinName, exterior, isStatTrak, isSouvenir };
  }
}

Implementacao nos Scripts CLI

Os scripts usam uma funcao parseItemName inline que faz o mesmo processamento, com uma diferenca: tambem remove o caractere de estrela (*estrela*) para facas/luvas:

typescript
function parseItemName(fullName: string) {
  let workingName = fullName;
  let isStatTrak = false;

  // StatTrak
  if (workingName.includes('StatTrak\u2122') || workingName.includes('StatTrak')) {
    isStatTrak = true;
    workingName = workingName.replace(/StatTrak\u2122?\s*/i, '');
  }

  // Souvenir
  if (workingName.includes('Souvenir')) {
    workingName = workingName.replace(/Souvenir\s*/i, '');
  }

  // Exterior
  let exterior: ItemExterior | undefined;
  const exteriorMatch = workingName.match(/\s*\(([^)]+)\)\s*$/);
  if (exteriorMatch) {
    const ext = exteriorMatch[1].trim();
    workingName = workingName.replace(/\s*\([^)]+\)\s*$/, '').trim();
    switch (ext) {
      case 'Factory New':    exterior = ItemExterior.FACTORY_NEW; break;
      case 'Minimal Wear':   exterior = ItemExterior.MINIMAL_WEAR; break;
      case 'Field-Tested':   exterior = ItemExterior.FIELD_TESTED; break;
      case 'Well-Worn':      exterior = ItemExterior.WELL_WORN; break;
      case 'Battle-Scarred': exterior = ItemExterior.BATTLE_SCARRED; break;
    }
  }

  // Remover estrela (facas/luvas)
  workingName = workingName.replace(/^\u2605\s*/, '').trim();

  return { name: workingName, marketHashName: fullName, exterior, isStatTrak };
}

Mapper Waxpeer (WaxpeerMapper)

Arquivo: src/infrastructure/external-apis/waxpeer.mapper.ts

O WaxpeerMapper tambem possui parsing mas com uma diferenca: ele retorna weaponName e skinName separados:

typescript
static parseItemName(name: string): ParsedItemName {
  // Mesma logica de StatTrak, Souvenir, Exterior
  // ...
  const parts = workingName.split('|').map(p => p.trim());
  const weaponName = parts[0]; // "M4A4"
  const skinName = parts[1] || ''; // "Poly Mag"
  return { weaponName, skinName, exterior, isStatTrak, isSouvenir };
}

Exterior: Mapeamento Codigo para Enum

O WaxpeerMapper tambem converte codigos curtos de exterior:

typescript
static mapExteriorCode(code: string): ItemExterior | undefined {
  const codeMap: Record<string, ItemExterior> = {
    'FN': ItemExterior.FACTORY_NEW,
    'MW': ItemExterior.MINIMAL_WEAR,
    'FT': ItemExterior.FIELD_TESTED,
    'WW': ItemExterior.WELL_WORN,
    'BS': ItemExterior.BATTLE_SCARRED,
  };
  return codeMap[code.toUpperCase()];
}

Tabela Completa de Exterior

Texto na WaxpeerCodigo CurtoEnum Prisma
Factory NewFNFACTORY_NEW
Minimal WearMWMINIMAL_WEAR
Field-TestedFTFIELD_TESTED
Well-WornWWWELL_WORN
Battle-ScarredBSBATTLE_SCARRED

O que Vai para o Campo name

O campo name do Item recebe nomes diferentes dependendo do script/servico:

Script/ServicoO que vai no nameExemplo
seed-items-from-api.tsArma + Skin (sem prefixos, sem exterior, sem estrela)"FAMAS | Sundown"
seed-all-items.tsIdem"FAMAS | Sundown"
seed-all-items-v3.tsIdem"FAMAS | Sundown"
seed-all-items-fast.tsIdem"FAMAS | Sundown"
reset-and-reseed-items.tsIdem"FAMAS | Sundown"
sync-all-items.tsApenas o nome da skin"Sundown"
oficial-sync-items.tsApenas o nome da skin"Sundown"
SyncItemsFromWaxpeerUseCaseApenas o nome da skin (via ItemNameParserService)"Poly Mag"
SyncWaxpeerItemsUseCaseNome completo (via WaxpeerMapper)"StatTrak(TM) AK-47 | Redline (Field-Tested)"

O campo marketHashName SEMPRE recebe o nome completo original da Waxpeer/Steam, que e a chave unica.


Deteccao de Categoria de Arma

Via Campo brand da Waxpeer (WaxpeerMapper)

Quando o endpoint /get-items-list retorna o campo brand, o mapeamento e direto:

typescript
static mapBrandToWeaponCategory(brand: string): WeaponCategory {
  const brandMap: Record<string, WeaponCategory> = {
    'pistol': WeaponCategory.PISTOL,
    'gloves': WeaponCategory.GLOVE,
    'knife': WeaponCategory.KNIFE,
    'rifle': WeaponCategory.RIFLE,
    'smg': WeaponCategory.SMG,
    'shotgun': WeaponCategory.SHOTGUN,
    'sniper_rifle': WeaponCategory.SNIPER,
    'machinegun': WeaponCategory.HEAVY,
    'key': WeaponCategory.OTHER,
    'pass': WeaponCategory.OTHER,
    'sticker': WeaponCategory.OTHER,
    'agent': WeaponCategory.OTHER,
    'patch': WeaponCategory.OTHER,
    'container': WeaponCategory.OTHER,
    'music_kit': WeaponCategory.OTHER,
    'graffiti': WeaponCategory.OTHER,
  };
  return brandMap[brand.toLowerCase()] || WeaponCategory.OTHER;
}

Via Inferencia pelo Nome (Fallback)

Quando o campo brand nao esta disponivel (endpoint /prices, scripts), a categoria e inferida pelo nome:

typescript
function inferWeaponCategory(name: string): WeaponCategory {
  const lower = name.toLowerCase();

  // KNIFE
  if (lower.includes('knife') || lower.includes('karambit') || lower.includes('bayonet') ||
      lower.includes('daggers') || lower.includes('m9')) return WeaponCategory.KNIFE;

  // GLOVE
  if (lower.includes('gloves') || lower.includes('hand wraps')) return WeaponCategory.GLOVE;

  // PISTOL
  if (lower.includes('glock') || lower.includes('usp') || lower.includes('deagle') ||
      lower.includes('p250') || lower.includes('tec-9') || lower.includes('five-seve') ||
      lower.includes('cz75') || lower.includes('r8') || lower.includes('dual')) return WeaponCategory.PISTOL;

  // RIFLE
  if (lower.includes('ak-47') || lower.includes('m4a4') || lower.includes('m4a1') ||
      lower.includes('famas') || lower.includes('galil') || lower.includes('aug') ||
      lower.includes('sg 553')) return WeaponCategory.RIFLE;

  // SMG
  if (lower.includes('mp9') || lower.includes('mac-10') || lower.includes('mp7') ||
      lower.includes('ump-45') || lower.includes('p90') || lower.includes('bizon')) return WeaponCategory.SMG;

  // SHOTGUN
  if (lower.includes('nova') || lower.includes('xm1014') || lower.includes('sawed-off') ||
      lower.includes('mag-7')) return WeaponCategory.SHOTGUN;

  // SNIPER
  if (lower.includes('awp') || lower.includes('ssg 08') || lower.includes('scar-20') ||
      lower.includes('g3sg1')) return WeaponCategory.SNIPER;

  // HEAVY
  if (lower.includes('m249') || lower.includes('negev')) return WeaponCategory.HEAVY;

  return WeaponCategory.OTHER;
}

Filtragem: Itens OTHER sao Excluidos

Em todos os scripts de sincronizacao e use cases, itens com categoria OTHER sao filtrados:

typescript
const validItems = waxpeerItems.filter(item => {
  const cat = inferWeaponCategory(item.name);
  return cat !== WeaponCategory.OTHER;
});

Isso exclui stickers, agents, patches, music kits, graffiti, containers e keys.


Deteccao de Raridade

O sistema utiliza quatro estrategias em cascata para determinar a raridade de um item, implementadas no RarityDetectorService:

1. CSFloat API (maior precisao)

typescript
const csfloatRarity = await this.csfloatService.getItemRarity(marketHashName);
if (csfloatRarity) return csfloatRarity;

2. Parser de items_game.txt (Steam Database)

typescript
const gameRarity = await this.itemsGameParser.getRarityForItem(marketHashName);
if (gameRarity) return gameRarity;

3. Padroes Conhecidos de Itens

Mapeamento hardcoded de facas/luvas:

typescript
const KNOWN_ITEM_RARITIES: Record<string, Rarity> = {
  'Bayonet': Rarity.EXTRAORDINARY,
  'Bowie Knife': Rarity.EXTRAORDINARY,
  'Butterfly Knife': Rarity.EXTRAORDINARY,
  'Karambit': Rarity.EXTRAORDINARY,
  'M9 Bayonet': Rarity.EXTRAORDINARY,
  // ... todas as facas e luvas
  'Sport Gloves': Rarity.EXTRAORDINARY,
  'Specialist Gloves': Rarity.EXTRAORDINARY,
  // ...
};

Alem disso, existe um array COLLECTION_RARITY_PATTERNS com centenas de padroes regex para skins conhecidas.

4. Inferencia por Cor Hex (WaxpeerSnapshotService)

Quando a Waxpeer retorna rarity_color, o mapeamento e:

typescript
const colorMap: Record<string, Rarity> = {
  '#b0c3d9': Rarity.CONSUMER,        // Cinza
  '#5e98d9': Rarity.INDUSTRIAL,      // Azul claro
  '#4b69ff': Rarity.MIL_SPEC,        // Azul
  '#8847ff': Rarity.RESTRICTED,      // Roxo
  '#d32ce6': Rarity.CLASSIFIED,      // Rosa
  '#eb4b4b': Rarity.COVERT,          // Vermelho
  '#ffd700': Rarity.EXTRAORDINARY,   // Dourado
  '#f8ae2a': Rarity.EXTRAORDINARY,   // Dourado (variante)
  '#e4ae39': Rarity.EXTRAORDINARY,   // Dourado (variante)
};

5. Inferencia por Preco (ultimo recurso)

Quando nenhuma outra fonte funciona:

WaxpeerMapper (por preco USD em centavos):

typescript
static inferRarityFromPrice(priceCents: bigint): Rarity {
  const price = Number(priceCents);
  if (price < 10) return Rarity.CONSUMER;       // < $0.10
  if (price < 50) return Rarity.INDUSTRIAL;     // $0.10 - $0.50
  if (price < 200) return Rarity.MIL_SPEC;      // $0.50 - $2.00
  if (price < 1000) return Rarity.RESTRICTED;   // $2.00 - $10.00
  if (price < 5000) return Rarity.CLASSIFIED;   // $10.00 - $50.00
  if (price < 20000) return Rarity.COVERT;      // $50.00 - $200.00
  return Rarity.EXTRAORDINARY;                   // > $200.00
}

sync-and-update-items.ts (por preco, faixa mais restrita para itens baratos):

typescript
function inferRarityFromPrice(priceCents: number): Rarity {
  if (priceCents < 10) return Rarity.CONSUMER;
  if (priceCents < 20) return Rarity.INDUSTRIAL;
  if (priceCents < 30) return Rarity.MIL_SPEC;
  return Rarity.RESTRICTED;
}

RarityDetectorService (por preco, com weapon category):

typescript
private inferFromWeaponAndPrice(weaponCategory: WeaponCategory, priceCents: bigint): Rarity {
  if (weaponCategory === WeaponCategory.KNIFE || weaponCategory === WeaponCategory.GLOVE) {
    return Rarity.EXTRAORDINARY;
  }
  const price = Number(priceCents);
  if (price > 100000) return Rarity.COVERT;      // > $1000
  if (price > 50000) return Rarity.CLASSIFIED;    // > $500
  if (price > 20000) return Rarity.RESTRICTED;    // > $200
  if (price > 5000) return Rarity.MIL_SPEC;       // > $50
  if (price > 1000) return Rarity.INDUSTRIAL;     // > $10
  return Rarity.CONSUMER;
}

6. Inferencia por String de Rarity (oficial-sync-items.ts)

Quando o endpoint retorna o campo rarity como texto:

typescript
function inferRarity(rarityStr?: string, name?: string): Rarity {
  if (rarityStr) {
    const upper = rarityStr.toUpperCase();
    if (upper.includes('CONSUMER')) return Rarity.CONSUMER;
    if (upper.includes('INDUSTRIAL')) return Rarity.INDUSTRIAL;
    if (upper.includes('MIL-SPEC') || upper.includes('MIL_SPEC')) return Rarity.MIL_SPEC;
    if (upper.includes('RESTRICTED')) return Rarity.RESTRICTED;
    if (upper.includes('CLASSIFIED')) return Rarity.CLASSIFIED;
    if (upper.includes('COVERT')) return Rarity.COVERT;
    if (upper.includes('EXTRAORDINARY') || upper.includes('CONTRABAND')) return Rarity.EXTRAORDINARY;
  }
  if (name) {
    const lower = name.toLowerCase();
    if (lower.includes('knife') || lower.includes('karambit') || lower.includes('bayonet'))
      return Rarity.EXTRAORDINARY;
    if (lower.includes('gloves')) return Rarity.EXTRAORDINARY;
  }
  return Rarity.CONSUMER;
}

Construcao de URL de Imagem

Fonte Principal: Campo image/img da Waxpeer

A maioria dos itens recebe a URL diretamente da resposta da Waxpeer:

typescript
imageUrl: item.image  // ou item.img (dependendo do endpoint)

Fallback: Construcao de URL

Quando a imagem nao esta disponivel, o WaxpeerSnapshotService constroi a URL:

typescript
static buildWaxpeerImageUrl(marketHashName: string): string {
  const slug = marketHashName
    .replace(/\u2605/g, '')                    // Remove estrela
    .replace(/StatTrak\u2122/g, 'stattrak')    // Normaliza StatTrak
    .replace(/\|/g, '')                        // Remove pipe
    .replace(/[()]/g, '')                      // Remove parenteses
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '-')                      // Espacos -> hifens
    .replace(/-+/g, '-')                       // Hifens duplicados
    .replace(/^-|-$/g, '');                    // Hifens nas bordas
  return `https://images.waxpeer.com/i/730-${slug}.webp`;
}

Exemplo:

Input:  "StatTrak(TM) AK-47 | Redline (Field-Tested)"
Slug:   "stattrak-ak-47-redline-field-tested"
Output: "https://images.waxpeer.com/i/730-stattrak-ak-47-redline-field-tested.webp"

Fallback do Steam Community

Usado pelo SyncSteamDatabaseUseCase quando nao ha imagem:

typescript
imageUrl: steamItem.imageUrl || `https://steamcommunity-a.akamaihd.net/economy/image/${steamItem.marketHashName}`;

Scripts de Seed e Sincronizacao

Visao Comparativa

ScriptArquivoFonteEstrategiaMargemBRLoriginalPriceCentsUso
seed-items-from-apiscripts/seed-items-from-api.tsGET /api/items (local)deleteMany + create individualAplicada pela APISimNaoSeed inicial (5 itens, teste)
seed-all-itemsscripts/seed-all-items.tsGET /api/items (local)Upsert individual (check + create/update)Aplicada pela APISimNaoSeed completo com upsert
seed-all-items-fastscripts/seed-all-items-fast.tsGET /api/items (local)createMany com skipDuplicatesAplicada pela APISimNaoSeed rapido (apenas novos)
seed-all-items-v3scripts/seed-all-items-v3.tsGET /api/items (local)createMany com skipDuplicates em batchesAplicada pela APISimSimSeed otimizado final
reset-and-reseed-itemsscripts/reset-and-reseed-items.tsGET /api/items (local)Deleta tudo + createMany + recria CaseItemsAplicada pela APISimSimReset completo com preservacao de caixas
sync-all-itemsscripts/sync-all-items.tsWaxpeer /v1/prices/snapshot (direto)Upsert individualAplicada no scriptSimNaoSincronizacao direta sem servidor
oficial-sync-itemsscripts/oficial-sync-items.tsWaxpeer /v1/get-items-list (direto)Upsert individualAplicada no scriptSimNaoSincronizacao direta com imagens
sync-and-update-itemsscripts/sync-and-update-items.tsWaxpeer /v1/get-items-list (direto)Upsert individualNao (direto)NaoNaoImportacao de itens baratos ($0-$0.50)
update-cheap-item-pricesscripts/update-cheap-item-prices.tsWaxpeer /v1/get-items-list (direto)Update apenas precoNao (direto)NaoNaoAtualizacao de precos de itens baratos

seed-items-from-api.ts (Seed de Teste)

Arquivo: scripts/seed-items-from-api.ts

  • Busca 5 itens de GET http://localhost:3000/api/items?limit=5
  • Limpa toda a tabela items (deleteMany)
  • Insere um por um com prisma.item.create()
  • Gera IDs como BigInt(Date.now()) * BigInt(1000) + BigInt(index)
  • Nao inclui originalPriceCents nem marginPercent

Execucao:

bash
npx ts-node scripts/seed-items-from-api.ts

seed-all-items.ts (Upsert Completo)

Arquivo: scripts/seed-all-items.ts

  • Busca todos os itens de GET http://localhost:3000/api/items
  • Carrega existentes via findMany({ select: { marketHashName, id } })
  • Para cada item:
    • Se existe: prisma.item.update() (atualiza preco e imagem)
    • Se nao existe: prisma.item.create()
  • Processa em batches de 100 com delay de 10ms entre batches
  • Filtra itens sem imagem
  • Gera IDs como BigInt(Date.now()) * BigInt(100000) + BigInt(created)
  • Exibe estatisticas no final: total, valor USD, valor BRL, contagem por categoria

Execucao:

bash
npx ts-node scripts/seed-all-items.ts

seed-all-items-fast.ts (Apenas Novos)

Arquivo: scripts/seed-all-items-fast.ts

  • Busca todos os itens de GET http://localhost:3000/api/items
  • Carrega existentes em um Set<string> de marketHashName
  • Filtra:
    • Sem imagem -> pula
    • Ja existe -> pula
  • Prepara array de Prisma.ItemCreateManyInput
  • Insere com createMany({ data: batch, skipDuplicates: true }) em batches de 5000
  • Mais rapido que o seed-all-items.ts por usar bulk insert

Execucao:

bash
npx ts-node scripts/seed-all-items-fast.ts

seed-all-items-v3.ts (Versao Final Otimizada)

Arquivo: scripts/seed-all-items-v3.ts

  • Script recomendado para seed em massa
  • Busca todos os itens de GET http://localhost:3000/api/items
  • Interface ApiItem inclui originalPriceCents e marginPercent
  • Filtra itens sem imagem
  • Insere com createMany({ data: batch, skipDuplicates: true }) em batches de 2000
  • Log de progresso a cada 5000 itens
  • Salva originalPriceCents e marginPercent no banco

Dados salvos:

typescript
{
  id: idCounter,
  name: parsed.name,                                    // "FAMAS | Sundown"
  marketHashName: parsed.marketHashName,                 // "FAMAS | Sundown (Field-Tested)"
  imageUrl: apiItem.imageUrl,
  rarity: mapRarity(apiItem.rarity),
  valueCents: BigInt(apiItem.priceCents),                // Com margem (vem da API)
  valueCentsBrl: BigInt(Math.round(apiItem.priceBrl * 100)),
  originalPriceCents: BigInt(apiItem.originalPriceCents), // Sem margem
  marginPercent: apiItem.marginPercent,                   // 20
  weaponCategory: mapWeaponCategory(apiItem.weaponCategory),
  exterior: parsed.exterior,
  isStatTrak: parsed.isStatTrak,
}

Execucao:

bash
npx ts-node scripts/seed-all-items-v3.ts

reset-and-reseed-items.ts (Reset Completo)

Arquivo: scripts/reset-and-reseed-items.ts

IMPORTANTE: Executar com o servidor PARADO.

Fluxo:

  1. Busca a caixa "Fade Chance" e seus CaseItems
  2. Deleta CaseItems da Fade Chance
  3. Verifica dependencias (UserInventory, CaseOpening, Upgrade)
  4. Se existem dependencias, deleta em ordem:
    • UpgradeItem -> Upgrade -> UserInventory -> CaseOpening -> BattleRound -> BattleSettlement
  5. Deleta todos os CaseItems restantes
  6. Deleta todos os Items
  7. Busca itens da API e insere com createMany
  8. Recria CaseItems da Fade Chance usando array hardcoded FADE_CASE_ITEMS

O array FADE_CASE_ITEMS contem 12 itens com pesos e hash ranges pre-definidos:

typescript
const FADE_CASE_ITEMS = [
  {
    marketHashName: "G3SG1 | Green Apple (Minimal Wear)",
    weight: 2984,
    hashRangeStart: 1,
    hashRangeEnd: 298417,
    dropChance: 2.9842,
    isRare: false,
  },
  // ... mais 11 itens
  {
    marketHashName: "StatTrak(TM) M9 Bayonet | Fade (Factory New)",
    weight: 10,
    hashRangeStart: 9998995,
    hashRangeEnd: 10000000,
    dropChance: 0.0101,
    isRare: true,
  },
];

Execucao:

bash
npx ts-node scripts/reset-and-reseed-items.ts

sync-all-items.ts (Direto da Waxpeer - Snapshot)

Arquivo: scripts/sync-all-items.ts

  • Acessa diretamente https://api.waxpeer.com/v1/prices/snapshot
  • Nao precisa do servidor rodando
  • Remove duplicatas (mantem menor preco por nome)
  • Aplica margem de 20% (PRICE_MARGIN = 1.2)
  • Busca cotacao USD->BRL da AwesomeAPI
  • Calcula valueCents e valueCentsBrl
  • Filtra itens com categoria OTHER
  • Upsert individual (check existente -> update ou create)
  • Processa em batches de 100 com delay de 50ms
  • Usa inferRarityFromColor() para deteccao de raridade

Calculo de precos:

typescript
const priceCentsOriginal = Math.round(item.price / 10);       // coins -> cents
const priceCentsUsd = Math.round(priceCentsOriginal * PRICE_MARGIN); // +20%
const priceBrl = (priceCentsUsd / 100) * rate;                 // USD -> BRL
const priceCentsBrl = Math.round(priceBrl * 100);              // BRL -> cents BRL

Execucao:

bash
WAXPEER_API_KEY=xxx npx ts-node scripts/sync-all-items.ts

oficial-sync-items.ts (Direto da Waxpeer - Items List)

Arquivo: scripts/oficial-sync-items.ts

  • Acessa diretamente https://api.waxpeer.com/v1/get-items-list
  • Paginacao com skip e limit=100
  • Nao precisa do servidor rodando
  • Mesma logica de margem e cotacao do sync-all-items.ts
  • Diferencial: retorna campo image com URL da imagem de cada item
  • Usa inferRarity() que aceita tanto rarity string quanto fallback por nome

Paginacao:

typescript
while (hasMore) {
  const url = `https://api.waxpeer.com/v1/get-items-list?api=${WAXPEER_API_KEY}&game=csgo&skip=${skip}&limit=${limit}&sort=best_deals`;
  const res = await axios.get(url, { timeout: 30000 });
  // ...
  if (items.length < limit) hasMore = false;
  else skip += limit;
  await new Promise(r => setTimeout(r, 100)); // rate limiting
}

Execucao:

bash
WAXPEER_API_KEY=xxx npx ts-node scripts/oficial-sync-items.ts

Use Cases (NestJS)

SyncItemsFromWaxpeerUseCase

Arquivo: src/application/use-cases/admin/sync-items-from-waxpeer.use-case.ts

Use case principal para sincronizacao via endpoint HTTP admin. Usa o WaxpeerSnapshotService como fonte.

Fluxo:

  1. Busca itens filtrados do WaxpeerSnapshotService.getFilteredItems() (ja com cache/stale-fallback)
  2. Remove itens com categoria OTHER
  3. Aplica filtros opcionais (categorias, maxItems)
  4. Obtem a margem atual via PriceMarginService.getCurrent() e a cotacao via ExchangeRateService.getUsdToBrlRate() -- a margem nao eh hardcoded 20%, e configuravel
  5. Passa cada item pelo OutlierDetectionService:
    • Aprovado -> usa o preco Waxpeer
    • Bloqueado com referencia (CSFloat base_price / mediana de peers) -> recupera sobrescrevendo o preco com o valor de referencia
    • Bloqueado sem referencia (no_reference_high_value / no_reference_souvenir_high_value) -> pula (mantem o preco atual no banco, "cura" no proximo sync)
  6. Carrega itens existentes do IItemRepository e processa em batches (default batchSize = 500):
    • Parseia nome com ItemNameParserService (pula itens sem skin)
    • Preco: valueCents (USD) = round(waxpeerCents * priceMargin), valueCentsBrl = round((valueCents/100) * exchangeRate * 100)
    • Se existe: itemRepository.update() (preco + imagem); se nao existe: itemRepository.create()
  7. Sibling fallback: para cada item do banco sem match direto na Waxpeer, procura um "irmao" (mesma arma+skin+variante NS/ST/SV) e usa siblingMax * margin -- apenas se o preco atual estiver mais de 50% acima da sugestao
  8. detectAffectedCases: recomputa o RTP das caixas afetadas (compara valueCentsBrl antes/depois, EV = Σ value*dropChance/100) e sinaliza alerta de RTP quando necessario
  9. Auditoria final ADMIN_SYNC_WAXPEER_ITEMS com o resumo do sync

Interface de opcoes:

typescript
export interface SyncItemsOptions {
  weaponCategories?: WeaponCategory[];
  skipExisting?: boolean;      // default false
  batchSize?: number;          // default 500
  maxItems?: number;
  adminUserId?: bigint;        // ator da auditoria
  ipAddress?: string;          // default '127.0.0.1'
  userAgent?: string;
}

Resultado:

typescript
export interface SyncItemsResult {
  totalFetched: number;
  outliersBlocked: number;
  outliersRecoveredWithReference: number;
  siblingFallbackUpdated: number;
  outlierDetectionStats?: Record<OutlierDetectionLevel, number>;
  created: number;
  updated: number;
  skipped: number;
  failed: number;
  errors: string[];
  notFoundInWaxpeer: Array<{ id: string; name: string; marketHashName: string }>;
  createdItems: Array<{
    id: string;
    name: string;
    marketHashName: string;
    valueCentsBrl: string;
    isStatTrak: boolean;
  }>;
  affectedCases: Array<{
    id: string;
    name: string;
    rtpBefore: number;
    rtpAfter: number;
    deltaPp: number;
    changedItemsCount: number;
  }>;
  auditLogId?: string | null;
  priceMargin?: number;
  exchangeRate?: number;
}

SyncWaxpeerItemsUseCase

Arquivo: src/application/use-cases/admin/sync-waxpeer-items.use-case.ts

Use case alternativo que usa o WaxpeerService.getAllItems() (endpoint /get-items-list com paginacao).

Diferenciais:

  • Usa WaxpeerMapper para mapeamento completo
  • Suporta filtros de preco (minPriceCents, maxPriceCents)
  • Campo brand da Waxpeer mapeado para WeaponCategory
  • Raridade inferida pelo preco via WaxpeerMapper.inferRarityFromPrice()

UpdateItemPricesUseCase

Arquivo: src/application/use-cases/admin/update-item-prices.use-case.ts

Atualiza apenas precos de itens ja existentes no banco.

Modos de operacao:

  • specificItems: Array de marketHashName para atualizar
  • updateAll: Atualiza todos os itens
  • Default: Atualiza apenas itens com preco zero ou muito baixo (valueCents <= 100)

Fluxo:

  1. Busca itens a atualizar do banco
  2. Busca todos os precos da Waxpeer via getAllItems()
  3. Cria Map<nome, WaxpeerItem> para lookup rapido
  4. Para cada item, atualiza valueCents e imageUrl

SyncSteamDatabaseUseCase

Arquivo: src/application/use-cases/admin/sync-steam-database.use-case.ts

Sincroniza metadados de itens do Steam Database (nome, raridade, categoria, colecao). Nao inclui precos (precisa rodar atualizacao de precos separadamente).

Dados do Steam Database:

  • name, marketHashName
  • rarity (mais preciso que inferencia)
  • weaponCategory
  • collection
  • isStatTrak
  • skinName

IMPORTANTE: Cria itens com valueCents = 0n. Os precos devem ser atualizados separadamente pelo UpdateItemPricesUseCase ou SyncItemsFromWaxpeerUseCase.


Controller Publico de Itens

Arquivo: src/presentation/controllers/items-public.controller.ts

O controller GET /api/items e a interface entre a Waxpeer e os scripts de seed. Ele:

  1. Busca itens do WaxpeerSnapshotService (cache de 5 minutos)
  2. Aplica filtros locais (search, category, rarity, minPrice, maxPrice, limit)
  3. Busca cotacao USD->BRL do ExchangeRateService
  4. Aplica margem de 20% em todos os precos
  5. Retorna itens com campos extras: originalPriceCents, priceBrl, marginPercent

Resposta:

typescript
{
  success: true,
  count: 12500,
  exchangeRate: 5.25,
  marginPercent: 20,
  items: [
    {
      name: "AK-47 | Redline (Field-Tested)",
      price: 4200,                // coins com margem
      priceCents: 420,            // centavos USD com margem
      originalPriceCents: 350,    // centavos USD sem margem
      priceBrl: 22.05,            // valor em BRL (float)
      steamPrice: 3500,
      imageUrl: "https://...",
      rarity: "RESTRICTED",
      weaponCategory: "RIFLE",
      marginPercent: 20,
    },
    // ...
  ]
}

Endpoints disponiveis:

EndpointMetodoDescricao
/api/itemsGETLista todos os itens com margem e BRL
/api/items/categoriesGETLista categorias com contagens
/api/items/raritiesGETLista raridades com contagens
/api/items/cheapGETItens de $0 a $0.50 (limit default: 100)
/api/items/statsGETEstatisticas gerais (total, valor, media)
/api/items/exchange-rateGETCotacao USD->BRL atual
/api/items/syncPOSTSincroniza itens para o banco (admin only)

Geracao de IDs

Os IDs dos itens sao gerados como Snowflake-like IDs usando BigInt:

typescript
// Pattern 1: timestamp * multiplicador + contador
let idCounter = BigInt(Date.now()) * BigInt(100000);
// ...
idCounter++;
const id = idCounter;

// Pattern 2: timestamp * multiplicador + indice
const id = BigInt(Date.now()) * BigInt(1000) + BigInt(index);

// Pattern 3: Servico Snowflake (em use cases NestJS)
const id = this.snowflakeService.generate();

Batch Import

createMany com skipDuplicates

Os scripts otimizados (seed-all-items-v3.ts, seed-all-items-fast.ts) usam createMany:

typescript
const BATCH_SIZE = 2000; // ou 5000
let inserted = 0;

for (let i = 0; i < itemsData.length; i += BATCH_SIZE) {
  const batch = itemsData.slice(i, i + BATCH_SIZE);

  const result = await prisma.item.createMany({
    data: batch as any,
    skipDuplicates: true,
  });

  inserted += result.count;

  if (i % 5000 === 0) {
    const progress = Math.round(((i + batch.length) / itemsData.length) * 100);
    console.log(`${Math.min(i + batch.length, itemsData.length)}/${itemsData.length} (${progress}%)`);
  }
}

Tamanhos de batch por script:

ScriptBatch Size
seed-all-items-v3.ts2000
seed-all-items-fast.ts5000
reset-and-reseed-items.ts2000
seed-all-items.ts (upsert)100
sync-all-items.ts (upsert)100
oficial-sync-items.ts (upsert)100

skipDuplicates

O skipDuplicates: true garante que itens com marketHashName duplicado sao ignorados silenciosamente, sem causar erro. Isso e possivel porque marketHashName possui constraint @unique.


Scripts Utilitarios

check-items.js

Arquivo: scripts/check-items.js

Exibe os 5 ultimos itens inseridos no banco:

javascript
const items = await prisma.item.findMany({
  take: 5,
  orderBy: { id: 'desc' }
});
items.forEach((item, i) => {
  console.log(`${i+1}. ${item.marketHashName}`);
  console.log(`   Skin: ${item.name} | StatTrak: ${item.isStatTrak} | Ext: ${item.exterior || 'N/A'}`);
  console.log(`   Preco: ${item.valueCents} cents (USD) / ${item.valueCentsBrl} cents (BRL)`);
  console.log(`   Img: ${item.imageUrl.substring(0, 60)}...`);
});

Execucao:

bash
node scripts/check-items.js

count-items.js

Arquivo: scripts/count-items.js

Conta o total de itens no banco:

javascript
const count = await prisma.item.count();
console.log('Total de itens no banco:', count);

Execucao:

bash
node scripts/count-items.js

clear-items.js

Arquivo: scripts/clear-items.js

Deleta todos os itens do banco:

javascript
const result = await prisma.item.deleteMany({});
console.log('Removidos:', result.count);

Execucao:

bash
node scripts/clear-items.js

update-cheap-item-prices.ts

Arquivo: scripts/update-cheap-item-prices.ts

Atualiza precos de itens baratos ($0 a $0.50) buscando diretamente da Waxpeer:

  1. Busca itens do banco com valueCents entre 0 e 50
  2. Busca precos da Waxpeer via /get-items-list (paginado, max 20 paginas)
  3. Para cada item, compara o preco e atualiza se mudou
  4. Conversao: priceCents = Math.round(waxpeerItem.price / 10)

Execucao:

bash
WAXPEER_API_KEY=xxx npx ts-node scripts/update-cheap-item-prices.ts

seed-event-shoot-items.ts

Arquivo: scripts/seed-event-shoot-items.ts

Seed especifico para itens do evento "Shoot" (mini-game). Nao e um seed de itens generico.

Busca itens baratos do banco por faixas de preco em BRL (valueCentsBrl) e os insere na tabela EventShootItem com pesos e hash ranges calculados.

Faixas de preco:

typescript
const PRICE_RANGES: PriceRange[] = [
  { minCents: 3, maxCents: 6, weight: 40, count: 10 },
  { minCents: 7, maxCents: 15, weight: 30, count: 8 },
  { minCents: 16, maxCents: 30, weight: 15, count: 6 },
  { minCents: 31, maxCents: 60, weight: 10, count: 5 },
  { minCents: 61, maxCents: 100, weight: 5, count: 4 },
];

Tratamento de Erros

Filtro de Itens Sem Imagem

Todos os scripts filtram itens sem imageUrl:

typescript
const itemsData = apiItems
  .filter((apiItem) => apiItem.imageUrl) // Pula itens sem imagem
  .map((apiItem) => { /* ... */ });

Contadores de Erro

Todos os scripts mantam contadores de sucesso/falha:

typescript
let created = 0;
let updated = 0;
let skipped = 0;
let failed = 0;

Log de Erros Limitado

Para evitar flood no console, apenas os 5 primeiros erros sao logados:

typescript
} catch (err) {
  failed++;
  if (failed <= 5) {
    console.error(`Erro em "${apiItem.name}": ${err.message}`);
  }
}

Timeout de Requisicoes

EndpointTimeout
GET /api/items (local)120.000ms (2 min)
Waxpeer /get-items-list30.000ms (30s)
Waxpeer /prices30.000ms (30s)
Waxpeer /prices/snapshot60.000ms (1 min)
AwesomeAPI (cotacao)10.000ms (10s)

Rate Limiting da Waxpeer

Todos os scripts e servicos incluem delays entre requisicoes:

typescript
// Entre paginas de paginacao
await new Promise(r => setTimeout(r, 100));

// Entre batches de processamento
await new Promise(r => setTimeout(r, 50));

Verificacao de ECONNREFUSED

Scripts que dependem do servidor local verificam o erro de conexao:

typescript
} catch (error) {
  console.error('Erro:', error.message);
  if (error.code === 'ECONNREFUSED') {
    console.error('O servidor da API nao esta rodando!');
    console.error('Execute: npm run start:dev');
  }
  process.exit(1);
}

Fallback de Cotacao

Todos os pontos que buscam cotacao tem fallback para 5.00:

typescript
try {
  const res = await axios.get('https://economia.awesomeapi.com.br/json/last/USD-BRL');
  exchangeRate = parseFloat(res.data.USDBRL.bid);
} catch {
  return 5.00; // fallback
}

Cache Expirado como Fallback

O WaxpeerSnapshotService e o ExchangeRateService retornam cache expirado em caso de falha:

typescript
} catch (error) {
  if (this.cache) {
    this.logger.warn('Retornando cache expirado devido a erro');
    return this.cache;
  }
  throw error;
}

Regras e Decisoes Criticas

1. SEMPRE usar valueCentsBrl para calculos internos

O sistema financeiro opera em BRL. O campo valueCents (USD) e apenas referencia.

2. SEMPRE aplicar margem de 20%

O preco no banco (valueCents) ja inclui a margem. O originalPriceCents e o preco sem margem (usado como teto para saques/withdrawals).

3. NUNCA inserir itens com categoria OTHER

Stickers, agents, patches, music kits, graffiti, containers e keys sao filtrados.

4. marketHashName e a chave unica

Dois itens nunca podem ter o mesmo marketHashName. E usado como chave de deduplicacao.

5. IDs sao BigInt (Snowflake-like)

Nunca use UUID ou auto-increment. IDs sao gerados com base em timestamp.

6. Precos sao BIGINT em centavos

Nunca use float/decimal para precos. Sempre centavos inteiros.

7. Cache da Waxpeer: 5 minutos

O WaxpeerSnapshotService cache a resposta do /prices por 5 minutos.

8. Cache da cotacao: 30 minutos

O ExchangeRateService cache a cotacao por 30 minutos.

9. Itens sem imagem sao descartados

Nenhum item sem imageUrl e inserido no banco.

10. Deduplicacao na Waxpeer

Quando a Waxpeer retorna multiplas entradas para o mesmo item, sempre manter o de menor preco:

typescript
const existing = allItems.get(item.name);
if (!existing || item.price < existing.price) {
  allItems.set(item.name, item);
}

Documentação Técnica - ProCases