BackTabCrypt / Writing
Blockchain /

Building a DApp on Monad Testnet

Dec 2025 · 8 min read

The idea for MonConnect was straightforward: freelancers and clients transacting without a platform in the middle taking a cut, handling disputes on their terms, locking everything up in a support ticket. Just a contract. You send money, it holds it, it releases on confirmation. Simple. Took me about a week to realise it wasn't that simple.

Why Monad Specifically

I had been on Sui before this. Sui is fast and the Move language is interesting, but the ecosystem is small compared to EVM. When I found Monad — EVM-compatible but with parallel transaction execution — it clicked. I could use Solidity and Hardhat, which I already knew. No new toolchain to fight with. And the parallel execution means multiple escrows being created at the same time don't queue up waiting for each other. For a platform where that could actually happen, that matters.

The Escrow Contract

The core is a state machine. PENDING → ACTIVE → COMPLETED or REFUNDED. Organizer creates an escrow and locks funds on-chain. Provider delivers. Organizer releases payment. If it falls through, refund. No one in the middle. I wrote it thinking about it as a flowchart before I wrote a single line of Solidity, which turned out to be the right call.

enum Status { PENDING, ACTIVE, COMPLETED, REFUNDED }

struct Escrow {
  address organizer;
  address provider;
  uint256 amount;
  Status status;
}

mapping(uint256 => Escrow) public escrows;

function createEscrow(address _provider) external payable {
  require(msg.value > 0, "Must send funds");
  escrows[escrowCount] = Escrow({
    organizer: msg.sender,
    provider: _provider,
    amount: msg.value,
    status: Status.ACTIVE
  });
  escrowCount++;
}

The NFT Identity Part

The second piece was identity. On a permissionless chain anyone can be anyone. We added soul-bound NFTs for verified providers — soul-bound means you can mint it but you can't transfer it. It's tied to your wallet address. Not perfect against Sybil attacks but it raises the cost of faking reputation enough to matter for a testnet use case.

Implementing non-transferable NFTs in Solidity is actually simple — you just override the transfer functions to always revert. The metadata stores a hash of the provider's profile which links to an IPFS document. Felt elegant. Probably needs more work before mainnet but for the scope I was working in, it held up.

Frontend Was the Ugly Part

The Next.js frontend connecting via ethers.js was where I spent the most time debugging. Specifically: transaction states. A user clicks "create escrow" and then... waits. In Web2 that wait is 200ms. On-chain it's seconds, sometimes longer. You have to build a loading state for everything and make it clear to the user that their transaction is in progress, not broken.

  • Optimistic UI updates while waiting for block confirmation
  • Contract event listeners for real-time escrow state changes
  • Decoding Solidity revert errors — they're hex encoded and not human-readable by default
  • Chain switching prompt — if the user is on the wrong network, everything fails silently

What I'd Do Differently

I deployed the contract seven times before the version I kept. On testnet that's fine — gas is free, mistakes cost nothing. On mainnet each bad deploy is real money. That freedom to break things on testnet is something I used aggressively and I think you should too. Don't be precious about testnet code.

The bigger lesson was to think in state machines from the start. Every smart contract is a set of states and rules for moving between them. If you can't draw that diagram on paper before opening your editor, you don't understand the problem yet.