Developers
Everything TRONAgg Radar does in the workspace, your backend can do with the API: add addresses as you create them, receive a signed webhook for every alert, sweep when an address is ready, and replay anything you missed. You don't need to run a TRON node or poll blocks.
- Base URL:
https://tronagg.ai/api/v1/watch - Authentication: your API key in the
X-API-Keyheader on every request. Create one under Workspace → API keys and keep it on your server. - Units: TRX and prices are integer SUN (1 TRX = 1,000,000 SUN). USDT amounts are decimal strings with six decimals (
"1250.000000"). Energy is an integer. Timestamps carry a time zone. - Reference: every endpoint with its parameters, responses and errors is in the API reference → TRONAgg Radar.
How an integration fits together
- Create a scenario once, in the workspace or with
POST /scenarios. It decides what Radar does and where alerts go. - Add addresses as you create them:
POST /addresses, up to 1,000 per call, with your ownexternal_id. - Receive webhooks. Verify the signature, de-duplicate on the envelope
id, answer2xxfast. - Sweep on
address.sweep.ready. Make your sweep idempotent byepisode_id. - Replay with
GET /events?since_id=after any downtime, and follow finality withupdated_sincebefore you credit a customer.
| Endpoint | What it does |
|---|---|
GET /status | Access, limits, counts, chain freshness (live or delayed) and today's Energy spend. Works before access is granted. |
GET /scenarios, POST /scenarios | List and create scenarios. |
GET, PATCH, DELETE /scenarios/{id} | Read, change or delete one scenario. |
POST /addresses/check | Preview which addresses are new, already watched or invalid. Changes nothing. |
POST /addresses | Start watching up to 1,000 addresses. |
GET /addresses | List with filters (needs_attention, not_ready, waiting_to_sweep, energy_paused), search and cursor paging. |
POST /addresses/bulk | Move, pause Energy, resume or stop up to 1,000 addresses. |
GET, PATCH, DELETE /addresses/{address} | Read one address, change its label, external ID, scenario or Energy pause, or stop monitoring it. |
POST /addresses/{address}/reviews | Mark a security or problem item as reviewed. |
GET /addresses/{address}/energy/quote, POST /addresses/{address}/energy | Price, then buy, Energy for the address's next transfer. |
GET /events | The event feed: replay with since_id, finality changes with updated_since. |
GET /events/{id}, POST /events/{id}/redeliver | One event with Radar's Energy decisions and every delivery; send it again. |
GET /pricing | What one Energy addition costs now, and today's range. |
Quickstart
This example registers an address, verifies Radar's webhooks, reacts to address.sweep.ready and replays events it may have missed. You need:
- an API key;
- a scenario: create one in TRONAgg Radar → Scenarios, or with the call below;
- a webhook connected in TRONAgg Radar → Alerts, and its signing secret, copied when it was shown. In production the webhook must be a public HTTPS URL; the examples listen locally.
export TRONAGG_API_KEY='your-api-key'
curl -sS -X POST https://tronagg.ai/api/v1/watch/scenarios \
-H "X-API-Key: $TRONAGG_API_KEY" -H 'Content-Type: application/json' \
-d '{"name":"Deposit addresses","action":"add_on_deposit","threshold":"100","energy_preset":"65k","daily_budget_sun":50000000}'
# Copy the "id" from the response, or use an existing scenario's id.
export RADAR_SCENARIO_ID='scenario-uuid-from-response'
export RADAR_ADDRESS='your-base58check-tron-address'
export RADAR_WEBHOOK_SECRET='secret-shown-once-when-connecting-the-webhook'
Both programs keep their de-duplication sets and the replay cursor in memory so they run without dependencies. In production, store them in your database: persist the cursor after each page you've processed. Sign and send a sweep only in onReady / on_ready, after your own checks, and only if the scenario's Energy preset fits the recipient (65,000 covers a recipient that already holds USDT).
JavaScript (Node 22+)
Save as radar.mjs and run node radar.mjs.
import { createHmac, timingSafeEqual } from 'node:crypto';
import { createServer } from 'node:http';
const root = 'https://tronagg.ai/api/v1/watch';
const key = process.env.TRONAGG_API_KEY;
const secret = process.env.RADAR_WEBHOOK_SECRET;
const address = process.env.RADAR_ADDRESS;
const scenarioId = process.env.RADAR_SCENARIO_ID;
if (![key, secret, address, scenarioId].every(Boolean)) throw new Error('Set the four Radar environment variables');
const seenIds = new Set();
const handledEpisodes = new Set();
let sinceId = Number(process.env.RADAR_SINCE_ID || 0);
async function api(path, options = {}) {
const res = await fetch(root + path, { ...options, headers: { 'X-API-Key': key, 'Content-Type': 'application/json', ...options.headers } });
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
return res.json();
}
function onReady(data) {
if (handledEpisodes.has(data.episode_id)) return; // one sweep per deposit
handledEpisodes.add(data.episode_id);
console.log(`Ready to sweep ${data.display_name} (${data.address}): ${data.balance} USDT, Energy ${data.energy_available}, expires ${data.expires_at}`);
// Queue your own sweep here, after checking the destination and your business rules.
}
function verify(timestamp, signature, rawBody) {
const sentAt = Number(timestamp);
if (!Number.isFinite(sentAt) || Math.abs(Date.now() / 1000 - sentAt) > 300) return false;
if (typeof signature !== 'string' || !/^v1=[0-9a-f]{64}$/i.test(signature)) return false;
const expected = createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
const supplied = Buffer.from(signature.slice(3), 'hex');
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}
// Without updatedSince: new events after sinceId. With updatedSince: finality changes since then.
async function replay(updatedSince) {
const nextWindow = new Date().toISOString(); // taken BEFORE the first page
let cursor = updatedSince ? 0 : sinceId;
while (true) {
const query = new URLSearchParams({ limit: '500', since_id: String(cursor) });
if (updatedSince) query.set('updated_since', updatedSince);
const { events, next_since_id } = await api(`/events?${query}`);
for (const event of events) {
if (event.type === 'address.sweep.ready') onReady(event.data);
console.log('event', event.id, event.type, event.finality);
}
const next = next_since_id ?? cursor;
if (!updatedSince) sinceId = next; // persist after each page
if (events.length < 500 || next === cursor) break;
cursor = next; // keep updatedSince fixed while paging
}
return updatedSince ? nextWindow : undefined; // persist only after every page succeeded
}
await api('/addresses', { method: 'POST', body: JSON.stringify({ scenario_id: scenarioId, existing: 'skip', items: [{ address, label: 'deposit-0042', external_id: 'invoice-4821' }] }) });
await replay();
createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/radar') { res.writeHead(404).end(); return; }
try {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const raw = Buffer.concat(chunks);
if (!verify(req.headers['x-tronagg-timestamp'], req.headers['x-tronagg-signature'], raw)) { res.writeHead(401).end(); return; }
const event = JSON.parse(raw.toString('utf8'));
if (!seenIds.has(event.id)) {
seenIds.add(event.id);
if (event.type === 'address.sweep.ready') onReady(event.data);
}
res.writeHead(204).end();
} catch (error) { console.error(error); res.writeHead(500).end(); }
}).listen(8080, '127.0.0.1', () => console.log('Listening on http://127.0.0.1:8080/radar'));
Python (3.11+)
Save as radar.py and run python3 radar.py. It uses only the standard library.
import hashlib, hmac, json, os, time
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlencode
from urllib.request import Request, urlopen
ROOT = 'https://tronagg.ai/api/v1/watch'
KEY = os.environ['TRONAGG_API_KEY']
SECRET = os.environ['RADAR_WEBHOOK_SECRET'].encode()
ADDRESS = os.environ['RADAR_ADDRESS']
SCENARIO_ID = os.environ['RADAR_SCENARIO_ID']
seen_ids, handled_episodes = set(), set()
since_id = int(os.getenv('RADAR_SINCE_ID', '0'))
def api(path, payload=None):
body = json.dumps(payload).encode() if payload is not None else None
req = Request(ROOT + path, data=body, headers={'X-API-Key': KEY, 'Content-Type': 'application/json'}, method='POST' if body else 'GET')
with urlopen(req, timeout=10) as response:
return json.load(response)
def on_ready(data):
if data['episode_id'] in handled_episodes: # one sweep per deposit
return
handled_episodes.add(data['episode_id'])
print('Ready to sweep', data['display_name'], data['address'], data['balance'], 'USDT, Energy', data['energy_available'], 'expires', data['expires_at'])
# Queue your own sweep here, after checking the destination and your business rules.
def verify(timestamp, signature, raw):
if not timestamp or not signature or not signature.startswith('v1='):
return False
try:
if abs(time.time() - int(timestamp)) > 300:
return False
except ValueError:
return False
expected = hmac.new(SECRET, timestamp.encode() + b'.' + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest('v1=' + expected, signature)
def replay(updated_since=None):
"""Without updated_since: new events after since_id. With it: finality changes since then."""
global since_id
next_window = datetime.now(timezone.utc).isoformat() # taken BEFORE the first page
cursor = 0 if updated_since else since_id
while True:
params = {'limit': 500, 'since_id': cursor}
if updated_since:
params['updated_since'] = updated_since
result = api('/events?' + urlencode(params))
for event in result['events']:
if event['type'] == 'address.sweep.ready':
on_ready(event['data'])
print('event', event['id'], event['type'], event['finality'])
next_cursor = result.get('next_since_id') or cursor
if not updated_since:
since_id = next_cursor # persist after each page
if len(result['events']) < 500 or next_cursor == cursor:
break
cursor = next_cursor # keep updated_since fixed while paging
return next_window if updated_since else None # persist only after every page succeeded
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != '/radar':
self.send_error(404)
return
raw = self.rfile.read(int(self.headers.get('Content-Length', '0')))
if not verify(self.headers.get('x-tronagg-timestamp'), self.headers.get('x-tronagg-signature'), raw):
self.send_error(401)
return
try:
event = json.loads(raw)
if event['id'] not in seen_ids:
seen_ids.add(event['id'])
if event['type'] == 'address.sweep.ready':
on_ready(event['data'])
except Exception as exc:
print('Radar webhook processing failed:', repr(exc))
self.send_error(500)
return
self.send_response(204)
self.end_headers()
api('/addresses', {'scenario_id': SCENARIO_ID, 'existing': 'skip', 'items': [{'address': ADDRESS, 'label': 'deposit-0042', 'external_id': 'invoice-4821'}]})
replay()
HTTPServer(('127.0.0.1', 8080), Handler).serve_forever()
Webhooks
Connect a webhook in TRONAgg Radar → Alerts, or with POST /webhooks. Radar sends one HTTPS POST per alert, to every webhook of the address's scenario that receives the alert's group. Webhooks never fold into a digest, and muting an address in Telegram doesn't affect them.
Every delivery is a JSON envelope:
{
"id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "address.usdt.received",
"created_at": "2026-10-04T18:05:18.123456+00:00",
"audience": "customer",
"data": { }
}
with these headers:
| Header | Value |
|---|---|
X-TronAgg-Event | The event type. |
X-TronAgg-Delivery | The delivery id, the same across retries. |
X-TronAgg-Timestamp | Unix seconds when this attempt was signed. |
X-TronAgg-Signature | v1=<hex>: HMAC-SHA256 of {timestamp}.{raw body}, keyed with the webhook's signing secret. |
Verify every delivery
- Take the raw request bytes, before any JSON parsing.
- Compute HMAC-SHA256 over
X-TronAgg-Timestamp, a dot, and the raw body, with your signing secret. Compare it with the hex afterv1=in constant time. - Reject a timestamp more than 5 minutes from your clock. Every retry is signed again with a fresh timestamp, so a genuine delivery always passes.
The verify functions in the quickstart do exactly this. The same check for any TRONAgg webhook is in Webhook events.
Delivery rules
- Answer
2xxwithin 10 seconds, and do slow work after answering. Redirects aren't followed. - Retries: a failed delivery is retried up to 4 more times, about 1 minute, 5 minutes, 30 minutes and 2 hours after each failure, then dropped. Use the event feed to catch up.
- Duplicates happen. De-duplicate on the envelope
id. If you combine webhooks with replay, also make your sweep idempotent byepisode_id. - Order isn't guaranteed. Use
data.event_id(it grows with time) orblock_numberto order events about one address. - Ignore types you don't know and still answer
2xx: new events may be added. - Resend one event with
POST /events/{id}/redeliver, to one channel or to the channels of the address's current scenario. Up to 5 resends per event and 60 per account per hour.
Which id is which
| Id | Where | Use it to |
|---|---|---|
Envelope id (UUID) | Webhook body | De-duplicate retried deliveries. |
data.event_id (integer) | Webhook data; equals the event id in GET /events | Find the event in the feed, review it, resend it. |
data.episode_id (UUID) | address.sweep.ready | Run each sweep once per deposit. |
data.action_id (UUID) | Energy events | Match Radar's Energy decision in GET /events/{id}. |
Webhook payloads
A complete address.sweep.ready delivery (example values):
{
"id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "address.sweep.ready",
"created_at": "2026-10-04T18:05:18.123456+00:00",
"audience": "customer",
"data": {
"address_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5c",
"address": "TXKc3d5gEqvKEayX9LySXNS5RLf4qDDEb1",
"display_name": "deposit-0042",
"label": "deposit-0042",
"external_id": "invoice-4821",
"scenario_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5d",
"scenario_name": "Deposit addresses",
"event_id": 123,
"tx_id": "a91853964b278f64a91853964b278f64a91853964b278f64a91853964b278f64",
"block_number": 86792808,
"block_time": "2026-10-04T18:05:15Z",
"finality": "head",
"episode_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5e",
"balance": "1250.000000",
"energy_available": 65000,
"energy_required": 64285,
"expires_at": "2026-10-04T19:05:15Z",
"source": "radar"
}
}
Fields in every event
| Field | Type | Meaning |
|---|---|---|
address_id | UUID string | Radar's id of your watched address. |
address | string | The address, Base58Check (T…). |
display_name | string | The label, or the shortened address when there's no label. |
label, external_id | string or null | Your label and your own reference. |
scenario_id, scenario_name | UUID string, string | The address's scenario. |
event_id | integer | The event's id in the event feed. |
tx_id, block_number, block_time, finality | string, integer, UTC timestamp, head / solid / reverted | The transaction behind the event. |
About the chain fields: events caused by a transaction carry all four. Events from Radar's own checks (such as Not enough Energy found on a regular check) carry the keys with null values; block_number may hold the latest block. Energy events include them only when a transaction triggered the decision. address.energy.expired has none of them. finality starts at head; see Finality.
USDT amounts are six-decimal strings, TRX amounts are integer SUN, Energy is an integer. Fields marked nullable are always present.
Every event
| Group | Event | Extra data fields (example values) | When it fires |
|---|---|---|---|
| Deposits & transfers | address.usdt.received | {"asset":"USDT","direction":"in","amount":"1250.000000","counterparty":"TYqHsaXKuUutMYADYKF15hVfVAYfmpggvo","balance":"1250.000000"} | USDT (the official contract) arrives, from 1 USDT. balance can be null when it couldn't be read right after the transfer. |
| Deposits & transfers | address.usdt.sent | {"asset":"USDT","direction":"out","amount":"1250.000000","counterparty":"TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb","balance":"0.000000"} | USDT leaves, from 1 USDT. A sweep that used Radar's Energy can later gain saved_sun: the estimated TRX it didn't burn, in SUN. |
| Deposits & transfers | address.trx.received | {"asset":"TRX","direction":"in","amount":10000000,"amount_trx":"10.000000","counterparty":"TYqHsaXKuUutMYADYKF15hVfVAYfmpggvo","balance":12000000} | TRX arrives, from 10 TRX, including TRX a smart contract sends to the address (an internal transfer). amount and nullable balance are SUN; amount_trx is a six-decimal string. |
| Deposits & transfers | address.trx.sent | {"asset":"TRX","direction":"out","amount":10000000,"amount_trx":"10.000000","counterparty":"TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb","balance":2000000} | TRX leaves, from 10 TRX, including TRX sent with a smart contract call. |
| Energy added | address.sweep.ready | {"episode_id":"0190f7a4-0000-7000-8000-000000000001","balance":"1250.000000","energy_available":65000,"energy_required":64285,"expires_at":"2026-10-04T19:05:15Z","source":"radar"} | A waiting deposit can be swept: Energy for one transfer of the preset, not frozen, bandwidth paid. Once per deposit. source is radar (Radar added the Energy) or existing (it was already there; expires_at is then null). |
| Energy added | address.energy.charged | {"action_id":"0190f7a4-0000-7000-8000-000000000002","order_id":"0190f7a4-0000-7000-8000-000000000003","energy":65000,"total_price_sun":1755000,"delegation_txid":"a91853964b278f64a91853964b278f64a91853964b278f64a91853964b278f64","expires_at":"2026-10-04T19:05:15Z"} | Radar's Energy arrived on the address. total_price_sun is what was actually paid. order_id, delegation_txid and expires_at are nullable. |
| Energy added | address.charge.skipped | {"action_id":"0190f7a4-0000-7000-8000-000000000004","reason":"budget"} | Energy wasn't added. reason: radar_disabled, actions_disabled, energy_paused, outside_hours, frozen, contract, autoenergy_active, no_bandwidth, budget, limit, insufficient_balance. At most one per scenario and reason per hour. |
| Energy added | address.charge.failed | {"action_id":"0190f7a4-0000-7000-8000-000000000005","reason":"order_failed"} | The Energy order failed. reason: purchase_failed or order_failed. |
| Energy added | address.energy.expired | {"action_id":"0190f7a4-0000-7000-8000-000000000006","order_id":"0190f7a4-0000-7000-8000-000000000007","energy":65000,"expires_at":"2026-10-04T19:05:15Z"} | Radar's Energy expired before the deposit was swept. order_id, energy and expires_at are nullable. |
| Problems | address.energy.insufficient | {"energy_available":12400,"energy_required":64285,"deficit":63999} | Not enough Energy for the next transfer of the preset. The gap is energy_required − energy_available; deficit is the order Radar would place for it: rounded up to the next 1,000, and never less than the minimum order. Not sent for Only alert me with Energy warnings off. |
| Problems | address.trx.low | {"trx_balance_sun":0,"needed_sun":345000,"reason":"bandwidth"} | Not enough TRX for the next transfer's bandwidth, or for the multi-signature fee. reason: bandwidth or multisig. Not sent for Only alert me with Energy warnings off. |
| Problems | address.trx.burned | {"trx_burned_sun":6428500,"energy_used":64285,"would_cost_sun":1755000} | A transaction from the address burned TRX for Energy. would_cost_sun is what that Energy costs from Radar now: null if the price couldn't be read, 0 if no order was needed. |
| Problems | address.tx.failed | {"reason":"out_of_energy","fee_limit_sun":1000000,"energy_required":64285} | A transaction from the address failed. reason: out_of_energy or reverted; for reverted, fee_limit_sun and energy_required are null: more Energy wouldn't have helped. |
| Problems | address.usdt.low | {"balance":"40.000000","threshold":"100.000000"} | The USDT balance fell below the scenario's low-USDT alert. |
| Security | address.permissions.changed | {"owner_before":{"threshold":1,"keys":[{"address":"TEAmEwsUMT9JkgtUmCG3eZrCbmkXRjA3Mc","weight":1}]},"owner_after":{"threshold":2,"keys":[{"address":"TEAmEwsUMT9JkgtUmCG3eZrCbmkXRjA3Mc","weight":1},{"address":"TXk8rQSAvPvBBNtqSoY6nCfsXWCSSpTVQF","weight":1}]},"active_changed":false} | The account's permissions changed, reported right after. owner_before and owner_after can be null; active_changed is null when there's no earlier snapshot. Never folded. |
| Security | address.usdt.approval | {"spender":"TXk8rQSAvPvBBNtqSoY6nCfsXWCSSpTVQF","amount":"1000000.000000","unlimited":false} | A USDT allowance was set. unlimited is true for an allowance of 2²⁵⁵ or more; unlimited approvals are never folded. |
| Security | address.usdt.frozen | {"frozen":true} | The issuer froze (true) or unfroze (false) the address's USDT. Never folded. |
| Security | address.poisoning.suspected | {"lookalike_of":"TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb","counterparty":"TWd4Wr7Lq2Xc9Tp3Vb8Nm1Kd6Hs4qn5jwb","direction":"in","asset":null,"token":"TFakeUSDTq8ZY4pL8otSzgjLj6tR7NHqje","amount":null} | A transfer with a lookalike of a known address. direction: in or out. For USDT or TRX, asset is set, token is null and amount is a six-decimal string; for another token, asset and amount are null and token is its contract (or null). |
Some decisions are recorded without an event: Radar doesn't send address.charge.skipped when the address was already ready (already_ready), the deposit no longer needed Energy (no_longer_needed), or the address was paused, moved or removed (scenario_changed). These still appear on the Action in GET /events/{id}.
Busy Telegram chats can get a text digest instead of separate messages. The digest is never a webhook event: webhooks always receive each event on its own.
Replay and finality
GET /events is a durable feed of every event of your addresses for the last 90 days, the same events your webhooks receive. Use it to catch up after downtime and to learn when an event became final.
Catch up with since_id
curl -sS "https://tronagg.ai/api/v1/watch/events?since_id=0&limit=500" -H "X-API-Key: $TRONAGG_API_KEY"
{
"events": [
{
"id": 124,
"type": "address.usdt.received",
"address": "TXKc3d5gEqvKEayX9LySXNS5RLf4qDDEb1",
"tx_id": "c3f1a0d29e7b4c5a8d6e0f1b2a3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e",
"block_number": 86792801,
"block_time": "2026-10-04T18:04:54Z",
"created_at": "2026-10-04T18:04:57Z",
"finality": "solid",
"finality_updated_at": "2026-10-04T18:05:51Z",
"late": false,
"data": {
"address_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5c",
"address": "TXKc3d5gEqvKEayX9LySXNS5RLf4qDDEb1",
"display_name": "deposit-0042",
"label": "deposit-0042",
"external_id": "invoice-4821",
"scenario_id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5d",
"scenario_name": "Deposit addresses",
"event_id": 124,
"tx_id": "c3f1a0d29e7b4c5a8d6e0f1b2a3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e",
"block_number": 86792801,
"block_time": "2026-10-04T18:04:54Z",
"finality": "head",
"asset": "USDT",
"direction": "in",
"amount": "1250.000000",
"counterparty": "TYqHsaXKuUutMYADYKF15hVfVAYfmpggvo",
"balance": "1250.000000"
},
"review": null
}
],
"next_since_id": 124
}
- Events come oldest first, after the
since_idyou send. - Store
next_since_idonce you've processed the page, and send it as the nextsince_id. With no new events, it's thesince_idyou sent. - Page until a page has fewer events than your
limit(up to 500). - Narrow the feed with
addressorkind(one event type, such asaddress.sweep.ready). datais the object your webhook received, as it was sent: itsfinalitystays as it was at that moment, while the event's ownfinalityshows the current state.late: truemeans Radar read the block when it was more than 100 blocks (about five minutes) behind the chain, for example after an outage.
Follow finality with updated_since
Radar alerts from the newest block, so every event starts with finality: "head". When the block becomes irreversible, the event turns solid. In the rare case the network drops the block, it turns reverted. Radar doesn't send a webhook when this changes. Ask for it:
curl -sS "https://tronagg.ai/api/v1/watch/events?updated_since=2026-10-04T18:00:00Z&since_id=0&limit=500" -H "X-API-Key: $TRONAGG_API_KEY"
returns events whose finality changed at or after updated_since, with the new finality and finality_updated_at. On a schedule:
- Before the first page, note the current time: it's your next
updated_since. - Keep
updated_sincefixed while you page withsince_id, starting from0. - Only after every page succeeded, store the time from step 1. Changes made while you were paging come again in the next round, so handle them idempotently.
updated_since must include a time zone. The replay(updatedSince) / replay(updated_since) functions in the quickstart implement this loop.
A rule that works: treat head as "payment seen" and credit the customer when the event turns solid. A reverted event never happened on the chain. If your own sweep already went out, it's a separate transaction of yours that the reversal doesn't undo.
Add Energy from your code
Buy Energy for an address's next transfer, in any scenario, including alert_only:
# 1. Price it. Buys nothing.
curl -sS "https://tronagg.ai/api/v1/watch/addresses/$RADAR_ADDRESS/energy/quote?energy_preset=131k" \
-H "X-API-Key: $TRONAGG_API_KEY"
# Example response: {"energy":131000,"total_price_sun":3537000,"activation_fee_sun":0}
# 2. Buy it, with the quote's total as your ceiling: total_price_sun + activation_fee_sun.
curl -sS -X POST "https://tronagg.ai/api/v1/watch/addresses/$RADAR_ADDRESS/energy" \
-H "X-API-Key: $TRONAGG_API_KEY" -H 'Content-Type: application/json' \
-d '{"energy_preset":"131k","max_total_price_sun":3537000}'
energy_presetis65kor131k. Leave it out to use the scenario's preset.- The quote prices only the Energy that's missing, at the current hour's rate;
activation_fee_sunis the one-time activation of an address that isn't activated yet. max_total_price_sunis optional. With it, a fresh price above your ceiling returns409 quote_expiredwith the newquoteand buys nothing. Without it, the live price applies, capped by the scenario's daily budget and your balance.- The response is the Action:
statusreserved,orderedordelivered, the Energy ordered, the price and, once delivered,ready_atandexpires_at. Theaddress.energy.chargedwebhook follows when the Energy arrives. - It counts toward the scenario's
daily_budget_sun, not towardtimes_per_address_day, and it respects the scenario's working hours.alert_onlyscenarios never buy by themselves and have no budget: there, your ceiling and your balance are the only caps, so sendmax_total_price_sun. Radar checks the address on the chain first. - Refusals:
409already_ready,in_flight,budget_exhausted,frozen,energy_paused,outside_hours,contract,no_bandwidth,autoenergy_active,quote_expired;400insufficient_balancewithcurrent_balanceandrequired_amountin SUN.
This never moves USDT and never signs anything: your sweep is still yours to send.
Errors
Radar refusals have one shape. Branch on error, never on message:
{ "detail": { "error": "radar_limit_exceeded", "message": "This change would exceed your Radar address limit", "kind": "with_energy", "used": 1000, "max": 1000 } }
| Status | error | When |
|---|---|---|
401 | (plain-string detail) | The API key is missing or invalid. |
403 | radar_access_required | Neither a Google sign-in nor 1 TRX of deposits yet. Includes required_deposit_sun and allowed_auth_methods. |
404 | radar_disabled | TRONAgg Radar isn't available for this account. |
404 | address_not_found, scenario_not_found, event_not_found, destination_not_found | Not found among your own addresses, scenarios, events or channels. |
409 | radar_limit_exceeded | The change doesn't fit your limit. Includes kind (watch_only or with_energy), used and max. |
409 | scenario_name_taken, external_id_taken, scenario_has_addresses, address_not_watched | A name or external ID already in use; deleting a scenario that has addresses without move_to; resending an event of an address you no longer watch without naming a channel. |
409, 400 | Add Energy refusals | See Add Energy from your code. |
422 | invalid_*, empty_batch, batch_too_large, scenario_required | Input Radar can never accept, with field naming it, for example invalid_address, invalid_threshold, invalid_energy_preset, invalid_energy_alerts, invalid_daily_budget, invalid_active_hours, invalid_cursor, invalid_timestamp. |
422 | (array detail) | The request didn't match the schema, for example an unknown field or a value of the wrong type. |
429 | redelivery_rate_limited, redelivery_backlogged | Too many resends, or 200 webhook deliveries are still queued. |
429 | (plain-string detail) | Over 1,000 requests a minute per account. Wait for Retry-After seconds. |
Batch calls (POST /addresses, POST /addresses/bulk) report a bad item in its own result, so the rest of the batch still goes through. Every endpoint's full list is in the API reference; the conventions shared by the whole API are in Errors & conventions.