Jettons are the fungible token standard on The Open Network (TON), defined by TEP-74. Unlike ERC-20 on Ethereum, a jetton balance is not stored in one central contract: a master contract tracks metadata and total supply, while each holder has a separate jetton wallet contract that stores their balance and executes transfers. This article explains that architecture, shows how to read jetton metadata and balances, walks through a transfer flow with runnable code, and describes how to verify results and diagnose common failures. It separates documented TEP-74 behavior from operational recommendations and from measurements you should run against your own RPC endpoint.
Scope and the two meanings of jettons
The word jettons has two unrelated histories. In numismatics, jetons (also spelled jettons) were coin-like counting tokens produced in Europe from roughly the 13th through the 18th centuries and used on counting boards. In blockchain engineering, jettons are the fungible token standard of The Open Network (TON), specified as TEP-74. This article covers only the TON meaning; the historical sense is useful context but not something you implement.
The direct answer to what a jetton is on TON: it is a fungible token whose master contract holds metadata and total supply, while every holder's balance lives in a separate jetton wallet contract owned by that holder. Transfers are messages between these contracts, not a single balance update in one ledger. That distinction drives almost every operational difference from ERC-20-style tokens.
- TEP-74 defines the jetton master, jetton wallet, and transfer message layout.
- Each holder has their own jetton wallet contract per jetton.
- Transfers are asynchronous internal messages, so confirmation is not instantaneous in the way a single-contract balance update is.
Prerequisites and assumptions
You need a TON RPC endpoint that supports the methods you plan to call, a funded wallet for any transfer you attempt, and a basic familiarity with TON addresses (workchains, bounceable versus non-bounceable forms). Examples below use placeholders such as RPC_URL, JETTON_MASTER_ADDRESS, and PRIVATE_KEY; substitute your own values and never commit real keys.
The code samples use standard HTTP JSON-RPC style calls against a TON endpoint. Exact method names and response shapes can vary by provider, so treat the snippets as a procedure to adapt rather than a fixed API contract. For endpoint selection, the RPC Assistant comparison of TON RPC endpoints is the right place to choose a concrete provider; this article stays focused on the jetton mechanics and verification method.
How the TEP-74 jetton model is structured
A jetton master contract is the registry. It stores the token's metadata (name, symbol, decimals), tracks total supply, and knows how to derive the address of a jetton wallet for a given owner. It does not store every holder's balance in one place.
A jetton wallet contract is deployed per (jetton, owner) pair. It holds that owner's balance and implements the transfer, burn, and burn-notification behaviors. When you send jettons, your wallet contract sends an internal message to the destination wallet contract, which credits the balance and may send a notification back to the destination owner's contract if one is specified.
This distributed design is the main mental model to keep: to find someone's jetton balance you must first resolve their jetton wallet address from the master, then query that wallet. Querying the master alone gives you supply and metadata, not individual balances.
- Master: metadata, total supply, wallet-address derivation.
- Jetton wallet: per-owner balance and transfer logic.
- Transfer message: amount, destination, response address, optional forward payload and forward amount.
Reading jetton metadata and total supply
The first diagnostic step for any jetton integration is confirming you are talking to the right master contract and reading its metadata. A runnable curl call against a TON JSON-RPC endpoint typically runs a get-method on the master. The exact method name depends on your provider; the pattern below shows the shape of a get-method invocation.
Run this against your own RPC_URL and compare the returned symbol and decimals with what a block explorer shows for the same master address. A mismatch usually means you queried the wrong contract or a proxy that rewrites responses.
curl -sS -X POST "$RPC_URL" \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "runGetMethod",
"params": {
"address": "JETTON_MASTER_ADDRESS",
"method": "get_jetton_data",
"stack": []
}
}'Resolving a holder's jetton wallet address
Because balances live in per-owner wallet contracts, you cannot read a balance directly from the master. You must derive or look up the jetton wallet address for the owner. Many providers expose a helper method for this; if yours does not, you can call the master's wallet-address get-method with the owner address as an argument.
The Python snippet below shows the procedure: call the master to obtain the wallet address for an owner, then call that wallet to read its balance. It uses a generic JSON-RPC client and placeholder values, so adapt method names to your provider's documented surface.
import json, os, urllib.request
RPC_URL = os.environ["RPC_URL"]
MASTER = os.environ["JETTON_MASTER_ADDRESS"]
OWNER = os.environ["OWNER_ADDRESS"]
def rpc(method, params):
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
req = urllib.request.Request(RPC_URL, data=body, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
# Step 1: resolve the jetton wallet for this owner from the master
wallet = rpc("runGetMethod", {
"address": MASTER,
"method": "get_wallet_address",
"stack": [["slice", OWNER]],
})
print("wallet lookup:", json.dumps(wallet, indent=2))
# Step 2: read the balance from the resolved wallet contract
# (substitute the wallet address returned above)
balance = rpc("runGetMethod", {
"address": "RESOLVED_JETTON_WALLET_ADDRESS",
"method": "get_wallet_data",
"stack": [],
})
print("wallet data:", json.dumps(balance, indent=2))Executing a jetton transfer and what to expect
A jetton transfer is an internal message from your jetton wallet to the destination jetton wallet. In practice you send a message to your own jetton wallet contract instructing it to transfer a specified amount to a destination owner, with a response address so you receive confirmation. Because TON messages are asynchronous, the correct verification pattern is: submit, capture the message hash or transaction reference, then poll until the destination wallet's balance reflects the transfer.
Do not treat a successful send as proof of settlement. The transfer can bounce if the destination wallet is not deployed and the message is bounceable, or it can fail at the wallet contract level. Always verify by reading the destination jetton wallet balance after the transaction is confirmed.
- Send to your jetton wallet, not directly to the recipient's raw address.
- Include a response address so you can observe the confirmation message.
- Use non-bounceable messages only when you intentionally accept the risk of lost funds to an undeployed wallet.
- Verify settlement by re-reading the destination wallet balance, not by trusting the send response.
A verification procedure with a results table
Use the following procedure to confirm a jetton transfer end to end. Record each observation in a table so you can compare runs and spot regressions. The table below is a template; fill in your own measured values rather than assuming any numbers.
| Step | What you call | What you record | Pass condition |
|---|---|---|---|
| 1 | Master get_jetton_data | symbol, decimals, total supply | Matches explorer for the same master |
| 2 | Master get_wallet_address(owner) | resolved wallet address | Matches explorer's jetton wallet for that owner |
| 3 | Wallet get_wallet_data (sender) | balance before send | Balance equals expected pre-transfer amount |
| 4 | Send transfer message | message hash / tx reference | Message accepted by endpoint |
| 5 | Poll destination wallet get_wallet_data | balance after confirmation | Increase equals transferred amount minus any fees |
| 6 | Re-read sender wallet | balance after confirmation | Decrease equals transferred amount plus fees |
The pass conditions are the verification logic; the measured values are yours to capture. If step 5 never shows an increase, the transfer likely bounced or the destination wallet was not deployed. If step 6 shows no decrease, the message may not have reached your wallet contract.
Troubleshooting common jetton failures
Most jetton integration problems fall into a few categories: wrong master address, wrong wallet derivation, asynchronous timing, and bounce handling. Work through them in order rather than guessing.
If metadata looks wrong, you are probably querying the wrong master or a cached response. If a balance reads zero for an address you believe holds jettons, confirm you resolved the jetton wallet from the correct master and owner, and that the owner address format (bounceable versus non-bounceable) matches how the wallet was created. If a transfer appears sent but the destination never credits, check whether the destination wallet was deployed and whether the message was bounceable.
- Wrong master address: metadata and supply will not match the explorer.
- Wrong owner format: wallet derivation returns a different contract than expected.
- Asynchronous settlement: polling too early shows stale balances.
- Bounceable message to undeployed wallet: funds return to sender after a bounce.
- Insufficient TON for gas: the transfer message never executes.
Limitations and tradeoffs of the distributed model
The per-owner wallet design improves scalability by avoiding a single hot contract that every transfer must touch, but it shifts complexity to clients. You must resolve wallet addresses, handle asynchronous confirmation, and reason about deployment state. Indexers and explorers hide some of this, but any serious integration should implement the resolution and verification steps itself.
There is also a measurement caveat: latency and success rates depend heavily on the RPC endpoint you use, not only on the protocol. Do not assume a transfer is fast or slow based on one provider. Run the verification procedure above against your chosen endpoint and record your own timings. For infrastructure choices, see the OnFinality dedicated node options and the RPC pricing page to compare commitment levels before you commit to a production path.
Next steps for production integrations
Once you can read metadata, resolve wallets, and verify transfers, the next step is hardening: add retries with backoff around get-method calls, persist message references so you can reconcile after restarts, and monitor destination balances rather than trusting send responses. If you operate at scale, consider a dedicated node so your jetton queries are not competing with shared traffic.
For endpoint selection specifically, use the RPC Assistant guidance on TON RPC endpoints rather than hardcoding a provider here. Pair that with the OnFinality networks directory to confirm which networks and services fit your architecture, and review the API service overview if you need managed access rather than self-hosted infrastructure.
- Persist message hashes and reconcile asynchronously.
- Add idempotency keys around transfer submission.
- Monitor both sender and destination jetton wallets.
- Re-run the verification table after any endpoint change.