Quick start
Base URL: https://tronenergyrentals.io/api/v1.
There are two ways to buy, and only one of them needs an account. Pick before you write code:
| Credited (API account) | Walk-up (no account) | |
|---|---|---|
| Needs | An API key, and TRX in credit | Nothing — just the two addresses |
| Paying | Spends your prepaid balance | You pay the order from your own wallet |
| Good for | Many orders, high volume, unattended bots | One-off orders, or trying it out |
| Start at | Account and key, then Single order | Ordering without an account |
Credited path, in four steps:
- Create an account with one call. You get an API key and a deposit address.
- Send TRX to the deposit address. It becomes prepaid credit.
- Read
/productsfor prices and the current maximum order size. - Place an order for each address that needs energy. Energy is delegated on-chain within seconds.
The API sends and accepts JSON. Account calls need your key in the X-API-Key header.
Account and key
No authentication. Returns 201 with your key and a deposit address that belongs to you alone.
curl -s -X POST https://tronenergyrentals.io/api/v1/account/signup
import requests
r = requests.post("https://tronenergyrentals.io/api/v1/account/signup")
print(r.json())
const r = await fetch("https://tronenergyrentals.io/api/v1/account/signup", { method: "POST" });
console.log(await r.json());
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func main() {
// No body and no authentication: one call creates the account.
resp, err := http.Post("https://tronenergyrentals.io/api/v1/account/signup", "application/json", nil)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(body))
}
{ "api_key": "tek_…", "deposit_address": "T…", "network": "…" }
Send TRX to deposit_address to fund the account, valued in TRX when it arrives. Anything above 0.0001 TRX is credited in full and appears in your statement straight away. Transfers below that are treated as address dust rather than deposits: they are neither credited nor shown.
Sign-up with a wallet instead
You can also open an account on the Your account tab by signing a short message with TronLink. Signing moves no funds and spends nothing; it only proves the wallet is yours. The account you get is the same, with the same deposit address and API key.
The advantage is recovery. An account opened with a wallet can have its key replaced at any time by signing again, and the old key stops working immediately. An account created by API call has no wallet attached, so a lost key cannot be recovered. If you want a recoverable account, open it with a wallet.
A top-up that did not show up
Deposits are credited automatically as they arrive. If one was missed, call this endpoint with the transaction ID of the transfer and it will be credited to your account.
Nothing is charged for asking, and re-sending a claim that already succeeded does nothing the second time.
curl -s -X POST https://tronenergyrentals.io/api/v1/account/deposits/claim \
-H "X-API-Key: tek_your_key_here" \
-H "Content-Type: application/json" \
-d "{\"txid\":\"YOUR_TRANSACTION_ID\"}"
import requests
r = requests.post("https://tronenergyrentals.io/api/v1/account/deposits/claim",
headers={"X-API-Key": "tek_your_key_here"},
json={"txid": "YOUR_TRANSACTION_ID"})
print(r.status_code, r.json())
const r = await fetch("https://tronenergyrentals.io/api/v1/account/deposits/claim", {
method: "POST",
headers: { "X-API-Key": "tek_your_key_here", "Content-Type": "application/json" },
body: JSON.stringify({ txid: "YOUR_TRANSACTION_ID" }),
});
console.log(r.status, await r.json());
package main
import (
"fmt"
"io"
"log"
"net/http"
"strings"
)
func main() {
body := strings.NewReader(`{"txid":"YOUR_TRANSACTION_ID"}`)
req, _ := http.NewRequest("POST", "https://tronenergyrentals.io/api/v1/account/deposits/claim", body)
req.Header.Set("X-API-Key", "tek_your_key_here")
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
out, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(out))
}
{ "ok": true, "credited": true, "already_credited": false,
"txid": "…", "amount_sun": "50000000", "block_number": 71243250,
"message": "your deposit has been credited" }
A claim answers 201 when it credits the deposit, 200 with already_credited: true when it had already been credited, so re-presenting the same proof adds nothing, and 202 with pending: true when the transfer is on chain but has not cleared yet. 404 means the transaction is not a transfer to your deposit address, and 409 is a refusal with its reason in code.
Check your balance
Send the key in the X-API-Key header on every account call. Keys in URLs are not accepted, because they end up in logs.
curl -s https://tronenergyrentals.io/api/v1/account -H "X-API-Key: tek_your_key_here"
import requests
r = requests.get("https://tronenergyrentals.io/api/v1/account",
headers={"X-API-Key": "tek_your_key_here"})
acct = r.json()
print(acct["balance_trx"], "TRX available")
const r = await fetch("https://tronenergyrentals.io/api/v1/account", {
headers: { "X-API-Key": "tek_your_key_here" },
});
const acct = await r.json();
console.log(acct.balance_trx, "TRX available");
package main
import (
"fmt"
"io"
"log"
"net/http"
)
const (
base = "https://tronenergyrentals.io/api/v1"
key = "tek_your_key_here"
)
func main() {
req, _ := http.NewRequest("GET", base+"/account", nil)
req.Header.Set("X-API-Key", key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(resp.StatusCode, string(body))
}
{
"deposit_address": "T…",
"status": "active",
"balance_sun": "50000000",
"balance_trx": "50.000000",
"rentals": 0,
"ledger": []
}
Prices
No key needed. The charge for an order is always energy_units × price_sun_per_unit. SUN is TRX × 1,000,000.
There are five durations, and the price per unit differs between them, so read the price from the API rather than assuming a shorter rental is cheaper. Current prices:
duration_seconds | Length | SUN per unit | Minimum order (32,000 units) |
|---|---|---|---|
| 300 | 5 minutes | 30 | 0.960 TRX |
| 600 | 10 minutes | 30 | 0.960 TRX |
| 900 | 15 minutes | 30 | 0.960 TRX |
| 1800 | 30 minutes | 32 | 1.024 TRX |
| 3600 | 1 hour | 34 | 1.088 TRX |
curl -s https://tronenergyrentals.io/api/v1/products
products = requests.get(f"{BASE}/products").json()["products"]
const { products } = await (await fetch(`${BASE}/products`)).json();
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
func main() {
resp, err := http.Get("https://tronenergyrentals.io/api/v1/products")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// Decode only what you need; the response carries many more fields.
var out struct {
Network string `json:"network"`
Products []struct {
DurationSeconds int `json:"duration_seconds"`
DurationLabel string `json:"duration_label"`
PriceSunPerUnit string `json:"price_sun_per_unit"`
MinUnits int64 `json:"min_units"`
LargestSingleOrderUnits int64 `json:"largest_single_order_units"`
DelegatableEnergy int64 `json:"delegatable_energy"`
} `json:"products"`
}
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
log.Fatal(err)
}
for _, p := range out.Products {
fmt.Printf("%s: %s SUN/unit, min %d, max %d\n",
p.DurationLabel, p.PriceSunPerUnit, p.MinUnits, p.LargestSingleOrderUnits)
}
}
{
"products": [
{
"duration_seconds": 600,
"duration_label": "10 minutes",
"price_sun_per_unit": "30.000000",
"price_trx_per_unit": "0.000030",
"cost_trx_at_reference": "1.95",
"min_units": 32000,
"largest_single_order_units": 4719497,
"free_energy": 4719497,
"delegatable_energy": 4719497,
"energy_per_trx": 9.629486
}
],
"network": "mainnet"
}
| Field | What it means |
|---|---|
duration_seconds | A rental length you can order. Read the list from here rather than hard-coding it. A duration is orderable only while it has a price. |
price_sun_per_unit | Price of one energy unit in SUN. It can change, so read it before pricing a job. |
min_units | Smallest order. Currently 32,000. |
largest_single_order_units | The most the pool can fill right now in one order. It rises as rentals expire and falls as energy is sold, so read it again on a retry. Asking for more returns insufficient_liquidity and charges nothing. |
max_units | Only present when a fixed ceiling is set. Otherwise the pool is the limit. |
free_energy, delegatable_energy | The same sellable figure as largest_single_order_units. |
energy_per_trx | Live chain ratio. |
cost_trx_at_reference | An illustration at a fixed 65,000 units, not your price. For your own amount, price per unit × energy units = cost in TRX. |
Single order
Spends your balance and delegates energy to receive_at, which can be any active TRON address, including your own.
| Field | Required | Notes |
|---|---|---|
energy_units | yes | Whole number, at least 32,000, at most largest_single_order_units. |
duration_seconds | yes | One of 300, 600, 900, 1800 or 3600 (5, 10, 15, 30 minutes or 1 hour). Confirm against /products. |
receive_at | yes | TRON address that receives the energy — the wallet that will SEND the transfer, not the address receiving the USDT. If it has no account on-chain yet it is activated automatically and the 1.1 TRX network fee is added, shown as its own activation line. See below to turn that off. |
idempotency_key | yes | Your own unique string, one per order (a UUID works). Retrying with the same value returns the original order instead of charging twice. |
activate_if_inactive | no | Defaults to true. Set false to turn activation OFF: an order to a wallet with no account is then refused with a 400 naming this field, and nothing is charged. It costs nothing to be refused — the check runs before anything is written. |
top_up_trx | no | Sends this much TRX to receive_at along with the energy, maximum 2, debited from your credit. No fee: face value. Off unless you ask. Useful for a brand-new wallet that needs TRX of its own to move the USDT, and it is sent only after the energy delegation has landed, so a failed order never sends it. |
What a wallet with no account costs. Energy cannot be delegated to a wallet that does not exist, so the chain rejects it. Rather than refuse the order, the account is created and the network’s 1.1 TRX create fee is added — a pass-through burn, not a fee kept by us. It is refunded in full if the account turns out to exist by the time we deliver. In a batch, only the wallets that actually need it are charged: two orders for one new wallet are both priced with it, and whichever one does not create the account is refunded, so the net is one activation either way.
IDEM=$(uuidgen 2>&- || python3 -c "import uuid;print(uuid.uuid4())")
curl -s -X POST https://tronenergyrentals.io/api/v1/account/rentals \
-H "X-API-Key: tek_your_key_here" \
-H "Content-Type: application/json" \
-d "{
\"energy_units\": 65000,
\"duration_seconds\": 600,
\"receive_at\": \"T_RECEIVING_ADDRESS\",
\"idempotency_key\": \"$IDEM\"
}"
import uuid, requests
r = requests.post(f"{BASE}/account/rentals", headers={"X-API-Key": KEY}, timeout=15, json={
"energy_units": 65000,
"duration_seconds": 600,
"receive_at": "T_RECEIVING_ADDRESS",
"idempotency_key": str(uuid.uuid4()),
})
print(r.status_code, r.json())
const r = await fetch(`${BASE}/account/rentals`, {
method: "POST",
headers: { "X-API-Key": KEY, "Content-Type": "application/json" },
body: JSON.stringify({
energy_units: 65000,
duration_seconds: 600,
receive_at: "T_RECEIVING_ADDRESS",
idempotency_key: crypto.randomUUID(),
}),
});
console.log(r.status, await r.json());
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
)
const base = "https://tronenergyrentals.io/api/v1"
// A NEW KEY FOR EVERY ORDER. Reusing one returns the earlier order
// (replayed: true) instead of placing a second, which reads like success.
func newKey() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func main() {
payload, _ := json.Marshal(map[string]any{
"energy_units": 65000,
"duration_seconds": 600,
"receive_at": "T_RECEIVING_ADDRESS",
"idempotency_key": newKey(),
})
req, _ := http.NewRequest("POST", base+"/account/rentals", bytes.NewReader(payload))
req.Header.Set("X-API-Key", os.Getenv("TEK_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// 402 out of funds, 409 insufficient liquidity, 400 validation error.
var out map[string]any
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(resp.StatusCode, out)
}
201
{
"id": "12",
"status": "queued",
"recipient_address": "T_RECEIVING_ADDRESS",
"energy_units": 65000,
"duration_seconds": 600,
"charged_sun": "1950000",
"balance_sun": "47270000",
"replayed": false
}
Order status
| Status | Meaning |
|---|---|
queued | Accepted and paid for. Orders are processed one recipient at a time. |
delegating | The delegation is being sent on-chain. |
active | The energy has landed on the recipient. |
The response to the order call is a snapshot and goes stale as the order moves on. Use the status call for the current state.
Idempotency: how retries behave
| Situation | Result |
|---|---|
| New key | 201 with replayed: false. A new order is placed and charged. |
| Same key again, such as a retry after a timeout | 200 with replayed: true. The original order comes back, charged_sun is unchanged and no new energy is ordered. |
| Same key reused for a different order | Not an error. You get a normal 200 describing the original order, so your new recipient receives nothing. |
replayed: true as a bug in your code: nothing new was bought. Keep a key stable while retrying one order, and generate a fresh one for every new order.Ordering without an account (walk-up)
The credited path above needs an account and an API key. This path needs neither. It is the one the buy site uses, and it is open to anyone: you get an order back, pay it from your own wallet, and the energy is delegated once the payment is seen on-chain. Nothing is charged to a balance, because there is no balance involved — you pay the order directly.
Two addresses matter and they are different: payer_address is who pays and
receive_at is who gets the energy. They may be the same address or different ones.
| Field | Required | Notes |
|---|---|---|
energy_units | yes | At least 32,000, at most largest_single_order_units from /products. |
duration_seconds | yes | One of the durations in /products (5, 10, 15, 30 minutes or 1 hour). |
receive_at | yes | Who receives the energy. Any active TRON address, including your own. An address with no account on-chain is activated automatically and the 1.1 TRX network fee is added. |
payer_address | yes | Who pays. The wallet that will send the TRX. It does not need an account first. |
idempotency_key | no | One per order if you retry. Also accepted as an Idempotency-Key header. |
What comes back, and where to send the money
Read payment.send_to for the address to pay and payment.amount_sun for the exact
amount. There is no memo field on a TRON transfer, so the amount is what identifies the payment:
send exactly the amount quoted. Paying from the payer_address you named is what binds the
transfer to your order.
curl -s -X POST https://tronenergyrentals.io/api/v1/orders \
-H "Content-Type: application/json" \
-d "{
\"energy_units\": 65000,
\"duration_seconds\": 600,
\"receive_at\": \"T_RECEIVING_ADDRESS\",
\"payer_address\": \"T_PAYING_WALLET\"
}"
import requests
r = requests.post(f"{BASE}/orders", timeout=15, json={
"energy_units": 65000,
"duration_seconds": 600,
"receive_at": "T_RECEIVING_ADDRESS",
"payer_address": "T_PAYING_WALLET",
})
order = r.json()
print("pay", order["payment"]["amount_trx"], "TRX to", order["payment"]["send_to"])
const r = await fetch(`${BASE}/orders`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
energy_units: 65000,
duration_seconds: 600,
receive_at: "T_RECEIVING_ADDRESS",
payer_address: "T_PAYING_WALLET",
}),
});
const order = await r.json();
console.log("pay", order.payment.amount_trx, "TRX to", order.payment.send_to);
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const base = "https://tronenergyrentals.io/api/v1"
func main() {
payload, _ := json.Marshal(map[string]any{
"energy_units": 65000,
"duration_seconds": 600,
"receive_at": "T_RECEIVING_ADDRESS",
"payer_address": "T_PAYING_WALLET",
})
resp, err := http.Post(base+"/orders", "application/json", bytes.NewReader(payload))
if err != nil {
panic(err)
}
defer resp.Body.Close()
var out map[string]any
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(resp.StatusCode, out["payment"])
}
201
{
"ref": "ord_cafdd74a324842d8c1",
"status": "awaiting_payment",
"receive_at": "T_RECEIVING_ADDRESS",
"payer_address": "T_PAYING_WALLET",
"total_trx": "0.960000",
"pay_to": "T_PAYMENT_ADDRESS",
"pay_by": "2026-11-03T13:43:07.422Z",
"seconds_remaining": 30,
"payment": {
"send_to": "T_PAYMENT_ADDRESS",
"pay_from": "T_PAYING_WALLET",
"amount_sun": "960000",
"amount_trx": "0.960000",
"txid": "…"
},
"walk_up": true,
"cancel_token": "9035ec3820..."
}
| Field | What it means |
|---|---|
ref | Your order reference. Poll it with the status call. |
payment.send_to | Send the TRX here. This is the operator’s address, not your own. |
payment.amount_sun | The exact amount to send. Send exactly this. |
pay_by, seconds_remaining | When the order stops accepting payment — 30 seconds. See below. |
cancel_token | Returned only here, never by a read route. Use it to cancel: POST /orders/<ref>/cancel. |
How the order is resolved once paid
After you send, the order moves on its own — you do not call anything to make it happen. Poll
GET /orders/<ref> and switch on status:
| Status | Meaning | What to do |
|---|---|---|
awaiting_payment | Created, not paid yet. | Send the TRX before pay_by. |
paid | The payment was seen on-chain. The delegation is being sent. | Keep polling. |
delegating | The delegation transaction is in flight. | Keep polling. |
active | The energy is with the recipient. | Done. rental.onchain_txid is the proof. |
expired, cancelled | The window lapsed, or the order was cancelled. | Place a new order. |
refunded | You paid but the energy could not be delivered. | Your TRX was returned on-chain. refund.txid is the proof. |
payer_address you named. The payment is matched to your order
by the paying wallet. A transfer from a different wallet is not a payment for this order.Where your walk-up history lives
A walk-up order belongs to the payer wallet, not to an account, so no key is needed to read it back:
GET /orders?payer=<address> lists them and
GET /orders/export.csv?payer=<address> downloads them. If you later sign in with that wallet
on the site, the same orders appear on your account, alongside anything bought with credit.
Multiple addresses (batch orders)
receive_at takes exactly one address. There is no list form and no separator, so to serve several wallets you send one request per wallet. The loop is the list.
In practice, place one order per address. Each is a normal single order with its own receive_at and its own idempotency_key, so every address can get a different amount and duration, and one failure never affects the others.
Your options
| You want to | How |
|---|---|
| Same amount to many addresses | Loop over the addresses with the same energy_units and duration_seconds. |
| Different amounts per address | Loop over (address, units) pairs. Units can differ for every order. |
| Different durations per address | Set duration_seconds per order, using any value from /products. |
| Several orders to one address | Allowed. Give each a different idempotency_key. Orders for the same recipient are processed one after another. |
| Energy to yourself and others in one run | Include your own address in the list like any other. |
Plan the batch before sending it
- Price it. Total cost is the sum of every order's
energy_units × price_sun_per_unitfor its duration. Compare it withbalance_sunfromGET /account. Top up first if it falls short. - Check order size. Each order must be at least
min_unitsand no more thanlargest_single_order_units. Orders are filled in turn, and each one reduces what the pool has left, so a large batch can hitinsufficient_liquiditypart-way through. - Brand-new addresses work: they are activated for you. A
receive_atwith no account on-chain yet is activated automatically and the 1.1 TRX network fee is added as its ownactivationline, so a loop over freshly generated addresses can run end to end. Two things change the sum: only wallets that need activating pay it, and activation is on by default, so setactivate_if_inactive: falseand such a row is refused withvalidation_errorbefore you are charged, while the rest of the batch continues. Budget the extra 1.1 TRX per new wallet, or fund them first. - Keep keys stable. Build each key from a batch ID plus the order's position, address and amount, for example
airdrop-17:3:T…:65000. If the whole script crashes and you run it again, orders that already went through replay instead of being bought twice. Use a new batch ID for a new run. - Pace requests. Order calls are limited to 120 per minute. About two per second stays under it. On
429, wait and retry with the same key.
How to handle each response in the loop
| Response | What to do |
|---|---|
201 | Order placed. Store the id. |
200 with replayed: true | Already placed on an earlier attempt. Store the id and move on. |
400 validation_error | Bad address, amount or duration. Log it, skip this address and continue. Retrying will not help. |
402 insufficient_funds | Stop the batch. Every remaining order would fail the same way. Top up, then run again with the same batch ID so finished orders replay. |
409 insufficient_liquidity | Nothing was charged. Wait, read /products again, then retry with a smaller amount (new key) or the same order later (same key). |
429 or a network timeout | Wait and retry with the same key. |
Example: one order per address
TEK_KEY="tek_your_key_here" # the key returned by signup
BATCH="airdrop-$(uuidgen 2>&- || python3 -c 'import uuid;print(uuid.uuid4())')" # NEW per run: no key can replay
DURATION=600
ORDERS="T_ADDRESS_ONE,65000
T_ADDRESS_TWO,131000
T_ADDRESS_THREE,65000"
i=0
echo "$ORDERS" | while IFS=, read -r ADDR UNITS; do
i=$((i+1))
curl -s -X POST https://tronenergyrentals.io/api/v1/account/rentals \
-H "X-API-Key: $TEK_KEY" -H "Content-Type: application/json" \
-d "{\"energy_units\":$UNITS,\"duration_seconds\":$DURATION,\"receive_at\":\"$ADDR\",\"idempotency_key\":\"$BATCH:$i:$ADDR:$UNITS\"}"
echo
sleep 0.5 # stay under 120 requests a minute
done
import os, time, requests
from decimal import Decimal
BASE = "https://tronenergyrentals.io/api/v1"
H = {"X-API-Key": os.environ["TEK_KEY"]}
BATCH = "airdrop-17" # new value for every new run
DURATION = 600
ORDERS = [("T_ADDRESS_ONE", 65000), ("T_ADDRESS_TWO", 131000), ("T_ADDRESS_THREE", 65000)]
# 1. Price the batch and compare it with your balance
product = next(p for p in requests.get(f"{BASE}/products").json()["products"]
if p["duration_seconds"] == DURATION)
cost = sum(u for _, u in ORDERS) * Decimal(product["price_sun_per_unit"])
balance = Decimal(requests.get(f"{BASE}/account", headers=H).json()["balance_sun"])
if cost > balance:
raise SystemExit(f"Need {cost} SUN, have {balance}. Top up first.")
def place(i, addr, units):
body = {"energy_units": units, "duration_seconds": DURATION, "receive_at": addr,
"idempotency_key": f"{BATCH}:{i}:{addr}:{units}"} # same key on every retry
for attempt in range(5):
try:
r = requests.post(f"{BASE}/account/rentals", json=body, headers=H, timeout=15)
except requests.RequestException:
time.sleep(2 ** attempt); continue
if r.status_code == 429:
time.sleep(2 ** attempt); continue
return r
return None
# 2. Place one order per address
placed, failed = [], []
for i, (addr, units) in enumerate(ORDERS, 1):
r = place(i, addr, units)
if r is None:
failed.append((addr, "no response"))
elif r.status_code in (200, 201):
placed.append(r.json()["id"])
elif r.status_code == 402:
print("Out of funds. Top up and rerun with the same BATCH."); break
else:
failed.append((addr, r.json()["error"]["code"])) # 400, 409
time.sleep(0.5)
print("placed:", placed, "failed:", failed)
const BASE = "https://tronenergyrentals.io/api/v1";
const H = { "X-API-Key": process.env.TEK_KEY, "Content-Type": "application/json" };
const BATCH = "airdrop-17"; // new value for every new run
const DURATION = 600;
const ORDERS = [["T_ADDRESS_ONE", 65000], ["T_ADDRESS_TWO", 131000], ["T_ADDRESS_THREE", 65000]];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// 1. Price the batch and compare it with your balance
const { products } = await (await fetch(`${BASE}/products`)).json();
const price = Number(products.find((p) => p.duration_seconds === DURATION).price_sun_per_unit);
const cost = ORDERS.reduce((s, [, u]) => s + u, 0) * price;
const { balance_sun } = await (await fetch(`${BASE}/account`, { headers: H })).json();
if (cost > Number(balance_sun)) throw new Error(`Need ${cost} SUN, have ${balance_sun}. Top up first.`);
async function place(i, addr, units) {
const body = JSON.stringify({
energy_units: units, duration_seconds: DURATION, receive_at: addr,
idempotency_key: `${BATCH}:${i}:${addr}:${units}`, // same key on every retry
});
for (let attempt = 0; attempt < 5; attempt++) {
try {
const r = await fetch(`${BASE}/account/rentals`, { method: "POST", headers: H, body });
if (r.status !== 429) return { status: r.status, data: await r.json() };
} catch {}
await sleep(2 ** attempt * 1000);
}
throw new Error(`no response for ${addr} after 5 attempts`);
}
// 2. Place one order per address
const placed = [], failed = [];
for (const [n, [addr, units]] of ORDERS.entries()) {
const r = await place(n + 1, addr, units);
if (!r) failed.push([addr, "no response"]);
else if (r.status === 200 || r.status === 201) placed.push(r.data.id);
else if (r.status === 402) { console.log("Out of funds. Top up and rerun with the same BATCH."); break; }
else failed.push([addr, r.data.error.code]); // 400, 409
await sleep(500);
}
console.log({ placed, failed });
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"time"
)
const (
base = "https://tronenergyrentals.io/api/v1"
batch = "airdrop-17" // a new value for every new run
duration = 600
)
type order struct {
Addr string
Units int64
}
var key = os.Getenv("TEK_KEY")
func do(method, path string, payload []byte) (int, map[string]any, error) {
req, err := http.NewRequest(method, base+path, bytes.NewReader(payload))
if err != nil {
return 0, nil, err
}
req.Header.Set("X-API-Key", key)
req.Header.Set("Content-Type", "application/json")
resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req)
if err != nil {
return 0, nil, err
}
defer resp.Body.Close()
var out map[string]any
json.NewDecoder(resp.Body).Decode(&out)
return resp.StatusCode, out, nil
}
func place(i int, o order) (int, map[string]any) {
payload, _ := json.Marshal(map[string]any{
"energy_units": o.Units,
"duration_seconds": duration,
"receive_at": o.Addr,
// THE SAME KEY ON EVERY RETRY OF THIS ORDER, so a retry cannot buy twice.
"idempotency_key": fmt.Sprintf("%s:%d:%s:%d", batch, i, o.Addr, o.Units),
})
for attempt := 0; attempt < 5; attempt++ {
status, out, err := do("POST", "/account/rentals", payload)
if err == nil && status != 429 {
return status, out
}
time.Sleep(time.Duration(1<<attempt) * time.Second)
}
return 0, nil
}
func main() {
orders := []order{{"T_ADDRESS_ONE", 65000}, {"T_ADDRESS_TWO", 131000}, {"T_ADDRESS_THREE", 65000}}
// 1. Price the batch and compare it with the balance BEFORE placing anything.
_, prod, _ := do("GET", "/products", nil)
var price float64
for _, p := range prod["products"].([]any) {
m := p.(map[string]any)
if int(m["duration_seconds"].(float64)) == duration {
price, _ = strconv.ParseFloat(m["price_sun_per_unit"].(string), 64)
}
}
if price == 0 {
fmt.Println("that duration is not in /products")
return
}
var units int64
for _, o := range orders {
units += o.Units
}
_, acct, _ := do("GET", "/account", nil)
balance, _ := strconv.ParseFloat(acct["balance_sun"].(string), 64)
if cost := float64(units) * price; cost > balance {
fmt.Printf("need %.0f SUN, have %.0f. Top up first.\n", cost, balance)
return
}
// 2. One order per address. 500 ms between calls stays under 120 requests a minute.
var placed, failed []string
for i, o := range orders {
status, out := place(i+1, o)
switch {
case status == 200 || status == 201:
placed = append(placed, fmt.Sprint(out["id"]))
case status == 402:
fmt.Println("out of funds -- top up and rerun with the SAME batch id")
fmt.Println("placed:", placed, "failed:", failed)
return
case status == 0:
failed = append(failed, o.Addr+": no response")
default:
code := ""
if e, ok := out["error"].(map[string]any); ok {
code = fmt.Sprint(e["code"])
}
failed = append(failed, o.Addr+": "+code)
}
time.Sleep(500 * time.Millisecond)
}
fmt.Println("placed:", placed, "failed:", failed)
}
queued and the delegations happen one recipient at a time, so a large batch finishes over a short period. Poll each id until it reads active.Track an order
Use the id from any order you placed. It returns the same fields as the order call, plus onchain_txid and delegate_expires_at. These two are always present and stay empty until the order reaches that stage: the delegation hash once it lands, and the time the energy returns to the pool. The call is scoped to your key, so another account gets not_found for your order, the same as for an order that never existed.
curl -s https://tronenergyrentals.io/api/v1/account/rentals/12 -H "X-API-Key: tek_your_key_here"
r = requests.get(f"{BASE}/account/rentals/12", headers={"X-API-Key": KEY}).json()
print(r["status"], r["onchain_txid"])
const r = await (await fetch(`${BASE}/account/rentals/12`, { headers: { "X-API-Key": KEY } })).json();
console.log(r.status, r.onchain_txid);
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
const base = "https://tronenergyrentals.io/api/v1"
func main() {
req, _ := http.NewRequest("GET", base+"/account/rentals/12", nil)
req.Header.Set("X-API-Key", os.Getenv("TEK_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
var r map[string]any
json.NewDecoder(resp.Body).Decode(&r)
fmt.Println(resp.StatusCode, r["status"], r["onchain_txid"])
}
Orders made from the website can also be looked up without a key using GET /orders?payer=<address> and GET /orders/<ref>.
Endpoints and limits
| Method | Path | Auth | Limit |
|---|---|---|---|
| POST | /account/signup | none | 5/hour |
| GET | /products | none | — |
| GET | /estimate | none | 60/min |
| GET | /account | X-API-Key | 60/min |
| POST | /account/rentals | X-API-Key | 120/min |
| GET | /account/rentals/<id> | X-API-Key | 120/min |
| POST | /account/deposits/claim | X-API-Key | 30/min |
| POST | /account/by-wallet | wallet signature | 30/min |
| GET | /orders?payer=<address> | none | 120/min |
| GET | /orders/<ref> | none | 120/min |
| GET | /orders/export.csv?payer=<address> | none | 30/min |
| GET | /account/orders.csv | API key | 30/min |
| POST | /orders | none | 120/min |
| POST | /orders/<ref>/cancel | cancel token | 60/min |
| GET | /quote | none | 120/min |
| GET | /account/session/orders.csv | wallet session | 30/min |
| POST | /account/challenge | none | 10/min |
| POST | /account/session | wallet signature | 30/min |
| GET | /account/session | wallet session | 120/min |
| POST | /account/session/end | wallet session | 30/min |
| POST | /account/key/reset | wallet signature | 5/hour |
| GET | /account/balance | none | 120/min |
| GET | /account/energy-needed | none | 60/min |
Downloading your history
Three routes, one per door, so each is scoped to what the caller can actually prove:
| Route | Who it is for | What is in it |
|---|---|---|
/account/orders.csv | An API account, with its key | Every rental bought from prepaid credit, plus every deposit and any reversal: the whole statement |
/orders/export.csv?payer=… | A walk-up buyer, no key needed | The orders that wallet paid for on the site |
/account/session/orders.csv | A buyer signed in on the Your account tab | The same statement as /account/orders.csv, by wallet session instead of a key — that is what the account tab’s own Download CSV button uses, since a signed-in buyer holds no API key |
user_id, your balance, or what you can download. A re-keyed account exports exactly the same file.amount_sun is signed: deposits positive, purchases negative. That column sums to your balance movement, which is what makes the file reconcilable rather than merely readable. A field that does not apply to a row is empty, never a zero, so a walk-up order has no balance_after_sun and a credit purchase has no payment_txid./v1/... is accepted as an alias for /api/v1/....
Errors
Every failure has the same shape. Switch on code, not on the message text.
{ "error": { "code": "insufficient_funds", "message": "…" } }
| HTTP | code | Meaning and fix |
|---|---|---|
| 400 | validation_error | A field is missing or malformed. The message names it. |
| 401 | unauthorized | The X-API-Key header is missing or the key is unknown. |
| 402 | insufficient_funds | Your balance does not cover the order. Top up. A refused order costs nothing. |
| 409 | insufficient_liquidity | The pool cannot fill that many units right now. Buy less or retry later. Nothing is charged. |
| 429 | rate_limited | Too many requests for that endpoint. Wait, then retry with the same key. |
Use it from an AI agent (MCP)
If your bot or assistant speaks MCP, it can ask about prices, energy and orders directly, with no curl and no glue code. Add a tronenergy entry to your client's MCP server configuration. The entry's shape is the same everywhere, but the file it goes in and the way you install it are not. Claude Desktop uses claude_desktop_config.json, Cursor .cursor/mcp.json, and Claude Code has its own claude mcp add command. See the pane beneath for each.
Choose the setup that suits your needs. Asking about prices needs no account at all.
// 1. PUBLIC: prices, estimates, order lookup. No account, no key.
{
"mcpServers": {
"tronenergy": { "command": "npx", "args": ["-y", "@tronenergy/mcp@1.3.3"] }
}
}
// 2. YOUR ACCOUNT: adds `account` and `rental` (balance, deposit address, your rentals).
{
"mcpServers": {
"tronenergy": {
"command": "npx",
"args": ["-y", "@tronenergy/mcp@1.3.3"],
"env": { "TRONENERGY_API_KEY": "tek_your_key_here" }
}
}
}
// 3. WRITE-ENABLED: adds the two spending tools, each named, with a per-order ceiling
// and an optional TOTAL budget across the window.
{
"mcpServers": {
"tronenergy": {
"command": "npx",
"args": ["-y", "@tronenergy/mcp@1.3.3"],
"env": {
"TRONENERGY_API_KEY": "tek_your_key_here",
"TRONENERGY_ALLOW_WRITES": "createOrder",
"TRONENERGY_MAX_ORDER_TRX": "5",
"TRONENERGY_MAX_SPEND_TRX": "50",
"TRONENERGY_SPEND_WINDOW": "24h"
}
}
}
}
# Claude Desktop: claude_desktop_config.json (JSON, same keys as the pane on the left)
# Cursor: .cursor/mcp.json (JSON, same keys)
# Claude Code: claude mcp add tronenergy -- npx -y @tronenergy/mcp@1.3.3
#
# The ENTRY is the same; the FILE and the INSTALL STEP are not. Claude Code also takes
# `claude mcp add-json` if you would rather paste the JSON than use flags.
// Programmatic: drive the server over stdio from your own agent.
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
// pins the version the same way the config examples do
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
const t = new StdioClientTransport({
command: 'npx', args: ['-y', '@tronenergy/mcp@1.3.3'],
env: { ...process.env, TRONENERGY_API_KEY: 'tek_your_key_here' },
});
const c = new Client({ name: 'bot', version: '1' }, { capabilities: {} });
await c.connect(t);
const r = await c.callTool({ name: 'products', arguments: {} });
console.log(r.content[0].text);
// Any MCP client works; the server is a plain stdio process.
// Nine tools: seven read-only and always on, two writes behind TRONENERGY_ALLOW_WRITES.
// The agent discovers them from tools/list: each description says what it does and when to
// use it instead of its neighbour. Nothing here needs to be memorised.
Read-only by default, enforced rather than promised. Out of the box nothing spends, signs or moves funds, and the package holds no private key. Two tools can spend, and are absent until you name them:
"env": {
"TRONENERGY_API_KEY": "tek_your_key_here",
"TRONENERGY_ALLOW_WRITES": "createOrder", // or createAccount; each name enables only itself
"TRONENERGY_MAX_ORDER_TRX": "5", // per ORDER, not per run
"TRONENERGY_MAX_SPEND_TRX": "50", // TOTAL across the window; unset = no budget
"TRONENERGY_SPEND_WINDOW": "24h" // 24h, 30m, 90s, 1d; a bare number is minutes
}
Configuration, the tools, the prices, the energy figures and the two kinds of account are documented in the package's own README.md, which your agent can read, and which is in the tarball beside index.js. The tools describe themselves. Four things are yours to decide. No MCP tool here can change any of them:
- A per-order ceiling is not a budget, so set the budget too.
TRONENERGY_MAX_ORDER_TRXbounds one order and says nothing about how many an agent may place. An agent that misreads its instructions can order again and again at a size it is allowed until the credit is gone, and every individual purchase looks fine.TRONENERGY_MAX_SPEND_TRXbounds the total (withTRONENERGY_SPEND_WINDOW, default24h;30m,90s,1dand a bare number of minutes all work). It is opt in: unset means no budget, exactly as before. It counts what the account actually pays, so it is spent faster than the energy price suggests: an order that activates a new wallet adds the 1.1 TRX create fee, and a top-up adds that amount. The count lives in the MCP server process, not on your account: restarting the server resets the window, and another client on the same key keeps its own separate count, so two agents can each spend the full budget. It contains a runaway agent inside one server's life; it is not an account-wide limit, and it cannot bound a determined human. For stronger containment, use a separate account with only the credit you are willing to lose. A balance dedicated to one agent is the only bound that survives a restart, a second client, or a config the agent can reach. - Treat the API key like the money it controls. Never commit a config holding one, and never paste one into a conversation. In a door-1 account it is the only credential, and it cannot be revoked.
- Pin the version. The examples above do; a bare
npx -yfetches the latest release on every launch, which is the wrong default for a package whose point is that it cannot spend by accident. - Check the no-key claim yourself.
npm pack @tronenergy/mcp, extract, thengrep -c "tronweb\|PRIVATE_KEY" index.jsprints0. The tarball IS what runs, there is no build step.
Before you rely on it
- Prices move. Read
/productsbefore pricing a job. - Recipients must be able to accept a delegation. An address with no account on-chain cannot receive energy, so it is activated automatically and the network's 1.1 TRX create fee is added as its own
activationline: priced, not refused. Sendactivate_if_inactive: falseto refuse it instead, before anything is charged. A wallet whose state the chain cannot be read for is refused, because guessing would take your money for energy that cannot arrive. - Your deposit wallet is separate. Credit comes from TRX sent to your deposit address. It is not tied to the wallet you connect on this site.
- Credit is spend-only. It cannot be refunded or withdrawn.