Tokens & Bonding Curves
Portfolio & PnL

Portfolio & PnL

Superhero keeps two views of a position: the holdings list, which is your current balances priced at spot, and the portfolio history, which is a series of valuation snapshots with profit and loss attached. Both are addressable for any account, not just your own.

From the command line

node scripts/superhero-portfolio.mjs holdings          # your positions
node scripts/superhero-portfolio.mjs holdings ak_...   # anyone's positions
node scripts/superhero-portfolio.mjs holdings ak_... 20 2   # limit 20, page 2
node scripts/superhero-portfolio.mjs summary           # holdings + latest snapshot
node scripts/superhero-portfolio.mjs history           # value over time

holdings and summary read the public API and need no signing key for another account's address. The script derives your address from AE_PRIVATE_KEY, so the no-argument forms require it to be set.

Holdings

Backed by GET /api/accounts/{address}/tokens, with includes=token so each row carries the full token record.

Field in the script's outputSourceMeaning
token_nametoken.nameThe token's name, which is also its symbol
token_symboltoken.symbolSame string in practice
sale_addresstoken.sale_addressThe bonding-curve contract, the handle for buy and sell
token_addressaex9_address or token.addressThe AEX-9 contract, for allowances and transfers
balancebalanceTokens held
buy_price_aetoken.priceCurrent spot buy price, per token
sell_price_aetoken.sell_priceCurrent spot sell price, per token, 0.5% below buy
trending_scoretoken.trending_scoreMomentum signal for that token

Value a position at sell_price_ae, never at buy_price_ae. The buy price is what the next person pays to enter; the sell price is the side you are on. Compute a position's AE value from balance and sell_price_ae, with the caveat below.

Portfolio history

Backed by GET /api/accounts/{address}/portfolio/history, which accepts startDate, endDate, interval, convertTo and include. Pass include=pnl or include=pnl-range to get the PnL block. The two differ; see below.

Snapshots are computed on request

Nothing is recorded periodically. The backend takes your startDate, endDate and interval, generates the timestamps, maps each to a block height, and reconstructs holdings at that height by replaying your own transaction rows. The AE side is a historical getBalance against the node.

Two consequences:

  • Every interval returns real data, not interpolation.

  • The interval you ask for is a floor, not a promise. The controller raises it based on the range length:

    Range lengthMinimum interval
    Under 1 month1 hour
    1 to 3 months4 hours
    3 to 6 months1 day
    6 months or more1 week
    No dates given at all1 day

    The effective interval is max(your interval, that minimum); the default when you pass none is 86,400 seconds. Ask for 5-minute granularity over a year and you get weekly points.

The AE balance lookup floors each block height to a multiple of 300 key blocks (roughly 15 hours), so snapshots inside the same bucket share one AE balance. Token values are exact per snapshot; the liquid-AE line is stepped. Responses are cached for 10 minutes.

Each snapshot:

FieldMeaning
timestampWhen the snapshot was taken
total_value_aeEverything the account holds, in AE
ae_balanceLiquid AE
tokens_value_aeValue held in token positions
total_value_usdThe same total, converted
total_pnlPresent when include=pnl-range; see below

And inside total_pnl:

FieldMeaning
percentagegain.ae / invested.ae × 100, or 0 when nothing was invested
investedCost basis, as ae and usd, defined differently in the two modes (see below)
current_valueMarket value of tokens still held, as ae and usd. Always cumulative
gainThe profit figure, as ae and usd
rangeThe from and to bounds, present only under include=pnl-range

How PnL is computed

Cost basis is average cost, not FIFO. There is no lot tracking: the backend sums what you spent and what you bought, and divides.

include=pnl is cumulative mode, per token, summed across tokens:

invested      = every AE you ever spent on this token       (all buy + create_community rows)
current_value = tokens still held × current unit price
gain          = proceeds from all your sells + current_value − invested

include=pnl-range is realised-only mode, per snapshot window:

avg_cost      = (all-time AE spent) / (all-time volume bought)
invested      = avg_cost × volume sold in this window
gain          = sell proceeds in this window − invested

Range mode excludes unrealised gains, so a day only turns green or red when a trade is closed. It is what the app's PnL calendar and sparkline are built on. Cumulative mode is what you want for "how is this position doing".

A partial sell is attributed at the all-time average purchase price, blended across every buy you ever made in that token, including buys made after the sell. A realised gain reported for a past day can therefore change if you buy more of the same token later.

Three details that decide whether a number looks right:

  • The 0.5% affiliation fee is inside invested. The cost basis comes from the transaction row's amount, decoded from the sale contract's Buy event, whose first argument is total_price, the fee-inclusive AE you paid.
  • Gas is not. The æternity transaction fee never reaches the contract, so it appears in no row and in no cost basis. Your real cost is slightly higher than invested says.
  • current_unit_price is the last trade's price, not a live quote. It is the buy_price.ae of the most recent transaction at or before the snapshot height. On a token nobody has traded in a week, your position is valued at last week's price. It is also a buy-side unit price, which overstates an exit for the reason below.

The per-token breakdown is on its own endpoint: GET /api/accounts/{address}/portfolio/tokens/history returns the same snapshot shape with a tokens_pnl map keyed by sale_address, each entry carrying current_unit_price, percentage, invested, current_value and gain. PnL is always included there, and it is not available on /portfolio/history at any include value. The payload is large, so use it deliberately.

The history command prints the newest and oldest snapshots, the count, the full array, and a period PnL computed as latest.total_value_ae − oldest.total_value_ae. That is a difference in valuation, not a realised result: it moves when prices move, even if you never traded.

Your position is worth less than it says

A holdings row prices your balance at the current spot sell price (one number times another). But selling is an integral: as your tokens burn, the supply falls and every subsequent token in your own sell fetches less than the one before it.

quoted value  =  balance × sell_price          ← a rectangle
actual exit   =  0.995 × ∫ from (S − balance) to S of buy(x) dx

For a token with 10,000,000 tokens in supply:

Your balanceShare of supplyQuoted value (AE)Actual sell return (AE)Shortfall
100,0001%10.4710.420.5%
1,000,00010%104.7499.265.2%
5,000,00050%523.72388.5325.8%

A small position is quoted accurately. A position that is a meaningful fraction of the supply is not, and the discrepancy grows with your share. If you hold half a token's supply, the portfolio page overstates your exit by a quarter.

For the exact figure, call sell_return(balance) on the sale contract. That is the number the curve will pay, and it costs nothing to ask. See Buying & Selling.

Why a position can lose value without you doing anything

  1. Other people sell. The supply falls, the curve slides down, and every remaining holder's position is repriced lower.
  2. Other people buy. Your position's AE value rises, but your share of the supply falls, because new tokens were minted. On a governance stake, that dilution matters even while the value line goes up.
  3. The AE price moves. total_value_usd can fall while total_value_ae is flat, or vice versa. Decide which unit you are measuring in and be consistent.

There is no mechanism by which a position gains or loses value from time alone: no staking yield, no rebasing, no inflation schedule.

Realised versus unrealised

Nothing in the portfolio view is realised until you sell, and the 1% spread means a freshly opened position starts about 1% underwater. The public trading leaderboard (opens in a new tab) ranks accounts by pnl_usd, roi_pct, aum_usd and mdd_pct (maximum drawdown) alongside buy_count, sell_count, created_tokens_count and owned_trends_count. These are the numbers to compare against if you want to know how a strategy is doing relative to other people rather than relative to its own entry.

Related