Skip to content

Integracao Waxpeer

O Waxpeer e o marketplace externo utilizado pelo ProCases para tres funcoes principais: saque de skins (withdrawal), swap de items (site swap) e importacao de items (seed scripts). Este documento cobre a integracao completa.

Visao Geral

ProCases <---> Waxpeer API (REST)
ProCases <---> Waxpeer WebSocket (real-time trade updates)

O sistema se comunica com Waxpeer de 3 formas:

  1. API REST (https://api.waxpeer.com/v1/) - consulta de precos, compra de items, verificacao de trades
  2. WebSocket (wss://waxpeer.com) - atualizacoes em tempo real de status de trades
  3. Scripts de importacao - seed de items do catalogo Waxpeer para o banco de dados

Configuracao

WAXPEER_API_KEY=chave_da_api

Se a chave nao estiver configurada, o servico WebSocket nao conecta e logs de warning sao emitidos.

Arquivos do Codigo

ArquivoResponsabilidade
src/infrastructure/external-apis/waxpeer.service.tsServico principal de API REST (~430 linhas)
src/infrastructure/external-apis/waxpeer-websocket.service.tsServico WebSocket real-time (~350 linhas)
src/infrastructure/external-apis/waxpeer-snapshot.service.tsCache/fallback de imagens
src/application/services/waxpeer-withdrawal-tracker.service.tsPolling cron de status (~200 linhas)
src/application/use-cases/inventory/request-withdraw-item.use-case.tsSaque de skin (~355 linhas)
src/application/use-cases/swap/perform-site-swap.use-case.tsSwap com Waxpeer
src/application/use-cases/swap/get-swap-targets.use-case.tsConsulta de items para swap
scripts/seed-items-from-api.tsImport individual
scripts/seed-all-items-v3.tsImport em massa

API REST Waxpeer

Endpoints Utilizados

EndpointMetodoUso no SistemaTimeout
/v1/pricesGETConsulta de precos (withdrawal + swap targets)15s
/v1/buy-one-p2p-nameGETCompra de item P2P (withdrawal)30s
/v1/check-many-steamGETStatus de trades por waxpeerId (tracker polling)30s
/v1/check-many-project-idGETStatus por projectId (nao usado atualmente)30s
/v1/check-tradelinkPOSTValidacao de trade URL do usuario15s
/v1/search-items-by-nameGETBusca de items (seed scripts + swap targets)15s

GET /v1/prices - Consulta de Precos

Usado por: WaxpeerService.getItemPrice(marketHashName)

typescript
// Query params
{
  api: WAXPEER_API_KEY,
  game: 'csgo',
  search: marketHashName  // nome exato do item
}

Response do Waxpeer:

json
{
  "success": true,
  "items": [
    {
      "name": "AK-47 | Redline (Field-Tested)",
      "count": 15,           // quantidade de listings
      "min": 13464,          // preco minimo em Waxpeer coins (1000 = $1)
      "steam_price": 15000,  // preco Steam em coins
      "img": "https://...",
      "rarity_color": "#4b69ff",
      "type": "Rifle"
    }
  ]
}

Logica de matching:

  1. Busca por nome exato (item.name === marketHashName)
  2. Se exato encontrado: isExactMatch: true
  3. Se nao encontrado: retorna primeiro resultado com isExactMatch: false
  4. Se API falha ou nenhum item: retorna null

Conversao de preco:

  • Waxpeer coins: 1000 = $1.00 USD
  • USD cents: Math.round(coins / 10)
  • Exemplo: 13464 coins = 1346 cents = $13.46 USD

GET /v1/buy-one-p2p-name - Compra P2P

Usado por: WaxpeerService.buyItemByName()

typescript
// Query params
{
  api: WAXPEER_API_KEY,
  name: marketHashName,
  token: tradeToken,      // do trade URL do usuario
  partner: tradePartner,  // do trade URL do usuario
  price: priceWaxpeerCoins,
  game: 'csgo',
  project_id: projectId   // nosso tracking ID
}

Response do Waxpeer:

json
{
  "success": true,
  "id": "12345",           // ID do trade no Waxpeer
  "price": 13464,          // preco pago em coins
  "name": "AK-47 | Redline (Field-Tested)",
  "send_until": 1707209856, // timestamp Unix (deadline para vendedor enviar)
  "average_time": 3600      // tempo medio de entrega em segundos
}

GET /v1/check-many-steam - Status de Trades

Usado por: WaxpeerService.checkTradeStatus(waxpeerIds[])

typescript
// Query params
{
  api: WAXPEER_API_KEY,
  id: [id1, id2, id3, ...]  // repetido para cada ID, max 100
}

Limite: Maximo 100 IDs por request. Se > 100, trunca silenciosamente (log warning).

Response do Waxpeer:

json
{
  "success": true,
  "trades": [
    {
      "id": 12345,
      "price": 13464,
      "name": "AK-47 | Redline (Field-Tested)",
      "status": 3,
      "trade_id": "steam_trade_offer_id",
      "done": false,
      "is_released": false,
      "reason": null,
      "seller_name": "SteamUser123",
      "send_until": 1707209856,
      "last_updated": 1707206256,
      "release_date": null
    }
  ]
}

POST /v1/check-tradelink - Validacao de Trade URL

Usado por: WaxpeerService.checkTradeLink(tradelink)

typescript
// Body
{ tradelink: "https://steamcommunity.com/tradeoffer/new/?partner=123&token=abc" }

Response do Waxpeer:

json
{
  "success": true,
  "steamid64": 76561198123456789,
  "steamid32": 123456789
}

WebSocket Waxpeer

Conexao

typescript
// waxpeer-websocket.service.ts
this.socket = io('wss://waxpeer.com', {
  path: '/socket.io/',
  transports: ['websocket'],
  extraHeaders: {
    authorization: this.apiKey,
  },
  reconnection: false,  // gerenciamos manualmente
});

URL: wss://waxpeer.comPath: /socket.io/Transport: websocket apenas (nao polling) Autenticacao: Header authorization com API key Reconnection: Desabilitada (gerenciada manualmente)

Lifecycle

  • onModuleInit(): Conecta automaticamente se API key esta configurada
  • onModuleDestroy(): Desconecta

Evento Principal: steamTrade

O unico evento processado pelo sistema. Emitido pelo Waxpeer quando o status de um trade muda.

typescript
interface WaxpeerSteamTradeEvent {
  type: 'create' | 'update';
  data: {
    id: string;              // ID do trade
    stage: number;           // Estagio do trade
    send_until: string;      // ISO datetime (deadline)
    release_date?: string;   // Quando item foi liberado
    is_released?: boolean;
    last_updated: string;
    done: boolean;
    seller: {
      id: string;
      name: string;
      avatar: string;
      success_trades: number;
      failed_trades: number;
    };
    items: [{
      id: number;            // ID do item no Waxpeer
      price: number;
      name: string;
      status: number;        // Status do item (0-6)
      trade_id: string | null; // Steam trade offer ID
      image: string;
    }];
  };
}

Reconnect com Backoff Linear

O delay cresce linearmente (reconnectDelay * tentativa), nao exponencialmente.

typescript
private scheduleReconnect() {
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
    this.logger.error('[WaxpeerWS] Max reconnection attempts reached');
    return;
  }

  this.reconnectAttempts++;
  const delay = this.reconnectDelay * this.reconnectAttempts;
  // Tentativa 1: 5s, 2: 10s, 3: 15s, ..., 10: 50s
  setTimeout(() => this.connect(), delay);
}
TentativaDelay
15s
210s
315s
420s
525s
630s
735s
840s
945s
1050s
> 10Desiste

Variaveis:

  • reconnectDelay: 5000ms (base)
  • maxReconnectAttempts: 10
  • isConnecting: mutex para prevenir conexoes concorrentes

Conversao de Moeda

Campos de Preco no Item

CampoMoedaDescricao
valueCentsUSDPreco do item em centavos de dolar (ja com a margem da casa)
valueCentsBrlBRLvalueCents convertido para reais
originalPriceCentsUSDPreco bruto Waxpeer sem margem
marginPercent%Margem aplicada (dinamica via PriceMarginService)

Formula de Conversao

O preco bruto do Waxpeer (USD cents) recebe a margem da casa e so entao e convertido para BRL:

valueCents (USD, com margem) = round(waxpeerRawCents * priceMargin)
valueCentsBrl                = round((valueCents / 100) * exchangeRate * 100)

priceMargin vem do PriceMarginService.getCurrent() (multiplicador dinamico) e exchangeRate do ExchangeRateService.getUsdToBrlRate() (fallback 5).

Exemplo (priceMargin = 1.20, exchangeRate = 5.25):

  • Preco bruto Waxpeer: $10.00 (waxpeerRawCents = 1000)
  • valueCents = round(1000 * 1.20) = 1200 ($12.00)
  • valueCentsBrl = round((1200 / 100) * 5.25 * 100) = 6300 (R$ 63,00)

Waxpeer Coins para USD

USD cents = Math.round(waxpeerCoins / 10)

Exemplo: 13464 coins / 10 = 1346 cents = $13.46

Protecao de Preco (Price Protection)

Como Funciona

O teto de saque e calculado dinamicamente: maxPayCents = ceil(valueCents / priceMargin), onde valueCents e o preco do item em centavos USD (ja com a margem) e priceMargin vem do PriceMarginService.getCurrent(). Na pratica o teto e aproximadamente o preco bruto do item (sem a margem da casa).

Regra: Se o preco minimo atual no Waxpeer > maxPayCents, a withdrawal e BLOQUEADA.

typescript
const valueCentsUsd = Number(item.valueCents);
const priceMargin = await this.priceMarginService.getCurrent();
const maxPayCents = Math.ceil(valueCentsUsd / priceMargin);
const isPriceValid = waxpeerMinPriceCents <= maxPayCents;
const canWithdraw = isExactMatch && hasStock && isPriceValid;

Cenarios

Preco WaxpeermaxPayCentsResultado
$8.00$10.00canWithdraw = true (mais barato)
$10.00$10.00canWithdraw = true (igual)
$12.00$10.00canWithdraw = false (mais caro)

Oportunidade de Lucro (Price Arbitrage)

Quando o preco no Waxpeer SOBE acima do originalPriceCents, ocorre uma oportunidade de negocio:

  1. Item valorizou: Waxpeer cobra mais do que o site pagou originalmente
  2. Withdrawal bloqueada: canWithdraw = false (site nao compra no prejuizo)
  3. SWAP oferecido: canSwap = true (se exact match encontrado)
  4. Usuario faz SWAP: Troca seus items por outro item no Waxpeer

Como o site lucra com SWAP:

  • No site swap, o valor dos items do usuario e calculado com base no valueCentsBrl (preco COM margem de 20%)
  • O item recebido do Waxpeer custa o preco REAL (sem margem) em USD
  • A diferenca entre o valor percebido pelo usuario e o custo real e GGR da casa

Exemplo concreto:

Item do usuario: "AK-47 | Redline" 
  - valueCentsBrl: R$ 63,00 (com margem 20%)
  - Custo real Waxpeer: $10.00 = R$ 52,50

Item do Waxpeer (swap target): "M4A4 | Asiimov"
  - Preco Waxpeer: $10.00 = R$ 52,50
  - Exibido ao usuario como: R$ 63,00 (com margem 20%)

Resultado:
  - Usuario entrega: items no valor de R$ 63,00 (percebido)
  - Site compra: item de $10.00 no Waxpeer = R$ 52,50 (real)
  - GGR da casa: R$ 10,50 (diferenca da margem)

Alem disso, quando o item do usuario valorizou no Waxpeer:

Item do usuario: "AK-47 | Redline"
  - valueCentsBrl: R$ 63,00 (valor no site)
  - Preco atual Waxpeer: $15.00 (valorizou!)
  - maxPayCents: ~$10.00 (teto de withdrawal = valueCents / margem)

Withdrawal: BLOQUEADA ($15 > $10)
SWAP: Disponivel

O usuario "perde" a valorizacao de $5.00 ao fazer swap,
porque o site usa o valor original (R$ 63,00) e nao o valor de mercado atual

Swap Targets (Site Swap)

Endpoint: GET /swap/targets

Consulta items disponiveis no Waxpeer para troca.

Query Parameters:

ParametroTipoDescricao
minValueCentsnumberValor minimo em centavos BRL
maxValueCentsnumberValor maximo em centavos BRL
searchstringBusca por nome do item
sortOrder'asc' | 'desc'Ordenacao por valor
limitnumberLimite de resultados
offsetnumberOffset para paginacao

Endpoint publico: Decorator @Public(), nao requer autenticacao.

Endpoint: POST /swap/site

Executa swap de items do inventario por um item do Waxpeer.

Body:

json
{
  "inventoryItemIds": ["123", "456"],
  "targetMarketHashName": "M4A4 | Asiimov (Field-Tested)"
}

Limites:

  • 1-6 items do inventario por swap
  • Tolerancia de preco: 10% (item Waxpeer pode custar ate 10% mais que o preco consultado)
  • Margem: dinamica via PriceMarginService.getCurrent() (aplicada no calculo de valor dos items)

Fluxo:

  1. Valida items pertencem ao usuario e status AVAILABLE
  2. Lock distribuido nos items
  3. Consulta preco atual no Waxpeer para o target
  4. Verifica se valor total dos items >= preco do target (com tolerancia)
  5. Compra item no Waxpeer via buyItemByName()
  6. Marca items como consumidos
  7. Adiciona item recebido ao inventario do usuario
  8. Emite WebSocket: balance:updated

Response:

json
{
  "success": true,
  "itemsSwapped": [
    {
      "id": "123",
      "name": "Glock-18 | Fade",
      "valueCentsBrl": "15000",
      "valueFormatted": "R$ 150,00"
    }
  ],
  "totalSwappedValueCents": 15000,
  "totalSwappedValueFormatted": "R$ 150,00",
  "receivedItem": {
    "name": "M4A4 | Asiimov (Field-Tested)",
    "valueCentsBrl": "14000"
  }
}

Waxpeer Status Codes

Codigos de status retornados pelo Waxpeer para trades:

StatusSignificadoNosso Status
0Compra pendente, aguardando vendedor aceitarWAITING_SELLER
1Vendedor aceitou, preparando envioWAITING_SELLER
2Trade offer criado no sistema WaxpeerTRADE_SENT
3Trade offer enviado ao Steam do compradorTRADE_SENT
4Trade confirmado, aguardando releaseTRADE_SENT ou COMPLETED
5Item released ao compradorCOMPLETED
6Cancelado/Falhou/RecusadoCANCELLED ou FAILED

Divergencia no Mapeamento

O mapeamento de status 6 difere entre o servico WebSocket e o tracker de polling:

WebSocket (waxpeer-websocket.service.ts):

typescript
if (waxpeerStatus === 6) {
  return WaxpeerWithdrawalStatus.CANCELLED; // Sempre CANCELLED
}

Tracker (waxpeer-withdrawal-tracker.service.ts):

typescript
if (waxpeerStatus === 6) {
  if (trade.reason?.toLowerCase().includes('cancel') ||
      trade.reason?.toLowerCase().includes('failed') ||
      trade.reason?.toLowerCase().includes('declined')) {
    return 'CANCELLED';
  }
  return 'FAILED'; // Diferencia CANCELLED de FAILED
}

O tracker tambem trata status 4 de forma diferente:

typescript
if (waxpeerStatus === 4 && trade.release_date) {
  return 'COMPLETED'; // WebSocket nao verifica release_date para status 4
}

Waxpeer Snapshot Service

Servico auxiliar que fornece fallback para imagens de items quando a API nao retorna URL.

typescript
static buildWaxpeerImageUrl(marketHashName: string): string {
  // Constroi URL de imagem a partir do nome do item
  // Usado como fallback quando item.img e null
}

Dead Code Identificado

CodigoArquivoStatus
checkTradeStatusByProjectId()waxpeer.service.tsDefinido mas nunca chamado
emitWithdrawalProcessing()websocket.gateway.tsDefinido mas nunca emitido

O checkTradeStatusByProjectId() usa o endpoint /v1/check-many-project-id com nosso projectId em vez do waxpeerId do Waxpeer. Pode ser util como fallback se o waxpeerId nao estiver disponivel, mas atualmente nao e utilizado.

Debugging

Logs Importantes

[Waxpeer] Buying item: {itemName} for {price} coins
[Waxpeer] Project ID: {projectId}
[Waxpeer] Buy response: {response}
[WaxpeerWS] Connected to Waxpeer WebSocket
[WaxpeerWS] Disconnected: {reason}
[WaxpeerWS] Trade {waxpeerId}: Waxpeer status {status}, stage {stage} -> Our status {ourStatus}
[Tracker] Checking {count} pending withdrawals...
[Tracker] Trade {id}: status={status}, done={done}, is_released={released}

Verificacoes

  1. API key configurada: Se nao, WebSocket nao conecta e precos retornam null
  2. Conexao WebSocket: Verificar log [WaxpeerWS] Connected
  3. Polling funcionando: Verificar log [Tracker] Checking X pending withdrawals
  4. Preco valido: waxpeerMinPriceCents <= maxPayCents
  5. Exact match: isExactMatch === true para withdrawal

Documentação Técnica - ProCases