开发者
TRONAgg Radar 在工作台中能做的一切,你的后端都可以通过 API 完成:在创建地址时添加地址、为每条提醒接收已签名的 Webhook、在地址就绪时进行归集,以及重放你错过的任何内容。你无需运行 TRON 节点,也无需轮询区块。
- 基础 URL:
https://tronagg.ai/api/v1/watch - 身份验证:每个请求都在
X-API-Key请求头中携带你的 API 密钥。在工作台 → API 密钥中创建,并将其保存在你的服务器上。 - 单位:TRX 和价格为整数 SUN(1 TRX = 1,000,000 SUN)。USDT 金额为六位小数的十进制字符串(
"1250.000000")。能量为整数。时间戳包含时区。 - 参考:每个端点及其参数、响应和错误都在 API 参考 → TRONAgg Radar 中。
集成的整体流程
- 创建一个场景,只需一次,可在工作台中创建,也可使用
POST /scenarios。它决定 Radar 执行什么操作以及提醒发送到哪里。 - 在创建地址时添加地址:
POST /addresses,每次调用最多 1,000 个,并附上你自己的external_id。 - 接收 Webhook。 验证签名,按信封
id去重,迅速返回2xx。 - 在
address.sweep.ready时归集。 按episode_id让你的归集保持幂等。 - 在任何停机之后使用
GET /events?since_id=重放,并在为客户入账之前使用updated_since跟踪确认状态。
| 端点 | 作用 |
|---|---|
GET /status | 访问权限、额度、计数、链数据新鲜度(live 或 delayed)以及今日的能量花费。在获得访问权限之前也可调用。 |
GET /scenarios、POST /scenarios | 列出和创建场景。 |
GET、PATCH、DELETE /scenarios/{id} | 读取、更改或删除单个场景。 |
POST /addresses/check | 预览哪些地址是新地址、已在监控或无效。不做任何更改。 |
POST /addresses | 开始监控最多 1,000 个地址。 |
GET /addresses | 带筛选(needs_attention、not_ready、waiting_to_sweep、energy_paused)、搜索和游标分页的列表。 |
POST /addresses/bulk | 移动、暂停添加能量、恢复或停止监控最多 1,000 个地址。 |
GET、PATCH、DELETE /addresses/{address} | 读取单个地址,更改其标签、外部 ID、场景或能量暂停状态,或停止监控它。 |
POST /addresses/{address}/reviews | 将安全事项或问题事项标记为已查看。 |
GET /addresses/{address}/energy/quote、POST /addresses/{address}/energy | 先报价,再为该地址的下一笔转账购买能量。 |
GET /events | 事件流:使用 since_id 重放,使用 updated_since 获取确认状态变化。 |
GET /events/{id}、POST /events/{id}/redeliver | 单个事件及 Radar 的能量决策和每一次投递;重新发送该事件。 |
GET /pricing | 当前一次能量添加的费用,以及今日的价格区间。 |
快速入门
此示例会注册一个地址、验证 Radar 的 Webhook、响应 address.sweep.ready,并重放可能错过的事件。你需要:
- 一个 API 密钥;
- 一个场景:在 TRONAgg Radar → 场景中创建,或使用下面的调用创建;
- 一个在 TRONAgg Radar → 提醒中连接的 Webhook,以及在显示时复制的签名密钥。生产环境中 Webhook 必须是公开的 HTTPS URL;示例在本地监听。
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}'
# 从响应中复制 "id",或使用现有场景的 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'
两个程序都把去重集合和重放游标保存在内存中,因此无需任何依赖即可运行。生产环境中,请把它们保存在你的数据库中:每处理完一页就持久化游标。只在 onReady / on_ready 中、在完成你自己的检查之后,并且仅当场景的能量预设与收款方相符时(65,000 覆盖已持有 USDT 的收款方),才签名并发送归集交易。
JavaScript(Node 22+)
保存为 radar.mjs,然后运行 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; // 每笔入账只归集一次
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}`);
// 在检查目标地址和你的业务规则之后,在这里把你自己的归集加入队列。
}
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);
}
// 不带 updatedSince:sinceId 之后的新事件。带 updatedSince:自该时间以来的确认状态变化。
async function replay(updatedSince) {
const nextWindow = new Date().toISOString(); // 在拉取第一页之前记录
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; // 每处理完一页就持久化
if (events.length < 500 || next === cursor) break;
cursor = next; // 分页期间保持 updatedSince 不变
}
return updatedSince ? nextWindow : undefined; // 仅在所有分页都成功后持久化
}
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+)
保存为 radar.py,然后运行 python3 radar.py。它只使用标准库。
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: # 每笔入账只归集一次
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'])
# 在检查目标地址和你的业务规则之后,在这里把你自己的归集加入队列。
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):
"""不带 updated_since:since_id 之后的新事件。带上它:自该时间以来的确认状态变化。"""
global since_id
next_window = datetime.now(timezone.utc).isoformat() # 在拉取第一页之前记录
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 # 每处理完一页就持久化
if len(result['events']) < 500 or next_cursor == cursor:
break
cursor = next_cursor # 分页期间保持 updated_since 不变
return next_window if updated_since else None # 仅在所有分页都成功后持久化
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()
Webhook
在 TRONAgg Radar → 提醒中连接 Webhook,或使用 POST /webhooks。对于每条提醒,Radar 会向地址所在场景中接收该提醒分组的每个 Webhook 发送一个 HTTPS POST。Webhook 从不并入汇总,在 Telegram 中将地址静音也不会影响 Webhook。
每次投递都是一个 JSON 信封:
{
"id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"type": "address.usdt.received",
"created_at": "2026-10-04T18:05:18.123456+00:00",
"audience": "customer",
"data": { }
}
并带有以下请求头:
| 请求头 | 值 |
|---|---|
X-TronAgg-Event | 事件的 type。 |
X-TronAgg-Delivery | 投递 ID,在重试之间保持不变。 |
X-TronAgg-Timestamp | 本次尝试签名时的 Unix 秒数。 |
X-TronAgg-Signature | v1=<hex>:使用 Webhook 签名密钥对 {timestamp}.{raw body} 计算的 HMAC-SHA256。 |
验证每一次投递
- 在进行任何 JSON 解析之前,获取原始请求字节。
- 使用你的签名密钥,对
X-TronAgg-Timestamp、一个点号和原始请求体计算 HMAC-SHA256。以恒定时间将其与v1=之后的十六进制值进行比较。 - 拒绝与你的时钟相差超过 5 分钟的时间戳。每次重试都会使用新的时间戳重新签名,因此真实的投递总能通过验证。
快速入门中的 verify 函数正是这样做的。适用于任何 TRONAgg Webhook 的同样检查见 Webhook 事件。
投递规则
- 在 10 秒内返回
2xx,耗时的工作放在响应之后进行。不会跟随重定向。 - 重试:投递失败后最多再重试 4 次,分别在每次失败后约 1 分钟、5 分钟、30 分钟和 2 小时进行,之后放弃。请使用事件流补齐。
- 重复投递在所难免。 按信封
id去重。如果你同时使用 Webhook 和重放,还要按episode_id让你的归集保持幂等。 - 不保证顺序。 使用
data.event_id(随时间递增)或block_number对同一地址的事件排序。 - 忽略你不认识的类型,并仍然返回
2xx:以后可能会新增事件。 - 使用
POST /events/{id}/redeliver重新发送单个事件,可以发送到某一个渠道,或发送到地址当前场景的各个渠道。每个事件最多重新发送 5 次,每个账户每小时最多 60 次。
各个 ID 的区别
| ID | 位置 | 用途 |
|---|---|---|
信封 id(UUID) | Webhook 请求体 | 对重试的投递去重。 |
data.event_id(整数) | Webhook data;等于 GET /events 中事件的 id | 在事件流中查找该事件、将其标记为已查看、重新发送。 |
data.episode_id(UUID) | address.sweep.ready | 让每笔入账的归集只执行一次。 |
data.action_id(UUID) | 能量事件 | 与 GET /events/{id} 中 Radar 的能量决策相对应。 |
Webhook 负载
一次完整的 address.sweep.ready 投递(示例值):
{
"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"
}
}
每个事件都有的字段
| 字段 | 类型 | 含义 |
|---|---|---|
address_id | UUID 字符串 | Radar 为你所监控地址分配的 ID。 |
address | 字符串 | 该地址,Base58Check 格式(T…)。 |
display_name | 字符串 | 标签;没有标签时为缩略地址。 |
label、external_id | 字符串或 null | 你的标签和你自己的参考编号。 |
scenario_id、scenario_name | UUID 字符串、字符串 | 该地址所在的场景。 |
event_id | 整数 | 该事件在事件流中的 ID。 |
tx_id、block_number、block_time、finality | 字符串、整数、UTC 时间戳、head / solid / reverted | 事件背后的交易。 |
关于链上字段:由交易引起的事件会带有全部四个字段。来自 Radar 自身检查的事件(例如在例行检查中发现的能量不足)带有这些键,但值为 null;block_number 可能是最新区块高度。能量事件只有在某笔交易触发了该决策时才包含这些字段。address.energy.expired 不包含其中任何一个。finality 从 head 开始;参见确认状态。
USDT 金额是六位小数的字符串,TRX 金额是整数 SUN,能量是整数。标记为可为空的字段始终存在。
所有事件
| 分组 | 事件 | 额外的 data 字段(示例值) | 触发时机 |
|---|---|---|---|
| 入账与转账 | address.usdt.received | {"asset":"USDT","direction":"in","amount":"1250.000000","counterparty":"TYqHsaXKuUutMYADYKF15hVfVAYfmpggvo","balance":"1250.000000"} | USDT(官方合约)转入,1 USDT 起。如果转账后未能立即读取余额,balance 可能为 null。 |
| 入账与转账 | address.usdt.sent | {"asset":"USDT","direction":"out","amount":"1250.000000","counterparty":"TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb","balance":"0.000000"} | USDT 转出,1 USDT 起。使用了 Radar 能量的归集之后可能会增加 saved_sun:估算未燃烧的 TRX,单位为 SUN。 |
| 入账与转账 | address.trx.received | {"asset":"TRX","direction":"in","amount":10000000,"amount_trx":"10.000000","counterparty":"TYqHsaXKuUutMYADYKF15hVfVAYfmpggvo","balance":12000000} | TRX 转入,10 TRX 起,包括智能合约发送到该地址的 TRX(内部转账)。amount 和可为空的 balance 单位为 SUN;amount_trx 是六位小数的字符串。 |
| 入账与转账 | address.trx.sent | {"asset":"TRX","direction":"out","amount":10000000,"amount_trx":"10.000000","counterparty":"TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb","balance":2000000} | TRX 转出,10 TRX 起,包括随智能合约调用发送的 TRX。 |
| 能量添加 | 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"} | 一笔等待中的入账可以归集:拥有足够完成一笔预设转账的能量、未被冻结、能够支付带宽。每笔入账一次。source 为 radar(Radar 添加了能量)或 existing(能量原本就在;此时 expires_at 为 null)。 |
| 能量添加 | 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 添加的能量已到达该地址。total_price_sun 是实际支付的金额。order_id、delegation_txid 和 expires_at 可为空。 |
| 能量添加 | address.charge.skipped | {"action_id":"0190f7a4-0000-7000-8000-000000000004","reason":"budget"} | 未添加能量。reason:radar_disabled、actions_disabled、energy_paused、outside_hours、frozen、contract、autoenergy_active、no_bandwidth、budget、limit、insufficient_balance。每个场景和每种原因每小时最多一次。 |
| 能量添加 | address.charge.failed | {"action_id":"0190f7a4-0000-7000-8000-000000000005","reason":"order_failed"} | 能量订单失败。reason:purchase_failed 或 order_failed。 |
| 能量添加 | 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 添加的能量在入账被归集之前已过期。order_id、energy 和 expires_at 可为空。 |
| 问题 | address.energy.insufficient | {"energy_available":12400,"energy_required":64285,"deficit":63999} | 能量不足以完成下一笔预设转账。缺口为 energy_required − energy_available;deficit 是 Radar 为此会下的订单量:向上取整到下一个 1,000,且不低于最低订单量。对于关闭了能量警告的仅发送提醒场景不发送。 |
| 问题 | address.trx.low | {"trx_balance_sun":0,"needed_sun":345000,"reason":"bandwidth"} | TRX 不足以支付下一笔转账的带宽或多签费用。reason:bandwidth 或 multisig。对于关闭了能量警告的仅发送提醒场景不发送。 |
| 问题 | address.trx.burned | {"trx_burned_sun":6428500,"energy_used":64285,"would_cost_sun":1755000} | 该地址的一笔交易燃烧了 TRX 来获取能量。would_cost_sun 是现在从 Radar 获取这些能量的费用:无法读取价格时为 null,无需下单时为 0。 |
| 问题 | address.tx.failed | {"reason":"out_of_energy","fee_limit_sun":1000000,"energy_required":64285} | 该地址的一笔交易失败。reason:out_of_energy 或 reverted;对于 reverted,fee_limit_sun 和 energy_required 为 null:更多的能量也无济于事。 |
| 问题 | address.usdt.low | {"balance":"40.000000","threshold":"100.000000"} | USDT 余额降至场景的 USDT 余额偏低提醒以下。 |
| 安全 | 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} | 账户权限已变更,在变更后第一时间报告。owner_before 和 owner_after 可能为 null;没有更早的快照时,active_changed 为 null。从不合并。 |
| 安全 | address.usdt.approval | {"spender":"TXk8rQSAvPvBBNtqSoY6nCfsXWCSSpTVQF","amount":"1000000.000000","unlimited":false} | 设置了 USDT 授权额度。授权额度达到或超过 2²⁵⁵ 时 unlimited 为 true;无限额授权从不合并。 |
| 安全 | address.usdt.frozen | {"frozen":true} | 发行方冻结(true)或解冻(false)了该地址的 USDT。从不合并。 |
| 安全 | address.poisoning.suspected | {"lookalike_of":"TWd4WrZ9wn84f5x1hZhL4DHvk738ns5jwb","counterparty":"TWd4Wr7Lq2Xc9Tp3Vb8Nm1Kd6Hs4qn5jwb","direction":"in","asset":null,"token":"TFakeUSDTq8ZY4pL8otSzgjLj6tR7NHqje","amount":null} | 一笔涉及已知地址仿冒地址的转账。direction:in 或 out。对于 USDT 或 TRX,asset 有值,token 为 null,amount 是六位小数的字符串;对于其他代币,asset 和 amount 为 null,token 为其合约地址(或 null)。 |
有些决策会被记录但不发送事件:当地址已经就绪(already_ready)、该入账已不再需要能量(no_longer_needed),或地址已被暂停、移动或移除(scenario_changed)时,Radar 不会发送 address.charge.skipped。这些决策仍会出现在 GET /events/{id} 的 Action 中。
繁忙的 Telegram 聊天可能会收到一条文本汇总,而不是单独的消息。汇总永远不是 Webhook 事件:Webhook 始终单独接收每个事件。
重放与确认状态
GET /events 是一个持久的事件流,包含你的地址在最近 90 天内的每个事件,与你的 Webhook 收到的事件相同。用它在停机后补齐事件,并了解事件何时被最终确认。
使用 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
}
- 事件按从旧到新排列,从你发送的
since_id之后开始。 - 处理完一页后保存
next_since_id,并将其作为下一次的since_id发送。没有新事件时,它就是你发送的since_id。 - 持续分页,直到某一页的事件数少于你的
limit(最多 500)。 - 使用
address或kind(一种事件类型,例如address.sweep.ready)缩小事件流范围。 data是你的 Webhook 收到的对象,保持发送时的原样:其中的finality保持当时的状态,而事件自身的finality显示当前状态。late: true表示 Radar 读取该区块时已落后于链超过 100 个区块(约五分钟),例如在故障之后。
使用 updated_since 跟踪确认状态
Radar 根据最新区块发送提醒,因此每个事件都以 finality: "head" 开始。当区块变为不可逆时,事件变为 solid。在网络丢弃该区块的罕见情况下,它会变为 reverted。状态变化时 Radar 不会发送 Webhook。请主动查询:
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"
它会返回确认状态在 updated_since 当时或之后发生变化的事件,并附上新的 finality 和 finality_updated_at。按计划执行时:
- 在拉取第一页之前,记下当前时间:它就是你下一次的
updated_since。 - 在使用
since_id(从0开始)分页期间,保持updated_since不变。 - 只有在所有分页都成功之后,才保存第 1 步记下的时间。分页期间发生的变化会在下一轮再次出现,因此请以幂等方式处理它们。
updated_since 必须包含时区。快速入门中的 replay(updatedSince) / replay(updated_since) 函数实现了这个循环。
一个可行的规则: 将 head 视为“已看到付款”,并在事件变为 solid 时为客户入账。reverted 事件在链上从未发生过。如果你自己的归集已经发出,那是你自己的另一笔交易,回滚不会撤销它。
从你的代码中添加能量
为地址的下一笔转账购买能量,适用于任何场景,包括 alert_only:
# 1. 报价。不会购买任何东西。
curl -sS "https://tronagg.ai/api/v1/watch/addresses/$RADAR_ADDRESS/energy/quote?energy_preset=131k" \
-H "X-API-Key: $TRONAGG_API_KEY"
# 示例响应:{"energy":131000,"total_price_sun":3537000,"activation_fee_sun":0}
# 2. 购买,以报价的总额作为你的上限: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_preset为65k或131k。省略时使用场景的预设。- 报价只针对缺少的能量,按当前小时的价格计算;
activation_fee_sun是尚未激活地址的一次性激活费。 max_total_price_sun是可选的。提供时,如果最新价格高于你的上限,会返回409 quote_expired并附上新的quote,且不会购买。不提供时,按实时价格执行,并受场景的每日预算和你的余额限制。- 响应为 Action:
status为reserved、ordered或delivered,包含已下单的能量、价格,以及交付后的ready_at和expires_at。能量到达时随后会发送address.energy.chargedWebhook。 - 它计入场景的
daily_budget_sun,不计入times_per_address_day,并遵守场景的工作时间。alert_only场景从不自行购买,也没有预算:在这类场景中,你的上限和余额是仅有的限制,因此请发送max_total_price_sun。Radar 会先在链上检查该地址。 - 拒绝情况:
409already_ready、in_flight、budget_exhausted、frozen、energy_paused、outside_hours、contract、no_bandwidth、autoenergy_active、quote_expired;400insufficient_balance,附带以 SUN 为单位的current_balance和required_amount。
这永远不会转移 USDT,也不会签署任何内容:你的归集仍由你自己发送。
错误
Radar 的拒绝响应只有一种结构。请根据 error 分支处理,而不要依赖 message:
{ "detail": { "error": "radar_limit_exceeded", "message": "This change would exceed your Radar address limit", "kind": "with_energy", "used": 1000, "max": 1000 } }
| 状态码 | error | 何时出现 |
|---|---|---|
401 | (纯字符串 detail) | API 密钥缺失或无效。 |
403 | radar_access_required | 既没有 Google 登录,也还没有 1 TRX 的充值。包含 required_deposit_sun 和 allowed_auth_methods。 |
404 | radar_disabled | 此账户无法使用 TRONAgg Radar。 |
404 | address_not_found、scenario_not_found、event_not_found、destination_not_found | 在你自己的地址、场景、事件或渠道中未找到。 |
409 | radar_limit_exceeded | 此更改超出你的额度。包含 kind(watch_only 或 with_energy)、used 和 max。 |
409 | scenario_name_taken、external_id_taken、scenario_has_addresses、address_not_watched | 名称或外部 ID 已被使用;删除仍有地址的场景但未提供 move_to;为你已不再监控的地址重新发送事件但未指定渠道。 |
409、400 | 添加能量的拒绝情况 | 参见从你的代码中添加能量。 |
422 | invalid_*、empty_batch、batch_too_large、scenario_required | Radar 永远无法接受的输入,并由 field 指明具体字段,例如 invalid_address、invalid_threshold、invalid_energy_preset、invalid_energy_alerts、invalid_daily_budget、invalid_active_hours、invalid_cursor、invalid_timestamp。 |
422 | (数组 detail) | 请求与结构定义不符,例如未知字段或类型错误的值。 |
429 | redelivery_rate_limited、redelivery_backlogged | 重新发送次数过多,或仍有 200 条 Webhook 投递在排队。 |
429 | (纯字符串 detail) | 每个账户每分钟超过 1,000 次请求。请等待 Retry-After 秒。 |
批量调用(POST /addresses、POST /addresses/bulk)会在单项结果中报告有问题的项,因此批次中的其余项仍会继续处理。每个端点的完整列表见 API 参考;整个 API 共享的约定见错误与约定。