Last chapter. Three things to cover: the bug that nearly killed the demo, what's still broken about the architecture, and where the next iteration goes.
the state-divergence bug
Halfway through building this thing, the testnet started behaving badly. Validators that connected as replicas would sync for a while and then trip a state root mismatch error at a specific block, drop out of consensus, and refuse to advance. We'd seen this kind of "code drift" issue before — it's a known class of failure for multi-validator chains, and our usual mitigation was to do a snapshot reset and re-bond everyone — but this time it was happening to fresh validators on a current binary. Something was wrong at a layer below the binary.
I had a debugging hunch but no data. So I built a state-diff probe — a single-block-trip dump of the entire account state to disk whenever a replica detects divergence — deployed it to the primary and to a fresh test secondary, and waited for drift.
It tripped within minutes, at block 2,093, on a brand new replica that should have agreed with the primary. The drift dump told me exactly what was different: at the end of block 2,093, our replica's state and the primary's state disagreed about one specific account's balance.
But the smoking gun was this: block 2,093 had zero transactions. Two consecutive empty blocks (2,092 and 2,093), zero state-mutating activity, and yet the primary's recorded stateRoot for those two blocks was different. That's only possible if something on the producer was mutating state between blocks without going through a transaction.
Tracing it from there was 20 minutes. The bug was in applyDeployTransaction and applyCallTransaction: when a contract call failed in certain ways (VM revert, post-validation balance check), the chain was charging gas (good — prevents free spam) but then dropping the failed transaction from the block (bad). The producer mutated state via the gas charge; the replica, seeing no transaction in the block, didn't replay the mutation. Block hash matched, transactions matched, but state diverged because state was mutating outside the block.
The fix is the standard one every other chain implements: a failed-but-charged transaction is included in the block with a status=0 receipt. Producer and replica see the same set of transactions, apply them identically (including the gas charge), and state stays synchronized. Boring, well-understood, exactly right.
I write this up because it's the kind of failure mode that's almost impossible to find without instrumentation, and almost trivial once you have the dump in hand. The general principle: when a system has a known class of pathological behavior, don't try harder to prevent it from happening — instrument it so that when it happens, you have all the data to fix it. A 50-line drift probe paid for itself in 20 minutes of debugging time.
The fix is on testnet now. Fresh replicas have synced through that block hundreds of times since without tripping. The full saga is in a separate devlog post for anyone who wants the chain-internal details.
what's still broken
Honest list of stuff that isn't great about this v1:
1. Image storage is centralized. Covered in chapter 5. Cloudinary is fine for a demo; for any real-world deployment we want IPFS or self-hosted S3 or something the project controls. The migration path is short but it isn't done.
2. Votes have zero sybil resistance. Anyone can spin up wallets and vote any number of times. The hot ranking is fully manipulable. For a demo this is fine — interactions on testnet are essentially free of consequence. For a real network we'd need vote weight tied to stake, follower count, post count, or some kind of proof-of-personhood. None of those are obvious wins.
3. There's no notification system per user. "Someone replied to me" or "someone followed me" isn't pushed anywhere. The toast lane shows it briefly if you have the page open at the right moment, then it's gone. A real social app needs a proper notifications inbox — that means the indexer needs a per-user notifications table, and the frontend needs a notifications page.
4. Profile names are non-unique. Deliberate decision (addresses are the canonical identity), but it does mean you can have ten "alice"s. We sidestep it visually by always showing the short hex next to the name when there's a profile, and by keying avatars off address (not name), but for a real network with handles that mean something, namespacing is going to come up.
5. There's no way to delete a post. Deliberate. Posts are append-only and permanent. This will surprise users who expect a delete button. We'll add a soft-hide flag at some point, but that opens the moderation can-of-worms which deserves its own design pass.
6. Replies don't exist. Mentioned in chapter 3. Adding a replyTo field to the post record is a one-line contract change; building the threading UI is a real frontend project on its own.
7. The indexer is a single point of failure. One Hetzner box, one SQLite file. If the box dies, the activity feed dies until we restart it. The chain is fine — it's still the source of truth — but the materialized view goes offline. For a demo it's fine; for production we'd want at least a hot standby and a periodic snapshot to S3.
I'm flagging these because the worst kind of writeup is the one that pretends everything is perfect. Every one of these is a known limitation with a known fix; we just haven't shipped them yet.
what JS contracts actually unlocked
Stepping back from the implementation details — what did this project actually prove?
Three contract files, 350 lines total, less than a week of work. A working social network. No third-party indexer service, no Solidity compiler, no foundry forge, no hardhat config. The entire backend (chain + indexer) and the frontend share one language, one type system, one mental model. I switched between writing contract code and frontend code constantly without context-switching cost. That's the core productivity claim of JS contracts and now I have a real artifact backing it up.
Real-time UX without compromise. The site feels like Twitter or Discord — toasts pop, feeds reorder, vote counts tick up, no refresh button anywhere. None of this required anything exotic on the chain itself. It's the indexer + WebSocket pattern that every consumer app has used since 2010, applied to chain data instead of database data. Other smart-contract chains can do this too; the difference is that on a JS-native chain the indexer can share validation logic, type definitions, and helper functions with the contract code without compilation steps.
Modular contracts that hold up. Five independent contracts, no cross-contract calls, frontend stitches the data. We added Gallery and Votes to the original three without touching the originals. We can iterate any one of them without the others noticing. The architecture survived adding two new features mid-build, which is the test you actually want.
Wallets that meet users where they are. Asentum's Dilithium3 keys + the Telegram bot pattern means a user with no extension installed and no crypto experience can be transacting on-chain in 30 seconds. The "extension" path is for crypto-native users; the "6-digit code" path is for everyone else. This is the unlock most of crypto has been missing — not "better-designed wallet UI" but "you don't need a wallet UI at all, you have an inbox in an app you already use."
what's next
The roadmap from here, in priority order:
- Replies / threads. This is the one feature that converts the demo from "look how nice this is" to "I'd actually use this." Contract change is one field, frontend change is a real lift but well-understood.
- Notifications inbox. Per-user materialized view in the indexer, dedicated page on the frontend. Closes the "I missed your reply" gap.
- IPFS image storage as an opt-in per upload, with Cloudinary as the fallback. Removes the centralization caveat.
- Stake-weighted vote ranking. Each voter's vote weight = some function of their bonded ASE. Real sybil resistance without proof-of-personhood.
- A real namespace registry. Probably a sixth contract —
Names— that maps human-readable handles to addresses, first-come-first-serve with stake-locking. Keeps profile name uniqueness optional rather than mandatory. - Mobile app. Same backend, native client. The Telegram bot already gives most of the wallet UX for free, so an app is mostly UI work.
I'm not going to ship all of those before the next case study. But each one is small enough to be a chapter in its own right, and each one teaches something specific about building on a chain like this.
the takeaway
The case I want to make is the one this project demonstrates: smart-contract chains can host real consumer applications. Not just defi primitives. Not just NFT mints. Actual apps with content, identity, social graphs, votes, real-time UX, mobile design. The thing keeping us in defi-land for the last decade hasn't been crypto's potential — it's been the gap between what consumer apps need (strings, JSON, fast iteration) and what the dominant smart-contract languages have given us (32-byte slots, byte-packed structs, slow tooling).
A JavaScript-native VM closes that gap. Hardened JavaScript with deterministic execution gives you exactly what you need on the chain side — strings as a first-class type, JSON as the lingua franca, BigInt for integer math, async-feeling control flow that's actually deterministic, gas costs that scale with what you're actually doing. You stop needing to think about your contract as a constraint and start thinking about your contract as just code you wrote in the same language as everything else.
Once you cross that line, social networks are easy. Marketplaces are easy. Forums, voting systems, reputation systems, follow graphs, content moderation, gamification — all of it is just code. The chain is the boring part.
Try the live demo at social.asentum.com. Read the code at github.com/asentum-network/social. Fork it, change something, ship your own version. We've got testnet ASE in the faucet and the rate limit is generous.
If this stuff lands for you, the place to keep an eye on is the rest of our case studies. I'll be writing one of these for every reference dapp we build.
— Milkie