TON 지갑 USDT 잔액을 호출 한 번으로 확인하는 법
TON 지갑의 USDT 잔액을 get_jetton_balance 호출 한 번으로 가져오는 법 — Jetton 지갑 주소는 온체인에서 계산되므로 별도 인덱서가 필요 없습니다.
에이전트의 지갑을 열었습니다. get_balance는 정직하게 몇 GRAM을 돌려주는데, USDT는 0입니다. 그 주소로 스테이블코인이 들어온 건 분명한데도 말이죠. 익숙한 상황인가요? 지갑에 500 USDT가 들어 있는데도 화면은 텅 비어 보입니다. 버그도 아니고 돈이 사라진 것도 아닙니다. 그냥 엉뚱한 컨트랙트에 물어본 것뿐입니다. 그리고 AI 에이전트를 TON에 붙이는 첫 연동의 대부분이 정확히 여기서 무너집니다. 에이전트가 잘못된 주소를 읽는 것이죠.
USDT 잔액은 왜 TON 지갑 잔액이 아닌가
Ethereum에서는 ERC-20 토큰이 "주소에 들어 있다"고 여기는 데 익숙합니다. 토큰 컨트랙트가 address → balance 테이블을 들고 있고, 잔액을 알고 싶으면 그 컨트랙트 하나에 물어보면 됩니다. TON의 모델은 다르고, 혼란은 여기서 시작됩니다.
네이티브 코인 GRAM(예전의 Toncoin이며, 네트워크 이름은 여전히 TON입니다)은 여러분 지갑의 스마트 컨트랙트에 그대로 들어 있습니다. get_balance가 계정 상태를 한 번 조회해서 바로 읽어옵니다. 반면 USDT는 Jetton(jetton, TON의 대체 가능 토큰 표준)입니다. 그리고 Jetton 잔액은 여러분의 메인 지갑이 아니라 별도의 작은 컨트랙트, 즉 Jetton 지갑(jetton wallet)에 저장됩니다.
비유하자면 이렇습니다. 메인 TON 지갑은 "사람"인 여러분 자신입니다. USDT Jetton 지갑은 특정 은행(USDT 마스터 컨트랙트)이 여러분 이름으로 열어준 별도의 계좌입니다. 사람한테 "내 달러 얼마 있어?"라고 물어봐야 소용이 없습니다. 돈은 주머니가 아니라 계좌에 있으니까요. "소유자 + Jetton" 조합마다 각자의 Jetton 지갑이 있습니다. USDT는 이 주소, NOT은 저 주소, 다른 Jetton은 또 다른 주소입니다.
좋은 소식은 이 Jetton 지갑 주소가 무작위가 아니라는 점입니다. 두 가지로부터 결정론적으로 유도됩니다.
- 소유자 주소 — 평범한 여러분의 TON 지갑(
EQ…/UQ…); - Jetton 마스터 컨트랙트 주소(jetton master — USDT의 경우 고정된 컨트랙트 하나입니다).
나쁜 소식은 이 주소를 정직하게 계산하려면 마스터 컨트랙트에 가서 get 메서드를 호출하고, 그다음 Jetton 지갑의 상태를 읽어야 한다는 점입니다. 손으로 하면 여러 단계이고, 연동이 걸려 넘어지는 지점이 정확히 여기입니다.
get_jetton_balance: 인덱서 대신 호출 한 번
보통 이 문제는 두 가지 방법으로 풀리는데, 둘 다 불편합니다.
- 직접 만든 인덱서. 노드를 띄우고, Jetton 전송을 DB에 색인하고, 그 DB를 계속 최신 상태로 유지합니다. 숫자 하나 얻자고 치르기엔 비싸고 취약하며, 데이터는 늘 체인보다 조금씩 뒤처집니다.
- 공개 HTTP API. 금방 한도에 부딪힙니다. 키 없이는 초당 요청 한 건 수준이고, 넘어서면 정직하게
HTTP 429 Too Many Requests가 돌아옵니다. 게다가 남의 인덱싱과 그 깊이에 의존하게 됩니다.
TONNode의 get_jetton_balance 도구는 두 문제를 모두 없앱니다. 넘겨줄 값은 두 개입니다.
- 소유자 주소 — 사용자의 평범한 TON 지갑(
EQ…/UQ…); - Jetton 식별자 — 예를 들면 USDT.
그다음은 서버가 전부 온체인으로 처리합니다.
- Jetton 마스터 컨트랙트의 get 메서드를 호출합니다. 이 메서드는 소유자 주소를 받아 그 소유자의 Jetton 지갑 주소를 돌려줍니다(앞서 말한 바로 그 결정론적 유도입니다);
- 그 Jetton 지갑에서 잔액을 읽어 여러분에게 돌려줍니다.
외부 인덱서도, 데이터베이스도, 동기화 지연도 없습니다. 컨트랙트의 get 메서드를 통한 네트워크 상태 직접 읽기뿐입니다.
TONNode는 TON용 hosted MCP 서버입니다. MCP(Model Context Protocol)는 AI 에이전트(Claude, Cursor, ChatGPT/Codex, 그 밖의 모든 MCP 클라이언트)가 외부 도구를 호출하는 방식을 정한 표준입니다. 즉 get_jetton_balance는 여러분 코드 안의 한 줄이 아니라, 사용자가 잔액을 물었을 때 에이전트가 스스로 집어 드는 도구입니다. 내부적으로 @tonnode/mcp 패키지(오픈소스, MIT)는 중간 HTTP 계층 없이 TON의 네이티브 ADNL 프로토콜로 동작합니다. 에이전트가 또 하나의 REST 게이트웨이를 거치지 않고 네트워크와 직접 대화하는 셈입니다.
예시: 에이전트 프롬프트와 돌아오는 값
연결은 로컬에서, 키도 카드도 없이 됩니다. MCP 클라이언트(Claude Desktop, Cursor, MCP 호환 클라이언트 아무거나) 설정에 다음을 추가합니다.
{
"mcpServers": {
"ton": {
"command": "npx",
"args": ["-y", "@tonnode/mcp"]
}
}
}
이후로는 평범한 자연어 프롬프트 하나면 충분합니다.
지갑
UQAbc…xyz에 USDT가 얼마나 있어? 사람이 읽는 값으로 돌려줘.
에이전트는 알아서 get_jetton_balance 도구를 고르고, 소유자 주소와 USDT Jetton을 넘깁니다. 서버는 잔액을 돌려주는데, 익숙한 "달러" 단위가 아니라 raw 단위(Jetton의 더 이상 쪼갤 수 없는 최소 단위)입니다. 응답에는 잔액 말고도 서버가 온체인에서 계산한 Jetton 지갑 주소가 함께 들어옵니다. 이후에 바로 그 컨트랙트의 전송을 추적하고 싶을 때 유용합니다. 예제에서 잔액 자체는 12500000 같은 값입니다.
그런데 12500000은 1250만 USDT가 아닙니다. 여기서 두 번째 함정이 시작됩니다. 실수하기 딱 좋은 지점입니다.
raw 단위와 decimals: USDT는 왜 9가 아니라 6인가
블록체인은 소수를 다루지 않습니다. 모든 금액은 정수 raw 단위로 저장되고, "사람이 읽는" 값은 10^decimals로 나눠서 얻습니다. 여기서 decimals는 개별 Jetton의 속성입니다. 달러와 센트의 관계와 같습니다. 낮은 층위에서는 전부 센트 단위이고, decimals는 소수점을 어디에 찍을지 알려줍니다.
TON에서 놓치기 쉬운 핵심은 이것입니다. Jetton마다 decimals 자릿수가 다릅니다.
- 네이티브 GRAM과 대부분의 TON Jetton은
decimals = 9입니다. - 그런데 TON의 USDT는
decimals = 6입니다.
그래서 똑같은 raw 값이 완전히 다른 금액을 뜻하게 됩니다. 예제를 제대로 다시 계산해봅시다.
raw = 12500000
decimals = 6 // 바로 USDT의 값
human = 12500000 / 10^6 = 12500000 / 1_000_000 = 12.5 USDT
1250만이 아니라 12.5 USDT입니다. 보통의 Jetton처럼 습관적으로 10^9으로 나눴다면 0.0125가 나왔을 겁니다. 1000배 오차입니다. 같은 숫자인데 자릿수로 세 칸 차이입니다. 순진한 연동에서 돈이 사라지는 방식이 정확히 이겁니다. 토큰을 가리지 않고 10^9 나눗수를 하드코딩해 박아 넣는 것 말입니다. 그러니 decimals는 절대 하드코딩하지 마십시오. Jetton 자체의 메타데이터에서 가져오십시오.
get_jetton_info: decimals와 Jetton 메타데이터는 어디서 가져오나
추측하지 않아도 되도록 짝이 되는 도구가 있습니다. get_jetton_info입니다. Jetton 마스터 컨트랙트를 읽어 메타데이터를 돌려줍니다.
- 이름(name);
- 심볼(symbol);
- decimals — 나눗수를 계산하는 바로 그 숫자;
- 발행량(total supply).
에이전트를 위한 안전한 시나리오는 호출 두 번입니다. 처음 보는 Jetton이라면 먼저 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
이 단계들을 에이전트가 알아서 이어 붙이도록 프롬프트를 이렇게 쓸 수 있습니다.
get_jetton_info로 USDT의 decimals를 가져오고, get_jetton_balance로 지갑
UQAbc…xyz의 잔액을 받아서 raw를 사람이 읽는 숫자로 바꿔줘.
USDT만이 아니라 임의의 Jetton을 다룬다면 이 순서는 선택이 아니라 필수입니다. 처음 보는 토큰의 decimals가 6인지 9인지는 미리 알 수 없으니까요. USDT의 decimals = 6은 상수지만, 그래도 get_jetton_info에서 끌어오는 습관이 비표준 Jetton을 처음 만나는 순간 여러분을 구해줍니다. TON에서 USDT 입금을 감지할 때도 같은 기법이 바탕이 됩니다. 거기서는 1 USDT를 미세한 먼지 금액과 혼동하지 않으려면 decimals가 결정적입니다.
parse_address: 요청 전에 주소를 오프라인으로 검증하기
"잔액 0"의 또 다른 흔한 원인은 잘못된 소유자 주소입니다. 사용자는 주소를 제각각의 형식으로 보냅니다. 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 패키지는 오픈소스(MIT)이며 npm과 GitHub에 있습니다. 위 예시의 공개 설정(npx -y @tonnode/mcp) 그대로 get_jetton_balance, get_jetton_info, parse_address를 포함한 읽기 도구 전체를 쓸 수 있습니다. 별도 키는 필요 없습니다.
클라이언트를 재시작하면 에이전트는 곧바로 ADNL로 Jetton 잔액을 읽습니다. 한도가 걸린 공개 게이트웨이 대신 에이전트에 왜 별도의 MCP 서버가 필요한지는 TON AI 에이전트용 MCP 이야기에서 다뤘습니다. 무료 공개 설정이 어디까지 감당되고 왜 거기서 끝나는지는 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를 맞는 일을 피할 수 있습니다.
요약
- TON의 USDT는 Jetton이며, 잔액은 메인 주소가 아니라 별도의 Jetton 지갑에 있습니다.
get_jetton_balance는 Jetton 지갑 주소를 온체인에서 직접 계산하고 잔액을 읽습니다. 인덱서도, 자체 DB도 필요 없습니다.- 잔액은 raw 단위로 옵니다. 표시할 때는
10^decimals로 나누십시오. - USDT는
decimals = 6(나눗수1_000_000)이고, 대부분의 Jetton은9입니다. 하드코딩하지 말고get_jetton_info에서 가져오십시오. - 전체 경로는 무료로 확인할 수 있습니다.
npx -y @tonnode/mcp, 공개 설정, 읽기 도구 전체입니다.
무료 Hobby 키는 60 요청/분, 카드 없이 로그인 직후 바로 발급됩니다. 키 받기. 실제 지갑에서 parse_address → get_jetton_info → get_jetton_balance를 한 번 돌려보고, 12500000 raw가 1250만도 0.0125도 아닌 정확히 12.5 USDT가 되는지 확인하기에 충분합니다.