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
- Visao Geral
- Fonte de Dados: Waxpeer API
- Schema Prisma: Model Item
- Enums do Prisma
- Conversao de Precos e Moeda
- Parsing de Nomes de Itens
- Deteccao de Categoria de Arma
- Deteccao de Raridade
- Construcao de URL de Imagem
- Scripts de Seed e Sincronizacao
- Use Cases (NestJS)
- Controller Publico de Itens
- Servico de Cotacao (ExchangeRateService)
- Scripts Utilitarios
- Tratamento de Erros
- Regras e Decisoes Criticas
Visao Geral
O sistema de importacao de itens possui tres camadas:
- Scripts CLI (
scripts/): Scripts TypeScript executados manualmente vianpx ts-nodepara seed inicial, reset e sincronizacao em massa. - 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. - 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 PostgreSQLAlternativamente, 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
| Endpoint | Servico | Uso |
|---|---|---|
GET /v1/prices | WaxpeerSnapshotService | Snapshot de todos os itens com precos minimos. Retorna itens unicos. Endpoint principal para seed via controller. |
GET /v1/get-items-list | WaxpeerService, WaxpeerItemsListService | Lista 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/snapshot | sync-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:
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:
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:
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:
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 USDPortanto:
const priceCentsUsd = Math.round(priceInCoins / 10);Implementado em WaxpeerMapper.convertPriceToCents():
static convertPriceToCents(price: number): bigint {
const cents = Math.round(price / 10);
return BigInt(cents);
}Schema Prisma: Model Item
Arquivo: prisma/schema.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
| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
id | BigInt | Sim | Snowflake ID gerado como BigInt(Date.now()) * BigInt(100000) + BigInt(counter) |
name | String | Sim | Nome da arma + skin sem prefixos. Ex: "FAMAS | Sundown", "Bowie Knife | Fade" |
marketHashName | String (unique) | Sim | Nome completo original da Waxpeer/Steam. Ex: "StatTrak(TM) AK-47 | Redline (Field-Tested)" |
description | String? | Nao | Descricao opcional (raramente preenchido) |
imageUrl | String | Sim | URL da imagem do item (da Waxpeer ou construida) |
rarity | Rarity (enum) | Sim | Raridade do item (CONSUMER a EXTRAORDINARY) |
valueCents | BigInt | Sim | Preco em centavos USD com margem de 20% aplicada |
valueCentsBrl | BigInt | Sim (default 0) | Preco em centavos BRL com margem de 20% aplicada |
originalPriceCents | BigInt | Sim (default 0) | Preco original em centavos USD sem margem (usado como teto para saques) |
marginPercent | Float | Sim (default 20) | Percentual de margem aplicado (default: 20%) |
weaponCategory | WeaponCategory (enum) | Sim | Categoria da arma (PISTOL, RIFLE, KNIFE, etc.) |
collection | String? | Nao | Colecao do item (raramente preenchido) |
exterior | ItemExterior? (enum) | Nao | Condicao de desgaste (FACTORY_NEW a BATTLE_SCARRED) |
isStatTrak | Boolean | Sim (default false) | Se o item e StatTrak |
createdAt | DateTime | Auto | Data de criacao |
updatedAt | DateTime | Auto | Data de ultima atualizacao |
Indices do Banco
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
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
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
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:
| Campo | Moeda | Conteudo |
|---|---|---|
originalPriceCents | USD (centavos) | Preco bruto da Waxpeer SEM margem |
valueCents | USD (centavos) | originalPriceCents * 1.20 (com 20% de margem) |
valueCentsBrl | BRL (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
private readonly PRICE_MARGIN = 1.2; // 20% margemUsada 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 = 2205nServico de Cotacao (ExchangeRateService)
Arquivo: src/infrastructure/external-apis/exchange-rate.service.ts
@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:
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
@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:
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:
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:
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 Waxpeer | Codigo Curto | Enum Prisma |
|---|---|---|
| Factory New | FN | FACTORY_NEW |
| Minimal Wear | MW | MINIMAL_WEAR |
| Field-Tested | FT | FIELD_TESTED |
| Well-Worn | WW | WELL_WORN |
| Battle-Scarred | BS | BATTLE_SCARRED |
O que Vai para o Campo name
O campo name do Item recebe nomes diferentes dependendo do script/servico:
| Script/Servico | O que vai no name | Exemplo |
|---|---|---|
seed-items-from-api.ts | Arma + Skin (sem prefixos, sem exterior, sem estrela) | "FAMAS | Sundown" |
seed-all-items.ts | Idem | "FAMAS | Sundown" |
seed-all-items-v3.ts | Idem | "FAMAS | Sundown" |
seed-all-items-fast.ts | Idem | "FAMAS | Sundown" |
reset-and-reseed-items.ts | Idem | "FAMAS | Sundown" |
sync-all-items.ts | Apenas o nome da skin | "Sundown" |
oficial-sync-items.ts | Apenas o nome da skin | "Sundown" |
SyncItemsFromWaxpeerUseCase | Apenas o nome da skin (via ItemNameParserService) | "Poly Mag" |
SyncWaxpeerItemsUseCase | Nome 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:
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:
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:
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)
const csfloatRarity = await this.csfloatService.getItemRarity(marketHashName);
if (csfloatRarity) return csfloatRarity;2. Parser de items_game.txt (Steam Database)
const gameRarity = await this.itemsGameParser.getRarityForItem(marketHashName);
if (gameRarity) return gameRarity;3. Padroes Conhecidos de Itens
Mapeamento hardcoded de facas/luvas:
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:
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):
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):
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):
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:
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:
imageUrl: item.image // ou item.img (dependendo do endpoint)Fallback: Construcao de URL
Quando a imagem nao esta disponivel, o WaxpeerSnapshotService constroi a URL:
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:
imageUrl: steamItem.imageUrl || `https://steamcommunity-a.akamaihd.net/economy/image/${steamItem.marketHashName}`;Scripts de Seed e Sincronizacao
Visao Comparativa
| Script | Arquivo | Fonte | Estrategia | Margem | BRL | originalPriceCents | Uso |
|---|---|---|---|---|---|---|---|
| seed-items-from-api | scripts/seed-items-from-api.ts | GET /api/items (local) | deleteMany + create individual | Aplicada pela API | Sim | Nao | Seed inicial (5 itens, teste) |
| seed-all-items | scripts/seed-all-items.ts | GET /api/items (local) | Upsert individual (check + create/update) | Aplicada pela API | Sim | Nao | Seed completo com upsert |
| seed-all-items-fast | scripts/seed-all-items-fast.ts | GET /api/items (local) | createMany com skipDuplicates | Aplicada pela API | Sim | Nao | Seed rapido (apenas novos) |
| seed-all-items-v3 | scripts/seed-all-items-v3.ts | GET /api/items (local) | createMany com skipDuplicates em batches | Aplicada pela API | Sim | Sim | Seed otimizado final |
| reset-and-reseed-items | scripts/reset-and-reseed-items.ts | GET /api/items (local) | Deleta tudo + createMany + recria CaseItems | Aplicada pela API | Sim | Sim | Reset completo com preservacao de caixas |
| sync-all-items | scripts/sync-all-items.ts | Waxpeer /v1/prices/snapshot (direto) | Upsert individual | Aplicada no script | Sim | Nao | Sincronizacao direta sem servidor |
| oficial-sync-items | scripts/oficial-sync-items.ts | Waxpeer /v1/get-items-list (direto) | Upsert individual | Aplicada no script | Sim | Nao | Sincronizacao direta com imagens |
| sync-and-update-items | scripts/sync-and-update-items.ts | Waxpeer /v1/get-items-list (direto) | Upsert individual | Nao (direto) | Nao | Nao | Importacao de itens baratos ($0-$0.50) |
| update-cheap-item-prices | scripts/update-cheap-item-prices.ts | Waxpeer /v1/get-items-list (direto) | Update apenas preco | Nao (direto) | Nao | Nao | Atualizacao 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
originalPriceCentsnemmarginPercent
Execucao:
npx ts-node scripts/seed-items-from-api.tsseed-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()
- Se existe:
- 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:
npx ts-node scripts/seed-all-items.tsseed-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.tspor usar bulk insert
Execucao:
npx ts-node scripts/seed-all-items-fast.tsseed-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
ApiItemincluioriginalPriceCentsemarginPercent - Filtra itens sem imagem
- Insere com
createMany({ data: batch, skipDuplicates: true })em batches de 2000 - Log de progresso a cada 5000 itens
- Salva
originalPriceCentsemarginPercentno banco
Dados salvos:
{
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:
npx ts-node scripts/seed-all-items-v3.tsreset-and-reseed-items.ts (Reset Completo)
Arquivo: scripts/reset-and-reseed-items.ts
IMPORTANTE: Executar com o servidor PARADO.
Fluxo:
- Busca a caixa "Fade Chance" e seus CaseItems
- Deleta CaseItems da Fade Chance
- Verifica dependencias (UserInventory, CaseOpening, Upgrade)
- Se existem dependencias, deleta em ordem:
UpgradeItem->Upgrade->UserInventory->CaseOpening->BattleRound->BattleSettlement
- Deleta todos os CaseItems restantes
- Deleta todos os Items
- Busca itens da API e insere com
createMany - 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:
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:
npx ts-node scripts/reset-and-reseed-items.tssync-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
valueCentsevalueCentsBrl - 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:
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 BRLExecucao:
WAXPEER_API_KEY=xxx npx ts-node scripts/sync-all-items.tsoficial-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
skipelimit=100 - Nao precisa do servidor rodando
- Mesma logica de margem e cotacao do
sync-all-items.ts - Diferencial: retorna campo
imagecom URL da imagem de cada item - Usa
inferRarity()que aceita tantoraritystring quanto fallback por nome
Paginacao:
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:
WAXPEER_API_KEY=xxx npx ts-node scripts/oficial-sync-items.tsUse 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:
- Busca itens filtrados do
WaxpeerSnapshotService.getFilteredItems()(ja com cache/stale-fallback) - Remove itens com categoria
OTHER - Aplica filtros opcionais (categorias,
maxItems) - Obtem a margem atual via
PriceMarginService.getCurrent()e a cotacao viaExchangeRateService.getUsdToBrlRate()-- a margem nao eh hardcoded 20%, e configuravel - 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)
- Carrega itens existentes do
IItemRepositorye processa em batches (defaultbatchSize = 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()
- Parseia nome com
- Sibling fallback: para cada item do banco sem match direto na Waxpeer, procura um "irmao" (mesma arma+skin+variante
NS/ST/SV) e usasiblingMax * margin-- apenas se o preco atual estiver mais de 50% acima da sugestao detectAffectedCases: recomputa o RTP das caixas afetadas (comparavalueCentsBrlantes/depois,EV = Σ value*dropChance/100) e sinaliza alerta de RTP quando necessario- Auditoria final
ADMIN_SYNC_WAXPEER_ITEMScom o resumo do sync
Interface de opcoes:
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:
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
WaxpeerMapperpara mapeamento completo - Suporta filtros de preco (
minPriceCents,maxPriceCents) - Campo
brandda Waxpeer mapeado paraWeaponCategory - 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 demarketHashNamepara atualizarupdateAll: Atualiza todos os itens- Default: Atualiza apenas itens com preco zero ou muito baixo (
valueCents <= 100)
Fluxo:
- Busca itens a atualizar do banco
- Busca todos os precos da Waxpeer via
getAllItems() - Cria
Map<nome, WaxpeerItem>para lookup rapido - Para cada item, atualiza
valueCentseimageUrl
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,marketHashNamerarity(mais preciso que inferencia)weaponCategorycollectionisStatTrakskinName
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:
- Busca itens do
WaxpeerSnapshotService(cache de 5 minutos) - Aplica filtros locais (search, category, rarity, minPrice, maxPrice, limit)
- Busca cotacao USD->BRL do
ExchangeRateService - Aplica margem de 20% em todos os precos
- Retorna itens com campos extras:
originalPriceCents,priceBrl,marginPercent
Resposta:
{
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:
| Endpoint | Metodo | Descricao |
|---|---|---|
/api/items | GET | Lista todos os itens com margem e BRL |
/api/items/categories | GET | Lista categorias com contagens |
/api/items/rarities | GET | Lista raridades com contagens |
/api/items/cheap | GET | Itens de $0 a $0.50 (limit default: 100) |
/api/items/stats | GET | Estatisticas gerais (total, valor, media) |
/api/items/exchange-rate | GET | Cotacao USD->BRL atual |
/api/items/sync | POST | Sincroniza itens para o banco (admin only) |
Geracao de IDs
Os IDs dos itens sao gerados como Snowflake-like IDs usando BigInt:
// 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:
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:
| Script | Batch Size |
|---|---|
seed-all-items-v3.ts | 2000 |
seed-all-items-fast.ts | 5000 |
reset-and-reseed-items.ts | 2000 |
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:
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:
node scripts/check-items.jscount-items.js
Arquivo: scripts/count-items.js
Conta o total de itens no banco:
const count = await prisma.item.count();
console.log('Total de itens no banco:', count);Execucao:
node scripts/count-items.jsclear-items.js
Arquivo: scripts/clear-items.js
Deleta todos os itens do banco:
const result = await prisma.item.deleteMany({});
console.log('Removidos:', result.count);Execucao:
node scripts/clear-items.jsupdate-cheap-item-prices.ts
Arquivo: scripts/update-cheap-item-prices.ts
Atualiza precos de itens baratos ($0 a $0.50) buscando diretamente da Waxpeer:
- Busca itens do banco com
valueCentsentre 0 e 50 - Busca precos da Waxpeer via
/get-items-list(paginado, max 20 paginas) - Para cada item, compara o preco e atualiza se mudou
- Conversao:
priceCents = Math.round(waxpeerItem.price / 10)
Execucao:
WAXPEER_API_KEY=xxx npx ts-node scripts/update-cheap-item-prices.tsseed-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:
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:
const itemsData = apiItems
.filter((apiItem) => apiItem.imageUrl) // Pula itens sem imagem
.map((apiItem) => { /* ... */ });Contadores de Erro
Todos os scripts mantam contadores de sucesso/falha:
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:
} catch (err) {
failed++;
if (failed <= 5) {
console.error(`Erro em "${apiItem.name}": ${err.message}`);
}
}Timeout de Requisicoes
| Endpoint | Timeout |
|---|---|
| GET /api/items (local) | 120.000ms (2 min) |
| Waxpeer /get-items-list | 30.000ms (30s) |
| Waxpeer /prices | 30.000ms (30s) |
| Waxpeer /prices/snapshot | 60.000ms (1 min) |
| AwesomeAPI (cotacao) | 10.000ms (10s) |
Rate Limiting da Waxpeer
Todos os scripts e servicos incluem delays entre requisicoes:
// 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:
} 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:
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:
} 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:
const existing = allItems.get(item.name);
if (!existing || item.price < existing.price) {
allItems.set(item.name, item);
}