API Conventions
Every endpoint on api.superhero.com is plain JSON over HTTPS. The rules below (base URLs,
pagination, shared query parameters, units, errors, rate limits and the signature-based write
flow) apply everywhere and are not repeated on the endpoint pages.
Base URLs
| Network | Base URL | Chain id |
|---|---|---|
| Mainnet | https://api.superhero.com | ae_mainnet |
| Testnet | Not publicly documented | ae_uat |
Both hosts serve the same paths. Every path is relative to the base and begins with /api/. The
only versioned prefix is /api/v2/mdw/*, the indexed chain mirror. See
Node & Middleware.
Outside production the server also serves Swagger UI at /api and the OpenAPI 3 document at
/api-json.
Authentication
Reads need no credentials. No API key, bearer token, cookie or session:
curl -s "https://api.superhero.com/api/tokens?limit=3&order_by=market_cap&order_direction=DESC"Writes are authorised by wallet signature. There is no login endpoint and no token to store. Every mutating flow is three steps:
POSTthe challenge endpoint with yourak_...address. It returns anonce(orverification_token), a human-readablemessageand an expiry.- Sign
messagewith the private key for that address. POSTthe submit endpoint with the nonce, the signature and the value you are claiming.
The server verifies the signature against the address in the request. Endpoints that use this pattern:
| Flow | Challenge | Submit |
|---|---|---|
| Link an X handle | POST /api/address-links/x/claim | POST /api/address-links/x/submit |
| Link a bio | POST /api/address-links/bio/claim | POST /api/address-links/bio/submit |
| Link a website | POST /api/address-links/site/claim | POST /api/address-links/site/submit |
Set a preferred .chain name | POST /api/address-links/prefered-aens-name/claim | POST /api/address-links/prefered-aens-name/submit |
| Link a Nostr key | POST /api/address-links/nostr/claim | POST /api/address-links/nostr/submit |
Sponsored .chain registration | POST /api/profile/chain-name/challenge | POST /api/profile/chain-name/claim |
| X-posting reward rescan | POST /api/profile/x-posting-reward/recheck-challenge | POST /api/profile/{address}/x-posting-reward/recheck |
| Push-notification device | POST /api/notifications/devices/challenge | POST /api/notifications/devices |
| Notification preferences | POST /api/notifications/preferences/challenge | POST /api/notifications/{address}/preferences |
Each claim flow has a matching unclaim and unclaim/submit pair, signed the same way. See
Profile for request bodies.
The notification feed uses a bearer session token. One wallet signature is exchanged for a
token, which is then sent as a normal Authorization: Bearer header:
POST /api/notifications/{address}/feed/challenge -> { nonce }
sign the message -> signature
POST /api/notifications/{address}/feed/session -> { token, expiresAt }
GET /api/notifications/{address}/feed?limit=50 -> Authorization: Bearer {token}GET /api/notifications/web-push/vapid-public-key is public and needs no token.
Two endpoints take an API key. POST /api/trending-tags and
PATCH /api/dex/tokens/{address}/listed accept it as either header:
x-api-key: YOUR_KEY
Authorization: Bearer YOUR_KEYA missing or wrong key is a 401.
POST /api/affiliations takes no credential. It accepts a sender_address and a list of
codes and returns a generated ten-character affiliation code. Its sibling
POST /api/affiliations/invites/{code}/{provider}/{access_code} verifies access_code with
GitHub, Google or X before handing out a private invite code.
Pagination
Most list endpoints return items plus meta:
{
"items": [],
"meta": {
"currentPage": 1,
"itemCount": 20,
"itemsPerPage": 20,
"totalItems": 4213,
"totalPages": 211
}
}| Field | Type | Meaning |
|---|---|---|
currentPage | number | The page just returned (1-based) |
itemCount | number | Records in this response |
itemsPerPage | number | The effective page size |
totalItems | number | Total matching records |
totalPages | number | Total pages at this page size |
Pass page (1-based) and limit to walk it.
The limit cap is 100
100 is both the default and the maximum. What happens when you exceed it depends on the endpoint family:
| Endpoint family | limit=101 | page=0 |
|---|---|---|
/api/tokens, /api/accounts, /api/posts, /api/trending-tags, /api/tips, /api/invitations and the other list endpoints | 400: Limit must be between 1 and 100 (wording varies per endpoint) | 400: Page must be greater than or equal to 1 |
/api/v2/mdw/* | 400: Maximum limit is 100 items per request | 400: Page must be greater than or equal to 1 |
/api/dex/* lists | Silently clamped to 100 | Silently clamped to 1 |
Read meta.itemsPerPage rather than assuming your limit was honoured. page on
/api/v2/mdw/* is capped at 1,000,000.
Invalid order_by
Most list endpoints hold an allow-set and return 400 Invalid order_by value: {value} for
anything outside it. GET /api/tokens is the exception: an unrecognised order_by falls back
to market_cap and an unrecognised order_direction to DESC, with no error.
Responses that are not the envelope
| Endpoint | Shape |
|---|---|
GET /api/tokens/{address}/history | A bare JSON array. Still takes page and limit; you have reached the end when a page comes back empty |
GET /api/accounts/{address}/portfolio/history | A bare JSON array of snapshots, bounded by startDate, endDate and interval rather than by pages |
GET /api/accounts/leaderboard | items plus a different meta: page, limit, totalItems, totalPages, window, sortBy, sortDir and timeFilter. Note page, not currentPage |
GET /api/feed | Keyset pagination: items plus next_cursor. Pass the cursor back as cursor; there is no page number |
The analytics series endpoints return bare arrays and the totals endpoints return a scalar or a small object. See Analytics.
Shared query parameters
| Parameter | Applies to | Notes |
|---|---|---|
page | list endpoints | 1-based |
limit | list endpoints | Capped at 100 |
order_by | most list endpoints | Enum per endpoint; the value set is not shared |
order_direction | most list endpoints | ASC or DESC |
search | tokens, posts, topics, accounts, DEX tokens, DEX pairs | Substring match, server-side |
convertTo | price and history endpoints | See below |
interval | history endpoints | Seconds, not a keyword |
includes | /api/transactions | Only token, which inlines the token object |
Parameter casing
Query keys are mostly snake_case, with exceptions. An unrecognised key returns unfiltered data rather than an error, so match the spelling per endpoint:
| Spelling | Used by |
|---|---|
start_date / end_date | All /api/analytics/*, GET /api/tokens/{address}/transactions, /api/bcl-affiliation/analytics* |
startDate / endDate | /api/accounts/leaderboard, all /api/accounts/{address}/portfolio/* |
from_date / to_date | GET /api/dex/transactions |
convertTo | Every endpoint that takes it |
sortBy / sortDir | /api/accounts/leaderboard only. Everything else uses order_by / order_direction |
Currency conversion
Endpoints that accept convertTo take one of ae (default), usd, eur, aud, brl, cad,
chf, gbp, xau. The same nine keys make up the PriceDto object returned wherever a value
has a price:
{ "ae": 0.0421, "usd": 0.0013, "eur": 0.0011, "aud": 0.002,
"brl": 0.0071, "cad": 0.0018, "chf": 0.001, "gbp": 0.00097, "xau": 0.0000004 }Live AE fiat rates come from GET /api/coins/aeternity/rates, a flat map of lowercase currency
code to number.
Units
The API mixes two conventions. Values in trade records and OHLC candles are in whole units (AE,
whole tokens). Values read live from a contract (a token's market_cap, total_supply and
dao_balance) are raw (aettos, base units).
| Field family | Unit | Conversion |
|---|---|---|
price, sell_price, and every PriceDto derived from them (price_data, sell_price_data, performance current / high / low) | AE | none |
Everything on /api/transactions: volume, total_supply, protocol_reward, and the amount, unit_price, buy_price, previous_buy_price, market_cap price objects | whole tokens / AE | none |
TokenDto.market_cap, market_cap_data, dao_balance | aettos | divide by 1e18 |
TokenDto.total_supply, circulating_supply, holder balance | token base units | divide by 10 raised to decimals (18 for every token the factory mints) |
Invitation amount, and the total_amount_ae aggregates built from it | AE | none |
market_cap therefore means two different scales depending on where you read it: on a token it
is aettos, on a transaction or an OHLC candle it is AE. They differ by exactly 1e18.
Every key of a PriceDto is on the same scale as its ae key. Each fiat key is
ae * rate[code], not a re-denomination: if ae is in aettos, so is usd. When rates are
unavailable every key except ae is null.
decimals is a string in TokenDto, not a number. Parse it before using it as an exponent.
Most numeric token fields are strings for the same reason: they are big integers that do not
survive a double.
Errors
Failures are ordinary HTTP status codes with a JSON body carrying statusCode, message and
error.
| Status | When you will see it |
|---|---|
400 | Invalid parameters: limit or page out of range, an order_by outside the allow-set, a malformed ak_/ct_/th_ argument, a search longer than 100 characters, a start date after an end date, or an unknown property in a validated request body |
401 | A missing or wrong API key on the two key-guarded endpoints, or a missing or expired feed session token |
404 | Unknown token, post, account or name |
429 | The per-IP rate limit on the routes listed below; and POST /api/profile/{address}/x-posting-reward/recheck, which enforces a once-per-24h cap and returns a body carrying nextAllowedAt |
Request bodies and DTO-bound query parameters (the leaderboard, portfolio history) are validated
strictly: an unknown property is a 400, and declared types are coerced ("5" becomes 5).
Individually bound query parameters (most list endpoints) ignore unrecognised keys silently.
Rate limits
| Property | Value |
|---|---|
| Allowance | 10 requests per minute |
| Window | 60 seconds, fixed, starting at your first request |
| Bucket key | client IP plus route path, per route, not per API |
| Response when exceeded | 429 |
| Body | { "status": 429, "message": "Rate limit exceeded. Maximum 10 requests per minute.", "retryAfter": <seconds> } |
retryAfter is a field in the JSON body, in seconds. There is no Retry-After header and no
X-RateLimit-* headers. Because the key includes the route path, the budget is spent separately
on each guarded endpoint.
Guarded routes
| Route |
|---|
GET and GET {id} on /api/v2/mdw/txs, /api/v2/mdw/micro-blocks, /api/v2/mdw/key-blocks, /api/v2/mdw/sync-state, /api/v2/mdw/plugin-sync-state |
GET /api/announcements |
POST /api/notifications/devices/challenge, POST /api/notifications/devices, DELETE /api/notifications/devices |
POST /api/notifications/preferences/challenge, GET and POST /api/notifications/{address}/preferences |
POST /api/notifications/{address}/feed/challenge, POST /api/notifications/{address}/feed/session |
GET /api/notifications/web-push/vapid-public-key |
GET /api/profile/{address}/x-posting-reward, POST /api/profile/x-posting-reward/recheck-challenge, POST /api/profile/{address}/x-posting-reward/recheck, POST /api/profile/{address}/x-reward/referral-link |
POST /api/profile/chain-name/challenge, POST /api/profile/chain-name/claim |
/api/rooms/* and the token-gated-room recheck and metrics routes |
Everything else (tokens, transactions, accounts, analytics, DEX, posts, trending tags, affiliations, search and feed) is unthrottled. Poll considerately and prefer the WebSocket for change notification.
CORS
ALLOWED_ORIGINS | Access-Control-Allow-Origin | Credentials |
|---|---|---|
| unset or empty (the default) | every origin | disabled |
* | every origin | disabled |
| a comma-separated list | just those origins | enabled |
Allowed methods are GET, POST, PUT, PATCH, DELETE, OPTIONS. A browser page on any
origin can call api.superhero.com directly. The same setting governs the Socket.IO gateways.
Responses are gzipped.
Caching
Several endpoints are served from an in-process cache rather than recomputed per request:
| Endpoint | Cached for |
|---|---|
GET /api/tokens with order_by=trending_score | 15 minutes |
GET /api/tokens, any other sort | 60 seconds |
GET /api/tokens/{address} | 3 seconds |
GET /api/tokens/{address}/trending-eligibility | 3 seconds |
GET /api/tokens/{address}/holders, .../rankings | 10 seconds |
GET /api/tokens/{address}/performance, .../performance-raw | 60 seconds |
GET /api/accounts | 60 seconds |
GET /api/feed | 15 seconds |
GET /api/analytics/total-unique-users | 20 seconds |
GET /api/accounts/leaderboard | up to 60 seconds. meta.timeFilter.start / .end are the times the cache entry was filled |
Some values are recomputed on a schedule: token performance and rank every five minutes, the
daily analytics rows hourly, AE fiat rates every ten minutes, and trending scores as described on
Trending.
Versioning and deprecation
There is no version negotiation. The OpenAPI document declares version 1.0, GET /api/stats
reports the deployed package version as apiVersion, and no header or path segment selects a
version. /api/v2/mdw/* is a name inherited from the middleware's own API.
Deprecation is by annotation. A retired parameter keeps validating and stops having an effect:
| What | Status |
|---|---|
GET /api/contracts | Deprecated. Still returns a single-element array naming the community factory. Use GET /api/factory |
includeOnChain on both profile read endpoints | Accepted and ignored; profiles are served from indexed data |
window on GET /api/posts/popular | Accepted and ignored; popular ranking is all-time |
points and maxCandidates on GET /api/accounts/leaderboard | Accepted and ignored |
Related
- API Reference: the full endpoint index
- Tokens API: the
TokenDtothese unit rules apply to, field by field - Accounts API: the portfolio and leaderboard endpoints with the camelCase date parameters
- Profile API: the challenge/signature write flow end to end
- Node & Middleware: when the REST API does not have the data