tonapi.io và rate limit: cách khắc phục lỗi 429
tonapi.io trả về HTTP 429 khi vượt rate limit. Cách khắc phục lỗi 429, vì sao “228” chỉ là meme của cộng đồng, và cách lấy key riêng qua MCP TONNode.
tonapi 429: tám rưỡi tối, production bốc cháy, log đỏ rực một màu
Con bot TON của bạn vừa lọt vào một bài tổng hợp, người dùng ùn ùn kéo vào, và ứng dụng bỗng dưng trả về toàn số dư rỗng. Bạn mở log ra — hàng trăm dòng như thế này:
HTTP 429 Too Many Requests
Nghe quen chứ? Bạn gọi tonapi.io để lấy số dư và lịch sử giao dịch, mọi thứ chạy ngon lành với mười người dùng, nhưng lên một nghìn thì truy cập ẩn danh đụng trần. Chuyện kinh điển: khi lưu lượng còn nhỏ, API công khai trông như thứ miễn phí và vô hạn. Tải vừa tăng lên — nó lập tức thành nút thắt cổ chai, và bạn ăn tonapi 429 từng chùm. Đây không phải bug trong code của bạn, cũng chẳng phải “node sập” — đây là rate limit của một API công khai, và cách chữa thì hoàn toàn có thể lường trước.
Ta sẽ mổ xẻ vì sao 429 xuất hiện, vì sao cái “228” khét tiếng chẳng liên quan gì ở đây, những cách sửa nào thực sự có tác dụng, và làm sao rời khỏi pool ẩn danh dùng chung để có hạn mức riêng khi đọc dữ liệu TON — thông qua MCP server TONNode.
Vì sao tonapi.io trả về 429 Too Many Requests
tonapi.io là một HTTP API công khai để truy cập dữ liệu TON. Như mọi dịch vụ công khai khác, nó có tonapi rate limit — giới hạn tần suất request, để một client không ngốn hết toàn bộ năng lực xử lý.
Khi gọi mà không có key, bạn đang dùng chung một pool ẩn danh với tất cả những người ẩn danh khác trên hành tinh. Trên thực tế, hạn mức đó vào khoảng ~1 request mỗi giây. Với một script đơn lẻ thì còn chịu được. Nhưng ngay khi bạn có một worker chạy song song, cứ mỗi ví lại gọi số dư, rồi số dư jetton, rồi lịch sử — bạn vượt hạn mức mỗi giây ngay lập tức. Đau nhất là lúc khởi động nguội, khi cần index cả một loạt địa chỉ cùng lúc.
Server trả lời như sau:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Cần hiểu rõ: 429 là một mã HTTP tiêu chuẩn nằm trong đặc tả, không phải mã riêng của tonapi. Bất kỳ ai cũng trả về nó khi bạn vượt tần suất cho phép — GitHub, Stripe, Cloudflare, mọi dịch vụ có rate limit. toncenter và đa số nhà cung cấp RPC cũng dùng đúng cơ chế ấy. Nghĩa là nó cũng chữa được bằng những kỹ thuật tiêu chuẩn, sẽ nói ngay dưới đây.
“Lỗi 228” không phải mã API mà là meme cộng đồng (mã thật là 429)
Nếu từng google vấn đề này trên các diễn đàn tiếng Nga, chắc chắn bạn đã gặp “lỗi 228”. Hãy nói cho rõ, vì chuyện này thật sự làm người mới rối trí.
“228” không phải mã lỗi chính thức của tonapi, cũng không phải của toncenter. Đó là một con số meme đã sống lâu trong cộng đồng TON, cứ thi thoảng lại nổi lên trong chat và trong mấy câu đùa. Sẽ không có HTTP status 228 nào bay về khi bạn vượt hạn mức — đơn giản là HTTP không hề có mã trạng thái đó.
Mã thật mà bạn sẽ thấy trong log và trong header phản hồi chính là 429. Khi ai đó viết trong chat rằng “dính 228 từ tonapi”, thực tế họ dính 429 (hoặc chỉ là timeout thường), còn “228” chỉ được dùng như một cách nói cho vui. Hãy tìm 429 trong log của bạn chứ đừng tìm “228” — làm vậy bạn mới ra được nguyên nhân thật sự. Kiểm tra chỉ tốn một dòng:
curl -s -o /dev/null -w "%{http_code}\n" https://tonapi.io/v2/blockchain/masterchain-head
# dưới tải và không có key, bạn sẽ thấy: 429
Sửa nhanh: retry, backoff, cache và key của riêng bạn
Vì 429 là chuyện tiêu chuẩn nên nó cũng có một bộ cách chữa tiêu chuẩn. Ta đi từ thứ đơn giản tới thứ quan trọng nhất.
1. Backoff theo cấp số nhân, tôn trọng Retry-After
Đừng dội request vào endpoint trong một vòng lặp chặt ngay sau lần bị từ chối đầu tiên — bạn chỉ làm mọi thứ tệ hơn. Khi trả 429, server thường gửi kèm header Retry-After — số giây cần chờ. Hãy tôn trọng nó, còn nếu không có thì tăng thời gian chờ theo cấp số nhân.
async function fetchWithBackoff(url, opts = {}, maxRetries = 5) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, opts);
if (res.status !== 429) return res;
// Tôn trọng Retry-After, nếu không có thì tăng thời gian chờ theo cấp số nhân
const retryAfter = Number(res.headers.get('retry-after'));
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: Math.min(1000 * 2 ** attempt, 30_000); // 1s, 2s, 4s… trần 30s
await new Promise(r => setTimeout(r, waitMs));
}
throw new Error('429 vẫn không hết: đã vượt số lần retry');
}
2. Giới hạn số request chạy đồng thời
429 thường bay tới không phải vì tổng lưu lượng, mà vì bạn bắn một loạt 50 request cùng lúc qua Promise.all. Đặt một semaphore giới hạn mức song song (1–4) — các đợt bùng phát sẽ được san phẳng và không còn chọc thủng hạn mức mỗi giây.
3. Cache những dữ liệu không đổi
Metadata của jetton (tên, ký hiệu, decimals), kết quả chuyển đổi địa chỉ, các giao dịch cũ — đều là dữ liệu không thay đổi. Hãy đưa chúng vào cache local với TTL hợp lý và đừng hỏi lại API. Chỉ riêng việc cache decimals của jetton cũng đã cắt được một phần đáng kể số lần gọi.
4. Cách sửa chính — key riêng thay cho truy cập ẩn danh
Ba điểm đầu chỉ là thuốc giảm đau. Cách chữa thật sự là rời pool ẩn danh để dùng key của chính mình. Một key riêng nâng hạn mức của bạn lên cao hơn nhiều bậc so với ~1 req/s của chế độ ẩn danh; bạn thôi phải cạnh tranh với cả internet, và 429 đơn giản là biến mất khỏi vận hành thường ngày. Trường hợp tương tự ở một nhà cung cấp khác đã được phân tích trong bài sửa lỗi 429 của toncenter — logic hoàn toàn giống nhau.
Phải làm gì khi hạn mức của tonapi vẫn không đủ
Giả sử bạn đã làm đúng hết: backoff, cache, hàng đợi. Nhưng ứng dụng cứ lớn dần, và ngay cả khi có key bạn vẫn đụng trần — hoặc là trần của gói cước, hoặc là trần của chính mô hình “một lớp HTTP đặt trên node”. Tới đây người ta thường cân nhắc hai hướng.
Hướng “tự dùng liteserver”. Sẽ có cám dỗ kiểu “cứ lấy đại một liteserver công khai từ global config của TON rồi gọi thẳng qua ADNL”. Nói thật lòng: đó không phải viên đạn bạc. Các liteserver công khai trong global config cũng là tài nguyên dùng chung và cũng bị giới hạn — dưới tải chúng thường xuyên trả về not ready hoặc rụng vì ADNL timeout, và cũng không lưu lịch sử giao dịch sâu. Riêng not ready là một nỗi đau kinh điển, được mổ xẻ trong bài xử lý “liteserver not ready”. Tức là chỉ “chuyển sang public config” không giải quyết được vấn đề hạn mức, đôi khi còn làm nó tệ hơn.
Hướng “đổi nhà cung cấp / đổi giao diện”. Vấn đề không nằm ở riêng tonapi.io, mà ở chỗ bạn đang ngồi trên một tài nguyên dùng chung. Bản tổng hợp các HTTP API thay thế nằm trong bài về các giải pháp thay thế tonapi năm 2026. Còn nếu bạn đang dựng một AI agent, sẽ hợp lý hơn nếu không tự tay bọc REST mà nối TON vào như một bộ tool qua MCP — khi đó việc đọc blockchain trở thành một lời gọi tool, chứ không phải một HTTP request thô mà bạn phải tự retry.
TONNode MCP: hạn mức riêng thay cho pool ẩn danh dùng chung
TONNode (website tonnode.io) 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 và mọi MCP client) gọi tool bên ngoài. Thay vì dạy agent cách gọi https://tonapi.io/v2/... rồi tự xử lý 429, bạn đưa cho nó một bộ tool có tên gọi rõ ràng để đọc dữ liệu TON.
Điểm mấu chốt: gói @tonnode/mcp chạy trên giao thức ADNL gốc của TON, không qua lớp trung gian HTTP nào. Nó là open source (MIT), có mặt trên npm và GitHub (tonnode/mcp). Đây không phải thêm một REST wrapper bọc quanh tonapi, mà là nói chuyện trực tiếp với mạng lưới.
Những truy vấn điển hình mà trước đây bạn phải gọi tonapi đều được bộ tool đọc dữ liệu phủ một-đối-một:
- get_balance — số dư GRAM tại một địa chỉ.
- get_jetton_balance — số dư USDT hoặc bất kỳ jetton nào; ví jetton được tính on-chain, bạn không phải tự suy ra địa chỉ của nó. Cách nó thay thế cả một chuỗi nhiều request được kể trong bài số dư USDT trên TON chỉ bằng một lời gọi.
- get_account_state — trạng thái tài khoản, các cờ, giao dịch cuối cùng.
- get_transactions — lịch sử giao dịch.
- run_get_method — bất kỳ get-method read-only nào của contract.
- get_masterchain_info — đầu masterchain (block hiện tại).
- get_jetton_info — metadata của jetton: tên, ký hiệu, tổng cung và decimals (USDT là 6, đa số jetton là 9; thiếu con số này bạn sẽ quy đổi sai các đơn vị raw).
- parse_address — chuyển đổi và kiểm tra địa chỉ EQ/UQ/raw ngay offline, hoàn toàn không cần chạm tới mạng.
Một ghi chú nhỏ về tên gọi: GRAM chính là Toncoin, được đổi tên vào tháng 6/2026. Mạng lưới vẫn tên là TON, chỉ có tên đồng coin thay đổi. Số dư mà
get_balancetrả về được tính bằng GRAM.
Ví dụ một prompt mà agent sẽ thực hiện qua tool chứ không phải qua HTTP viết tay:
Kiểm tra số dư GRAM và số dư USDT tại địa chỉ
UQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XF_wm
rồi hiển thị 5 giao dịch gần nhất của ví này.
Agent sẽ tự gọi get_balance, rồi get_jetton_balance (tự tính ví jetton on-chain), rồi get_transactions — không một HTTP request nào do bạn viết, không Retry-After, không backoff thủ công trong code của bạn.
Cách kết nối trong một phút: chạy local để dev, hoặc key hosted cho production
Có hai cách, và cả hai đều sòng phẳng. Quan trọng là đừng nhầm lẫn chúng: chạy npx local không cần key thì tiện cho phát triển, nhưng nó đi qua public config của TON; còn hạn mức riêng — thứ thật sự dập được cơn đau 429 dưới tải — thì chỉ key hosted mới đem lại.
Phương án A — chạy local, miễn phí, không cần key (dành cho phát triển)
Toàn bộ bộ tool đọc dữ liệu chỉ cần một lệnh npx là dựng xong — không cần đăng ký, không cần thẻ. Thêm vào config của MCP client:
{
"mcpServers": {
"ton": {
"command": "npx",
"args": ["-y", "@tonnode/mcp"]
}
}
}
Khởi đầu lý tưởng để phát triển và kiểm thử ở local: đầy đủ bộ tool đọc, không cần cấu hình gì, open source bên dưới. Nhưng nói thẳng: chế độ này đi qua public config của TON, tức là chịu đúng những giới hạn dùng chung như mọi liteserver công khai khác (not ready, ADNL timeout khi tải cao). Với lưu lượng production, nó không thay thế được hạn mức riêng — muốn có thì sang phương án B.
Phương án B — endpoint hosted với key riêng (dành cho production)
Khi cần băng thông được đảm bảo và một hạn mức riêng dưới tải production, bạn kết nối tới endpoint hosted bằng Bearer key của mình:
{
"mcpServers": {
"ton": {
"type": "http",
"url": "https://mcp.tonnode.io/mcp",
"headers": { "Authorization": "Bearer tn_live_…" }
}
}
}
Các gói cước
Ở mọi gói cước, bạn đều dùng được toàn bộ 16 tool của TONNode — bạn chỉ trả tiền cho băng thông:
- Hobby — miễn phí vĩnh viễn, 60 request/phút. Key được cấp ngay sau khi đăng nhập, không cần thẻ.
- Pro — $29/tháng, 300 request/phút.
- Scale — $199/tháng, 1200 request/phút.
Khác biệt so với tonapi ẩn danh là quá rõ: ở đó bạn chia ~1 req/s với cả internet, ở đây bạn có trần của riêng mình — ngay trên key hosted Hobby miễn phí đã là 60 request mỗi phút chỉ dành cho bạn, khỏi phải vật lộn với backoff và khỏi ăn 429 bất chợt. Đây không còn là cùng một con đường miễn phí như npx local không key: key hosted Hobby có hạn mức riêng, chứ không phải pool công khai dùng chung.
Tóm lại
429 từ tonapi.io không phải bug, cũng chẳng phải cái “228” huyền thoại, mà là một tín hiệu sòng phẳng: bạn đang ngồi trên pool ẩn danh dùng chung và đã đụng trần của nó. Retry kèm backoff, tôn trọng Retry-After, giới hạn số request đồng thời và cache sẽ dập được cơn đau cấp tính. Nhưng vấn đề chỉ thật sự biến mất khi bạn có năng lực xử lý của riêng mình — còn nếu bạn xây trên các AI agent thì làm việc đó qua MCP còn tiện hơn nữa, vì việc đọc TON đi theo giao thức gốc chứ không qua một lớp trung gian HTTP.
Lấy key hosted Hobby miễn phí (60 req/min, không cần thẻ) và thôi ăn 429 → tonnode.io/dashboard?plan=hobby
Cần dư địa cho tải production — hãy so sánh gói Pro và Scale: tonnode.io/pricing.
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ẻ.