# Account — API reference

Source: https://tenergy.me/docs/api/account
Last updated: 2026-09-27

Who am I, what is my balance, where do I send money.

| Method | Path | Summary |
|---|---|---|
| `GET` | `/v1/account` | Account information |
| `PATCH` | `/v1/account` | Edit the account's display name |
| `GET` | `/v1/balance` | Balances only |
| `GET` | `/v1/deposit-addresses` | Deposit addresses for this account |
| `GET` | `/v1/ledger` | The account's ledger statement |
| `GET` | `/v1/account/addresses` | The account's address book |
| `POST` | `/v1/account/addresses` | Save an address |
| `DELETE` | `/v1/account/addresses/{entryId}` | Remove an address |
| `GET` | `/v1/account/stats` | Order analytics, aggregated server-side |

Generated from [`openapi.yaml`](https://tenergy.me/openapi.yaml) at build time. Base URL `https://api.tenergy.me/v1`, or `https://api-nile.tenergy.me/v1` on Nile ([Environments](https://tenergy.me/docs/environments)). Every request below is signed as in [Authentication](https://tenergy.me/docs/authentication) unless its **Auth** line says otherwise.

## Account information

`GET /v1/account` · `getAccount`

**Auth:** API key (HMAC) or bootstrap token.

Identity, balances, deposit addresses, the limits in force and lifetime counters.
This is the endpoint a CatFee-compatible facade maps `GET /v1/account` onto.

Also readable with a bootstrap token (`Authorization: Bearer abt_…`), so the signup
flow can poll for the first confirmed deposit without an API key.

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `401` | Missing, malformed or rejected credentials. |
| `429` | Too many requests. |
| `500` | Something broke on our side. |

#### Response fields (`Account`)

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `brand` | string | yes | The brand this account belongs to. Accounts are not shared across brands. |
| `network` | enum: `mainnet`, `nile` | yes | The environment this account belongs to. An account, its ledger and its keys do not span environments: a Nile account is a separate account on the `api-nile.<brand-domain>` host. |
| `owner_address` | string \| null |  | The address that signed the challenge this account was created with; the root of recovery. `null` only for legacy accounts created before the challenge model. |
| `label` | string \| null |  | The same value as `display_name`, under the name this field has always had. New clients should read `display_name`; this one is not going away. |
| `display_name` | string \| null |  | The account's own title, chosen by its owner and editable with `PATCH /v1/account`. `null` when it has none, in which case a UI shows the shortened `owner_address` instead of an internal id. |
| `status` | enum: `unfunded`, `active`, `suspended`, `closed` | yes |  |
| `balance_sun` | integer (int64) | yes | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `balance_usdt` | integer (int64) |  | USDT balance in the token's smallest unit (6 decimals). |
| `reserved_sun` | integer (int64) |  | Held against in-flight orders. Not spendable. |
| `deposit_addresses` | array of object (DepositAddress) | yes |  |
| `deposit_addresses[].currency` | enum: `TRX`, `USDT` | yes |  |
| `deposit_addresses[].address` | string | yes | Base58Check TRON address (starts with `T`, 34 characters). |
| `deposit_addresses[].memo` | string \| null |  | Always `null` since 2026-09-26: every account has an address of its own, so no memo is needed and any memo is ignored. Kept so existing clients do not break. |
| `deposit_addresses[].confirmations_required` | integer |  | Blocks waited before the deposit is credited. |
| `deposit_addresses[].contract` | string \| null |  | The TRC-20 contract accepted at this address (`USDT` row only; `null` for TRX). |
| `deposit_addresses[].rate_now` | null \| object |  | `USDT` row only. The current SunSwap (or fixed) TRX-per-USDT rate BEFORE the spread, cached 60 s. `null` when no rate is available right now; the deposit is still accepted and the worker prices it at credit time. Indicative: the credit uses the rate the worker reads when it credits, not this one. |
| `deposit_addresses[].rate_now.trx_per_usdt` | string | yes |  |
| `deposit_addresses[].rate_now.source` | enum: `sunswap_v3`, `fixed_env` | yes |  |
| `deposit_addresses[].rate_now.at` | string (date-time) | yes |  |
| `deposit_addresses[].spread_bps` | integer \| null |  | `USDT` row only: basis points taken off the rate (5 = 0.05 %). |
| `deposit_addresses[].min` | string \| null |  | `USDT` row only: smallest credited USDT deposit; smaller ones are held (`held_below_min`). |
| `deposit_addresses[].max` | string \| null |  | `USDT` row only: largest auto-credited USDT deposit; larger ones are held (`held_for_review`). |
| `limits` | object |  |  |
| `limits.max_order_energy` | integer |  |  |
| `limits.max_batch_receivers` | integer |  |  |
| `limits.orders_per_second` | integer |  |  |
| `totals` | object |  | Lifetime counters. Informational; not a substitute for the ledger. |
| `totals.deposited_sun` | integer (int64) |  | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `totals.spent_sun` | integer (int64) |  | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `totals.refunded_sun` | integer (int64) |  | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `totals.orders_created` | integer |  |  |
| `totals.energy_delegated` | integer (int64) |  |  |
| `created_at` | string (date-time) | yes |  |

Example `200` response, from the contract (illustrative values — live numbers come from the API):

```json
{
  "id": "acc_01J9Z4K2M7Q8",
  "brand": "tenergy.me",
  "network": "mainnet",
  "label": "Acme payments",
  "status": "active",
  "balance_sun": 1250400000,
  "balance_usdt": 0,
  "reserved_sun": 0,
  "deposit_addresses": [{"currency":"TRX","address":"TDepositAddressExample1111111111111"}],
  "limits": {"max_order_energy":3000000,"max_batch_receivers":100,"orders_per_second":30},
  "totals": {
    "deposited_sun": 5000000000,
    "spent_sun": 3749600000,
    "refunded_sun": 0,
    "orders_created": 1842,
    "energy_delegated": 119730000
  },
  "created_at": "2026-04-02T09:11:00.000Z"
}
```

## Edit the account's display name

`PATCH /v1/account` · `updateAccount`

**Auth:** Dashboard session cookie + `X-CSRF-Token`.

The account's own title, shown wherever the account is named. `null` clears it and the
display falls back to the shortened owner address — never to an internal id.

A **dashboard session** operation, and the account's **owner**: the title is what every
member sees and what support reads back in a ticket. It is a setting; nothing about who
may reach what changes here.

### Request body

JSON (`AccountPatch`), required.

| Field | Type | Required | Description |
|---|---|---|---|
| `display_name` | string \| null |  | The account's title. Trimmed; must not be blank and must carry no control characters. `null` clears it. |

### Responses

| Status | Meaning |
|---|---|
| `200` | Updated |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | Authenticated, but this key is not allowed to do it. |
| `422` | Syntactically valid but semantically impossible. |
| `500` | Something broke on our side. |

#### Response fields (`Account`)

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `brand` | string | yes | The brand this account belongs to. Accounts are not shared across brands. |
| `network` | enum: `mainnet`, `nile` | yes | The environment this account belongs to. An account, its ledger and its keys do not span environments: a Nile account is a separate account on the `api-nile.<brand-domain>` host. |
| `owner_address` | string \| null |  | The address that signed the challenge this account was created with; the root of recovery. `null` only for legacy accounts created before the challenge model. |
| `label` | string \| null |  | The same value as `display_name`, under the name this field has always had. New clients should read `display_name`; this one is not going away. |
| `display_name` | string \| null |  | The account's own title, chosen by its owner and editable with `PATCH /v1/account`. `null` when it has none, in which case a UI shows the shortened `owner_address` instead of an internal id. |
| `status` | enum: `unfunded`, `active`, `suspended`, `closed` | yes |  |
| `balance_sun` | integer (int64) | yes | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `balance_usdt` | integer (int64) |  | USDT balance in the token's smallest unit (6 decimals). |
| `reserved_sun` | integer (int64) |  | Held against in-flight orders. Not spendable. |
| `deposit_addresses` | array of object (DepositAddress) | yes |  |
| `deposit_addresses[].currency` | enum: `TRX`, `USDT` | yes |  |
| `deposit_addresses[].address` | string | yes | Base58Check TRON address (starts with `T`, 34 characters). |
| `deposit_addresses[].memo` | string \| null |  | Always `null` since 2026-09-26: every account has an address of its own, so no memo is needed and any memo is ignored. Kept so existing clients do not break. |
| `deposit_addresses[].confirmations_required` | integer |  | Blocks waited before the deposit is credited. |
| `deposit_addresses[].contract` | string \| null |  | The TRC-20 contract accepted at this address (`USDT` row only; `null` for TRX). |
| `deposit_addresses[].rate_now` | null \| object |  | `USDT` row only. The current SunSwap (or fixed) TRX-per-USDT rate BEFORE the spread, cached 60 s. `null` when no rate is available right now; the deposit is still accepted and the worker prices it at credit time. Indicative: the credit uses the rate the worker reads when it credits, not this one. |
| `deposit_addresses[].rate_now.trx_per_usdt` | string | yes |  |
| `deposit_addresses[].rate_now.source` | enum: `sunswap_v3`, `fixed_env` | yes |  |
| `deposit_addresses[].rate_now.at` | string (date-time) | yes |  |
| `deposit_addresses[].spread_bps` | integer \| null |  | `USDT` row only: basis points taken off the rate (5 = 0.05 %). |
| `deposit_addresses[].min` | string \| null |  | `USDT` row only: smallest credited USDT deposit; smaller ones are held (`held_below_min`). |
| `deposit_addresses[].max` | string \| null |  | `USDT` row only: largest auto-credited USDT deposit; larger ones are held (`held_for_review`). |
| `limits` | object |  |  |
| `limits.max_order_energy` | integer |  |  |
| `limits.max_batch_receivers` | integer |  |  |
| `limits.orders_per_second` | integer |  |  |
| `totals` | object |  | Lifetime counters. Informational; not a substitute for the ledger. |
| `totals.deposited_sun` | integer (int64) |  | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `totals.spent_sun` | integer (int64) |  | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `totals.refunded_sun` | integer (int64) |  | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `totals.orders_created` | integer |  |  |
| `totals.energy_delegated` | integer (int64) |  |  |
| `created_at` | string (date-time) | yes |  |

## Balances only

`GET /v1/balance` · `getBalance`

**Auth:** API key (HMAC).

A deliberately small response for callers that poll before every order. Cheaper than
`/account` and on the read rate-limit budget.

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `401` | Missing, malformed or rejected credentials. |
| `429` | Too many requests. |
| `500` | Something broke on our side. |

#### Response fields (`Balance`)

| Field | Type | Required | Description |
|---|---|---|---|
| `balance_sun` | integer (int64) | yes | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `balance_usdt` | integer (int64) |  |  |
| `reserved_sun` | integer (int64) |  | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `available_sun` | integer (int64) | yes | `balance_sun - reserved_sun`. This is what an order can draw on. |
| `as_of` | string (date-time) | yes |  |

Example `200` response, from the contract (illustrative values — live numbers come from the API):

```json
{
  "balance_sun": 1250400000,
  "balance_usdt": 0,
  "reserved_sun": 0,
  "available_sun": 1250400000,
  "as_of": "2026-09-11T18:04:05.123Z"
}
```

## Deposit addresses for this account

`GET /v1/deposit-addresses` · `listDepositAddresses`

**Auth:** API key (HMAC).

The account's own deposit address: a TRON address that belongs to this account alone, so
no memo is needed (`memo` is always `null`). Two rows, same address: `TRX`, and `USDT`
(TRC-20, `contract`), which the ledger credits as TRX at `rate_now` minus `spread_bps`,
between `min` and `max` USDT. The address is issued on the first read if the account has none yet.

A deposit is credited after the configured number of block confirmations
(`confirmations_required`), and produces a `balance.credited` webhook.

Support can replace the address (never the customer; at most once per
`rotation.min_interval_days` and `rotation.max_per_window` times per
`rotation.window_days`). A replaced address is listed under `retired` and still credits
until `credited_until` (`rotation.retired_credit_days` after it was retired).

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `401` | Missing, malformed or rejected credentials. |
| `429` | Too many requests. |
| `500` | Something broke on our side. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of object (DepositAddress) | yes |  |
| `data[].currency` | enum: `TRX`, `USDT` | yes |  |
| `data[].address` | string | yes | Base58Check TRON address (starts with `T`, 34 characters). |
| `data[].memo` | string \| null |  | Always `null` since 2026-09-26: every account has an address of its own, so no memo is needed and any memo is ignored. Kept so existing clients do not break. |
| `data[].confirmations_required` | integer |  | Blocks waited before the deposit is credited. |
| `data[].contract` | string \| null |  | The TRC-20 contract accepted at this address (`USDT` row only; `null` for TRX). |
| `data[].rate_now` | null \| object |  | `USDT` row only. The current SunSwap (or fixed) TRX-per-USDT rate BEFORE the spread, cached 60 s. `null` when no rate is available right now; the deposit is still accepted and the worker prices it at credit time. Indicative: the credit uses the rate the worker reads when it credits, not this one. |
| `data[].rate_now.trx_per_usdt` | string | yes |  |
| `data[].rate_now.source` | enum: `sunswap_v3`, `fixed_env` | yes |  |
| `data[].rate_now.at` | string (date-time) | yes |  |
| `data[].spread_bps` | integer \| null |  | `USDT` row only: basis points taken off the rate (5 = 0.05 %). |
| `data[].min` | string \| null |  | `USDT` row only: smallest credited USDT deposit; smaller ones are held (`held_below_min`). |
| `data[].max` | string \| null |  | `USDT` row only: largest auto-credited USDT deposit; larger ones are held (`held_for_review`). |
| `retired` | array of object |  | Addresses this account used before, newest first. |
| `retired[].address` | string | yes | Base58Check TRON address (starts with `T`, 34 characters). |
| `retired[].retired_at` | string (date-time) | yes |  |
| `retired[].credited_until` | string (date-time) | yes | A transfer to this address in a block after this instant is not credited automatically; contact support. |
| `rotation` | object |  | Who may replace the address, and how often. |
| `rotation.by` | enum: `support` | yes |  |
| `rotation.min_interval_days` | integer | yes |  |
| `rotation.max_per_window` | integer | yes |  |
| `rotation.window_days` | integer | yes |  |
| `rotation.retired_credit_days` | integer | yes |  |

Example `200` response, from the contract (illustrative values — live numbers come from the API):

```json
{
  "data": [
    {
      "currency": "TRX",
      "address": "TDepositAddressExample1111111111111",
      "memo": null,
      "confirmations_required": 19
    }
  ],
  "retired": [
    {
      "address": "TRetiredAddressExample111111111111",
      "retired_at": "2026-09-20T10:00:00.000Z",
      "credited_until": "2026-10-20T10:00:00.000Z"
    }
  ],
  "rotation": {
    "by": "support",
    "min_interval_days": 7,
    "max_per_window": 3,
    "window_days": 90,
    "retired_credit_days": 30
  }
}
```

## The account's ledger statement

`GET /v1/ledger` · `listLedger`

**Auth:** Dashboard session cookie + `X-CSRF-Token`.

Every journal that moved this account's balance, newest first: deposits, order charges,
refunds and the rest. `amount_sun` / `amount_usdt` are the net effect on the balance —
positive when it went up. `kind=deposit` is the deposit history of the top-up screen.

A deposit appears here once it is **credited**, i.e. after `confirmations_required`
block confirmations; a transfer still confirming is not listed yet. `sender` and
`block_number` are `null` on deposits credited before 2026-09-25.

**Dashboard session only** (member role `viewer` or above). No API-key scope opens it:
the ledger shows senders and transaction ids that no key-readable endpoint shows today,
and extending `balance.read` to it is an access decision that has not been taken.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `kind` | query | enum: `deposit`, `all` |  |  |
| `limit` | query | integer |  |  |
| `cursor` | query | string |  | Opaque cursor from a previous response's `next_cursor`. |

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | Authenticated, but this key is not allowed to do it. |
| `500` | Something broke on our side. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of object (LedgerEntry) | yes |  |
| `data[].id` | string | yes | The ledger journal id. |
| `data[].kind` | enum: `deposit`, `order_charge`, `order_refund`, `referral_payout`, `subscription_fee`, `subscription_usage`, `reserve_fee`, `invoice`, `withdrawal`, `adjustment` | yes |  |
| `data[].amount_sun` | integer (int64) | yes | Net effect on the TRX balance in SUN; negative for a charge. |
| `data[].amount_usdt` | integer (int64) | yes | Net effect on the USDT balance in the token's smallest unit. |
| `data[].order_id` | string \| null | yes | The order a charge or refund belongs to. |
| `data[].deposit` | null \| object | yes | Set for `kind = deposit`, `null` otherwise. |
| `data[].deposit.txid` | string \| null | yes | The deposit transaction hash. |
| `data[].deposit.sender` | string \| null | yes | The address the TRX came from; `null` on deposits credited before 2026-09-25. |
| `data[].deposit.block_number` | integer \| null | yes |  |
| `data[].deposit.status` | enum: `credited`, `pending_rate`, `held_below_min`, `held_for_review` | yes | `credited` for every journal. With `kind=deposit`, USDT deposits the worker saw but did not credit are listed too (first page only): `held_below_min`, `held_for_review` or `pending_rate`, with `amount_sun` 0 — nothing was credited. |
| `data[].deposit.confirmations_required` | integer | yes |  |
| `data[].deposit.asset` | enum: `TRX`, `USDT` |  |  |
| `data[].deposit.usdt_amount` | string \| null |  | USDT received (decimal string); `null` for TRX. |
| `data[].deposit.rate` | string \| null |  | SUN credited per 1 USDT, after the spread; `null` for TRX and held rows. |
| `data[].deposit.rate_source` | string \| null |  | Where the rate came from (`fixed_env`, `sunswap_v3@<block>`, ...); `null` for TRX. |
| `data[].deposit.spread_bps` | integer \| null |  |  |
| `data[].created_at` | string (date-time) | yes |  |
| `next_cursor` | string \| null | yes |  |

Example `200` response, from the contract (illustrative values — live numbers come from the API):

```json
{
  "data": [
    {
      "id": "jrn_01K5Y8Q2V9M3ZP4T7R1A2B3C4D",
      "kind": "order_charge",
      "amount_sun": -3900000,
      "amount_usdt": 0,
      "order_id": "ord_01K5Y8Q2V9M3ZP4T7R1A2B3C4E",
      "deposit": null,
      "created_at": "2026-09-25T10:12:04.000Z"
    },
    {
      "id": "jrn_01K5Y7A1B2C3D4E5F6G7H8J9K0",
      "kind": "deposit",
      "amount_sun": 50000000,
      "amount_usdt": 0,
      "order_id": null,
      "deposit": {
        "txid": "9f3c1e7a2b4d6f8091a3c5e7f9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7",
        "sender": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
        "block_number": 61000123,
        "status": "credited",
        "confirmations_required": 19
      },
      "created_at": "2026-09-25T09:58:40.000Z"
    }
  ],
  "next_cursor": null
}
```

## The account's address book

`GET /v1/account/addresses` · `listAddressBook`

**Auth:** Dashboard session cookie + `X-CSRF-Token`.

Receivers this account saved for reuse, newest first. At most `limit` (100) entries.

**Dashboard session only** (member role `viewer` or above). No API-key scope opens it
yet; opening it to `balance.read` keys is an access decision that has not been taken.

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `401` | Missing, malformed or rejected credentials. |
| `403` | Authenticated, but this key is not allowed to do it. |
| `500` | Something broke on our side. |

#### Response fields

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of object (AddressBookEntry) | yes |  |
| `data[].id` | string | yes |  |
| `data[].label` | string | yes |  |
| `data[].address` | string | yes | Base58Check TRON address (starts with `T`, 34 characters). |
| `data[].created_at` | string (date-time) | yes |  |
| `data[].updated_at` | string (date-time) | yes |  |
| `limit` | integer | yes |  |
| `used` | integer | yes |  |

Example `200` response, from the contract (illustrative values — live numbers come from the API):

```json
{
  "data": [
    {
      "id": "adr_01K5Y9B7C8D9E0F1G2H3J4K5M6",
      "label": "Payouts hot wallet",
      "address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
      "created_at": "2026-09-25T10:20:00.000Z",
      "updated_at": "2026-09-25T10:20:00.000Z"
    }
  ],
  "limit": 100,
  "used": 1
}
```

## Save an address

`POST /v1/account/addresses` · `saveAddressBookEntry`

**Auth:** Dashboard session cookie + `X-CSRF-Token`.

Adds `address` under `label`. The address is unique per account: saving one that is
already in the book replaces its label, answers `200` with the existing entry and uses
no slot. A new address on a full book is `3019 address_book_full`.

**Dashboard session**, member role `editor` or above, with `X-CSRF-Token`.

### Request body

JSON (`AddressBookRequest`), required.

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | string | yes | Trimmed; 1–64 characters after trimming. |
| `address` | string | yes | Base58Check TRON address (starts with `T`, 34 characters). |

Example request body, from the contract (illustrative values):

```json
{"label":"Payouts hot wallet","address":"TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE"}
```

### Responses

| Status | Meaning |
|---|---|
| `200` | The address was already saved; its label was replaced. |
| `201` | Saved as a new entry. |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | Authenticated, but this key is not allowed to do it. |
| `409` | `3019 address_book_full` — `details.limit`, `details.used`. |
| `500` | Something broke on our side. |

#### Response fields (`AddressBookEntry`)

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `label` | string | yes |  |
| `address` | string | yes | Base58Check TRON address (starts with `T`, 34 characters). |
| `created_at` | string (date-time) | yes |  |
| `updated_at` | string (date-time) | yes |  |

## Remove an address

`DELETE /v1/account/addresses/{entryId}` · `deleteAddressBookEntry`

**Auth:** Dashboard session cookie + `X-CSRF-Token`.

Frees the slot immediately. An id of another account answers `404`, like an id that
does not exist. **Dashboard session**, member role `editor`, with `X-CSRF-Token`.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `entryId` | path | string | yes |  |

### Responses

| Status | Meaning |
|---|---|
| `204` | Removed. No body. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | Authenticated, but this key is not allowed to do it. |
| `404` | No such object, or it belongs to another account. The two are not distinguished. |
| `500` | Something broke on our side. |

## Order analytics, aggregated server-side

`GET /v1/account/stats` · `getAccountStats`

**Auth:** API key (HMAC) or dashboard session cookie + `X-CSRF-Token`.

What the Analytics screen draws, computed next to the data rather than by summing
orders in the page. Covers orders **created** in `[from, to)`:

| field | what is counted |
|---|---|
| `orders` | every order, whatever its status |
| `energy` | delivered energy of energy orders that reached the chain (`delegated` … `reclaimed`): `delivered_amount`, else `amount`; failed and refunded orders add none |
| `spent_sun` | `total_amount_sun − refunded_amount_sun` of every order, activation included |
| `avg_price_sun_per_65k` | the unit price those energy orders were sold at, weighted by delivered energy, × 65,000, rounded to a whole SUN; `null` without energy |

Days and hours are UTC shifted by `utc_offset_minutes`, so the page can ask for its
visitor's calendar. `series` has one row per day of the range, zeros included.
`weekday_hour[d][h]` counts orders on ISO weekday `d + 1` (Monday first) at hour `h`.

**Retention: 13 months.** A `from` older than that is clamped to the retention start;
`clamped` is `true` and `requested_from` echoes what was asked. Defaults: `to` = now,
`from` = `to` − 30 days. `to` must be after `from` (`2001` otherwise).

API key scope **`orders.read`** (the aggregates are sums over the orders that scope
already lists), or a dashboard session with member role `viewer`.

### Parameters

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `from` | query | string (date-time) |  |  |
| `to` | query | string (date-time) |  |  |
| `bucket` | query | enum: `day` |  |  |
| `utc_offset_minutes` | query | integer |  |  |

### Responses

| Status | Meaning |
|---|---|
| `200` | OK |
| `400` | Malformed request — bad JSON, unknown field, wrong type. |
| `401` | Missing, malformed or rejected credentials. |
| `403` | Authenticated, but this key is not allowed to do it. |
| `429` | Too many requests. |
| `500` | Something broke on our side. |

#### Response fields (`AccountStats`)

| Field | Type | Required | Description |
|---|---|---|---|
| `from` | string (date-time) | yes | The start actually used |
| `to` | string (date-time) | yes |  |
| `requested_from` | string (date-time) | yes |  |
| `clamped` | boolean | yes | True when `from` was older than the retention window and was moved forward. |
| `retention_months` | integer | yes |  |
| `bucket` | enum: `day` | yes |  |
| `utc_offset_minutes` | integer | yes |  |
| `totals` | object | yes |  |
| `totals.orders` | integer | yes |  |
| `totals.energy` | integer | yes |  |
| `totals.spent_sun` | integer (int64) | yes | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `totals.avg_price_sun_per_65k` | integer \| null | yes |  |
| `series` | array of object | yes |  |
| `series[].date` | string (date) | yes |  |
| `series[].orders` | integer | yes |  |
| `series[].energy` | integer | yes |  |
| `series[].spent_sun` | integer (int64) | yes | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |
| `weekday_hour` | array of array of integer | yes | Seven rows (Monday first) of 24 hourly order counts. |
| `top_receivers` | array of object | yes |  |
| `top_receivers[].receiver` | string | yes | Base58Check TRON address (starts with `T`, 34 characters). |
| `top_receivers[].orders` | integer | yes |  |
| `top_receivers[].energy` | integer | yes |  |
| `top_receivers[].spent_sun` | integer (int64) | yes | An amount in SUN (1 TRX = 1 000 000 SUN). Always an integer. |

Example `200` response, from the contract (illustrative values — live numbers come from the API):

```json
{
  "from": "2026-09-23T00:00:00.000Z",
  "to": "2026-09-25T00:00:00.000Z",
  "requested_from": "2026-09-23T00:00:00.000Z",
  "clamped": false,
  "retention_months": 13,
  "bucket": "day",
  "utc_offset_minutes": 0,
  "totals": {"orders":3,"energy":196000,"spent_sun":11760000,"avg_price_sun_per_65k":3900000},
  "series": [
    {"date":"2026-09-23","orders":1,"energy":65000,"spent_sun":3900000},
    {"date":"2026-09-24","orders":2,"energy":131000,"spent_sun":7860000}
  ],
  "weekday_hour": [[0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0]],
  "top_receivers": [
    {
      "receiver": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
      "orders": 2,
      "energy": 131000,
      "spent_sun": 7860000
    }
  ]
}
```
