# SunLend Energy API v1

API для покупки энергии и bandwidth TRON с баланса аккаунта SunLend. Ресурс делегируется на указанный адрес на 15 минут, 1 час или 1 сутки; оплата списывается с баланса аккаунта. Цены — персональные для аккаунта, если они назначены.

- Базовый адрес: `https://sunlend.io/api/v1/energy`
- Формат: JSON, UTF-8. Ответы не кэшируются.
- Деньги везде в **sun**: 1 TRX = 1 000 000 sun, все суммы — целые числа.
- Машиночитаемый контракт OpenAPI 3.1: `https://sunlend.io/docs/openapi.json` — из него генерируются клиенты на любом языке.

## Доступ

1. Зарегистрируйтесь на сайте и пополните баланс в личном кабинете (раздел «Кошелёк»). Адрес пополнения возвращает и `GET /account`.
2. В кабинете откройте раздел «API» и выпустите ключ. Полный ключ показывается один раз — сохраните его. Действующих ключей может быть до 5; ненужный ключ отзывается там же и перестаёт работать сразу.
3. Передавайте ключ в каждом запросе:

```
Authorization: Bearer slk_a1b2c3d4e5_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
```

Ключ даёт право тратить баланс аккаунта — храните его только на сервере, не в браузере и не в мобильном приложении. Лимит: 60 запросов в минуту на ключ.

### Уровень доступа и дневной лимит

У ключа один из уровней — они вложены:

| Уровень | Что можно |
|---|---|
| `read` | баланс, цены, заказы, правила, вебхуки — только чтение |
| `purchase` | ещё покупка (`POST /orders`) и активация (`POST /activations`) |
| `full` | ещё автоматизации и вебхуки: создание, пауза, удаление |

Запрос сверх уровня — `403 insufficient_scope`. Ключу можно задать **дневной лимит трат** в TRX (сутки по UTC): покупка, которая его превысит, получит `403 daily_limit`, баланс не тронут. Уровень и лимит меняются в кабинете без перевыпуска ключа. Каждый заказ помнит ключ, которым оформлен (`apiKeyId`).

## Методы

| Метод | Путь | Назначение |
|---|---|---|
| GET | `/account` | Баланс и адрес пополнения |
| GET | `/quote?resource=&volume=&term=` | Цена заказа до покупки |
| POST | `/orders` | Купить энергию или bandwidth |
| GET | `/orders` | Список заказов, новые сверху |
| GET | `/orders/{id}` | Один заказ |
| GET | `/activations` | Цена активации адреса |
| POST | `/activations` | Активировать новый адрес |
| GET | `/automations` | Правила автоматизации |
| POST | `/automations` | Новое правило: автопополнение, автопродление, Tron Charger |
| GET, PATCH, DELETE | `/automations/{id}` | Правило; пауза и возобновление; удаление |
| GET, POST | `/webhooks` | Вебхуки; новый вебхук |
| PATCH, DELETE | `/webhooks/{id}` | Изменить (url, events, isActive); удалить |
| POST | `/webhooks/{id}/rotate` | Новый секрет подписи |
| POST | `/webhooks/{id}/ping` | Тестовое событие |
| GET | `/webhooks/{id}/deliveries` | Журнал доставок |
| POST | `/webhooks/{id}/deliveries/{deliveryId}` | Повторить проваленную доставку |

### GET /account

```json
{
  "data": {
    "balanceSun": 25000000,
    "depositAddress": { "network": "TRC20", "address": "TXyz…" }
  }
}
```

`depositAddress` — постоянный адрес пополнения аккаунта в сети TRON: принимаются USDT TRC-20 и TRX, USDT пересчитывается в TRX по курсу в момент зачисления. Отправляйте только эти монеты и только в сети TRON. Если процессинг недоступен, поле равно `null`.

### GET /quote

Параметры: `resource` — `energy` (по умолчанию) или `bandwidth`; `volume` — целое число: энергия от 65000 до 5000000, bandwidth от 1000 до 300000; `term` — `15m`, `1h` или `1d`.

```json
{ "data": { "resource": "energy", "volume": 65000, "term": "1h", "priceSun": 2090000, "sunPerUnit": 26, "feeSun": 400000, "trxPer1k": null, "balanceSun": 25000000 } }
```

Для энергии цена — `volume × sunPerUnit + feeSun`: плавающая цена за единицу и фиксированная наценка на заказ любого объёма; для bandwidth — `trxPer1k` TRX за 1 000.

Цена считается по текущему прайсу и может меняться; при покупке списывается цена на момент заказа.

### POST /orders

Тело:

| Поле | Тип | Описание |
|---|---|---|
| `address` | string | Адрес TRON, на который делегируется ресурс (T…, 34 символа) |
| `resource` | `"energy"` \| `"bandwidth"` | Необязательно, по умолчанию `energy` |
| `volume` | integer | Объём: энергия от 65000 до 5000000, bandwidth от 1000 до 300000 |
| `term` | `"15m"` \| `"1h"` \| `"1d"` | Срок аренды; дольше суток — автопродлением (`POST /automations`, `kind: "renewal"`) |

Другие поля отклоняются с 422. Нужен заголовок `Content-Type: application/json`.

Необязательный заголовок **`Idempotency-Key`** (1–100 символов: латиница, цифры, `_ . : -`) — уникальный id заказа на вашей стороне. Повтор запроса с тем же ключом не создаёт новый заказ и ничего не списывает, а возвращает уже созданный (ответ 200 вместо 201). Используйте его всегда: при обрыве связи запрос можно безопасно повторить. Ключ привязан к заказу навсегда — повтор с ним вернёт и неудачный заказ (`failed`), поэтому новую попытку после отказа отправляйте с новым ключом.

Ответ `201`:

```json
{
  "data": {
    "order": {
      "id": 1042,
      "resource": "energy",
      "address": "TXyz…",
      "volume": 65000,
      "term": "1h",
      "priceSun": 2090000,
      "status": "filled",
      "txId": "a1b2…",
      "createdAt": "2026-09-29T12:00:00.000Z"
    },
    "balanceSun": 22910000
  }
}
```

### Статусы заказа

| status | Значение |
|---|---|
| `filled` | Энергия делегирована, `txId` — транзакция в сети TRON |
| `pending` | Поставщик не подтвердил заказ вовремя. Заказ проверяется автоматически в течение 30 минут: станет `filled` или `failed` с возвратом денег. Не создавайте повторный заказ — опрашивайте `GET /orders/{id}` раз в минуту |
| `failed` | Заказ не выполнен, деньги вернулись на баланс |

### GET /orders

Параметры: `limit` — от 1 до 100 (по умолчанию 20), `offset` — с какого заказа (по умолчанию 0).

```json
{ "data": [ { "id": 1042, "status": "filled", "…": "…" } ], "pagination": { "total": 57, "limit": 20, "offset": 0 } }
```

### GET /orders/{id}

Тот же объект заказа, что в `POST /orders`, в поле `data`.

### POST /activations

Новый адрес TRON нужно активировать, прежде чем на него делегировать ресурс. Тело: `{ "address": "T…" }`. Цена активации — `GET /activations` (`priceSun`).

```json
{ "data": { "activation": { "id": 7, "address": "TXyz…", "priceSun": 1500000, "status": "done", "createdAt": "…" }, "balanceSun": 23500000 } }
```

Уже активный адрес — ошибка `already_active` (409), деньги не списываются. `status`: `done` — адрес активирован, `pending` — ответ задержался, проверка идёт автоматически, `failed` — не прошла, деньги вернулись.

## Автоматизации

Правило живёт у нас и у поставщика; каждое срабатывание (доставка энергии) списывается с баланса по текущей цене — один раз на доставку.

`POST /automations`, тело:

| Поле | Тип | Описание |
|---|---|---|
| `kind` | `"refill"` \| `"renewal"` \| `"smart"` | refill — докупка, когда энергии меньше `minimal`; renewal — продление раз в сутки; smart — адрес всегда заправлен |
| `address` | string | Адрес TRON |
| `volume` | integer | Объём одной доставки, от 65 000 до 5000000 (для smart не нужен) |
| `minimal` | integer | Только refill: порог, от 1 000 и меньше `volume` |
| `duration` | `"1h"` \| `"1d"` | Только refill: срок каждой доставки |
| `autoResume` | boolean | Возобновлять после пополнения, по умолчанию true |

Ответ — правило с полем `firePriceSun` (цена одного срабатывания по текущему прайсу). На балансе должно хватать хотя бы на одно срабатывание, иначе `402`. Когда денег не хватает на следующее срабатывание, правило ставится на паузу (`status: "paused"`, `pauseReason: "system"`) и само возобновляется после пополнения.

`PATCH /automations/{id}`: `{ "action": "pause" }`, `{ "action": "resume" }` и/или `{ "autoResume": false }`. Пауза вручную (`pauseReason: "user"`) сама не снимается.

## Вебхуки

Мы отправляем `POST` с JSON на ваш https-адрес, когда происходит событие. `POST /webhooks`: `{ "url": "https://…", "events": ["order.filled", …] }` — в ответе `secret` для проверки подписи, показывается один раз.

События: `order.filled`, `order.failed`, `deposit.credited`, `automation.fired`, `automation.paused`, `automation.resumed`, `activation.done` (и `ping` — тестовое).

```json
{ "id": "evt_…", "type": "order.filled", "createdAt": "2026-09-29T12:00:00.000Z", "data": { "orderId": 1042, "resource": "energy", "address": "T…", "volume": 65000, "term": "1h", "priceSun": 2090000, "txId": "…", "apiKeyId": 3 } }
```

Заголовки: `X-SunLend-Event`, `X-SunLend-Event-Id`, `X-SunLend-Timestamp` (unix-секунды) и `X-SunLend-Signature: sha256=<hex>`, где hex — HMAC-SHA256 от строки `"{timestamp}.{тело}"` с вашим секретом. Проверяйте подпись и отклоняйте запросы старше 5 минут:

```js
const crypto = require('crypto');
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature.replace('sha256=', '')));
```

Ответ 2xx — доставлено. Иначе повторяем через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч и 24 ч; после 20 проваленных доставок подряд вебхук выключается (включите заново через `PATCH` с `isActive: true`). Одно событие может прийти повторно — отсекайте дубли по `id`. Адрес — только https, не во внутреннюю сеть; до 5 вебхуков.

## Ошибки

Ошибка всегда приходит в одном формате:

```json
{ "error": { "code": "insufficient_funds", "message": "Не хватает средств на балансе. Пополните баланс." } }
```

Опирайтесь на `code` — `message` предназначен для человека и может меняться.

| HTTP | code | Когда |
|---|---|---|
| 400 | `invalid_json` | Тело не разбирается как JSON или это не объект |
| 400 | `invalid_query` | Недопустимые limit / offset |
| 400 | `invalid_idempotency_key` | Idempotency-Key не подходит по формату |
| 401 | `unauthorized` | Нет ключа, ключ неверен или отозван, аккаунт заблокирован |
| 402 | `insufficient_funds` | На балансе не хватает средств на заказ |
| 404 | `not_found` | Нет такого заказа (или он принадлежит другому аккаунту) |
| 409 | `idempotency_conflict` | Idempotency-Key уже использован для заказа с другими параметрами |
| 415 | `unsupported_media_type` | Нет заголовка Content-Type: application/json |
| 422 | `invalid_address` | Адрес TRON должен начинаться с T и содержать 34 символа |
| 422 | `invalid_volume` | Объём вне допустимого диапазона или не целое число |
| 422 | `invalid_resource` | resource не energy и не bandwidth |
| 403 | `insufficient_scope` | Уровень доступа ключа ниже нужного |
| 403 | `daily_limit` | Покупка превысит дневной лимит ключа |
| 409 | `already_exists` | У адреса уже есть правило этого типа |
| 422 | `invalid_minimal` | Порог автопополнения вне допустимого |
| 422 | `invalid_url` | Адрес вебхука не https или ведёт во внутреннюю сеть |
| 422 | `invalid_events` | Неизвестное событие вебхука |
| 409 | `already_active` | Адрес уже активирован — активация не нужна, деньги не списаны |
| 409 | `already_requested` | Активация этого адреса уже оформлена |
| 503 | `check_failed` | Не удалось проверить адрес в сети, повторите позже |
| 422 | `invalid_term` | Срок не 15m, 1h или 1d |
| 422 | `validation_failed` | Неизвестные поля в теле запроса (перечислены в details) |
| 429 | `rate_limited` | Превышен лимит запросов на ключ |
| 502 | `provider_failed` | Поставщик отклонил заказ; деньги уже вернулись на баланс. Повторить можно с новым Idempotency-Key |
| 503 | `provider_off` | Покупка временно недоступна |
| 503 | `price_unavailable` | Цена временно недоступна |
| 504 | `provider_unknown` | Поставщик не ответил вовремя; заказ создан в статусе pending — не создавайте новый, проверяйте этот |

## Примеры

Баланс:

```bash
curl 'https://sunlend.io/api/v1/energy/account' \
  -H 'Authorization: Bearer slk_a1b2c3d4e5_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
```

Цена 65 000 энергии на час:

```bash
curl 'https://sunlend.io/api/v1/energy/quote?volume=65000&term=1h' \
  -H 'Authorization: Bearer slk_a1b2c3d4e5_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
```

Покупка:

```bash
curl -X POST 'https://sunlend.io/api/v1/energy/orders' \
  -H 'Authorization: Bearer slk_a1b2c3d4e5_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: my-order-000123' \
  -d '{"address":"TXyz…","volume":65000,"term":"1h"}'
```

Статус заказа:

```bash
curl 'https://sunlend.io/api/v1/energy/orders/1042' \
  -H 'Authorization: Bearer slk_a1b2c3d4e5_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
```

## MCP для ИИ-агентов

Тот же API доступен как MCP-сервер: `https://sunlend.io/api/mcp` (Streamable HTTP), ключ — в заголовке `Authorization: Bearer …`. Инструменты: `get_balance`, `get_price`, `buy_energy`, `list_orders`, `get_order`, `activate_address`, `list_automations`, `create_automation`, `pause_automation`, `resume_automation`, `delete_automation` — каждый требует свой уровень доступа ключа. Для клиентов, которые подключают MCP только через stdio:

```bash
npx mcp-remote https://sunlend.io/api/mcp --header "Authorization: Bearer slk_a1b2c3d4e5_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
```

## Быстрый старт

### JavaScript (Node 18+)

```js
const API = 'https://sunlend.io/api/v1/energy';
const KEY = process.env.SUNLEND_KEY; // slk_…

async function call(method, path, body, headers = {}) {
  const res = await fetch(API + path, {
    method,
    headers: { Authorization: `Bearer ${KEY}`, ...(body ? { 'Content-Type': 'application/json' } : {}), ...headers },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = res.status === 204 ? null : await res.json();
  if (!res.ok) throw Object.assign(new Error(json.error.message), { code: json.error.code, status: res.status });
  return json.data;
}

// Энергия под один перевод USDT на час; id заказа на своей стороне — ключ идемпотентности
const { order } = await call('POST', '/orders', { address: 'T…', volume: 65000, term: '1h' }, { 'Idempotency-Key': 'payout-000123' });
console.log(order.status, order.txId);
```

### Python 3

```python
import os, requests

API = 'https://sunlend.io/api/v1/energy'
S = requests.Session()
S.headers['Authorization'] = f"Bearer {os.environ['SUNLEND_KEY']}"

def call(method, path, json=None, headers=None):
    r = S.request(method, API + path, json=json, headers=headers, timeout=30)
    data = None if r.status_code == 204 else r.json()
    if not r.ok:
        raise RuntimeError(f"{r.status_code} {data['error']['code']}: {data['error']['message']}")
    return data['data']

res = call('POST', '/orders', {'address': 'T…', 'volume': 65000, 'term': '1h'}, {'Idempotency-Key': 'payout-000123'})
print(res['order']['status'], res['order']['txId'])
```

## Типовые сценарии

**Энергия перед каждым переводом.** Перед отправкой USDT купите 65 000 энергии (получатель уже держит USDT) или 131 000 (у получателя нет USDT) на 15 минут, дождитесь `status: "filled"` и отправляйте. Если переводы идут постоянно — дешевле и проще правило Tron Charger (`POST /automations`, `kind: "smart"`).

**Повтор после обрыва связи.** Отправляйте каждый заказ с `Idempotency-Key` — например, id выплаты в вашей системе. Не дождались ответа — повторите тот же запрос с тем же ключом: вернётся уже созданный заказ, второго списания не будет.

**Заказ в статусе pending** (ответ `504 provider_unknown` или `status: "pending"`). Не создавайте новый заказ. Опрашивайте `GET /orders/{id}` раз в 30–60 секунд: в течение 30 минут он станет `filled` или `failed` с возвратом денег. С вебхуком `order.filled` / `order.failed` опрос не нужен.

**Контроль расходов.** Выпустите для сервера выплат отдельный ключ с уровнем `purchase` и дневным лимитом — даже утёкший ключ не потратит больше лимита и не сможет создавать правила и вебхуки.

**Приём вебхука (Express).**

```js
app.post('/sunlend', express.raw({ type: 'application/json' }), (req, res) => {
  const ts = req.get('X-SunLend-Timestamp');
  const sig = (req.get('X-SunLend-Signature') || '').replace('sha256=', '');
  const expected = crypto.createHmac('sha256', process.env.SUNLEND_WEBHOOK_SECRET).update(`${ts}.${req.body}`).digest('hex');
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300 || sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return res.sendStatus(400);
  const event = JSON.parse(req.body);
  // Дубли возможны — обрабатывайте event.id один раз
  res.sendStatus(200);
});
```

## Справка

- Перевод USDT TRC-20 на адрес, где уже есть USDT, расходует около 65 000 энергии; если у получателя нет USDT — около 131 000.
- Энергия, не израсходованная за срок аренды, не возвращается.
