TON Jetton의 decimals란 (USDT는 9가 아니라 6입니다)
TON Jetton의 decimals 완전 정리 — USDT는 왜 6이고 대부분의 Jetton은 9인지, get_jetton_info가 decimals를 어떻게 돌려주는지, 잔액에 왜 치명적인지
USDT 잔액을 보여주는 에이전트를 만들고 있다고 해봅시다. Jetton 지갑의 잔액을 조회해 5000000을 받고, 10^9로 나눴더니 화면에는 정직한 5 USDT 대신 0.005 USDT가 뜹니다. 트랜잭션은 잘 나가고, RPC도 응답하고, get 메서드도 숫자를 돌려주는데 — 금액만 정확히 1000배 어긋납니다. 범인은 거의 모두가 습관적으로 하드코딩하는 숫자 하나, 바로 decimals입니다.
AI 에이전트를 TON에 연결하거나 Jetton 금액을 직접 계산하고 있다면, decimals는 잔액과 스왑에 손대기 전에 가장 먼저 이해해야 할 개념입니다. USDT는 왜 TON에서 흔한 9가 아니라 decimals = 6인지, 이 숫자가 대체 어디서 오는지, 그리고 추측하지 않고 호출 한 번으로 가져오는 방법을 정리해보겠습니다.
TON Jetton의 decimals: raw 단위 대 사람이 읽는 숫자
블록체인은 소수를 저장하지 못합니다. 아예 못 합니다. TON에서도 다른 대부분의 체인과 마찬가지로 모든 금액은 컨트랙트 안에 정수로, 이른바 raw 단위(더 이상 쪼갤 수 없는 최소 단위)로 들어 있습니다. 컨트랙트의 딕셔너리에 5.5 같은 값은 없고, 있을 수도 없습니다. 오직 5500000 같은 정수만 존재합니다.
이 정수에서 사람이 보는 금액을 뽑아내려면 배율이 필요합니다. 그 배율이 바로 decimals, 즉 저장된 정수가 사람이 읽는 금액에 대해 몇 자리 밀려 있는지를 나타내는 10의 거듭제곱입니다:
human = raw / 10^decimals
raw = human * 10^decimals
비유는 간단합니다. 지갑 속 돈은 달러 단위로 세지만, 은행 장부에서는 모든 금액이 센트 단위 정수로 기록됩니다. 100센트 = 1달러, 즉 decimals = 2인 셈입니다. 사람에게 달러로 보여주려면 센트를 10^2 = 100으로 나누면 됩니다. Jetton도 똑같고, 10의 거듭제곱 크기만 다를 뿐입니다.
핵심은 이것입니다. 블록체인 자체는 숫자의 "소수점"이 어디인지 모릅니다. 블록체인은 raw 단위 정수만 다룹니다. 사용자에게 보여줄 때 소수점을 어디에 찍을지는 클라이언트가 Jetton 메타데이터의 decimals를 읽어서 결정합니다. decimals를 틀리면 소수점이 엉뚱한 자리로 옮겨 가고, 금액은 아무 의미 없는 숫자가 됩니다.
TON의 USDT decimals는 6, 대부분의 Jetton은 9
외워야 할 숫자는 두 개입니다:
- TON의 USDT(Tether) →
decimals = 6. 즉1 USDT = 1 000 000 raw 단위입니다. - 그 밖의 대부분의 TON Jetton →
decimals = 9. 이 값이 기본값이기도 합니다. 메타데이터에decimals필드가 아예 없으면, 표준상 클라이언트는 이를9로 간주해야 합니다.
9는 어디서 왔을까요. GRAM(2026년 6월에 이름이 바뀐 Toncoin이며, 네트워크는 여전히 TON입니다) 자체가 9자리를 씁니다. 1 GRAM = 10^9 나노그램이고, 여기서 "나노"가 바로 raw 단위입니다. TON Jetton 표준은 이 값을 기본값으로 물려받았고, 네트워크의 압도적 다수 토큰이 decimals = 9로 살아갑니다. 개발자들은 자연히 아무 생각 없이 / 1e9를 쓰는 데 익숙해집니다.
그런데 USDT는 Ethereum 세계에서 건너온 손님이고, 그쪽에서 Tether는 역사적으로 6자리를 씁니다. 발행사는 TON에서도 익숙한 정밀도를 그대로 유지했습니다. 그래서 USDT는 "그냥 9로 하드코딩한" 거의 모든 사람이 걸려 넘어지는 바로 그 예외이며 — 하필 결제에 가장 많이 쓰이는 Jetton입니다.
실수의 대가를 계산해봅시다. Jetton 지갑에 USDT 5 000 000 raw 단위가 들어 있다고 하면:
- 올바른 경우(
decimals = 6):5 000 000 / 10^6 = 5 USDT - 틀린 경우(
decimals = 9):5 000 000 / 10^9 = 0.005
정확히 10^(9−6) = 1000배 어긋납니다. 반대 방향도 마찬가지입니다. 사용자가 "5 USDT 보내줘"라고 입력했는데 10^9를 곱하면 1000배 큰 금액을 옮기려 시도하게 됩니다. 결제 봇에게 이건 "결제 완료"와 "거절"의 차이입니다.
decimals는 어디서 오는가: Jetton 메타데이터와 TEP-64 표준
중요한 지점입니다. decimals는 지갑 코드의 필드도, 프로토콜 상수도 아닙니다. 이것은 TEP-64(Token Data Standard)가 규정하는 개별 Jetton의 메타데이터 일부입니다. Jetton마다 자기 decimals, 이름, 심볼, 이미지를 스스로 선언합니다.
TEP-64는 메타데이터를 세 가지 형식으로 저장할 수 있게 합니다:
- on-chain — 모든 필드가 Jetton 컨트랙트 안의 딕셔너리(dictionary)에 그대로 들어 있습니다. 아무것도 내려받을 필요가 없습니다.
- off-chain — 컨트랙트에는 링크(
uri)만 있고, 필드가 담긴 JSON 전체(name,symbol,decimals,image)는 그 URI가 가리키는 웹 서버나 IPFS에 있습니다. - semi-chain(하이브리드) — 일부 필드는 온체인 딕셔너리에, 일부는
uri쪽에 있습니다. 클라이언트는 off-chain 콘텐츠를 내려받아 딕셔너리 값과 병합합니다.
TEP-64의 병합 규칙은 이렇습니다. 딕셔너리에 uri 키가 있으면 클라이언트는 그 링크로 off-chain 콘텐츠를 내려받아 온체인 딕셔너리 값과 합쳐야 합니다. 형식이 다르면 읽기 비용도 다릅니다. on-chain은 get 메서드 한 번이면 되지만, off-chain은 HTTP 요청까지 필요합니다. 이 세 경우를 전부 직접 처리하는 건 그 자체로 꽤나 성가신 일입니다. 그래서 도구 하나가 이걸 대신 맡아주면 편합니다.
USDT 메타데이터: get_jetton_info는 어떻게 decimals = 6을 돌려주는가
TON의 USDT는 메타데이터를 off-chain 형식으로 보관합니다. 마스터 Jetton 컨트랙트에는 off-chain JSON으로 가는 링크가 있고, 그 JSON 안에 name, symbol, 그리고 무엇보다 decimals = 6이 기록되어 있습니다. Jetton 정보 카드를 제대로 구성하려면 클라이언트가 컨트랙트를 읽고, 링크를 꺼내고, 그 링크로 JSON을 내려받아 필드를 파싱해야 합니다.
TEP-64 형식을 머릿속에 넣어두고 딕셔너리 셀을 직접 파싱할 필요는 없습니다. 바로 그 일을 도구 하나가 대신 떠맡기 때문입니다. get_jetton_info가 알아서 컨트랙트를 조회하고, 메타데이터를 해석하고, 완성된 decimals = 6을 돌려줍니다. 이후의 모든 계산은 그 값 위에서 시작됩니다.
호출 한 번으로 decimals 가져오기: get_jetton_info
딕셔너리를 직접 읽고, 형식(on/off/semi-chain)을 식별하고, URI를 따라가고, JSON을 병합하는 대신 — get_jetton_info가 있습니다. TONNode MCP 서버의 도구로, Jetton 마스터 컨트랙트 주소를 받아 파싱까지 끝난 메타데이터, 즉 이름, 심볼, decimals, 발행량을 돌려줍니다.
TONNode는 TON용 hosted MCP 서버입니다. MCP(Model Context Protocol)는 AI 에이전트(Claude, Cursor, ChatGPT/Codex, 그 밖의 모든 MCP 클라이언트)가 도구를 호출하는 방식을 정한 표준입니다. 로컬에서 무료로 연결할 수 있고, 읽기 도구는 전부 포함됩니다:
{
"mcpServers": {
"ton": { "command": "npx", "args": ["-y", "@tonnode/mcp"] }
}
}
@tonnode/mcp 패키지는 오픈소스(MIT)이며, HTTP 중간 계층 없이 TON의 네이티브 ADNL 프로토콜로 동작합니다. 이후로는 에이전트에게 평범한 사람 말로 부탁하면 됩니다:
get_jetton_info도구로 TON의 USDT Jettondecimals를 알아내고(마스터 컨트랙트 주소는 이러이러함), 잔액 5 000 000 raw가 사람이 읽는 USDT로 얼마인지 계산해줘.
get_jetton_info는 decimals = 6을 돌려주고, 이후의 모든 계산은 추측이 아니라 이 숫자 위에 놓입니다. 다른 Jetton에 같은 호출을 하면 그 Jetton 고유의 값이 돌아옵니다(대개 9이지만, 확인은 언제나 필요합니다). 핵심 규칙은 이것입니다:
9를 하드코딩하지 마십시오. Jetton마다get_jetton_info로decimals를 읽으십시오.
9는 대부분의 토큰에서는 잘 맞다가 USDT에서 조용히 깨집니다. 그리고 하필 그 Jetton이 실제 돈이 가장 많이 도는 곳입니다. AI 에이전트에게 왜 제한 걸린 공개 RPC가 아니라 별도의 TON MCP 접근이 필요한지는 TON 에이전트용 MCP 서버 이야기에서 다뤘습니다.
잘못된 decimals가 잔액과 스왑을 망가뜨리는 이유
decimals는 화면을 예쁘게 꾸미는 장식이 아닙니다. 금액이 "raw ⇄ 사람" 경계를 넘는 모든 지점에 관여하고, 한 번 어긋나면 그 뒤의 계산 전체로 오차가 번집니다.
잔액
get_jetton_balance는 Jetton 잔액을 raw 단위로 돌려줍니다(필요한 Jetton 지갑은 온체인에서 계산되므로 직접 찾을 필요가 없습니다). 이 정수 자체는 decimals 없이는 아무 의미가 없습니다. 10^decimals로 나누기 전까지는 사용자에게 제대로 보여줄 수 없습니다:
raw = 5 000 000
decimals = 6 → 5 USDT ✅
decimals = 9 → 0.005 ❌ (1000배 작음)
에이전트에서의 올바른 순서는 이렇습니다. 먼저 get_jetton_info → decimals를 확보하고, 그다음 get_jetton_balance → raw를 10^decimals로 나눕니다. 이런 읽기 요청에서 공개 라이트서버가 어떻게 한도에 부딪히는지는 TON 공개 라이트서버 한도 분석에 정리했습니다.
스왑
get_swap_quote는 GRAM⇄Jetton DEX의 확정 견적(Omniston 프로토콜, STON.fi + DeDust 유동성)을 돌려주는데, 입력 금액도 수령 예상액도 마찬가지로 Jetton의 raw 단위입니다. 여기서 잘못된 decimals는 두 번 타격을 줍니다. 사용자가 10 USDT를 스왑하려 한다고 해봅시다. decimals = 6이면 견적에 10 000 000 raw를 넘겨야 합니다. 실수로 9를 적용하면 10 000 000 000 raw, 즉 지갑에 있지도 않은 10 000 USDT짜리 스왑을 요청하게 됩니다. 응답을 해석할 때 같은 실수를 반대로 저지르면 예상 수령액이 1000배 작게 표시되고, 사용자는 스왑 레이트가 터무니없다고 판단할 겁니다.
같은 원리가 크로스체인 견적과 모든 트랜잭션에 적용됩니다. 블록체인에서는 전부 raw 단위 정수이고, 사람이 읽는 금액으로 가는 유일한 다리는 올바른 decimals입니다.
실전 확인: get_jetton_balance와 get_swap_quote
에이전트가 하드코딩된 숫자 하나 없이 스스로 돌리는 짧은 시나리오를 짜봅시다.
단계별 시나리오
1. decimals 확인. get_jetton_info(USDT master) → decimals = 6.
2. 잔액 읽기. get_jetton_balance(소유자, USDT master) → 5 000 000 (raw). 계산하면 5 000 000 / 10^6 = 5 USDT입니다. 9를 썼다면 0.005가 나왔겠죠. 이건 좋은 조기 경보이기도 합니다. USDT에서 예상보다 터무니없이 작은 숫자를 봤다면, 가장 먼저 6 대신 9를 적용한 건 아닌지 확인하십시오.
3. 견적 요청. 5 USDT를 GRAM으로 스왑하고 싶습니다. raw로는 5 × 10^6 = 5 000 000이고, 바로 이 금액을 get_swap_quote에 넘깁니다. 결과도 raw로 돌아오는데, GRAM은 9자리이므로 사용자에게 보여주려면 응답을 10^9로 나눕니다.
4. 메타데이터를 못 믿겠다면? 온체인에서 직접 재확인할 수 있습니다. run_get_method는 컨트랙트의 모든 읽기 전용 get 메서드를 호출하므로, 같은 방식으로 마스터 컨트랙트의 get_jetton_data를 불러 숫자가 맞아떨어지는지 확인할 수 있습니다. 이 방법이 더 유연하긴 하지만, TEP-64 형식을 알고 있어야 하고 딕셔너리도 직접 파싱해야 합니다. 속도와 신뢰성이 중요할 때는 get_jetton_info가 답 하나로 문제를 끝내줍니다. TON RPC 제공자들이 get 메서드와 아카이브 접근에서 어떻게 다른지는 TON RPC 제공자 정직한 비교에 있습니다.
에이전트 프롬프트
전체 시나리오를 담은 프롬프트는 지극히 평범하게 생겼습니다:
TON의 USDT를 살펴봐.
get_jetton_info로decimals를 읽어.get_jetton_balance로 내 잔액을 raw로 받아서 사람이 읽는 USDT로 바꿔줘. 그다음get_swap_quote로 5 USDT를 GRAM으로 스왑하는 견적을 계산해줘 — 읽어온decimals로 5 USDT를 raw로 바꾸는 걸 잊지 말고.
에이전트 시나리오를 위한 디테일 하나 더. TONNode의 스왑 도구는 엄격하게 논커스터디얼입니다. 서버는 절대 서명하지도, 키를 보관하지도 않습니다. build_swap_tx는 서명되지 않은 TonConnect 메시지를 돌려주고, 서명은 사용자의 지갑이 합니다. decimals가 아무리 정확해도 서버가 자금을 통제하게 되지는 않습니다. 서버는 계산하고 트랜잭션을 조립할 뿐입니다. TON 스왑에서 밀리초와 지연이 어디서 새는지는 TON 고속 트레이딩 분석에 정리했습니다.
요약
decimals는 블록체인의 raw 단위와 사람이 읽는 금액 사이의 10의 거듭제곱입니다:human = raw / 10^decimals.- TON의 기본값은 9자리이고, USDT는 6입니다(
1 USDT = 1 000 000raw). 헷갈리면 1000배 오차입니다. decimals는 코드가 아니라 Jetton 메타데이터(TEP-64)에 있습니다. USDT의 메타데이터는 off-chain에 있지만,get_jetton_info는 그래도decimals = 6을 답 하나로 돌려줍니다.9를 하드코딩하지 마십시오.get_jetton_info로decimals를 읽고, 모든 잔액(get_jetton_balance)과 견적(get_swap_quote) 계산을 그 값 위에 올리십시오.
무료 Hobby 키(60 요청/분, 카드 불필요)를 받아서 지금 바로 get_jetton_info로 아무 Jetton의 decimals나 읽어보십시오: https://tonnode.io/dashboard?plan=hobby