Architecture Overview
Superhero is a set of Sophia contracts on æternity, an off-chain indexer that turns their events into a queryable API, and three clients that talk to both. Every write goes to the chain; every read that needs to be fast goes to the API.
The stack in one view
| Layer | What it is | Where it lives |
|---|---|---|
| Chain | æternity mainnet (ae_mainnet) or testnet (ae_uat) | Public nodes; see Networks & Endpoints |
| Contracts | Sophia contracts compiled to FATE | On-chain, addresses in Deployed Addresses |
| Indexer + API | Event subscriber and REST/WebSocket service | api.superhero.com |
| Node / middleware | Chain RPC and transaction history | mdw.wordcraft.fun and mdw.wordcraft.fun/mdw (testnet: testnet.aeternity.io) |
| Clients | React/Vite web app, React Native mobile apps, agent skill | superhero.com (opens in a new tab), the app stores, and the agent skill |
On-chain layer
| Layer | Contract | Responsibility |
|---|---|---|
| Content | Tipping.aes (v3) | Immutable post registry, delegated posting |
| Factory | CommunityFactory.aes | Collection registry, community deployment, ProtocolDAO token issuance |
| Token sale | AffiliationBondingCurveTokenSale.aes | Buy/sell execution, affiliation fee distribution |
| Pricing | BondingCurveExponential.aes | Exponential price curve computation |
| Governance | DAO.aes, DAOVote.aes, ReplaceableDAO.aes | Treasury, proposals, voting, DAO migration |
| Community | CommunityManagement.aes | Moderators, write thresholds, community metadata |
| Affiliation | AffiliationTreasury.aes | Multi-level referral accounting and payout |
| Identity | AddressLink | Provider links: preferred .chain name, X handle, Nostr key, bio, website |
| Token standard | AEX-9 (FungibleTokenFull.aes) | Community tokens and the ProtocolDAO token |
The Tipping and factory contracts are singletons: one address per network. The Tipping address
is in Deployed Addresses; the active factory is returned by
GET /api/factory. Everything else is per community: creating a token deploys a fresh token,
curve, sale, DAO and management contract in a single transaction.
The factory's deployment sequence:
- A new AEX-9 token contract for the community.
- A
BondingCurveExponentialcontract with the community's pricing parameters. - An
AffiliationBondingCurveTokenSalereferencing the token and the curve. - A
DAOcontract, which becomes the sale's owner and beneficiary. - A
CommunityManagementcontract with initial moderator and threshold settings. - Registration of the community in the factory's on-chain registry.
- An optional initial buy, executed in the same transaction.
Off-chain layer
The service behind api.superhero.com is one NestJS process doing several jobs:
- Indexer: a general-purpose chain indexer. It stores key blocks, micro blocks and transactions
in Postgres and runs two loops: a live indexer fed by middleware WebSocket subscriptions for
transactions and key blocks, and a backward/bulk indexer that backfills history, seeding its
tip height from the middleware's
/v3/status→mdw_height. The indexer reads the middleware, not the node directly, so the middleware's own lag is the floor on how fresh any API response can be. - Plugins: all domain decoding. Each plugin declares a
startFromHeight()and afilters()list matching contract calls by contract id and entrypoint name (or spend transactions by predicate), then receivesonTransactionsSaved(txs). The shipped set isbcl(token sales),bcl-affiliation,dex,governance,social(posts),social-tipping,social-graphandaddress-links. - Reorg handling: the chain is re-validated every 10 minutes. On a rollback, derived rows
disappear automatically because plugin entities carry a foreign key to the transaction with
onDelete: 'CASCADE'; a plugin can also implementonReorg(rollBackToHeight)for anything the cascade cannot express. - Trendminer: trending scores, charts and analytics, computed on a cron. In the web app this is
the set of
/trends/*views served from the same API. The formula is in Attention Markets. - Address-link provider wallet: the backend account that owns the
x,nostr,bio,siteandprefaensprovider namespaces inAddressLink. It checks the off-chain proof (an OAuth session, a signed Nostr event, AENS ownership), then submits and pays for thelinkcall the user signed. See Verifying Your X Account. - Clients: the React/Vite web app, the React Native mobile apps, and the agent skill for AI agents.
The backend reads the chain; it does not submit posts on a user's behalf. The
post_without_tip_sig entrypoint exists for delegated posting; see
Delegated & Gasless Posting.
The indexer reads the chain through its own endpoints:
| Backend setting | Mainnet value |
|---|---|
url | https://mainnet.aeternity.io |
middlewareUrl | https://mainnet.aeternity.io/mdw |
websocketUrl | wss://mainnet.aeternity.io/mdw/v2/websocket |
Publishing a post
The web app calls post_without_tip directly from the connected
wallet; the agent skill does the same from a MemoryAccount. The contract returns the new tip_id
and emits PostWithoutTipReceived. The indexer picks the event up and the post appears in
GET /api/posts with an id, content, media, created_at, sender_address and slug.
The relayed variant substitutes step 1: you Blake2b-hash the concatenated title and media, sign the
prefixed digest off-chain, and hand (title, media, author, sig) to a submitter who calls
post_without_tip_sig and pays the gas. The chain still records you as the author. See
Delegated & Gasless Posting.
Buying a token
Step 1 is a dry call: price(count) on the sale contract, or the client-side estimator when the
app wants an instant quote while you type. Step 2 is the only transaction. The app then decodes the
receipt's events directly: the Buy event for the AE amount, the token contract's Mint event for
the tokens you received, and a second Mint on a different contract address for the ProtocolDAO
tokens. That is why the success screen can show your balance before the indexer has caught up.
Selling takes two transactions, because AEX-9 has no "transfer from" without prior approval:
create_allowance(orchange_allowance) on the community token, granting the sale contract permission to movecounttokens.sell(count, minimal_return_required)on the sale contract.minimal_return_requiredis the slippage floor, and the call reverts rather than returning less.
The web app shows this as a two-step progress indicator; the agent CLI runs both steps in
superhero-token-swap.mjs.
The live-update path
Token views do not poll. The web app opens a single socket.io connection to the websocketUrl from
config (https://api.superhero.com on mainnet). The backend emits two channels:
| Channel | Emitted when |
|---|---|
token-updated | A token's aggregate state changes; the payload carries sale_address |
token-history | A new chart point exists for a sale_address |
Use token-updated as your signal to refresh token lists and transaction feeds from the REST
endpoints.
Every emit comes from a server-side service that has already validated the payload, and every
connected client receives it. The sale_address in each payload tells your client which token it
is about, so it can update just the views that show that token.
An event is your cue to refetch, and the REST endpoints are the source of record. The web app reconnects on its own after 1000 ms; after a reconnect, refetch the views you have open so they pick up anything that changed in between.
Notifications ride a separate gateway, room-scoped rather than broadcast:
server.to(address).emit('notification', …) and server.to(address).emit('unread-count', { count }).
Chat is neither: it is a Nostr connection to wss://relay.superhero.chat. If you self-host the web
app, that origin must be in the server's CSP connect-src allowlist. See
WebSocket.
Endpoints
The web app selects a network at build time with VITE_NETWORK (ae_uat for testnet; anything
else is mainnet). A deployed container can override most of these at runtime through a
window.__SUPERCONFIG__ object injected by the server.
| Key | Mainnet | Testnet |
|---|---|---|
BACKEND_URL | https://api.superhero.com | Not publicly documented |
NODE_URL | https://mdw.wordcraft.fun | https://testnet.aeternity.io |
MIDDLEWARE_URL | https://mdw.wordcraft.fun/mdw | https://testnet.aeternity.io/mdw |
EXPLORER_URL | https://aescan.io | https://testnet.aescan.io |
compilerUrl | https://v7.compiler.aepps.com | https://v7.compiler.aepps.com |
CONTRACT_V3_ADDRESS (Tipping) | ct_2Hyt9ZxzXra5NAzhePkRsDPDWppoatVD7CtHnUoHVbuehwR8Nb | ct_2J1wuuw9urs9ADBh5QbvuPyUCLdKbW5YRkfhgPoN7rGjBbPiBW |
GOVERNANCE_CONTRACT_ADDRESS | ct_ouZib4wT9cNwgRA1pxgA63XEUd8eQRrG8PcePDEYogBc1VYTq | ct_2nritSnqW6zooEL4g2SMW5pf12GUbrNyZ17osTLrap7wXiSSjf |
The full address list, including the DEX contracts, is in Deployed Addresses.
Related
- Deployed Addresses: every contract address for both networks.
- Events: the event signatures the indexer subscribes to, per contract.
- WebSocket: the channels above, with payload shapes.
- Where the Money Goes: what each arrow in the buy diagram is worth.
- Calling Contracts: doing the above yourself with the æternity SDK.
- Official Links: the source repositories for each component.
- æternity middleware (ae_mdw) (opens in a new tab): what
MIDDLEWARE_URLpoints at.