We built a DexTools-style trading terminal for Auras, our native AMM — live candles, a real-time trade tape, one-click swaps from the browser extension or the Telegram wallet. It looked great in a demo. Then I made a real buy, watched it leave my wallet, and watched the tape not show it. This is the story of a bug that only existed because our chain is honest about finality.
The setup
Auras is a native AMM: the gas token is the traded asset, no wrapper contract in the middle. Every swap emits a Swap event with the reserves and amounts, and an indexer turns those events into pairs, candles, and the live tape you see on the terminal. Standard stuff — poll the chain for new logs, write them to a database, serve them over an API.
The indexer did the obvious thing: ask the node for the current block number, fetch logs up to there, advance a cursor, repeat. On most chains that's correct and boring. On ours it quietly lost money.
Two heads, not one
Asentum finalizes with GRANDPA, which means at any instant the chain has two heads: the Aura tip — the latest block produced — and the finalized head, which trails it by around six blocks while validators vote. They're usually 20 to 40 seconds apart.
Here's the trap. eth_getLogs on our node only returns logs from finalized blocks — it won't hand you an event that could still, in principle, be reorged away. And eth_blockNumber returns the finalized head, not the tip. So the indexer was asking "what's the latest block?", getting the finalized head, and scanning logs up to it. Fine so far.
The bug was in the cursor. A block that's unfinalized on one poll becomes finalized on the next. But by then the cursor had already moved past its height on the previous tick — because eth_blockNumber had reported a height that was itself finalized-lagged, and the scan window had marched forward. Blocks that crossed from unfinalized to finalized in between two polls fell into the gap between the cursor and the window. The logs in them were never requested. My 50-ASE buy was in one of those blocks. The tape never saw it, because nothing ever asked for it.
It wasn't dropping events randomly. It was dropping exactly the events that finalized during the blind spot — which, over an ocean and a two-second block time, was a lot of them.
Two tiers
The fix is to stop pretending there's one head. The indexer now scans in two tiers on every poll:
- A durable pass runs
eth_getLogsup to the finalized head and advances the cursor. This is the source of truth — finalized logs never move, so once written they're permanent. - A live pass scans the unfinalized window — finalized-head-plus-one up to the Aura tip — by pulling each block and its receipts directly, not through the finality-gated log endpoint. These show on the tape immediately, marked provisional, and get reconciled when the durable pass catches up to them.
The cursor only ever advances to the finalized head, so nothing can slip past it. The live window re-scans the same few blocks every tick until they finalize — cheap, because it's six blocks, and correct, because a trade now appears within a poll of happening instead of never. I reset the cursor back to before the incident and the missing trades reappeared, including mine.
The other gotcha
While I was in there: eth_getLogs returns log.address as an ase1 bech32 string, not a 0x hex address. Our chain isn't an EVM wearing a costume — the address filter I was passing had to be the pool's ase1 address, or the node matched nothing and the tape stayed empty for a completely different reason. An hour of "why are there zero logs" traced to comparing an ase1 string against a 0x one. Two representations of the same thing, and every consumer has to agree on which one — a lesson this chain keeps teaching me in new outfits.
What shipped
The terminal is live: real pairs, candlestick charts, a tape that updates within a couple seconds of a swap, buy and sell straight from the extension or the Telegram bot, and a create-pool flow for any token. The interesting part isn't the UI — it's that making it correct forced me to treat finality as a first-class fact instead of an implementation detail. A chain that tells you the truth about what's final will punish any tool that assumes "latest block" means one thing.
