A TON domain is an on-chain name under the .ton first-level namespace that resolves to wallet addresses, ADNL addresses for TON Sites, TON Storage bag IDs, text records or a delegated sub-resolver. Domains are NFT items owned by a wallet, and resolution happens by calling the dnsresolve get-method on the relevant DNS smart contract rather than by querying a conventional DNS server. This article explains the contract tree, the record categories, and a reproducible procedure for resolving a .ton name through an RPC endpoint and verifying the returned record. It also covers what can go wrong, what a domain does and does not guarantee, and how to move from a single lookup to production-grade resolution.
What a TON domain actually is
A TON domain is a human-readable identifier registered under the .ton first-level namespace on The Open Network. Instead of copying a long smart contract address, a user can send funds to a name such as example.ton, and a client can resolve that name to the underlying wallet contract address before building a transfer. The same naming layer also points at ADNL addresses used by TON Sites, TON Storage bag IDs, arbitrary text records, and nested subdomains.
The important mental model is that TON DNS is not a separate naming database bolted onto the chain. It is a tree of smart contracts, with the root contract stored in the blockchain configuration, and each registered .ton domain represented as an NFT item owned by a wallet. Resolution is therefore a contract read: you call a get-method and interpret the returned cell. That distinction matters for anyone building wallets, explorers, payment flows or site gateways, because caching, error handling and verification all follow from it.
.tonis the only first-level domain in the TON DNS tree.- Each registered domain is an NFT item, so ownership transfers through NFT-compatible wallets and marketplaces.
- The
.t.menamespace operates as a delegated sub-resolver using the same resolution interface. - Records can point to wallets, ADNL addresses, storage bag IDs, text, or another resolver contract.
Assumptions and prerequisites for the walkthrough
The procedure below assumes you have an HTTP endpoint that can execute TON get-methods, a domain name you are authorised to inspect, and a basic familiarity with JSON and hex-encoded cells. You do not need to run your own node to follow along, but you do need an endpoint that exposes the runGetMethod API rather than only indexed account history.
If you are still choosing an access path, compare hosted and self-hosted options in the TON RPC endpoints guide and review the API service overview for how get-method calls are exposed. For teams that need predictable throughput or private access, a dedicated TON node removes shared-endpoint variability from the measurement.
The contract tree and the dnsresolve interface
Every DNS smart contract in TON exposes a get-method with the signature (int, cell) dnsresolve(slice subdomain, int category). The subdomain argument is the remaining portion of the name to resolve, and category is the SHA-256 hash of a record category name, or zero to request all records. The method returns the number of bits it consumed and a cell containing the DNS record.
The return value is where most integration bugs originate. If the contract consumed fewer bits than the full subdomain length, the returned cell is a dns_next_resolver record pointing at another contract that handles the remainder. A resolver must follow that pointer rather than treating the partial result as final. This is exactly how delegated namespaces such as .t.me and user-defined subdomains work: the parent resolver hands off the suffix it does not own.
// Conceptual resolution loop (pseudocode)
// 1. Start at the root resolver from config param #4.
// 2. Call dnsresolve(remaining_subdomain, category).
// 3. If bits_consumed < remaining_subdomain.length:
// read dns_next_resolver from the returned cell
// set resolver = next_resolver
// remaining_subdomain = remaining_subdomain.slice(bits_consumed)
// repeat from step 2
// 4. Otherwise, decode the returned record by its TL-B type.Record types and what each one resolves to
TON DNS record values are described by TL-B schemas. The record type determines how a client should interpret the payload, and mixing them up is a common source of incorrect address display. The documented record types cover smart contract addresses, ADNL addresses, storage references, text, and resolver delegation.
A dns_smc_address record stores a smart contract address, which is what a wallet needs in order to send funds to a .ton name. A dns_adnl_address record stores a 256-bit ADNL address plus an optional protocol list, used to locate TON Sites. Storage records carry bag IDs for TON Storage, text records carry arbitrary UTF-8 strings, and dns_next_resolver delegates a suffix to another contract. When you build a resolver, decode by type first and only then apply business logic.
dns_smc_address: wallet or contract address for transfers.dns_adnl_address: network address for TON Sites, with optional protocol flags.- Storage records: bag IDs for content addressed through TON Storage.
- Text records: arbitrary UTF-8 metadata such as profile fields.
dns_next_resolver: delegation pointer for subdomains and delegated namespaces.
Resolving a .ton domain through an RPC endpoint
The following curl call invokes the dnsresolve get-method on a resolver contract. Replace RPC_URL with your endpoint and RESOLVER_ADDRESS with the resolver you are querying. The subdomain is passed as a slice, and the category is set to zero to request all records. The response contains the exit code, the stack, and the returned cell, which you then decode.
This is a diagnostic call, not a complete resolver. It confirms that the endpoint can execute get-methods and shows you the raw shape of the response before you add decoding logic. Run it against a domain you control first so you can compare the returned record with what you expect.
curl -sS "$RPC_URL" \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "runGetMethod",
"params": {
"address": "RESOLVER_ADDRESS",
"method": "dnsresolve",
"stack": [
["slice", "ZXhhbXBsZQ=="],
["int", "0"]
]
}
}'A JavaScript resolution helper with verification
The script below wraps the same get-method call and adds two checks that matter in production: it verifies the JSON-RPC response shape before reading the stack, and it records the raw cell so a failed decode can be replayed. It deliberately does not hard-code a public endpoint. Point RPC_URL at the endpoint you have chosen and keep credentials in environment variables.
The decode step is intentionally left as a labelled function because TL-B decoding depends on the record type returned. In a real integration you would branch on the record type, decode the matching schema, and only then convert the payload into a wallet address or ADNL address. Logging the undecoded cell alongside the decoded value makes discrepancies auditable.
const RPC_URL = process.env.RPC_URL;
const RESOLVER = process.env.RESOLVER_ADDRESS;
async function resolveDomain(subdomainSliceB64, category = 0) {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'runGetMethod',
params: {
address: RESOLVER,
method: 'dnsresolve',
stack: [
['slice', subdomainSliceB64],
['int', String(category)]
]
}
})
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const body = await res.json();
if (body.error) throw new Error(body.error.message);
const result = body.result;
if (!result || !Array.isArray(result.stack)) {
throw new Error('Unexpected runGetMethod response');
}
const bitsConsumed = Number(result.stack[0]?.value ?? 0);
const recordCell = result.stack[1]?.value ?? null;
return { bitsConsumed, recordCell, raw: result };
}
resolveDomain(process.env.SUBDOMAIN_SLICE_B64)
.then(r => console.log(JSON.stringify(r, null, 2)))
.catch(err => { console.error(err); process.exit(1); });Verifying the result against expected state
A successful get-method call is not the same as a correct resolution. Verification means comparing the decoded record with an independent source of truth: the domain's NFT ownership, the record you set when registering, and the behaviour of a second client. If the decoded wallet address does not match the address you configured, the fault is usually in the decode path or in which resolver you queried, not in the chain.
Use the table below as a results template. Fill it in with your own observed values; do not copy numbers from any article, including this one. The point is to make each check reproducible so that a regression is visible the next time you run the procedure.
- Check 1:
bitsConsumedequals the full subdomain length, or adns_next_resolveris present. - Check 2: the decoded record type matches the category you requested.
- Check 3: the decoded address equals the value shown by an independent wallet or explorer.
- Check 4: repeating the call returns the same record, confirming endpoint consistency.
Results table procedure
Record each run in a table with columns for the domain, resolver address, category, bits consumed, decoded record type, decoded value, and whether it matched the independent source. A completed row might look like the header below, with your own values substituted. | Domain | Resolver | Category | Bits consumed | Record type | Decoded value | Match |. Leave the data rows empty until you have run the procedure; the table is a measurement instrument, not a claim about network state.
If you are comparing endpoints, run the same table against each endpoint in sequence and record latency and consistency separately from correctness. Endpoint performance is a separate question from resolution correctness, and conflating them hides which layer failed.
Troubleshooting common resolution failures
Most failures fall into a small number of categories. A get-method that returns a non-zero exit code usually means the resolver address is wrong or the method is not present on that contract. A response with a missing stack means the endpoint returned an error envelope you did not check. A decode failure usually means you assumed a record type instead of reading it from the returned cell.
Delegation is the subtlest case. If bitsConsumed is less than the subdomain length and you ignore the dns_next_resolver, you will report a partial or empty result for a name that actually resolves correctly one hop later. Similarly, querying the wrong category returns a record set that does not include the type you expected, which looks like a missing record rather than a filtering mistake.
- Non-zero exit code: confirm the resolver address and that the contract exposes
dnsresolve. - Empty or malformed stack: validate the JSON-RPC envelope before reading values.
- Decode error: branch on record type instead of assuming
dns_smc_address. - Partial resolution: follow
dns_next_resolveruntil the subdomain is fully consumed. - Inconsistent results: repeat the call and compare endpoints before changing decode logic.
Limitations, tradeoffs and what a domain does not guarantee
A .ton domain is a naming convenience, not an identity proof. Ownership of the NFT establishes control of the name, but a resolved wallet address is only as trustworthy as the record the owner set. Clients should display the resolved address and let users confirm it, especially for high-value transfers. A domain can be transferred, and records can be changed, so a cached resolution can become stale.
Resolution also depends on the resolver tree being reachable through your endpoint. If you rely on a single shared endpoint, a transient failure looks like a missing domain. Caching improves latency but adds staleness risk; the right balance depends on whether you are displaying a name or preparing a transaction. For transaction preparation, resolve close to the moment of signing and verify the decoded address against user intent.
Next steps for production integration
Move from a single diagnostic call to a resolver that follows delegation, decodes by record type, caches with an explicit expiry, and logs the raw cell for every failure. Add a second endpoint as a cross-check so that a single provider outage does not present as a resolution error. If you operate your own infrastructure, review the node hosting guide and the network directory before sizing capacity.
For endpoint selection specifically, the RPC pricing page explains how request volume maps to plans, and the TON RPC endpoints guide covers the access patterns that support get-method calls. Keep the resolution logic independent of any single provider so that switching endpoints is a configuration change rather than a rewrite.