Как узнать баланс USDT на кошельке TON одним вызовом
Как получить баланс USDT на кошельке TON одним вызовом get_jetton_balance — джеттон-кошелёк считается он-чейн, индексер не нужен.
Кошелёк агента открыт, get_balance вернул честные несколько GRAM — а USDT ноль. Хотя вы точно знаете, что стейблкоины на этот адрес приходили. Знакомо? Кошелёк выглядит пустым, хотя на нём лежит 500 USDT. Это не баг и не пропавшие деньги — вы просто спросили не у того контракта. И именно здесь падает большинство первых интеграций ИИ-агента с TON: агент читает не тот адрес.
Почему баланс USDT — это не баланс TON-кошелька
В Ethereum вы привыкли, что токен ERC-20 «лежит на адресе»: контракт токена хранит табличку address → balance, и чтобы узнать баланс, вы спрашиваете этот один контракт. В TON модель другая, и это первый источник путаницы.
Нативная монета GRAM (бывший Toncoin; сеть по-прежнему называется TON) лежит прямо на смарт-контракте вашего кошелька — её читает get_balance одним обращением к состоянию аккаунта. А USDT — это джеттон (jetton, стандарт TON для взаимозаменяемых токенов). И баланс джеттона хранится не на вашем основном кошельке, а на отдельном маленьком контракте — джеттон-кошельке (jetton wallet).
Аналогия: ваш основной TON-кошелёк — это вы как человек. А джеттон-кошелёк USDT — это отдельный счёт на ваше имя, открытый конкретным банком (мастер-контрактом USDT). Спрашивать «сколько у меня долларов» у самого человека бессмысленно — деньги на счету, а не в кармане. У каждой пары «владелец + жетон» — свой джеттон-кошелёк: для USDT один адрес, для NOT другой, для любого жетона третий.
Хорошая новость: адрес этого джеттон-кошелька не случайный. Он детерминированно выводится из двух вещей:
- адреса владельца (ваш обычный TON-кошелёк,
EQ…/UQ…); - адреса мастер-контракта жетона (jetton master — для USDT это один фиксированный контракт).
Плохая новость: чтобы честно вычислить этот адрес, нужно сходить в мастер-контракт и вызвать его get-метод, а потом прочитать состояние джеттон-кошелька. Вручную это несколько шагов, и именно на них спотыкаются интеграции.
get_jetton_balance: один вызов вместо индексера
Обычно эту задачу решают одним из двух способов, и оба неудобны:
- Свой индексер. Поднять ноду, проиндексировать джеттон-переводы в базу, поддерживать её в актуальном состоянии. Дорого и хрупко ради одного числа, и данные всегда чуть отстают от чейна.
- Публичный HTTP-API. Быстро упирается в лимиты: без ключа это порядка одного запроса в секунду, а при превышении вы получаете честный
HTTP 429 Too Many Requests. Плюс вы зависите от чужой индексации и её глубины.
Инструмент get_jetton_balance из TONNode убирает обе проблемы. Вы передаёте ему:
- адрес владельца — обычный TON-кошелёк пользователя (
EQ…/UQ…); - идентификатор жетона — например, USDT.
Дальше сервер сам делает всю работу он-чейн:
- вызывает get-метод мастер-контракта жетона, который по адресу владельца возвращает адрес его джеттон-кошелька (тот самый детерминированный вывод);
- читает баланс с этого джеттон-кошелька и возвращает его вам.
Никакого стороннего индексера, никакой базы, никакой рассинхронизации — только прямое чтение состояния сети через get-методы контрактов.
TONNode — это hosted MCP-сервер для TON. MCP (Model Context Protocol) — стандарт, по которому ИИ-агенты (Claude, Cursor, ChatGPT/Codex и любой MCP-клиент) вызывают внешние инструменты. То есть get_jetton_balance — это не строчка в вашем коде, а инструмент, который агент дёргает сам, когда пользователь спрашивает про баланс. Под капотом пакет @tonnode/mcp (open source, MIT) работает по нативному ADNL-протоколу TON, без промежуточных HTTP-прослоек — агент общается с сетью напрямую, а не через очередной REST-шлюз.
Пример: промпт агенту и что возвращается
Подключение локальное, без ключа и без карты. Добавьте в конфиг MCP-клиента (Claude Desktop, Cursor, любой MCP-совместимый):
{
"mcpServers": {
"ton": {
"command": "npx",
"args": ["-y", "@tonnode/mcp"]
}
}
}
После этого агенту достаточно обычного промпта на естественном языке:
Сколько USDT на кошельке
UQAbc…xyz? Верни человекочитаемое значение.
Агент сам выберет инструмент get_jetton_balance, передаст адрес владельца и жетон USDT, а сервер вернёт баланс — но не в привычных «долларах», а в raw-единицах (минимальных неделимых единицах жетона). Помимо самого баланса в ответе приходит и адрес джеттон-кошелька, который сервер вычислил он-чейн, — полезно, если дальше вы хотите отслеживать переводы именно этого контракта. Сам баланс в нашем примере — значение вида 12500000.
Но 12500000 — это не 12.5 миллиона USDT. Здесь начинается вторая ловушка, на которой легко ошибиться.
Raw-единицы и decimals: почему у USDT 6, а не 9
Блокчейны не работают с дробями. Все суммы хранятся в целочисленных raw-единицах, а «человеческое» значение получается делением на 10^decimals, где decimals — свойство конкретного жетона. Это как копейки и рубли: на низком уровне всё в копейках, а decimals говорит, где поставить запятую.
Ключевой нюанс TON: у разных жетонов разное число decimals.
- У нативного GRAM и у большинства джеттонов TON
decimals = 9. - А у USDT на TON
decimals = 6.
Поэтому одно и то же raw-значение означает совершенно разные суммы. Пересчитываем наш пример правильно:
raw = 12500000
decimals = 6 // именно для USDT
human = 12500000 / 10^6 = 12500000 / 1_000_000 = 12.5 USDT
12.5 USDT, а не 12.5 миллиона. Если бы вы по инерции поделили на 10^9 (как для обычного жетона), получили бы 0.0125 — ошибка в тысячу раз. Одно и то же число — три порядка разницы. Ровно так теряются деньги в наивных интеграциях: жёстко зашитый делитель 10^9 для всего подряд. Поэтому никогда не хардкодьте decimals — берите их из метаданных самого жетона.
get_jetton_info: откуда взять decimals и метаданные жетона
Чтобы не гадать, есть парный инструмент — get_jetton_info. Он читает мастер-контракт жетона и отдаёт его метаданные:
- имя (name);
- символ (symbol);
- decimals — то самое число, на которое считается делитель;
- эмиссию (total supply).
Надёжный сценарий для агента — два вызова: если жетон незнакомый, сначала get_jetton_info, чтобы узнать decimals, затем get_jetton_balance, чтобы взять raw-баланс, и только потом деление raw / 10^decimals для отображения.
1) get_jetton_info(USDT) -> decimals = 6
2) get_jetton_balance(owner, USDT) -> raw = 12500000
3) human = 12500000 / 10^6 -> 12.5 USDT
Промпт можно сформулировать так, чтобы агент сам склеил эти шаги:
Возьми decimals для USDT через get_jetton_info, затем баланс кошелька
UQAbc…xyzчерез get_jetton_balance и переведи raw в человекочитаемое число.
Такой порядок обязателен, если вы работаете с произвольными жетонами, а не только с USDT: для незнакомого токена вы заранее не знаете, 6 у него decimals или 9. Для USDT decimals = 6 — константа, но привычка тянуть её из get_jetton_info спасёт вас на первом же нестандартном жетоне. Тот же приём лежит в основе детекта входящих платежей USDT на TON — там decimals критичны, чтобы не спутать 1 USDT с микроскопической пылью.
parse_address: офлайн-проверка адреса перед запросом
Ещё одна частая причина «нулевого» баланса — кривой адрес владельца. Пользователи присылают адреса в разных форматах: EQ… (bounceable), UQ… (non-bounceable), raw (0:…). Перед тем как спрашивать баланс, адрес полезно нормализовать через parse_address — он офлайн (без обращения к сети) берёт адрес в любом из форматов, конвертирует его во все три (EQ/UQ/raw) и говорит, корректен ли он вообще.
Это дёшево, мгновенно и убирает целый класс ошибок «баланс 0, потому что адрес не тот» — особенно если адрес пришёл из ненадёжного источника, пользовательского ввода или чата.
Как подключить: бесплатно локально или hosted
Три инструмента — parse_address, get_jetton_info, get_jetton_balance — дают полный, честный ответ на вопрос «сколько USDT на кошельке» без единого стороннего индексера.
Бесплатно локально
Пакет @tonnode/mcp — open source (MIT), лежит на npm и GitHub. Тот же публичный конфиг из примера выше (npx -y @tonnode/mcp) даёт полный набор инструментов чтения, включая get_jetton_balance, get_jetton_info и parse_address — отдельный ключ не нужен.
Перезапускаете клиент — и агент уже читает балансы жетонов по ADNL. Зачем агенту вообще отдельный MCP-сервер, а не публичный шлюз с лимитами, разбирали в заметке про MCP для ИИ-агентов на TON. А где заканчивается бесплатный публичный конфиг и почему — в разборе лимитов публичных лайтсерверов TON.
Hosted — когда нужна пропускная способность
Когда локального рейта уже мало (боты, бэкенды, продакшн-нагрузка), переключаетесь на hosted-эндпоинт со своим ключом. Набор инструментов везде одинаковый — все 16, включая блок чтения; тарифы отличаются только гарантированной пропускной способностью:
- Hobby — бесплатно навсегда, 60 запросов/мин;
- Pro — $29/мес, 300 запросов/мин;
- Scale — $199/мес, 1200 запросов/мин.
Hosted-подключение отличается только тем, что вы обращаетесь к общему эндпоинту со своим ключом:
{
"mcpServers": {
"ton": {
"type": "http",
"url": "https://mcp.tonnode.io/mcp",
"headers": { "Authorization": "Bearer tn_live_…" }
}
}
}
Если делаете дашборд, бота для проверки балансов или агента, который часто опрашивает адреса, — берите ключ, чтобы не упираться в лимиты публичных API и не ловить 429 в самый неподходящий момент.
Коротко
- USDT на TON — джеттон, и баланс лежит на отдельном джеттон-кошельке, а не на основном адресе.
get_jetton_balanceсам вычисляет адрес джеттон-кошелька он-чейн и читает баланс — индексер и своя база не нужны.- Баланс приходит в raw-единицах; для отображения делите на
10^decimals. - У USDT
decimals = 6(делитель1_000_000), у большинства жетонов —9. Не хардкодьте — берите изget_jetton_info. - Проверить весь путь можно бесплатно:
npx -y @tonnode/mcp, публичный конфиг, полный набор чтения.
Бесплатный ключ Hobby — 60 запросов/мин, без карты — выдаётся сразу после входа: получить ключ. Его хватает, чтобы прогнать parse_address → get_jetton_info → get_jetton_balance на реальном кошельке и убедиться, что 12500000 raw превращаются ровно в 12.5 USDT — а не в 12.5 миллиона и не в 0.0125.
Дайте вашему агенту доступ к TON
16 MCP-инструментов: чтение, некастодиальные свапы, кроссчейн и кошельки. Бесплатный тариф — 60 запр/мин, карта не нужна.