Todos los artículos
9 min de lectura

Cómo detectar un pago USDT entrante en TON de forma fiable

Detecta pagos USDT en TON: transfer_notification del jetton vía get_transactions, decimals con get_jetton_info, verificación del importe e idempotencia.

USDT en TONdetección de pagostransfer_notificationjettons TEP-74idempotenciaget_transactions

Todo el que haya montado alguna vez cobros en TON ha caído en la misma trampa: el pago llegó, pero el backend «no lo vio». El cliente envió 50 USDT, la wallet muestra el ingreso y la factura sigue colgada en estado «pendiente». Diez minutos después, un ticket furioso en soporte. Y una hora más tarde descubres que acreditaste el pago dos veces porque el poller se disparó en un retry. Detectar de forma fiable un pago USDT entrante (incoming USDT payment) en TON no consiste en «mirar el balance una vez por minuto», sino en parsear los mensajes internos concretos del jetton, con idempotencia y protección contra falsificaciones. Veamos paso a paso cómo hacerlo bien, con las herramientas reales del servidor MCP de TONNode, que puedes invocar gratis en local.

Por qué detectar un pago USDT en TON no va del balance de la wallet

El enfoque ingenuo: leer periódicamente el balance de la wallet y, si ha crecido, dar el pago por recibido. Es como una caja registradora que solo sabe mirar el total del cajón: entraron +10 USDT, pero ¿de quién, por qué factura, por un pedido viejo o por uno nuevo? Y si dos clientes pagaron 10 a la vez, solo verás +20 de un golpe.

En TON este enfoque se desmorona por varias razones a la vez:

  • USDT no es la moneda nativa, sino un jetton (estándar TEP-74). Los fondos no llegan directamente a la wallet principal, sino a su jetton wallet: un contrato aparte, vinculado al contrato maestro concreto del jetton. El balance en GRAM de la wallet principal ni siquiera cambia.
  • La delta del balance no dice quién pagó ni por qué. El balance no guarda ninguna vinculación con la factura.
  • Condiciones de carrera y agregación. Entre dos sondeos llegan varios pagos: ves la delta total, no los eventos individuales.
  • Acreditaciones dobles. Un retry del polling o un reinicio del worker, y un mismo pago queda contabilizado dos veces.
  • Decimals. USDT tiene 6, no los 9 habituales de los jettons. Basta con equivocarse de divisor para «acreditarle» al cliente 1000 veces de más o de menos.

El camino correcto es trabajar no con el balance, sino con el flujo de transacciones del jetton wallet del receptor, parseando cada evento de transferencia por separado. Toda la lectura está disponible gratis en local:

{ "mcpServers": { "ton": { "command": "npx", "args": ["-y", "@tonnode/mcp"] } } }

Cómo se ve un pago USDT entrante por dentro: internal_transfer del jetton (op 0x178d4519)

Cuando alguien te transfiere USDT, se desencadena esta cadena de eventos:

  1. El jetton wallet del remitente descuenta el importe y envía a tu jetton wallet un mensaje interno internal_transfer (op 0x178d4519).
  2. Tu jetton wallet acredita los fondos y — solo si el remitente adjuntó forward_ton_amount > 0 — envía además a tu wallet principal un mensaje interno transfer_notification (op 0x7362d09c).

Aquí se esconde la bifurcación principal. transfer_notification es una notificación cómoda de «te llegó un jetton», pero va a la wallet principal del propietario y solo cuando forward_ton_amount > 0. Si el remitente puso cero, el jetton wallet acreditará los fondos igualmente, pero el propietario no recibirá notificación alguna.

En cambio, en el historial del propio jetton wallet la acreditación queda registrada siempre: como un internal_transfer entrante, independientemente de forward_ton_amount. Por eso la detección fiable se construye sondeando el historial del jetton wallet del receptor y parseando precisamente el internal_transfer. Su estructura según TEP-74:

internal_transfer#178d4519
  query_id:           uint64
  amount:             (VarUInteger 16)     // unidades raw del jetton
  from:               MsgAddress           // dirección del propietario-PAGADOR (la persona)
  response_address:   MsgAddress
  forward_ton_amount: (VarUInteger 16)
  forward_payload:    (Either Cell ^Cell)  // comentario/memo

Dos puntos importantes:

  • El campo from es la dirección del pagador real (el propietario humano), no la de su jetton wallet. Es justamente esa la que debes registrar como remitente. Ojo: la dirección a nivel de mensaje (tx.in_msg.source) aquí es el jetton wallet del pagador; la persona que paga está en el campo from del cuerpo del mensaje.
  • El importe del campo amount viene en unidades raw; a la conversión volvemos más abajo.

De ahí la conclusión principal sobre la arquitectura

La detección fiable se construye no esperando la notificación en la wallet principal, sino sondeando el historial del propio jetton wallet del receptor. En su historial la acreditación se ve siempre, independientemente de forward_ton_amount. Si además escuchas la wallet principal, allí capturas el transfer_notification (op 0x7362d09c, el remitente va en el campo sender), pero eso es solo un extra para el caso forward_ton_amount > 0, no la única fuente de verdad.

Leemos el historial con get_transactions y parseamos internal_transfer

Primero necesitas la dirección del jetton wallet del receptor. No la derives offline ni te fíes del ticker: pregúntale al propio contrato maestro de USDT. La llamada get_wallet_address (vía run_get_method) sobre el maestro de USDT devuelve de forma determinista la dirección de tu jetton wallet de USDT, vinculada al maestro auténtico. Después sondeamos las transacciones de ese jetton wallet con get_transactions.

Prompt para el agente en el ciclo de polling:

Llama a run_get_method sobre el maestro de USDT
EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs con el método
get_wallet_address y como argumento la dirección del propietario
UQ...myservice. Devuelve la dirección de nuestro jetton wallet de USDT.
Luego, con get_transactions, lee las últimas transacciones de ese
jetton wallet. Para cada internal_transfer entrante (op 0x178d4519)
devuelve query_id, amount (raw), el campo from (dirección del pagador),
el comentario de texto del forward_payload, y también lt y hash de la
transacción.

Pseudocódigo del procesamiento de una transacción:

for (const tx of txs) {
  const body = tx.in_msg?.decoded;          // cuerpo decodificado del mensaje entrante
  if (body?.op !== 0x178d4519) continue;    // no es internal_transfer — lo saltamos

  const rawAmount = BigInt(body.amount);    // unidades raw, NO legibles para humanos
  const payer     = body.from;              // dirección del propietario-pagador
  const memo      = parseComment(body.forward_payload);
  const key       = `${tx.lt}:${tx.hash}`;  // identificador para el dedupe
  // …verificación y acreditación más abajo
}

También conviene hacer una comprobación puntual del estado de la cuenta principal con get_account_state (si está activa, cuándo fue su última transacción): ayuda a saber si el poller sigue «vivo» y si no se ha quedado atrás.

Los decimals lo deciden todo: get_jetton_info y la conversión del importe raw (USDT = 6, no 9)

El campo amount de la transferencia viene en unidades raw del jetton: un entero sin coma. Para convertirlo en un importe legible necesitas los decimals. Y es justo aquí donde se rompen más integraciones.

La mayoría de los jettons tienen decimals = 9. USDT (Tether) en TON tiene decimals = 6. Es decir:

1 USDT = 1 000 000 raw   (10^6, no 10^9)

Si por inercia divides el importe raw de USDT entre 10^9, el pago de 50 USDT de tu cliente se convierte en 0.05: 1000 veces menos. El error en sentido contrario acredita con la misma facilidad 1000 veces de más. No hardcodees el divisor: toma los decimals de los metadatos on-chain con get_jetton_info:

const info = await getJettonInfo(USDT_MASTER); // name, symbol, decimals, emisión
const decimals = info.decimals;                // para USDT = 6
const human = Number(rawAmount) / 10 ** decimals; // 50000000 → 50.0

Cachea los decimals por dirección del maestro, no por ticker: el ticker se falsifica, los decimals deben ir ligados a un contrato concreto. La regla es simple: los decimals, siempre de get_jetton_info; el importe, solo en BigInt/raw hasta el momento de mostrarlo.

Verificamos origen, importe y comentario — y descartamos los jettons falsos

Ahora, lo más importante para la seguridad. Las comprobaciones sin las cuales no puedes acreditar nada.

1. El contrato maestro: solo el USDT auténtico. Cualquiera puede emitir un jetton con el nombre «USDT» y el símbolo «USD₮» y mandarte una transferencia de 1000 «USDT». Si matcheas el pago por ticker, te estafan en cinco minutos. La única vinculación fiable con el Tether auténtico es trabajar con el jetton wallet que el contrato maestro auténtico de USDT calculó para tu propietario:

Tether USD₮ master: EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs

Precisamente por eso la dirección del jetton wallet la obtenemos de la llamada on-chain get_wallet_address sobre ese maestro (ver arriba), y el historial lo sondeamos solo en ese contrato, y en ningún otro. Cualquier internal_transfer acreditado en él proviene necesariamente de un jetton wallet hermano legítimo del mismo maestro: el «USDT» falso vive en wallets de otro maestro y simplemente nunca llega a tu dirección. Por eso, con esta arquitectura, no hace falta comparar tx.in_msg.source con nada: la protección la da la propia elección del punto de observación. Y ten en cuenta: parse_address es solo una conversión offline entre los formatos EQ/UQ/raw; no calcula la dirección del jetton wallet — para eso hace falta justamente la llamada on-chain get_wallet_address. Sí resulta útil para normalizar direcciones antes de compararlas, y así no tratar como distintas dos representaciones de una misma dirección.

2. El comentario (memo) para matchear con la factura. El comentario de texto va en forward_payload en el formato estándar: prefijo op 0x00000000 (4 bytes a cero) + cadena UTF-8. Con él vinculas la transferencia entrante a un pedido concreto.

3. Importe y pagador. Compara el importe convertido con el esperado según la factura; si hace falta, registra from (la dirección del pagador) para el historial y el antifraude.

// 1. El origen está garantizado: leemos el historial EXACTAMENTE del
//    jetton wallet cuya dirección devolvió get_wallet_address
//    del maestro auténtico de USDT — cualquier transferencia aquí es real.

// 2. Comentario → factura
const invoice = invoices.get(memo);
if (!invoice) continue;

// 3. ¿Cuadra el importe?
if (human < invoice.expectedAmount) markUnderpaid(invoice);

// 4. El pagador — para logs/antifraude
log({ payer, memo, human, lt: tx.lt, hash: tx.hash });

Idempotencia: dedupe por lt + hash para no acreditar un pago dos veces

El polling siempre se solapa consigo mismo: se lanza de forma programada, se cae, se reintenta, las ventanas de sondeo se cruzan. Sin dedupe, tarde o temprano acreditarás un mismo pago dos veces.

En TON cada transacción se identifica de forma única por el par logical time (lt) + hash. Esa es tu clave natural de idempotencia:

const key = `${tx.lt}:${tx.hash}`;
if (await seen.has(key)) continue;   // ya procesada — salimos
await creditInvoice(invoice, human); // acreditación
await seen.add(key);                 // lo fijamos en la misma transacción de BD

Como clave también puedes usar el query_id de la transferencia, pero (lt, hash) funciona para cualquier transacción entrante del jetton wallet, incluido el caso forward_ton_amount = 0. Guarda las claves procesadas de forma persistente y haz la acreditación en una única transacción de BD con comprobación de unicidad: así los workers en paralelo no crearán duplicados.

Periódicamente conviene hacer una reconciliación: cada N minutos, comparar el balance real del jetton wallet (con get_jetton_balance, que calcula la dirección on-chain y devuelve el balance en una sola llamada) con la suma de todos los pagos acreditados. Una discrepancia es señal de que en algún sitio se perdió o se duplicó una transferencia, aunque solo haya fallado un ciclo de polling.

Throughput estable bajo carga: tu propia key en lugar de los límites públicos

El polling de pagos es un flujo de peticiones constante y uniforme: N wallets × frecuencia de sondeo. Y ahí la infraestructura pública se convierte en el cuello de botella.

Los liteservers públicos de la config global son compartidos y limitados: bajo carga responden not ready o se van a timeout de ADNL. Las API HTTP públicas (toncenter, tonapi.io) sin key aguantan del orden de 1 petición por segundo y, al superarlo, devuelven un honesto HTTP 429 «Too Many Requests». Fíjate: el código real del límite es precisamente el 429, y no el «228» de los memes, que circula por la comunidad de TON y que no es ningún código de la API. Para una caja registradora que sondea el historial cada pocos segundos, esos límites se traducen en acreditaciones perdidas y retrasadas. Cómo elegir un proveedor con key propia lo cubrimos en un análisis aparte.

Todas las herramientas de lectura necesarias para detectar pagos — get_transactions, run_get_method, get_jetton_balance, get_jetton_info, parse_address, get_account_state — están disponibles gratis en local: npx -y @tonnode/mcp. El paquete @tonnode/mcp es open source (MIT) y funciona sobre el protocolo nativo ADNL de TON, sin capas HTTP intermedias. Cuando el polling pasa a producción y necesitas un throughput garantizado, se conecta el endpoint hosted con tu propia key:

{
  "mcpServers": {
    "ton": {
      "type": "http",
      "url": "https://mcp.tonnode.io/mcp",
      "headers": { "Authorization": "Bearer tn_live_…" }
    }
  }
}

Los planes se diferencian solo en el límite de peticiones por minuto — las 16 herramientas están disponibles en todos:

  • Hobby — gratis para siempre, 60 peticiones/min. La key se entrega nada más iniciar sesión, sin tarjeta.
  • Pro — $29/mes, 300 peticiones/min.
  • Scale — $199/mes, 1200 peticiones/min.

Para arrancar el polling de pagos, las 60 peticiones gratuitas por minuto bastan para mantener un sondeo estable sin 429. Si sobre esto construyes un agente de IA que parsee los pagos por su cuenta, en la guía de MCP para agentes de IA en TON explicamos cómo se monta.

Empieza con la key gratuita Hobby — 60 req/min para el polling de pagos, sin tarjeta: tonnode.io/dashboard?plan=hobby. Cuando la carga crezca y un solo worker no baste, compara los límites de Pro y Scale.


Checklist de detección fiable de USDT en TON:

  1. Sondea el historial del jetton wallet del receptor con get_transactions, no el balance de la wallet principal.
  2. La dirección de ese jetton wallet, obtenla de la llamada on-chain get_wallet_address (run_get_method) sobre el maestro auténtico de USD₮ (EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs): así tienes la garantía de que cualquier transferencia que le llegue es USDT auténtico.
  3. Busca los internal_transfer entrantes con op 0x178d4519: son los que siempre están en el historial del jetton wallet, independientemente de forward_ton_amount. El transfer_notification (0x7362d09c) va a la wallet principal y solo con forward_ton_amount > 0.
  4. La dirección del pagador, del campo from; el importe, de amount (raw).
  5. Toma los decimals de get_jetton_info. USDT = 6, no 9.
  6. Matchea la factura por el comentario del forward_payload (op 0x00000000 + UTF-8).
  7. Dedupe por (lt, hash): acredita estrictamente una sola vez.
  8. Mantén un throughput estable con tu propia key, no con los límites públicos.

Dale a tu agente acceso a TON

16 herramientas MCP: lectura, swaps no custodiales, cross-chain y billeteras. Plan gratuito — 60 req/min, sin tarjeta.