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:
- API REST (
https://api.waxpeer.com/v1/) - consulta de precos, compra de items, verificacao de trades - WebSocket (
wss://waxpeer.com) - atualizacoes em tempo real de status de trades - Scripts de importacao - seed de items do catalogo Waxpeer para o banco de dados
Configuracao
WAXPEER_API_KEY=chave_da_apiSe a chave nao estiver configurada, o servico WebSocket nao conecta e logs de warning sao emitidos.
Arquivos do Codigo
| Arquivo | Responsabilidade |
|---|---|
src/infrastructure/external-apis/waxpeer.service.ts | Servico principal de API REST (~430 linhas) |
src/infrastructure/external-apis/waxpeer-websocket.service.ts | Servico WebSocket real-time (~350 linhas) |
src/infrastructure/external-apis/waxpeer-snapshot.service.ts | Cache/fallback de imagens |
src/application/services/waxpeer-withdrawal-tracker.service.ts | Polling cron de status (~200 linhas) |
src/application/use-cases/inventory/request-withdraw-item.use-case.ts | Saque de skin (~355 linhas) |
src/application/use-cases/swap/perform-site-swap.use-case.ts | Swap com Waxpeer |
src/application/use-cases/swap/get-swap-targets.use-case.ts | Consulta de items para swap |
scripts/seed-items-from-api.ts | Import individual |
scripts/seed-all-items-v3.ts | Import em massa |
API REST Waxpeer
Endpoints Utilizados
| Endpoint | Metodo | Uso no Sistema | Timeout |
|---|---|---|---|
/v1/prices | GET | Consulta de precos (withdrawal + swap targets) | 15s |
/v1/buy-one-p2p-name | GET | Compra de item P2P (withdrawal) | 30s |
/v1/check-many-steam | GET | Status de trades por waxpeerId (tracker polling) | 30s |
/v1/check-many-project-id | GET | Status por projectId (nao usado atualmente) | 30s |
/v1/check-tradelink | POST | Validacao de trade URL do usuario | 15s |
/v1/search-items-by-name | GET | Busca de items (seed scripts + swap targets) | 15s |
GET /v1/prices - Consulta de Precos
Usado por: WaxpeerService.getItemPrice(marketHashName)
// Query params
{
api: WAXPEER_API_KEY,
game: 'csgo',
search: marketHashName // nome exato do item
}Response do Waxpeer:
{
"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:
- Busca por nome exato (
item.name === marketHashName) - Se exato encontrado:
isExactMatch: true - Se nao encontrado: retorna primeiro resultado com
isExactMatch: false - 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()
// 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:
{
"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[])
// 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:
{
"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)
// Body
{ tradelink: "https://steamcommunity.com/tradeoffer/new/?partner=123&token=abc" }Response do Waxpeer:
{
"success": true,
"steamid64": 76561198123456789,
"steamid32": 123456789
}WebSocket Waxpeer
Conexao
// 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 configuradaonModuleDestroy(): Desconecta
Evento Principal: steamTrade
O unico evento processado pelo sistema. Emitido pelo Waxpeer quando o status de um trade muda.
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.
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);
}| Tentativa | Delay |
|---|---|
| 1 | 5s |
| 2 | 10s |
| 3 | 15s |
| 4 | 20s |
| 5 | 25s |
| 6 | 30s |
| 7 | 35s |
| 8 | 40s |
| 9 | 45s |
| 10 | 50s |
| > 10 | Desiste |
Variaveis:
reconnectDelay: 5000ms (base)maxReconnectAttempts: 10isConnecting: mutex para prevenir conexoes concorrentes
Conversao de Moeda
Campos de Preco no Item
| Campo | Moeda | Descricao |
|---|---|---|
valueCents | USD | Preco do item em centavos de dolar (ja com a margem da casa) |
valueCentsBrl | BRL | valueCents convertido para reais |
originalPriceCents | USD | Preco 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.
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 Waxpeer | maxPayCents | Resultado |
|---|---|---|
| $8.00 | $10.00 | canWithdraw = true (mais barato) |
| $10.00 | $10.00 | canWithdraw = true (igual) |
| $12.00 | $10.00 | canWithdraw = false (mais caro) |
Oportunidade de Lucro (Price Arbitrage)
Quando o preco no Waxpeer SOBE acima do originalPriceCents, ocorre uma oportunidade de negocio:
- Item valorizou: Waxpeer cobra mais do que o site pagou originalmente
- Withdrawal bloqueada:
canWithdraw = false(site nao compra no prejuizo) - SWAP oferecido:
canSwap = true(se exact match encontrado) - 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 atualSwap Targets (Site Swap)
Endpoint: GET /swap/targets
Consulta items disponiveis no Waxpeer para troca.
Query Parameters:
| Parametro | Tipo | Descricao |
|---|---|---|
minValueCents | number | Valor minimo em centavos BRL |
maxValueCents | number | Valor maximo em centavos BRL |
search | string | Busca por nome do item |
sortOrder | 'asc' | 'desc' | Ordenacao por valor |
limit | number | Limite de resultados |
offset | number | Offset 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:
{
"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:
- Valida items pertencem ao usuario e status AVAILABLE
- Lock distribuido nos items
- Consulta preco atual no Waxpeer para o target
- Verifica se valor total dos items >= preco do target (com tolerancia)
- Compra item no Waxpeer via
buyItemByName() - Marca items como consumidos
- Adiciona item recebido ao inventario do usuario
- Emite WebSocket:
balance:updated
Response:
{
"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:
| Status | Significado | Nosso Status |
|---|---|---|
| 0 | Compra pendente, aguardando vendedor aceitar | WAITING_SELLER |
| 1 | Vendedor aceitou, preparando envio | WAITING_SELLER |
| 2 | Trade offer criado no sistema Waxpeer | TRADE_SENT |
| 3 | Trade offer enviado ao Steam do comprador | TRADE_SENT |
| 4 | Trade confirmado, aguardando release | TRADE_SENT ou COMPLETED |
| 5 | Item released ao comprador | COMPLETED |
| 6 | Cancelado/Falhou/Recusado | CANCELLED 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):
if (waxpeerStatus === 6) {
return WaxpeerWithdrawalStatus.CANCELLED; // Sempre CANCELLED
}Tracker (waxpeer-withdrawal-tracker.service.ts):
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:
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.
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
| Codigo | Arquivo | Status |
|---|---|---|
checkTradeStatusByProjectId() | waxpeer.service.ts | Definido mas nunca chamado |
emitWithdrawalProcessing() | websocket.gateway.ts | Definido 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
- API key configurada: Se nao, WebSocket nao conecta e precos retornam null
- Conexao WebSocket: Verificar log
[WaxpeerWS] Connected - Polling funcionando: Verificar log
[Tracker] Checking X pending withdrawals - Preco valido:
waxpeerMinPriceCents <= maxPayCents - Exact match:
isExactMatch === truepara withdrawal
