If your Python backend sends USDT (TRC-20) — a bot, an exchange, a payments service — you’ll want to buy TRON energy in Python automatically instead of by hand. This is a complete, runnable example using the TronSave API: it checks your balance, places an energy order, and polls until the energy is delivered.
Before you start
- Get an API key from the TronSave dashboard (it spends from your internal TronSave balance — never expose it client-side).
- Install the HTTP client:
pip install requests. Prefer a typed wrapper? Use the official SDK instead:pip install tronsave— see the SDK overview. - Fund the internal account. The minimum deposit is 10 TRX per transaction, and your first deposit needs roughly 1 extra TRX to activate the new address. The balance updates in about 3 seconds.
The endpoints
The script uses four v2 endpoints, authenticated with an apikey header:
GET /v2/user-info— your internal account balance.GET /v2/order-book— available liquidity and prices.POST /v2/buy-resource— place the order, returns anorderId.GET /v2/order/{orderId}— poll untilfulfilledPercenthits 100.
Every one of them is rate-limited to 15 requests per second. Every one returns the same envelope: error, message, and a data object. Write your parsing once and it works everywhere.
Full Python example
import time, requests
API_KEY = "your_api_key"
TRONSAVE_API_URL = "https://api.tronsave.io" # testnet: https://api-dev.tronsave.io
RECEIVER_ADDRESS = "your_receiver_address"
BUY_AMOUNT = 65000 # ~1 USDT (TRC-20) transfer
DURATION = 3600 # rental duration, in seconds (1 hour)
MAX_PRICE_ACCEPTED = 100 # max price in SUN per unit
RESOURCE_TYPE = "ENERGY" # ENERGY or BANDWIDTH
HEADERS = {"apikey": API_KEY}
def get_account_info():
return requests.get(f"{TRONSAVE_API_URL}/v2/user-info", headers=HEADERS).json()
def get_order_book():
return requests.get(f"{TRONSAVE_API_URL}/v2/order-book",
headers=HEADERS,
params={"address": RECEIVER_ADDRESS}).json()
def buy_resource():
body = {
"resourceType": RESOURCE_TYPE,
"unitPrice": "MEDIUM", # a price in SUN, or SLOW / MEDIUM / FAST
"resourceAmount": BUY_AMOUNT,
"receiver": RECEIVER_ADDRESS,
"durationSec": DURATION,
"options": {
"allowPartialFill": True,
"maxPriceAccepted": MAX_PRICE_ACCEPTED,
"preventDuplicateIncompleteOrders": True,
},
}
return requests.post(f"{TRONSAVE_API_URL}/v2/buy-resource",
headers={**HEADERS, "Content-Type": "application/json"},
json=body).json()
def get_order(order_id):
return requests.get(f"{TRONSAVE_API_URL}/v2/order/{order_id}", headers=HEADERS).json()
def main():
print(get_order_book()["data"]) # [{"price": 54, "availableResourceAmount": 2403704}, ...]
balance = int(get_account_info()["data"]["balance"])
if balance < MAX_PRICE_ACCEPTED * BUY_AMOUNT:
raise SystemExit("Insufficient TronSave balance")
order = buy_resource()
if order["error"]:
raise SystemExit(f"Buy failed: {order['message']}")
order_id = order["data"]["orderId"]
while True: # poll until the energy is delivered
time.sleep(3)
detail = get_order(order_id)["data"]
if detail["fulfilledPercent"] == 100 or detail["remainAmount"] == 0:
print(f"Energy delivered - paid {detail['payoutAmount'] / 1e6} TRX")
for d in detail["delegates"]:
print(d["delegator"], d["amount"], d["txid"])
break
print(f"Waiting for fill... {detail['fulfilledPercent']}%")
if __name__ == "__main__":
main()
How it works
- Read the market first.
get_order_book()returns a list of{price, availableResourceAmount}levels — the cheapest supply first. If you need 500,000 energy and the two cheapest levels only hold 300,000 between them, your fill price is set by the third level. This is why a hard-coded lowmaxPriceAcceptedsilently fails on busy days. - Check the balance.
get_account_info()returnsbalance,depositAddressandrepresentAddress— all in SUN, as a string, so cast it. 1 TRX = 1,000,000 SUN. A returned"50000000"is 50 TRX. - Place the order.
buy_resource()posts to/v2/buy-resource. Set yourAPI_KEY,RECEIVER_ADDRESS, and how much energy to buy — 65,000 covers one USDT transfer, roughly 130,000 for a first-time recipient (see how much energy a USDT transfer needs).durationSecdefaults to 259,200 (3 days) if you omit it. On success you get back nothing but anorderId— that’s expected. - Poll for the fill.
get_order()returns the live order:fulfilledPercent(0 = still pending, 1–99 = partial, 100 = complete),remainAmount, the actualpriceyou paid in SUN,payoutAmountin SUN, and adelegatesarray with the provider address, amount and on-chaintxidfor each match. Log those txids — they are your receipt, verifiable on Tronscan.
Choosing unitPrice
unitPrice takes either an integer price in SUN or one of three tiers, and the tiers are defined against the live book rather than being fixed markups:
| Value | Behaviour | Use when |
|---|---|---|
"SLOW" |
The lowest price the order can be set at | Not time-sensitive, saving matters more than speed |
"MEDIUM" |
Default. Lowest price that still gets the maximum market fill; if the market can’t fill at all, MEDIUM = SLOW + 10 | Almost everything |
"FAST" |
If the market is 100% ready, FAST = MEDIUM; below that, MEDIUM + 10; at 0% ready, SLOW + 20 | The transfer has to go out now |
80 (number) |
A fixed price in SUN, full control | You are pricing from the order book yourself |
Handling the errors that actually happen
Non-2xx responses come back with error: true and a code embedded in message, like TSAS:106 API_KEY_REQUIRED. Branch on the code, not the human text — the wording can change. The ones a Python bot hits in production:
INTERNAL_BALANCE_ACCOUNT_TOO_LOW— top up the internal account. Worth alerting on before it happens, not after.PRICE_EXCEED_MAX_PRICE_REQUIRED— the market moved above yourmaxPriceAccepted. Re-read the order book and retry with a higher ceiling, or wait.CANNOT_FULFILLED— you setonlyCreateWhenFulfilled: trueand the market can’t fill 100% right now. Drop that flag or enableallowPartialFill.MUST_BE_WAIT_PREVIOUS_ORDER_FILLED— you sent identical parameters while an earlier order is still open. That’spreventDuplicateIncompleteOrders: truedoing its job; it’s the option that stops a retry loop from double-buying.429 RATE_LIMIT— back off exponentially (1s, 2s, 4s) and stay inside 15 requests per second.
Schema failures look different: they come from the HTTP layer as FST_ERR_VALIDATION and name the missing field, e.g. "body must have required property 'receiver'". Useful during development, and a sign of a bug rather than a market condition.
Testnet vs mainnet
Develop against testnet with https://api-dev.tronsave.io (Nile, no real TRX), then switch to https://api.tronsave.io for production. Only the domain changes — every path stays identical, so a single TRONSAVE_API_URL constant is all you need to flip.
Get a Nile key at testnet.tronsave.io with a wallet set to the Nile network, then fund it from the Nile faucet. One thing to plan for: API keys, balances and orders are completely separate between the two environments. A mainnet key returns INVALID_API_KEY against the testnet host, which is the correct behaviour but confusing at 2am. TronSave does not support Shasta — use Nile.
Working in another language? The same flow is available as an SDK for Node.js, Rust, Java and PHP, and there’s a general API automation guide. If you’d rather not run a polling loop at all, Auto-Buy tops up a watched address on a threshold you set. Full reference lives in the TronSave docs.
FAQ
Do I need TronWeb?
Not for the API-key flow above — plain requests is enough. TronWeb is only needed if you sign and pay directly with a private key, which is the signed-transaction flow.
How much energy per USDT transfer?
About 65,000 (≈130,000 to a wallet that has never held USDT).
Where does payment come from?
Your prepaid TronSave balance, topped up on the dashboard. Nothing is signed on-chain per order, which is why the script needs no private key.
What if the order only fills partially?
With allowPartialFill: true you get whatever the market could match, fulfilledPercent lands between 1 and 99, and you’re charged only for the matched portion. Loop on remainAmount and place a follow-up order for the gap, or set onlyCreateWhenFulfilled: true so the order is never created unless it can be completed.
Build it: grab an API key on the TronSave market and read the developer docs.
