Пошаговое руководство по настройке приёма USDT от пользователей вашей платформы.
Вы — обменник или биржа. Ваши пользователи хотят пополнить баланс в USDT:
- Пользователь запрашивает адрес для пополнения
- Ваша система создаёт уникальный кошелёк
- Пользователь отправляет USDT
- Lumo проводит AML-проверку и пересылает средства на ваш адрес
- Вы получаете webhook и зачисляете сумму пользователю
- Без блокчейн-кода — не нужно работать с TRON напрямую
- AML из коробки — автоматическая проверка входящих транзакций
- Автопересылка — средства собираются на вашем адресе
depositForwardAddress - Webhooks — real-time уведомления
Деньги пользователей не зачисляются на баланс компании в Lumo: они пересылаются в сети на ваш адрес. С баланса в Lumo списывается только комиссия за обработку каждого депозита — держите там запас.
Сначала настройте endpoint для получения уведомлений. Подпись проверяйте по сырому телу запроса:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post(
'/webhooks/lumo',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.get('x-lumo-signature') || '';
const expected = crypto
.createHmac('sha256', process.env.LUMO_WEBHOOK_SECRET)
.update(req.body) // сырое тело, Buffer
.digest('hex');
if (
signature.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
) {
return res.status(401).send('Invalid signature');
}
res.status(200).send('OK');
await handleWebhook(JSON.parse(req.body.toString('utf8')));
},
);
async function handleWebhook(payload) {
// У депозитов на кошельки пользователей всегда есть externalId
if (!('externalId' in payload)) return;
switch (payload.event) {
case 'deposit.completed':
await creditUserBalance(payload);
break;
case 'deposit.aml_failed':
await handleAmlFailed(payload);
break;
case 'deposit.failed':
await notifySupport(payload);
break;
}
}Сгенерируйте секрет подписи и обновите профиль компании — адрес для вебхуков и TRON-адрес, куда будут пересылаться депозиты:
curl -X POST 'https://b2b.lumowallet.io/auth/company/webhook-secret/rotate' \
-H 'X-API-Key: YOUR_API_KEY'
curl -X PATCH 'https://b2b.lumowallet.io/auth/company/profile' \
-H 'X-API-Key: YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"webhookUrl": "https://your-server.com/webhooks/lumo",
"depositForwardAddress": "TQ2x4bW8ZGq3NkL2bUQvX8sJ7d1cQh1xYz"
}'Задайте depositForwardAddress до создания первого кошелька: пока он не задан, депозиты пересылаются на казначейский адрес Lumo, а не вам.
Когда пользователь запрашивает адрес для пополнения:
async function getDepositAddress(userId) {
const response = await fetch('https://b2b.lumowallet.io/user-wallets', {
method: 'POST',
headers: {
'X-API-Key': process.env.LUMO_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
externalId: userId // Ваш внутренний ID пользователя
})
});
const wallet = await response.json();
return wallet.address;
}Ответ:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"companyId": "9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d",
"externalId": "user-12345",
"address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"isBlocked": false,
"blockReason": null,
"lastUsdtBalance": 0,
"lastTrxBalance": 0,
"createdAt": "2026-03-10T12:00:00.000Z"
}💡 Идемпотентность: Повторный запрос с тем же
externalIdвернёт существующий незаблокированный кошелёк. Создание кошелька требует правоbalance.withdraw.
function DepositPage({ userId }) {
const [address, setAddress] = useState(null);
useEffect(() => {
getDepositAddress(userId).then(setAddress);
}, [userId]);
return (
<div>
<h2>Пополнение баланса</h2>
<p>Отправьте USDT (TRC-20) на адрес:</p>
<AddressBox>{address}</AddressBox>
<QRCode value={address} />
<Warning>
⚠️ Отправляйте только USDT в сети TRON (TRC-20)
</Warning>
</div>
);
}Когда пользователь отправляет USDT, Lumo проверяет перевод и пересылает его на ваш depositForwardAddress. По кошелькам пользователей приходят три события: deposit.completed, deposit.aml_failed и deposit.failed. Промежуточных событий (deposit.created, deposit.aml_pending) нет — если хотите показывать пользователю ожидание, отслеживайте входящий перевод на адрес сами или опрашивайте GET /user-wallets/deposits.
{
"event": "deposit.completed",
"depositId": "770e8400-e29b-41d4-a716-446655440002",
"externalId": "user-12345",
"txHash": "c4d2e8...19fa",
"walletAddress": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"amount": 100,
"status": "completed",
"amlCheckId": null,
"amlResult": null,
"sweepTxHash": "d9a0b1...77e3",
"error": null,
"blockTimestamp": null,
"createdAt": "2026-03-10T12:05:00.000Z",
"updatedAt": "2026-03-10T12:05:00.000Z"
}Пересылка на ваш адрес отправлена в сеть (sweepTxHash). Зачислите сумму пользователю — ровно один раз на depositId:
async function creditUserBalance({ depositId, externalId, amount }) {
await db.transaction(async (tx) => {
// уникальный индекс по depositId не даст зачислить дважды
const inserted = await tx.deposits.insertIfAbsent({ depositId, externalId, amount });
if (!inserted) return;
await tx.users.increment(externalId, 'balance', amount);
});
await notifyUser(externalId, { type: 'deposit_success', amount });
}deposit.failed — переслать депозит не удалось, причина в error. Например, на балансе компании в Lumo не хватило средств на комиссию за обработку (Insufficient company balance for processing fee): пересылка повторяется через 5 минут, 1 час и 4 часа, и только потом приходит deposit.failed. Средства при этом остаются на адресе пользователя — напишите в поддержку с depositId.
deposit.failed не всегда окончательный: после некоторых ошибок пересылка повторяется, и по тому же depositId может прийти deposit.completed. Поэтому не помечайте депозит навсегда проваленным и зачисляйте по deposit.completed.
При deposit.aml_failed кошелёк пользователя блокируется, средства остаются на его адресе, в amlResult — показатель риска:
async function handleAmlFailed(payload) {
const { externalId, amount, depositId, amlResult } = payload;
// Уведомить пользователя
await notifyUser(externalId, {
type: 'deposit_blocked',
message: 'Депозит заблокирован для проверки'
});
// Создать тикет для поддержки
await createSupportTicket({
type: 'aml_review',
userId: externalId,
depositId,
amount,
risk: amlResult,
});
}Новые депозиты на заблокированный адрес не обрабатываются. Чтобы пользователь мог пополнять снова, выдайте ему новый адрес — повторный POST /user-wallets с тем же externalId создаст новый кошелёк. Средства с заблокированного кошелька можно вывести на указанный адрес через POST /user-wallets/{walletId}/withdraw, см. User Wallets.
async function getUserDeposits(userId) {
const response = await fetch(
`https://b2b.lumowallet.io/user-wallets/deposits?externalId=${userId}`,
{ headers: { 'X-API-Key': process.env.LUMO_API_KEY } }
);
return response.json();
}При подозрительной активности заблокируйте кошелёк:
async function blockUserWallet(walletId, reason) {
await fetch(`https://b2b.lumowallet.io/user-wallets/${walletId}/block`, {
method: 'POST',
headers: {
'X-API-Key': process.env.LUMO_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ reason })
});
}Если пользователь просит новый адрес:
async function replaceUserWallet(walletId) {
const response = await fetch(
`https://b2b.lumowallet.io/user-wallets/${walletId}/replace`,
{
method: 'POST',
headers: {
'X-API-Key': process.env.LUMO_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ reason: 'User requested new address' })
}
);
const newWallet = await response.json();
return newWallet.address; // Новый адрес
}class DepositService {
constructor(apiKey, webhookSecret) {
this.apiKey = apiKey;
this.webhookSecret = webhookSecret;
this.baseUrl = 'https://b2b.lumowallet.io';
}
async getOrCreateWallet(userId) {
const response = await fetch(`${this.baseUrl}/user-wallets`, {
method: 'POST',
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({ externalId: userId })
});
return response.json();
}
// rawBody — Buffer с телом запроса в том виде, в каком оно пришло
verifyWebhook(rawBody, signature) {
const expected = crypto
.createHmac('sha256', this.webhookSecret)
.update(rawBody)
.digest('hex');
return (
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
);
}
async handleWebhook(payload) {
switch (payload.event) {
case 'deposit.completed':
await this.onDepositCompleted(payload);
break;
case 'deposit.aml_failed':
await this.onAmlFailed(payload);
break;
case 'deposit.failed':
await this.onDepositFailed(payload);
break;
}
}
async onDepositCompleted({ depositId, externalId, amount }) {
// Зачисляем баланс ровно один раз на depositId
const isNew = await db.processedDeposits.insertIfAbsent(depositId);
if (!isNew) return;
await db.users.increment(externalId, 'balance', amount);
// Уведомляем пользователя
await notifications.send(externalId, {
title: 'Депозит зачислен',
body: `+${amount} USDT`
});
}
async onAmlFailed({ externalId, depositId }) {
await notifications.send(externalId, {
title: 'Депозит на проверке',
body: 'Свяжитесь с поддержкой'
});
}
async onDepositFailed(payload) {
await support.notify('Депозит не переслан', payload);
}
}- Настроен webhook endpoint
- Webhook URL и
depositForwardAddressзаданы в профиле - Сгенерирован секрет подписи, подпись проверяется по сырому телу
- На балансе компании в Lumo есть запас на комиссии за обработку
- Создание кошельков для пользователей
- UI для показа адреса + QR-код
- Обработка
deposit.completed - Обработка
deposit.aml_failedиdeposit.failed - Зачисление баланса пользователю ровно один раз на
depositId - Уведомления пользователю
- Логирование всех событий