Profile API
The write endpoints on api.superhero.com take no password, API key or session cookie. Every
write is authorised by a wallet signature over a server-issued challenge: request a
challenge, sign it with the key that owns the address, post the signature back.
All paths are relative to https://api.superhero.com.
Reading a profile
| Method & path | Query | Returns |
|---|---|---|
GET /api/profile/{address} | None | One profile aggregate |
GET /api/profile | addresses | A bare array of profile aggregates |
GET /api/profile/feed | limit, offset | A page of cached profiles with its own envelope |
GET /api/accounts/{address} is the primary read for a linked X handle, bio, website and
preferred .chain name, and its links block is authoritative. GET /api/profile/{address} fills
in the remaining display fields. See Accounts.
curl -s "https://api.superhero.com/api/profile/ak_2ruXgsLy9jMwEqsgyQgEsxw8chYDfv2QyBfCsR6qtpQYkektWB"| Field | Type | Meaning |
|---|---|---|
address | string | The ak_... |
public_name | string | Resolved display name. Never null: it falls back to the address |
profile.fullname | string | Display name from the legacy profile cache. "" when unset |
profile.bio | string | The verified links.bio if present, else the cached bio, else "" |
profile.site | string or null | The verified links.site |
profile.avatarurl | string | "" when unset |
profile.username | string or null | Legacy custom username |
profile.prefered_aens_name | string or null | The verified links.prefaens. Note the one-r spelling |
profile.x_username | string or null | The verified links.x |
profile.chain_name | string or null | The account's chain_name first, then the cached copy |
profile.chain_expires_at | string or null | When the .chain name expires |
profile.followers_count / following_count | number | Single-profile read only, and only when the social-graph feature is enabled |
The display name resolves as:
public_name = prefered_aens_name || chain_name || username || addressAn X handle never becomes the display name. fullname, avatarurl and username come from a
legacy profile cache; the account's chain_name is refreshed hourly and is the live one.
includeOnChain is accepted on both read endpoints and ignored; profiles are served from indexed
data.
The batch form
GET /api/profile?addresses=ak_a,ak_b returns a plain JSON array with no items wrapper:
one element per unique address, in the order supplied, after trimming and de-duplication.
Addresses with no cached profile still get an element with the empty-string defaults above.
| Rule | Behaviour |
|---|---|
| More than 100 addresses | 400 addresses must contain at most 100 entries |
| Any malformed address | 400 addresses contains invalid account address ak_..., and the whole call fails |
Empty or missing addresses | [] |
curl -s "https://api.superhero.com/api/profile?addresses=ak_ONE,ak_TWO"GET /api/profile/feed returns { items, pagination }, where pagination carries limit,
offset and count. limit defaults to 20 and offset to 0.
The write pattern
Every write endpoint is one of two shapes:
claim -> server returns { message, nonce, value, verification_token }
sign -> wallet signs `message`
submit -> server verifies, returns { txHash }or, for the reward and sponsorship flows:
challenge -> server returns { nonce, expires_at, message }
sign -> wallet signs `message`
submit -> server verifies, performs the actionThe nonce is single-use and the challenge expires.
Address links
An address link is a verified statement that an ak_... owns something off-chain: an X
handle, a bio, a website, a preferred .chain name, or a Nostr key. Five families, each with
the same four endpoints.
| Link | Base path |
|---|---|
| X handle | /api/address-links/x |
| Bio | /api/address-links/bio |
| Website | /api/address-links/site |
Preferred .chain name | /api/address-links/prefered-aens-name |
| Nostr key | /api/address-links/nostr |
The path is spelled prefered-aens-name, with one r. Each base path has POST {base}/claim,
POST {base}/submit, POST {base}/unclaim and POST {base}/unclaim/submit.
Claim
For bio, site, preferred name and Nostr, the body is the address plus the value you are claiming:
curl -s -X POST "https://api.superhero.com/api/address-links/bio/claim" \
-H "Content-Type: application/json" \
-d '{"address":"ak_YOUR_ADDRESS","value":"Building things on aeternity."}'| Field | Type | Meaning |
|---|---|---|
message | string | The exact text the wallet must sign |
nonce | number | The address's current nonce on the AddressLink contract, echoed back on submit |
value | string | The normalised value the server will record |
verification_token | string | Server-side proof that this claim was issued. X only |
nonce is the AddressLink contract's get_nonce counter for the address; it advances after
a successful submit, and sending it as a string ("42") is a 400. The chain-name and X-reward
flows use a different, server-issued string nonce.
The message the wallet signs is deterministic:
link:<address>:<provider>:<value>:<nonce>
unlink:<address>:<provider>:<nonce><provider> is x, bio, site, prefered-aens-name or nostr. A signature over anything
else fails contract-side verification.
Submit
Sign message with the address's key, then post everything back:
curl -s -X POST "https://api.superhero.com/api/address-links/bio/submit" \
-H "Content-Type: application/json" \
-d '{"address":"ak_YOUR_ADDRESS","value":"Building things on aeternity.",
"nonce":42,"signature":"sg_...","verification_token":"..."}'Response is { "txHash": "th_..." }. The server broadcasts the on-chain write on your behalf.
The API sponsors the transaction; your signature authorises it. value is capped at 200
characters, signature is a hex-encoded wallet signature, and address is validated as an
ak_ account address.
Unclaim
POST {base}/unclaim with { "address": "ak_..." } returns { message, nonce }. Sign
message, then POST {base}/unclaim/submit with { address, nonce, signature } and receive a
txHash.
Linking X
The X value comes from an OAuth round trip rather than being typed. Two accepted claim bodies:
{ "address": "ak_...", "x_access_token": "..." }{ "address": "ak_...", "x_code": "...", "x_code_verifier": "...", "x_redirect_uri": "https://superhero.com/profile/x/callback" }The second is the PKCE form: redirect the user to X, receive the code at your redirect URI, and
post the code, verifier and redirect URI; the backend exchanges the code itself. From there the
flow is identical: claim returns message plus verification_token, the wallet signs it,
submit returns a txHash.
POST /api/address-links/x/claim { address, x_access_token } or { address, x_code, x_code_verifier, x_redirect_uri }
POST /api/address-links/x/submit { address, value, nonce, signature, verification_token }
POST /api/address-links/x/unclaim { address }
POST /api/address-links/x/unclaim/submit { address, nonce, signature }Read the result back from GET /api/accounts/{address} as links.x, falling back to
profile.x_username and then the legacy top-level x_username.
Sponsored .chain name registration
Registering an AENS name normally costs AE. Superhero funds it from a sponsor account when the name qualifies.
| Method & path | Purpose |
|---|---|
GET /api/profile/chain-name/sponsorship/{name} | Can the sponsor fund this name? |
POST /api/profile/chain-name/challenge | Get a challenge for address |
POST /api/profile/chain-name/claim | Submit the signed request |
GET /api/profile/{address}/chain-name-claim | Poll the claim's status |
{name} is the label without the .chain suffix. AENS rules apply, and a sponsored claim
requires a minimum of 13 characters.
curl -s "https://api.superhero.com/api/profile/chain-name/sponsorship/mylongenoughname"| Field | Type | Meaning |
|---|---|---|
name | string | The name checked |
sponsorable | boolean | True when the sponsor can fund it, making the claim free for the user |
sponsor_configured | boolean | Whether a sponsor account is configured at all |
sponsor_balance_aettos | string or null | Sponsor balance, in aettos |
required_balance_aettos | string | Total estimated on-chain cost (preclaim, claim, update and transfer), in aettos |
reason | string or null | Why it is not sponsorable, when it is not |
The claim body is address, name, challenge_nonce, challenge_expires_at and
signature_hex (hex or an sg_ string). It returns { status, message }.
Registration is asynchronous and takes four on-chain transactions. Poll
GET /api/profile/{address}/chain-name-claim, which returns status plus, as they happen,
preclaim_tx_hash, claim_tx_hash, update_tx_hash, transfer_tx_hash, an error, and the
name's expiry under expires_at (older responses use approximate_expire_time,
approximateExpireTime, expire_time or expireTime).
X posting reward
All four routes carry the per-IP rate limit: 10 requests per minute per route, returning 429
with a retryAfter field. See Conventions.
| Method & path | Purpose |
|---|---|
GET /api/profile/{address}/x-posting-reward | Read status. No signature |
POST /api/profile/x-posting-reward/recheck-challenge | Get a challenge for address |
POST /api/profile/{address}/x-posting-reward/recheck | Run the capped scan |
POST /api/profile/{address}/x-reward/referral-link | Get a referral link |
curl -s "https://api.superhero.com/api/profile/ak_YOUR_ADDRESS/x-posting-reward"The status payload carries onboarding_status (not_started, pending, paid or failed),
qualified_posts_count, tx_hash, next_check_allowed_at, x_username,
per_post_total_paid_count, current_streak_days, tier_amount_ae, follower_count,
referral_link, and an error string explaining why a reward was not sent on a 200 (below the
follower minimum, the X identity already rewarded, or the payout failed).
The recheck and referral-link endpoints take the same signed proof body: challenge_nonce,
challenge_expires_at, signature_hex. The recheck is capped at once per 24 hours;
exceeding it returns 429 with nextAllowedAt in the body, and the scan is skipped, not queued.
Deprecated: the legacy profile update
Two endpoints implement an older generic profile edit. Use the address-link endpoints instead.
| Method & path | Body | Returns |
|---|---|---|
POST /api/profile/{address}/challenge | The editable payload | challenge, payload_hash, expires_at, ttl_seconds |
PATCH /api/profile/{address} | The payload plus challenge and signature | The updated profile row |
The editable payload is fullname, bio, nostrkey, avatarurl, username, x_username,
chain_name, sol_name; the response adds address, created_at and updated_at.
Related
- API Conventions: the full list of challenge/submit pairs across the API
- Accounts API:
GET /api/accounts/{address}and thelinksprecedence rule - Posts API:
PostDto.sender, the inlined profile on every post - Profiles: what a Superhero profile is, beyond its wire format
- Chain names: what a
.chainname buys you - X verification: the OAuth half of the X link flow