Tipping
A tip on Superhero is a plain AE transfer with a payload that names the post: a spend transaction from your account straight to the author's. The indexer reads the payload to attribute it. There is no fee, no escrow and no intermediary.
The transaction
sdk.spend(amountInAettos, recipientAddress, { payload })| Part | Value |
|---|---|
| Amount | The AE amount converted to aettos, 18 decimals |
| Recipient | The post author's ak_… address, directly |
| Payload | TIP_POST: plus the post id with its _v3 suffix |
| Payload for a profile tip | TIP_PROFILE |
| Encoding | The payload string is byte-array encoded, arriving on chain as a ba_… value |
The post id is normalized before it goes into the payload: an id without _v3 gets the suffix
appended, so TIP_POST:3903_v3 is the canonical form regardless of how the caller spelled it.
Because it is an ordinary spend, the AE lands in the author's balance in the same transaction. There is nothing to claim, no vesting, and no minimum enforced by the protocol.
The Tipping v3 contract is not involved: it stores posts, has no payable entrypoint and
holds no balance. (A companion Tipping_v3_Getter contract adds one read helper,
get_tip_by_id(c : Tipping, tip_id : int) : option(Tipping.tip), for fetching a single post
without pulling the whole state.)
Practical consequences:
- A tip cannot fail because of contract logic; it fails only if you cannot pay.
- Attribution happens off chain, by the indexer reading the payload. If the indexer misses a tip, the AE still arrived; only the displayed total is affected.
- Anyone can reconstruct tip attribution by scanning spend transactions to an address and
decoding payloads that start with
TIP_POST:.
How the indexer attributes a tip
- The transaction must have
tx.type === 'SpendTx'. Contract calls are never tips. - The payload is decoded from its
ba_…form to a string. It must start withTIP_PROFILEorTIP_POST, otherwise the transaction is skipped. - For a
TIP_POST:payload, everything after the colon is taken as a post id and looked up directly byid. A miss leaves the tip unattached to a post. - Self-tips are dropped, not stored. For a post tip that means
sender == post.sender_address(tipping your own post), and for a profile tipsender == receiver. - The row is keyed on
tx_hash, so reprocessing the same transaction is idempotent. - The amount is stored as
toAe(tx.amount), a decimal AE string, not aettos.
Two things follow:
- Tips sent outside the app count. A manually constructed
spendwith aTIP_POST:3903_v3payload is indistinguishable from one the app sent, and is indexed identically. - There is no minimum and no dust filter. Any positive amount is stored and summed. The web composer requires a positive amount within your balance; the indexer applies no floor.
Tip from the app
The tip button sits on every post in the feed and on the post page, showing the post's current total in AE.
- Click Tip on a post. The modal opens with the recipient resolved to the author's display name.
- Enter an amount, or use a quick amount: 1, 10, 100 or 500 AE.
- Confirm in your wallet.
The button shows Sending, then Tipped once the transaction returns. An amount above your balance disables the button.
Why the total takes a moment
The displayed total comes from the indexer, which has to see the transaction confirm. The app
adds your amount to the cached total immediately, then polls
GET /api/tips/posts/{postId}/summary (5 seconds after the send, then every 3 seconds for up
to 18 more attempts, roughly a minute) until the backend total reaches the expected value. If
the minute elapses, the app does a final refetch and leaves the real value in place.
Read tip totals
| Endpoint | Returns |
|---|---|
GET /api/tips/posts/{postId}/summary | totalTips, a decimal AE string, summing every tip on the post except those sent by the post's own author. 404 if the post id is unknown. |
GET /api/tips/accounts/{address}/summary | totalTipsSent and totalTipsReceived, both decimal AE strings, each excluding rows where sender equals receiver |
GET /api/tips | The paginated tip list |
The post id in the path takes the _v3 suffix, matching the payload form.
GET /api/tips accepts sender, receiver, type and post_id filters,
order_by of amount, type or created_at, an order_direction, and page/limit with a
maximum limit of 100. Ordering by amount casts the stored string to numeric first, so it
sorts by value rather than lexically. type holds the whole decoded payload
(TIP_PROFILE, or TIP_POST:3903_v3), so filtering by type is an exact-string match on that,
not on the bare word TIP_POST.
Tip signals also feed the popular feed: tipsAmountAE, tipsCount and uniqueTippers are
three of the eight weighting inputs a reader can scale, all computed with self-tips excluded.
See The Feed.
Tipping an address that has never used Superhero
The recipient is an æternity address, which exists whether or not anyone has opened it in a wallet. A tip increases that address's balance; if the key is lost, so is the AE. There is no pending-tip pool and no reclaim path.
Tips and notifications
There is no tip notification type. The closest is incoming-transfer, which fires on any
incoming SpendTx, a tip included, above NOTIF_MIN_AMOUNT_AETTOS (default 0, meaning
off). That type is mobile push only, so an incoming tip produces a mobile push if the recipient
has a registered device, and nothing in the web bell. See
Notifications.
Related
- Posting: the ids and
_v3suffix convention the payload depends on. - The Feed: how tip signals influence popular ranking.
- Notifications: why a tip does not raise a bell notification.
- Value flow: where money moves elsewhere in the protocol.
- Transactions: reading spend transactions and their payloads.