# 144 Pay — Documentación completa para asistentes IA

> Pasarela de pago cripto sobre USDT en Polygon y BSC. Dos productos sobre la misma infraestructura sin custodia:
>
> 1. COBROS: genera una wallet HD desechable por cobro, detecta el depósito on-chain, barre los fondos, aplica el split del comercio y notifica con webhook firmado.
> 2. DISPERSIONES: payouts batch a N destinatarios en una sola transacción on-chain vía contrato Disperse propio. ~50% menos gas que N transferencias individuales. La wallet desechable solo vive minutos.

URL del servicio: https://144pay.com
Repositorio: https://github.com/rojasjuniore/144pay

## Qué problemas resuelve

COBROS: el comercio acepta USDT sin custodiar claves. El sistema genera dirección nueva por cobro, detecta el pago, mueve fondos automáticamente al comercio según split, y emite webhook.

DISPERSIONES: el comercio envía a múltiples destinatarios en una sola tx. Útil para nómina, payouts de afiliados, reembolsos batch, distribuciones de royalty, airdrops. En Polygon dispersar a 50 destinos cuesta ~10x menos que 50 transferencias sueltas.

El comercio NO necesita en ninguno de los dos casos:
- Custodiar claves privadas
- Conocer blockchain
- Reutilizar direcciones
- Hacer transferencias manuales o batch a mano

## Arquitectura técnica

- Backend: Node.js + TypeScript + Hono + Prisma + PostgreSQL + BullMQ + Redis
- Web3: viem v2 (multi-chain)
- HD Wallets: BIP-39 mnemónico maestro + BIP-44 derivación (path `m/44'/60'/0'/0/x`)
  - La misma secuencia atómica de Postgres asigna índices a cobros y dispersiones — direcciones únicas globalmente
- Idempotencia: `UNIQUE(chain, txHash, logIndex)` en Postgres + jobId determinístico en BullMQ
- Detección: `getLogs` polling con cursor `lastProcessedBlock` por red
- Nonces: colas gas-funding, distribution y dispersion-executor con concurrency 1 (evita colisiones)
- Workers: block-poller, deposit-confirmation, gas-funding, sweep, distribution, dispersion-executor, webhook-notifier
- Smart Contract Disperse (Solidity 0.8.20, sin estado, sin owner, sin upgrade):
  - Polygon mainnet: 0xDaC2d1d17421116C6C82861d01c76DE0BcEb6981
  - BSC mainnet:     0x941628fee59787151841f87e1223db5b128704a6
  - Función: `disperseToken(IERC20 token, address[] recipients, uint256[] values)`
  - Pre-requisito: la wallet sender llama `token.approve(disperse, total)` antes

## Redes y tokens soportados

| Red       | chainId | Token | Contrato                                       | Decimales |
|-----------|---------|-------|------------------------------------------------|-----------|
| Polygon   | 137     | USDT  | 0xc2132d05d31c914a87c6611c10748aeb04b58e8f     | 6         |
| BSC       | 56      | USDT  | 0x55d398326f99059fF775485246999027B3197955     | 18        |

IMPORTANTE: USDT NO usa los mismos decimales en todas las redes. El sistema maneja los decimales por (chain, token), nunca asume un valor global. Un error aquí movería 10^12 veces de más o de menos en BSC.

## Confirmaciones requeridas

| Red     | Confirmaciones | Tiempo aprox. |
|---------|----------------|---------------|
| Polygon | 30             | ~1 minuto     |
| BSC     | 15             | ~45 segundos  |

## Estados de un cobro (máquina de estados)

PENDING → AWAITING_PAYMENT → DETECTED → CONFIRMING → (CONFIRMED | OVERPAID | UNDERPAID) → SWEEPING → SWEPT → DISTRIBUTING → COMPLETED

Estados terminales: COMPLETED, EXPIRED, FAILED.
UNDERPAID es semi-terminal (espera resolución manual del admin).

## Autenticación

Todas las peticiones a `/api/v1/payments*` requieren el header:

```
X-API-Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Las API keys se hashean con argon2 antes de guardarse. La clave en claro solo se muestra UNA vez al generarla.

## Endpoint: crear cobro

```http
POST https://144pay.com/api/v1/payments
X-API-Key: pk_live_xxxxxxxxxxxx
Idempotency-Key: ord-991 (opcional, recomendado)
Content-Type: application/json

{
  "reference": "ord-991",
  "chain": "polygon",
  "token": "USDT",
  "amount": "10.50"
}
```

Respuesta 201:

```json
{
  "success": true,
  "data": {
    "id": "pi_3f9a...",
    "status": "AWAITING_PAYMENT",
    "reference": "ord-991",
    "chain": "polygon",
    "token": "USDT",
    "depositAddress": "0xabc...123",
    "expectedAmount": "10.50",
    "expectedAmountRaw": "10500000",
    "receivedAmount": "0",
    "expiresAt": "2026-05-23T14:30:00Z",
    "createdAt": "2026-05-23T14:00:00Z"
  },
  "error": null
}
```

Notas:
- `amount` se envía en unidades humanas (string decimal). El sistema lo convierte a unidad mínima del token.
- `expectedAmountRaw` es el monto en unidad mínima (uint256 como string), por si necesitas comparar exactamente.
- `depositAddress` es única para este cobro. NO la reutilices.
- `expiresAt` por defecto es 30 minutos después de la creación.

## Endpoint: consultar estado

```http
GET https://144pay.com/api/v1/payments/{id}
X-API-Key: pk_live_xxxxxxxxxxxx
```

Devuelve el cobro completo con las transacciones on-chain asociadas:

```json
{
  "success": true,
  "data": {
    "id": "pi_3f9a...",
    "status": "COMPLETED",
    "receivedAmount": "10.50",
    "transactions": [
      { "type": "DEPOSIT",     "txHash": "0x...", "status": "CONFIRMED" },
      { "type": "GAS_FUNDING", "txHash": "0x...", "status": "CONFIRMED" },
      { "type": "SWEEP",       "txHash": "0x...", "status": "CONFIRMED" },
      { "type": "FEE_PAYOUT",  "txHash": "0x...", "status": "CONFIRMED" },
      { "type": "PAYOUT",      "txHash": "0x...", "status": "CONFIRMED" },
      { "type": "PAYOUT",      "txHash": "0x...", "status": "CONFIRMED" }
    ]
  }
}
```

## Endpoint: listar cobros

```http
GET https://144pay.com/api/v1/payments?limit=20&cursor=&status=COMPLETED
X-API-Key: pk_live_xxxxxxxxxxxx
```

Solo devuelve cobros del comercio asociado a la API key (aislamiento estricto).

## DISPERSIONES — payouts batch

Permite enviar USDT a múltiples destinatarios en una sola transacción on-chain. Ahorro medido: ~50% de gas en batch de 10 vs 10 transferencias individuales (153k gas vs 350k).

Flujo conceptual:

1. El comercio define `destinations: [{address, amount}, ...]` (max 200)
2. La pasarela calcula `totalAmount` = Σ amounts y `feeAmount` (fee de mantenimiento del comercio, % o fijo)
3. La pasarela genera una wallet desechable HD y devuelve `{depositAddress, totalToDeposit}`
4. El comercio (o quien sea) deposita `totalToDeposit` USDT en `depositAddress`
5. El block-poller detecta el depósito y, tras las confirmaciones, encola al executor
6. El executor:
   a. fundGas: fee payer envía POL/BNB a la wallet desechable
   b. approve(disperse, totalAmount) desde la wallet desechable
   c. disperseToken(token, recipients[], values[]) — 1 sola tx batch
   d. feeSweep: barre el fee restante al feeWalletAddress del comercio
7. Webhook `dispersion.completed` con hashes de las 3 txs

Estados (máquina de estados):

PENDING → AWAITING_DEPOSIT → DETECTED → CONFIRMING → (CONFIRMED | OVERPAID | UNDERPAID) → FUNDING_GAS → APPROVING → DISPERSING → COMPLETED

Terminales: COMPLETED, EXPIRED, FAILED, UNDERPAID (UNDERPAID es terminal en dispersiones — el comercio decide qué hacer fuera del flujo).

## Endpoint: crear dispersión

```http
POST https://144pay.com/api/v1/dispersions
X-API-Key: pk_live_xxxxxxxxxxxx
Idempotency-Key: payout-001 (opcional, recomendado)
Content-Type: application/json

{
  "chain": "polygon",
  "token": "USDT",
  "reference": "payout-001",
  "destinations": [
    { "address": "0xA1...", "amount": "50.00" },
    { "address": "0xB2...", "amount": "75.00" },
    { "address": "0xC3...", "amount": "30.00" }
  ]
}
```

Respuesta 201:

```json
{
  "success": true,
  "data": {
    "id": "d_8a3f...",
    "status": "AWAITING_DEPOSIT",
    "reference": "payout-001",
    "chain": "polygon",
    "token": "USDT",
    "depositAddress": "0x4f...e2",
    "totalAmount": "155.00",
    "totalAmountRaw": "155000000",
    "feeAmount": "1.55",
    "feeAmountRaw": "1550000",
    "totalToDeposit": "156.55",
    "totalToDepositRaw": "156550000",
    "receivedAmount": "0",
    "destinations": [
      { "address": "0xa1...", "amount": "50.00", "amountRaw": "50000000", "position": 0 },
      { "address": "0xb2...", "amount": "75.00", "amountRaw": "75000000", "position": 1 },
      { "address": "0xc3...", "amount": "30.00", "amountRaw": "30000000", "position": 2 }
    ],
    "txHashApprove": null,
    "txHashDisperse": null,
    "txHashFeeSweep": null,
    "expiresAt": "2026-05-23T15:00:00Z",
    "createdAt": "2026-05-23T14:30:00Z"
  },
  "error": null
}
```

Códigos de error específicos:
- 400 EMPTY_BATCH — destinations vacío
- 400 BATCH_TOO_LARGE — > 200 destinatarios
- 400 INVALID_ADDRESS — alguna dirección no es 0x + 40 hex
- 400 INVALID_AMOUNT — algún monto <= 0
- 400 CHAIN_NOT_SUPPORTED — chain no habilitada
- 400 TOKEN_NOT_SUPPORTED — token no habilitado en esa chain
- 503 DISPERSE_NOT_DEPLOYED — chain válida pero falta DISPERSE_ADDRESS_* en config

Notas:
- `totalToDeposit = totalAmount + feeAmount`. El comercio debe depositar EXACTAMENTE ese monto. Si deposita menos, queda en UNDERPAID y NO se dispersa. Si deposita más, el sobrante se barre junto con el fee.
- Misma API key que cobros. El aislamiento por merchant también aplica aquí.
- Reusa el sistema HD wallet existente: misma raíz, mismo fee payer.

## Endpoint: consultar dispersión

```http
GET https://144pay.com/api/v1/dispersions/{id}
X-API-Key: pk_live_xxxxxxxxxxxx
```

Devuelve la dispersión completa con los hashes de las txs ejecutadas (approve, disperse, fee sweep).

## Endpoint: listar dispersiones

```http
GET https://144pay.com/api/v1/dispersions?limit=20&cursor=&status=COMPLETED
X-API-Key: pk_live_xxxxxxxxxxxx
```

## Webhooks

Cuando un cobro o dispersión cambia a un estado terminal o relevante, el sistema hace POST al `webhook_url` configurado en el comercio.

### Eventos de cobros
- `payment.completed` — cobro finalizado correctamente, fondos distribuidos
- `payment.underpaid` — recibido < esperado (queda en UNDERPAID hasta resolución manual)
- `payment.overpaid` — recibido > esperado (se procesa el total)
- `payment.expired` — venció sin pago
- `payment.failed` — fallo irrecuperable

Cuerpo de webhook de cobro:

```json
{
  "event": "payment.completed",
  "paymentId": "pi_3f9a...",
  "reference": "ord-991",
  "status": "COMPLETED",
  "expectedAmount": "10500000",
  "receivedAmount": "10500000",
  "chain": "polygon",
  "timestamp": "2026-05-23T14:16:42Z"
}
```

### Eventos de dispersiones
- `dispersion.deposit_received` — la pasarela detectó el depósito del comercio
- `dispersion.completed` — el batch on-chain se ejecutó con éxito (los destinos recibieron)
- `dispersion.underpaid` — recibido < totalToDeposit (NO se dispersa, fondos quedan en la wallet)
- `dispersion.expired` — venció sin depósito
- `dispersion.failed` — error irrecuperable durante la ejecución on-chain

Cuerpo de webhook de dispersión:

```json
{
  "event": "dispersion.completed",
  "dispersionId": "d_8a3f...",
  "reference": "payout-001",
  "status": "COMPLETED",
  "totalAmount": "155000000",
  "feeAmount": "1550000",
  "totalToDeposit": "156550000",
  "receivedAmount": "156550000",
  "chain": "polygon",
  "token": "USDT",
  "destinationsCount": 3,
  "txHashApprove": "0x...",
  "txHashDisperse": "0x...",
  "txHashFeeSweep": "0x...",
  "timestamp": "2026-05-23T14:33:00Z"
}
```

Header de firma:

```
X-Signature: sha256=<hex>
```

El cliente verifica:

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

const expected = 'sha256=' + createHmac('sha256', WEBHOOK_SECRET)
  .update(JSON.stringify(req.body))
  .digest('hex');

const valid = timingSafeEqual(
  Buffer.from(req.headers['x-signature']),
  Buffer.from(expected),
);
```

Reintentos: backoff exponencial (1m, 5m, 30m, 2h, 6h), hasta 8 intentos.
Los webhooks son AT-LEAST-ONCE: tu endpoint debe ser idempotente.

## Ejemplos completos

### curl

```bash
# Crear cobro
curl -X POST https://144pay.com/api/v1/payments \
  -H "X-API-Key: pk_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ord-991" \
  -d '{"reference":"ord-991","chain":"polygon","token":"USDT","amount":"10.50"}'

# Consultar
curl https://144pay.com/api/v1/payments/pi_3f9a... \
  -H "X-API-Key: pk_live_xxxxxxxxxxxx"
```

### Node.js (cobro)

```js
const res = await fetch('https://144pay.com/api/v1/payments', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.PAY144_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': order.id,
  },
  body: JSON.stringify({
    reference: order.id,
    chain: 'polygon',
    token: 'USDT',
    amount: order.total,
  }),
});
const { data } = await res.json();
// data.depositAddress → mostrar al comprador
```

### Node.js (dispersión a múltiples destinos)

```js
const res = await fetch('https://144pay.com/api/v1/dispersions', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.PAY144_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    chain: 'polygon',
    token: 'USDT',
    reference: payoutBatch.id,
    destinations: recipients.map(r => ({
      address: r.wallet,
      amount: r.amount, // string decimal, ej. "50.00"
    })),
  }),
});
const { data } = await res.json();
// data.depositAddress → depositar exactamente data.totalToDeposit
// Escuchar webhook 'dispersion.completed' para confirmar la dispersión
```

### Python

```python
import requests, os

res = requests.post(
    'https://144pay.com/api/v1/payments',
    headers={
        'X-API-Key': os.environ['PAY144_KEY'],
        'Idempotency-Key': order.id,
    },
    json={
        'reference': order.id,
        'chain': 'polygon',
        'token': 'USDT',
        'amount': str(order.total),
    },
)
deposit_address = res.json()['data']['depositAddress']
```

## Errores

Respuesta estándar:

```json
{
  "success": false,
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "el monto debe ser mayor que cero"
  }
}
```

Códigos:
- `VALIDATION_ERROR` (400) — payload inválido
- `HTTP_ERROR` (401, 404, 429, etc.) — error HTTP
- `PaymentIntentError` (400) — error de dominio (chain no soportada, etc.)
- `MerchantServiceError` (400) — error del comercio (split no suma 100%, etc.)
- `INTERNAL_ERROR` (500) — error inesperado (no expone detalles)

## Garantías de integridad

- Invariante: `fee + Σ destinos == bruto` validada antes de cada distribución
- Decimales por (chain, token) — nunca asumimos un valor global
- Idempotencia en dos capas: `UNIQUE(chain, txHash, logIndex)` en Postgres + jobId determinístico en BullMQ
- Asignación atómica del índice HD: secuencia Postgres garantiza que dos cobros nunca compartan dirección
- Nonces serializados: colas gas-funding y distribution con concurrency 1
- Manejo de reorgs: `safeBlockLag` + revalidación de receipt antes de marcar CONFIRMED
- Estados con compare-and-set: `UPDATE WHERE status = <esperado>` evita transiciones duplicadas

## Para empezar

1. Pedir al admin del sistema (admin@144pay.com) un comercio + API key
2. Configurar tu `webhook_url` (URL pública que recibe los POST de la pasarela)
3. Verificar firmas con tu `webhook_secret`
4. Crear tu primer cobro y mostrar `depositAddress` al comprador
5. Escuchar `payment.completed` para marcar el pedido como pagado

## Recursos

- API Reference: https://144pay.com/docs
- OpenAPI 3.0 spec: https://144pay.com/openapi.json
- Repositorio: https://github.com/rojasjuniore/144pay
- Estado: https://144pay.com/api/v1/health
- Contacto: hola@144pay.com
