Python: pytoniq

Connect pytoniq's LiteClient and LiteBalancer to a private TON liteserver: custom config dicts, async balance queries in GRAM, and proof trust levels.

pytoniq is a pure-Python TON SDK that speaks the native ADNL liteserver protocol. There is no HTTP gateway in the path: your process opens an encrypted connection straight to a liteserver, sends TL requests, and can cryptographically verify the responses. That makes it a natural fit for a private liteserver — you get the raw protocol at your guaranteed rate, with no shared API key ceilings in between.

Install

pip install pytoniq

This pulls in pytoniq-core (TL/TLB schemas, cells, proof verification) automatically.

Connect with your liteserver entry

Your TONNode dashboard gives you three values per liteserver: a host, a port, and a base64 Ed25519 public key. The LiteClient constructor takes exactly those — no global config file needed for a single endpoint:

from pytoniq import LiteClient

client = LiteClient(
    host="<your-host>",       # from the dashboard
    port=41000,               # from the dashboard
    server_pub_key="<base64-key>",  # base64 Ed25519 key from the dashboard
    trust_level=2,
    timeout=15,
)

trust_level=2 skips block-proof verification — reasonable when the liteserver is your own dedicated endpoint rather than an anonymous public node. See trust levels below before choosing a value for production.

Custom config dict vs from_mainnet_config

LiteClient.from_mainnet_config(ls_i=0, trust_level=0) downloads the public global config from ton.org and connects to one of the shared public liteservers listed there. It works for experiments, but those servers are rate-limited, shared with everyone, and prone to ADNL timeouts under load — the TON docs themselves recommend a dedicated liteserver for production.

To keep the config-file workflow but point it at your own infrastructure, build the same dict shape and pass it to from_config. Both LiteClient.from_config(config, ls_i=0, trust_level=2) and LiteBalancer.from_config(config, trust_level=2) accept it; the balancer connects to every entry in liteservers and spreads requests across them.

import socket
import struct

from pytoniq import LiteBalancer


def ip_to_int(host: str) -> int:
    """Global-config format stores IPv4 as a signed 32-bit integer."""
    return struct.unpack(">i", socket.inet_aton(socket.gethostbyname(host)))[0]


config = {
    "liteservers": [
        {
            "ip": ip_to_int("<your-host>"),
            "port": 41000,
            "id": {"@type": "pub.ed25519", "key": "<base64-key>"},
        },
        # add more entries here if your plan includes several endpoints
    ],
    # Required by the parser. Copy the current "validator" block from
    # https://ton.org/global.config.json — it is only *verified* when
    # trust_level=0, but the key must be present.
    "validator": {
        "init_block": {
            "workchain": -1,
            "shard": -9223372036854775808,
            "seqno": 27747086,
            "root_hash": "<base64>",
            "file_hash": "<base64>",
        },
    },
}

client = LiteBalancer.from_config(config, trust_level=2)

Two format quirks to note: the ip field is a signed 32-bit integer, not a dotted string (the helper above handles hostnames too), and config["validator"]["init_block"] must exist even at nonzero trust levels because from_config reads it unconditionally.

Fetch masterchain info and a balance

A complete, runnable example. get_account_state returns a SimpleAccount whose balance is an integer in nanogram — divide by 10^9 for GRAM (the native coin, formerly Toncoin; the network itself is still called TON).

import asyncio

from pytoniq import LiteClient

ADDRESS = "EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG"


async def main():
    client = LiteClient(
        host="<your-host>",
        port=41000,
        server_pub_key="<base64-key>",
        trust_level=2,
        timeout=15,
    )
    await client.connect()

    info = await client.get_masterchain_info()
    print("last masterchain seqno:", info["last"]["seqno"])

    account = await client.get_account_state(ADDRESS)
    if account.is_active():
        print(f"balance: {account.balance / 10**9:.9f} GRAM")
    else:
        print("account is not active; balance:", account.balance)

    await client.close()


asyncio.run(main())

get_account_state always returns a SimpleAccount, even for addresses that do not exist on-chain (balance 0, uninitialized state) — check is_active() / is_uninitialized() instead of catching exceptions. LiteClient also works as an async context manager (async with LiteClient(...) as client:), which closes the connection for you.

With LiteBalancer the calls are identical; start it with await client.start_up() and shut down with await client.close_all(). The balancer also exposes run_get_method(address=..., method=..., stack=[]) for contract getters.

Trust levels and proofs

The trust_level argument controls how much pytoniq verifies:

  • trust_level=0 — full verification. pytoniq proves the block link from the config's init_block all the way to the current masterchain block, then checks Merkle proofs on responses. The first run walks the key-block chain (slow); verified blocks are cached in a local .blockstore directory, so subsequent runs are fast. Requires a real init_block, and pytoniq logs a warning if that block is not one of the known mainnet/testnet init blocks — verify its hash yourself before trusting it.
  • trust_level=1 — response proofs are checked, but the chain-of-trust walk from the init block is skipped.
  • trust_level=2 — no proof verification; responses are trusted as-is.

Against anonymous public liteservers, run trust_level=0. Against your own private endpoint the threat model is different — you are trusting your provider either way — so trust_level=2 is a pragmatic default, and trust_level=0 remains available if you want end-to-end proofs regardless.

At trust_level=0, keep the .blockstore cache on persistent storage in containerized deployments, or every cold start repeats the initial proof walk.

Rate limits

pytoniq applies no client-side throttling — it sends requests as fast as you issue them. On a TONNode private liteserver that is the point: plans guarantee 10 to 400 req/s (pricing), and pay-per-request bursts to 500 req/s, so a LiteBalancer over your endpoints can run at full speed without the 429-style backoff logic public APIs force on you. If a request hangs, lower timeout and let the balancer retry on another peer rather than blocking the event loop. Questions about limits or extra endpoints: t.me/tonnode_support.