CryptoRabbit: System Architecture of a Crypto-Native Marketplace

By Roman Khrystynych · May 9, 2025

A marketplace that settles on-chain is not one system — it is two, and the interesting engineering is at the seam between them. The escrow contract is the trust anchor, but a contract on its own cannot show a product listing, send a shipping notification, or let a buyer recover after losing a wallet. This post walks through the rest of the architecture behind CryptoRabbit and where we chose to put each responsibility.

What belongs on-chain, and what does not

We kept one rule: on-chain if the platform being compromised would let it steal or freeze user funds; off-chain otherwise. That gives a clean split:

On-chain:

  • Escrow state and transitions (FundedShippedDeliveredReleased/Refunded).
  • Fee routing — the platform fee is a parameter enforced by the contract, not a line item the backend could rewrite.
  • Dispute arbitration role — held by an address the contract recognizes, not a database flag.

Off-chain:

  • Listings, search, images, reviews, messaging.
  • Shipping metadata, tracking numbers, delivery confirmations before they are posted to the contract.
  • User profiles, verification, anti-fraud signals.

A useful heuristic: if the worst-case answer to "what if our backend were fully compromised tomorrow?" is "users can still get their money back", the split is right.

The transaction lifecycle, end to end

Walking through a normal happy-path order reveals every moving piece:

  1. Listing. Seller creates a listing in the product backend. No on-chain action — the listing is just rows and object-store URLs.
  2. Checkout. Buyer hits "Buy". The backend composes a transaction that deploys (or initializes) an escrow instance with the agreed amount, fee, and timeout parameters, and returns it unsigned.
  3. Wallet signature. Buyer signs in their own wallet (MetaMask / Rabby on Ethereum; Phantom / Backpack on Solana). Funds move into the escrow — not into any platform-controlled account.
  4. Indexing. Our indexer sees the Funded event, writes an order record, and the product UI flips to "waiting for seller".
  5. Seller accepts and ships. Seller confirms intent in the app; the app posts accept on-chain. When the seller has a tracking number, the backend writes it against the order and posts shipped on-chain. Tracking metadata itself stays off-chain; only the state transition is logged.
  6. Delivery. Our shipping-aggregator integration polls the carrier. When delivery is confirmed — or the buyer confirms manually — the contract is moved to Delivered.
  7. Release. After a short review window, a permissionless release call moves the funds to the seller and the fee to the platform's fee address. Anyone can trigger it; the contract does not care who pays the gas.

The critical property is that steps 3, 5, and 7 are on-chain and visible; steps 1, 4, 6 can be rebuilt from chain events if the backend is wiped. The indexer is a cache, not a source of truth.

The indexer

Two classes of reader need chain state in a hurry: the product UI (so order pages feel instant) and the operator (so we can alert on stuck orders). Both are served by an indexer that subscribes to the escrow events and materializes them into a Postgres table keyed by escrow address. On Ethereum we run our own node-subscription worker; on Solana we subscribe to program logs. In both cases the indexer is idempotent and replayable — give it a starting block/slot and it rebuilds the world.

Indexing is the boring-but-critical plumbing. The mistake we made early was treating it as a one-way projection; in practice you need the reverse path too — when a user complains an order is stuck, you want to diff indexer state vs. chain state and know instantly which side is wrong.

Wallet UX is the product

Most of the product polish effort goes into the wallet hand-off, not the contract. A few lessons:

  • Pre-flight simulation. Before asking a user to sign, we simulate the transaction and show the exact amount, fee, and recipient the contract will use. A wallet popup that says "Contract Interaction: Unknown" is where conversion goes to die.
  • Idempotent retries. Network glitches happen. Every action the app posts to chain is keyed by a client-generated nonce so retrying the same request does not double-spend.
  • Progressive disclosure. Casual buyers never need to see the escrow address. Power users want it in one click. Both are reachable; neither is in the default path.

Two chains, one product surface

Because we run on both Ethereum (EVM) and Solana, the product code treats "chain" as a per-order attribute, not a global switch. A user's listings might settle on Base while their purchases settle on Solana, and the order page looks identical either way. The cost of this is an abstraction layer over the chain client; the payoff is that we can route each category of goods to whichever chain gives the best fee and finality for that order size. High-ticket items tend to settle on an EVM L2 where tooling is deeper; sub-$20 items tend to go to Solana where the fee disappears into the rounding.

Operational surprises

A few things we did not predict until we ran the system:

  • Stuck-state alerts matter more than error alerts. The contract never "errors" in the traditional sense; the problem is orders that sit in one state past their timeout because a carrier API was down. We monitor age-per-state, not just exceptions.
  • Gas reimbursement for the seller's release path. In the happy path the buyer can trigger release. In the timeout path someone has to pay gas to finalize; on Ethereum L1 this can be enough to matter. On Solana it is a rounding error. We built a small keeper service that sweeps eligible escrows automatically, and we absorb the cost as part of the platform fee.
  • Wallet recovery is an off-chain problem. If a buyer loses their wallet mid-order, the money is not lost — it is in the escrow, keyed to their address. Recovering access to that address is a standard wallet-recovery problem and not something the marketplace should try to solve. Having a clear support script for "you still own that money, here is what to do" is more useful than any extra on-chain logic.

Takeaway

The most tempting thing when you first write a smart contract is to keep adding to it. The right move is usually the opposite: keep the contract minimal, move everything you can off-chain, and invest the saved complexity into indexing, wallet UX, and operational visibility. The contract is the part that cannot be fixed with a deploy — everything else should be boring, observable, and easy to change.


Roman Khrystynych

Written by Roman Khrystynych, founder of Khrystynych Innovations Inc — an AI and Web3 consultancy specializing in multimodal RAG, AI automation, AI training, and smart contract engineering on Ethereum and Solana.

Have a project in mind? Let's talk.