Signup — API reference
Creating an account without a credential yet: the address challenge, its verification, account creation, and reading the deposit address before the first API key exists.
| Method | Path | Summary |
|---|---|---|
POST | /v1/accounts/challenge | Issue a signing challenge for a TRON address |
POST | /v1/accounts/challenge/verify | Verify a signed challenge and get a bootstrap token |
POST | /v1/accounts | Create an account |
GET | /v1/accounts/deposit-address | Deposit address before the first API key exists |
Generated from openapi.yaml at build time. Base URL https://api.tenergy.me/v1, or https://api-nile.tenergy.me/v1 on Nile (Environments). Every request below is signed as in Authentication unless its Auth line says otherwise.
Issue a signing challenge for a TRON address
POST /v1/accounts/challenge · createAccountChallenge
Auth: Public — no credentials.
Step 1 of the address-challenge signup and recovery model — the platform’s one signup model: ownership of an account is proved by signing a nonce with a TRON address, off our infrastructure. We never see a private key, and there is nothing here to phish — the signature proves control of the address and nothing else.
The returned message is human-readable and domain-bound (brand, purpose, nonce,
expiry) so that whoever signs it in a wallet can see what they are agreeing to. Sign it
with a TIP-191-style personal-sign; the signature must recover address.
Anonymous, rate-limited per IP and per address. Calling it for an address that already has an account is indistinguishable from calling it for one that does not — this endpoint is deliberately not an account-existence oracle.
Request body
JSON (AccountChallengeRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
address | string | yes | Base58Check TRON address (starts with T, 34 characters). |
purpose | enum: signup, login, recovery | What the signature will be used for; it appears in the signed message. |
Example request body, from the contract (illustrative values):
{"address":"TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE","purpose":"signup"}
Responses
| Status | Meaning |
|---|---|
201 | Challenge issued. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
429 | Too many requests. |
500 | Something broke on our side. |
Response fields (AccountChallenge)
| Field | Type | Required | Description |
|---|---|---|---|
nonce | string | yes | Single-use |
address | string | yes | Base58Check TRON address (starts with T, 34 characters). |
purpose | enum: signup, login, recovery | ||
message | string | yes | The exact string to sign, TIP-191 personal-sign style. Domain-bound and human-readable, so that a person signing it in a wallet can see what it says. Sign these bytes verbatim — do not re-wrap, trim or re-encode them. |
issued_at | string (date-time) | yes | |
expires_at | string (date-time) | yes | Ten minutes after issue. An expired nonce gives 1010 challenge_invalid. |
Example 201 response, from the contract (illustrative values — live numbers come from the API):
{
"nonce": "9f2a7c1e4b6d8a0f3e5c7b9d1f3a5c7e",
"address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"purpose": "signup",
"message": "tenergy.me wants you to prove control of this address.\nPurpose: signup\nAddress: TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE\nNonce: 9f2a7c1e4b6d8a0f3e5c7b9d1f3a5c7e\nExpires: 2026-09-11T18:14:05.123Z\nSigning this creates no transaction and moves no funds.\n",
"issued_at": "2026-09-11T18:04:05.123Z",
"expires_at": "2026-09-11T18:14:05.123Z"
}
Verify a signed challenge and get a bootstrap token
POST /v1/accounts/challenge/verify · verifyAccountChallenge
Auth: Public — no credentials.
Step 2. Checks that signature recovers address over the challenge message and
returns a short-lived bootstrap token — the credential that carries the caller from
“no account” to “first API key”, and the answer to the contract’s long-standing gap of
having no way to authenticate the calls that precede a key.
The bootstrap token is sent as Authorization: Bearer abt_… and opens exactly four
operations, nothing else: POST /v1/accounts, GET /v1/accounts/deposit-address,
GET /v1/account and POST /v1/api-keys. It expires in 15 minutes, is single-account,
and is never a substitute for an API key.
If the address already has an account on this brand, account_id and account_status
are returned — to this caller only, because only this caller proved the signature.
A nonce is single-use. A reused, expired or wrongly-signed nonce is one error,
1010 challenge_invalid, whatever went wrong, so that failures reveal nothing about
which addresses exist.
Request body
JSON (AccountChallengeVerifyRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
address | string | yes | Base58Check TRON address (starts with T, 34 characters). |
nonce | string | yes | |
signature | string | yes | Hex signature over message. Must recover address; 0x prefix optional. |
Example request body, from the contract (illustrative values):
{
"address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"nonce": "9f2a7c1e4b6d8a0f3e5c7b9d1f3a5c7e",
"signature": "1c3e5b7d9f1a3c5e7b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1a3c5e7b9d1f3a5c7e9b1d3f5a7c9e1b3d5f7a9c1e3b5d7f9a1c3e5b"
}
Responses
| Status | Meaning |
|---|---|
200 | Signature valid. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
401 | 1010 challenge_invalid — unknown, expired or already-used nonce, or a signature that does not recover the address. Take a fresh challenge. |
429 | Too many requests. |
500 | Something broke on our side. |
Response fields (BootstrapToken)
| Field | Type | Required | Description |
|---|---|---|---|
bootstrap_token | string | yes | Send as Authorization: Bearer abt_…. Fifteen-minute lifetime, four permitted operations, no ordering and no spending. Store it no longer than the signup flow. |
expires_at | string (date-time) | yes | |
account_id | string | null | The account this address already owns, or null when there is none yet. | |
account_status | enum: unfunded, active, suspended, closed | null |
Example 200 response, from the contract (illustrative values — live numbers come from the API):
{
"bootstrap_token": "abt_3f8c2d1e9b7a4c6e8f0a2b4d6e8f0a2b",
"expires_at": "2026-09-11T18:19:05.123Z",
"account_id": "acc_01J9Z4K2M7Q8",
"account_status": "active"
}
Create an account
POST /v1/accounts · createAccount
Auth: Bootstrap token or public.
Step 3. Creates the account owned by the signing address and returns it together with
its deposit address. The account is created in status: "unfunded": it exists, it can
be read, it can receive money and it can issue API keys; it cannot spend until a
deposit confirms.
Authenticate either with the bootstrap token from
POST /v1/accounts/challenge/verify, or by passing nonce + signature in the body
for a one-shot create. Both prove the same thing.
Idempotent by address. If the address already owns an account on this brand, the
existing account is returned with HTTP 200 and nothing is created.
email is optional and is not verified here — it is a recovery and invoicing channel,
attached later from the dashboard. Email magic-link and Telegram login are additional
human logins on the same account object, not a second signup model.
API keys do not wait for a deposit (since 2026-09-26). The balance, not
the credential, holds spending back: POST /v1/orders answers 4001 insufficient_funds
until the ledger has a confirmed credit.
Request body
JSON (AccountCreateRequest), required.
| Field | Type | Required | Description |
|---|---|---|---|
address | string | yes | Base58Check TRON address (starts with T, 34 characters). |
nonce | string | ||
signature | string | ||
email | string (email) | null | Optional recovery and invoicing contact. Not verified here and not required; an account with no recovery channel is a support incident waiting to happen, so the dashboard nudges for one later. | |
label | string | null |
Example request body, from the contract (illustrative values):
{"address":"TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE"}
Responses
| Status | Meaning |
|---|---|
200 | The address already owns an account; the existing one is returned. |
201 | Account created, unfunded. |
400 | Malformed request — bad JSON, unknown field, wrong type. |
401 | 1010 challenge_invalid, or 1011 bootstrap_token_expired. |
422 | Syntactically valid but semantically impossible. |
429 | Too many requests. |
500 | Something broke on our side. |
Response fields (AccountCreated)
| Field | Type | Required | Description |
|---|---|---|---|
account_id | string | yes | |
brand | string | yes | |
network | enum: mainnet, nile | yes | |
status | enum: unfunded, active, suspended, closed | yes | |
owner_address | string | yes | The address whose signature owns this account and can recover it. |
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). | |
next | object | What the caller should do next, machine-readable, because an agent that cannot infer the next step will invent one. | |
next.action | string | ||
next.address | string | Base58Check TRON address (starts with T, 34 characters). | |
next.reason | string | ||
created_at | string (date-time) | yes |
Deposit address before the first API key exists
GET /v1/accounts/deposit-address · getSignupDepositAddress
Auth: Bootstrap token.
The address to fund so that the account becomes active and can issue a key. Readable
with the bootstrap token, so an agent that has just created an account can report the
address and the minimum to its user without holding any long-lived credential.
Once a key exists, use GET /v1/deposit-addresses, which returns the same addresses.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
currency | query | enum: TRX, USDT |
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 |
|---|---|---|---|
account_id | string | yes | |
status | enum: unfunded, active, suspended, closed | yes | |
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). | |
min_deposit_sun | integer (int64) | Below this a transfer is held as an unclaimed residual rather than credited. The value is a brand parameter. |
Example 200 response, from the contract (illustrative values — live numbers come from the API):
{
"account_id": "acc_01J9Z4K2M7Q8",
"status": "unfunded",
"data": [
{
"currency": "TRX",
"address": "TDepositAddressExample1111111111111",
"memo": null,
"confirmations_required": 19
}
],
"min_deposit_sun": 1000000
}