Decimals của jetton trên TON — vì sao USDT = 6 chứ không phải 9
Decimals của jetton trên TON: vì sao USDT dùng 6 còn đa số jetton dùng 9, cách get_jetton_info trả về decimals và vì sao nó quyết định số dư lẫn swap
Bạn đang viết một agent hiển thị số dư USDT. Bạn truy vấn số dư của ví jetton, nhận về 5000000, chia cho 10^9 — và trên màn hình hiện ra 0.005 USDT thay vì 5 USDT đúng nghĩa. Giao dịch vẫn chạy, RPC vẫn trả lời, get-method vẫn trả về một con số — vậy mà số tiền lệch đúng 1000 lần. Thủ phạm là một chữ số mà gần như ai cũng hardcode theo thói quen: decimals.
Nếu bạn đang nối một AI agent vào TON hoặc tự tay tính số tiền jetton, decimals là thứ đầu tiên phải hiểu trước khi động tới bất kỳ số dư hay lệnh swap nào. Hãy cùng mổ xẻ vì sao USDT có decimals = 6 chứ không phải con số 9 quen thuộc của TON, con số đó từ đâu ra và làm sao lấy được nó bằng đúng một lời gọi thay vì đoán mò.
Decimals của jetton trên TON: đơn vị raw so với con số dành cho người đọc
Blockchain không lưu được số thập phân. Hoàn toàn không. Trên TON, cũng như gần như mọi nơi khác, mọi khoản tiền đều nằm trong contract dưới dạng số nguyên — gọi là đơn vị raw (đơn vị nhỏ nhất, không chia được nữa). Trong dictionary của contract không có và không thể có 5.5 — chỉ có số nguyên kiểu 5500000.
Để từ số nguyên đó ra được khoản tiền mà con người nhìn thấy, ta cần một hệ số tỷ lệ. Hệ số ấy chính là decimals — số mũ của 10 thể hiện độ lệch giữa con số nguyên đang lưu và khoản tiền dành cho người đọc:
human = raw / 10^decimals
raw = human * 10^decimals
Hãy hình dung thế này. Tiền trong ví bạn tính bằng đô la, nhưng trong sổ sách của ngân hàng thì mọi thứ đều tính bằng cent, toàn số nguyên: 100 cent = 1 đô la, tức decimals = 2. Muốn hiển thị cho người dùng ra đô la thì chia số cent cho 10^2 = 100. Với jetton cũng y như vậy, chỉ khác số mũ của 10.
Điểm mấu chốt: bản thân blockchain không biết "dấu phẩy" nằm ở đâu. Nó chỉ làm việc với các đơn vị raw nguyên. Đặt dấu phẩy ở đâu khi hiển thị cho người dùng là việc của client, dựa trên decimals đọc từ metadata của jetton. Sai decimals là dấu phẩy chạy lệch chỗ, và số tiền hiện ra thành một con số vô nghĩa.
USDT trên TON có decimals = 6, còn đa số jetton là 9
Hai con số cần thuộc lòng:
- USDT (Tether) trên TON →
decimals = 6. Nghĩa là1 USDT = 1 000 000 đơn vị raw. - Đa số jetton còn lại trên TON →
decimals = 9. Đây cũng là giá trị mặc định: nếu trườngdecimalshoàn toàn không có trong metadata, theo chuẩn thì client bắt buộc phải coi nó bằng9.
Vậy con số 9 từ đâu ra. Bản thân GRAM (Toncoin cũ, đổi tên vào tháng 6/2026 — mạng lưới vẫn gọi là TON) có 9 chữ số: 1 GRAM = 10^9 nanogram, và "nano" ở đây chính là đơn vị raw. Chuẩn jetton của TON kế thừa luôn giá trị này làm mặc định, và tuyệt đại đa số token trong mạng sống với decimals = 9. Lập trình viên quen tay gõ / 1e9 mà chẳng buồn nhìn.
Còn USDT là khách từ thế giới Ethereum, nơi Tether xưa nay vẫn dùng 6 chữ số. Bên phát hành giữ nguyên độ chính xác quen thuộc đó khi đưa token lên TON. Thành ra USDT chính là ngoại lệ khiến gần như tất cả những ai "cứ hardcode 9 cho nhanh" đều vấp — và trớ trêu thay, đó lại đúng là jetton được dùng nhiều nhất cho thanh toán.
Thử tính cái giá của sai lầm. Giả sử ví jetton đang giữ 5 000 000 đơn vị raw USDT:
- đúng (
decimals = 6):5 000 000 / 10^6 = 5 USDT; - sai (
decimals = 9):5 000 000 / 10^9 = 0.005.
Lệch đúng 10^(9−6) = 1000 lần. Và ngược lại: nếu người dùng nhập "gửi 5 USDT" mà bạn nhân với 10^9, bạn sẽ định chuyển đi số tiền lớn gấp 1000 lần. Với một bot thanh toán, đó là khác biệt giữa "đã thanh toán" và "bị từ chối".
Decimals đến từ đâu: metadata của jetton và chuẩn TEP-64
Cần hiểu rõ: decimals không phải một trường trong code của ví, cũng không phải hằng số của giao thức. Nó là một phần metadata của chính jetton đó, được mô tả trong chuẩn TEP-64 (Token Data Standard). Mỗi jetton tự khai báo decimals, tên, ký hiệu và ảnh của mình.
TEP-64 cho phép lưu metadata theo ba định dạng:
- on-chain — toàn bộ các trường nằm trong dictionary ngay trong contract của jetton; không phải tải thêm gì cả;
- off-chain — trong contract chỉ có một đường dẫn (
uri), còn toàn bộ JSON với các trường (name,symbol,decimals,image) nằm ở URI đó trên web server hoặc IPFS; - semi-chain (lai) — một phần các trường nằm trong dictionary on-chain, phần còn lại ở
uri; client tải nội dung off-chain rồi merge nó với các giá trị trong dictionary.
Theo TEP-64, quy tắc gộp như sau: nếu trong dictionary có khóa uri, client bắt buộc phải tải nội dung off-chain theo đường dẫn đó và gộp nó với các giá trị từ dictionary on-chain. Định dạng khác nhau thì chi phí đọc cũng khác: on-chain đọc bằng một get-method, off-chain còn cần thêm một HTTP request. Tự tay xử lý cả ba trường hợp là một cực hình riêng — và đây đúng là chỗ mà có một công cụ làm hộ thì dễ thở hơn hẳn.
Metadata của USDT: get_jetton_info trả về decimals = 6 như thế nào
USDT trên TON lưu metadata ở định dạng off-chain: trong contract của jetton master chỉ có đường dẫn tới JSON off-chain, còn chính trong JSON đó mới ghi name, symbol và quan trọng nhất là decimals = 6. Muốn dựng đúng thẻ thông tin của jetton, client phải đọc contract, lấy đường dẫn, tải JSON về rồi bóc tách các trường.
Không cần phải nhớ định dạng TEP-64 trong đầu hay tự parse các cell của dictionary: đó đúng là phần việc mà một công cụ gánh thay bạn. get_jetton_info sẽ tự vào contract, bóc metadata và trả về sẵn decimals = 6 — và mọi phép tính phía sau đều dựng trên chính con số ấy.
Lấy decimals bằng một lời gọi: get_jetton_info
Thay vì tự tay đọc dictionary, nhận diện định dạng (on/off/semi-chain), đi theo URI rồi merge JSON — đã có get_jetton_info. Đây là một tool của MCP server TONNode: nó nhận địa chỉ master contract của jetton và trả về metadata đã được tổng hợp sẵn — tên, ký hiệu, decimals và tổng cung.
TONNode là một MCP server hosted dành cho TON. MCP (Model Context Protocol) là chuẩn để các AI agent (Claude, Cursor, ChatGPT/Codex, mọi MCP client) gọi tool. Kết nối miễn phí ngay ở máy local, đủ bộ tool đọc dữ liệu:
{
"mcpServers": {
"ton": { "command": "npx", "args": ["-y", "@tonnode/mcp"] }
}
}
Gói @tonnode/mcp là open source (MIT), chạy trên giao thức ADNL gốc của TON, không qua lớp trung gian HTTP. Sau đó chỉ cần yêu cầu agent bằng ngôn ngữ đời thường:
Dùng tool
get_jetton_infođể lấydecimalscủa jetton USDT trên TON (địa chỉ master contract là ...) và tính xem số dư 5 000 000 raw quy ra bao nhiêu USDT cho người đọc.
get_jetton_info sẽ trả về decimals = 6, và toàn bộ phép tính về sau dựng trên con số đó chứ không phải trên phỏng đoán. Vẫn lời gọi đó, áp cho một jetton bất kỳ, sẽ trả về giá trị riêng của jetton ấy (thường là 9, nhưng lúc nào cũng phải kiểm tra). Quy tắc quan trọng nhất:
Đừng hardcode
9. Hãy đọcdecimalstừget_jetton_infocho từng jetton.
Số 9 sẽ chạy đúng với đa số token rồi âm thầm gãy ở USDT — mà đó lại chính là jetton nơi tiền thật chảy qua nhiều nhất. Còn vì sao một AI agent lại cần quyền truy cập TON riêng qua MCP thay vì RPC công khai đầy giới hạn thì đã được phân tích trong bài về MCP server cho agent trên TON.
Vì sao sai decimals làm hỏng số dư và swap
decimals không phải thứ trang trí cho phần hiển thị. Nó có mặt ở mọi chỗ mà một khoản tiền đi qua ranh giới "raw ⇄ người", và sai sót sẽ lan ra toàn bộ chuỗi.
Số dư
get_jetton_balance trả về số dư jetton theo đơn vị raw (ví jetton tương ứng được tính on-chain, bạn không phải tự đi tìm). Bản thân con số nguyên đó chẳng có ý nghĩa gì nếu thiếu decimals — không thể hiển thị nó đúng cho người dùng chừng nào bạn chưa chia cho 10^decimals:
raw = 5 000 000
decimals = 6 → 5 USDT ✅
decimals = 9 → 0.005 ❌ (nhỏ hơn 1000 lần)
Thứ tự đúng trong agent: trước hết get_jetton_info → lấy decimals, sau đó get_jetton_balance → chia raw cho 10^decimals. Còn chuyện các liteserver công khai đụng trần giới hạn ra sao với đúng những request đọc kiểu này — có trong bài phân tích giới hạn của liteserver công khai trên TON.
Swap
get_swap_quote trả về báo giá chắc chắn cho DEX GRAM⇄jetton (qua giao thức Omniston, thanh khoản STON.fi + DeDust) — cả số tiền đầu vào lẫn ước tính nhận về đều theo đơn vị raw của jetton. Ở đây sai decimals gây thiệt hại hai lần. Giả sử người dùng muốn swap 10 USDT: với decimals = 6 thì phải truyền vào báo giá 10 000 000 raw. Nếu lỡ áp 9 — bạn hỏi giá cho 10 000 000 000 raw, tức là swap 10 000 USDT mà ví không hề có. Sai theo chiều ngược lại, lúc bóc tách phản hồi, sẽ làm số tiền dự kiến thấp đi 1000 lần, và người dùng sẽ nghĩ tỷ giá đúng là cắt cổ.
Nguyên tắc y hệt cũng áp dụng cho báo giá cross-chain và mọi giao dịch: trên blockchain mọi thứ đều là số nguyên theo đơn vị raw, và cây cầu duy nhất dẫn tới các con số dành cho người đọc chính là decimals đúng.
Kiểm chứng thực tế: get_jetton_balance và get_swap_quote
Hãy dựng một kịch bản ngắn mà agent tự chạy được, không có lấy một con số hardcode.
Kịch bản từng bước
1. Lấy decimals. get_jetton_info(USDT master) → decimals = 6.
2. Đọc số dư. get_jetton_balance(chủ sở hữu, USDT master) → 5 000 000 (raw). Tính ra: 5 000 000 / 10^6 = 5 USDT. Nếu lấy 9 thì đã ra 0.005. Đây chính là tín hiệu cảnh báo cho bạn: thấy một con số nhỏ bất thường ở USDT thì việc đầu tiên là kiểm tra xem có nhỡ áp 9 thay vì 6 hay không.
3. Hỏi báo giá. Muốn swap 5 USDT sang GRAM. Quy ra raw là 5 × 10^6 = 5 000 000 — truyền đúng con số này vào get_swap_quote. Kết quả trả về cũng theo raw, mà GRAM thì có 9 chữ số, nghĩa là phải chia phản hồi cho 10^9 để hiển thị cho người dùng.
4. Không tin metadata? Bạn có thể kiểm tra lại trực tiếp on-chain. run_get_method gọi được bất kỳ get-method read-only nào của contract — bằng cách đó bạn cũng có thể gọi get_jetton_data trên master contract và xác nhận các con số khớp nhau. Cách này linh hoạt hơn nhưng đòi hỏi hiểu định dạng TEP-64 và tự parse dictionary; khi cần nhanh và chắc, get_jetton_info giải quyết gọn câu hỏi chỉ bằng một phản hồi. Các nhà cung cấp RPC cho TON khác nhau thế nào về quyền truy cập get-method và dữ liệu archive — có trong bài so sánh thẳng thắn các RPC provider của TON.
Prompt cho agent
Prompt cho cả kịch bản trông rất bình thường:
Lấy USDT trên TON. Dùng
get_jetton_infođể đọcdecimals. Dùngget_jetton_balancelấy số dư của tôi theo raw rồi quy ra USDT cho người đọc. Sau đó dùngget_swap_quotetính báo giá swap 5 USDT sang GRAM — nhớ quy đổi 5 USDT ra raw theodecimalsvừa đọc được.
Còn một chi tiết nữa cho các kịch bản agent: các tool swap của TONNode nghiêm ngặt phi lưu ký (non-custodial). Server không bao giờ ký và không bao giờ giữ khóa — build_swap_tx trả về một message TonConnect chưa ký, và ví của người dùng mới là bên ký. Kể cả decimals đúng cũng không trao cho server quyền kiểm soát tiền: nó chỉ tính toán và dựng giao dịch. Còn trong các lệnh swap trên TON, mili-giây và độ trễ bị mất ở đâu — có trong bài mổ xẻ trading tốc độ cao trên TON.
Tóm lại
decimalslà số mũ của 10 nằm giữa đơn vị raw trên blockchain và khoản tiền dành cho người đọc:human = raw / 10^decimals.- Mặc định trên TON là 9 chữ số; USDT là 6 (
1 USDT = 1 000 000raw). Nhầm lẫn = lệch 1000 lần. decimalsnằm trong metadata của jetton (TEP-64), không nằm trong code. USDT lưu metadata off-chain, nhưngget_jetton_infovẫn trả vềdecimals = 6chỉ bằng một phản hồi.- Đừng hardcode
9. Hãy đọcdecimalsquaget_jetton_infovà dựng trên đó mọi số dư (get_jetton_balance) lẫn báo giá (get_swap_quote).
Lấy key Hobby miễn phí (60 request/phút, không cần thẻ) và đọc decimals của bất kỳ jetton nào qua get_jetton_info ngay bây giờ: https://tonnode.io/dashboard?plan=hobby
Cho agent của bạn quyền truy cập TON
16 công cụ MCP: đọc, swap phi lưu ký, cross-chain và ví. Gói miễn phí — 60 req/phút, không cần thẻ.