Merx

Merx: Pay for anything with any token on any chain. Swap via Uniswap, settle on Arc.

Merx

Created At

ETHGlobal Cannes 2026

Project Description

Merx is a cross-chain ebook webshop where customers can pay with any ERC-20 token on any of 18 supported testnet chains. If a customer doesn't hold USDC, they can swap from tokens like WETH, UNI, LINK, DAI, or WBTC through the Uniswap Trading API using Permit2 — a gasless signature followed by a single swap transaction. The swap output (USDC) is then
transferred to the merchant.

Once the merchant receives USDC on any chain, the backend automatically bridges it to Arc via Circle's CCTPv2: it calls TokenMessengerV2.depositForBurn on the source chain, polls the CCTP attestation API, then self-relays the message on Arc by calling MessageTransmitter.receiveMessage. The USDC lands in the merchant's wallet on Arc, consolidating revenue from
all chains into one place.

From the merchant dashboard (wallet-gated to the merchant address), the operator can monitor USDC balances across all 18 chains plus Arc in real-time, view recent invoices with block explorer links, and sweep idle USDC into Compound V3 on Ethereum Sepolia to earn yield — or withdraw it back. The sweep uses another CCTPv2 bridge from Arc to Sepolia, where a
CompoundDepositor contract receives the CCTP mint and supplies directly into the Compound lending vault.

The frontend is a React + Vite + TypeScript SPA with wagmi/viem for wallet interactions, custom chain and token selector dropdowns with icons, and a token registry (registry.yaml)
that defines all chains, USDC addresses, RPC endpoints, explorers, and available swap tokens. The Go backend proxies the Uniswap Trading API (keeping the API key server-side),
manages invoices, handles CCTPv2 bridging and relay, and serves chain/balance data.

How it's Made

The backend is a single Go binary that orchestrates all cross-chain operations. It loads a YAML registry defining 18 supported chains with their CCTP domains, USDC addresses, RPCs, and explorers. When a customer
wants to pay, the backend calls Circle's CCTP fee API (GET /v2/burn/USDC/fees/{src}/{dst}?forward=true) to estimate the exact forwarding fee, then ABI-encodes a depositForBurnWithHook calldata with the forwarding
hook magic bytes (0x636374702d666f7277617264...) baked in. The customer gets back the raw calldata, the TokenMessengerV2 target address, and the approval info — they just approve and send.

The frontend is React 19 + TypeScript with wagmi and viem for wallet interactions. One notable challenge was nonce management: after the USDC approve tx confirms, the next tx (the CCTP burn) would fail because the
RPC hadn't updated its pending state yet. We solved this by explicitly fetching getTransactionCount with blockTag "pending" before sending the burn — this gives us the correct nonce even on fast L2s like Unichain
(~2s blocks) where the state propagation lag is real.

For the Uniswap swap flow, we integrated the Uniswap Trading API with Permit2. The frontend fetches a quote, the user signs a Permit2 permit, and then we build and submit the swap tx through the Universal Router. A subtle bug we hit: we were re-fetching a fresh quote right before the swap to avoid expiry, but the Permit2 signature was tied to the original quote's permit data. The ecrecover would return a different address and the tx would revert silently. The fix was to keep the exact same quote object for both signing and swap building.

Circle's CCTP V2 Forwarding Service is the core of the architecture. Instead of deploying a ShopPaymaster contract on every source chain (which we actually built first with EIP-2612 permits for gasless payments), we realized the Forwarding Service eliminates all per-chain deployment. The customer calls depositForBurnWithHook directly on Circle's TokenMessengerV2 (same address on all chains: 0x8FE6...2DAA), and Circle handles
attestation + minting on the destination. We initially tried using Circle's Gateway protocol but found it added a depositFor transaction on every payment without proportional benefit — pure CCTP turned out simpler
and cheaper.

For refunds, the backend calls depositForBurnWithHook on Arc with the customer's address as mintRecipient and the forwarding hook enabled. We discovered that the Forwarding Service takes the entire maxFee (not just the minimum), so we integrated the fee estimation API to calculate protocolFee + forwardFee(med) + 10% margin. The backend sends amount + estimatedFee in the burn so the customer receives the exact refund amount. We also learned that forwardState goes PENDING → SENT → CONFIRMED (not COMPLETE as the docs suggest), so our polling handles both terminal states.

Arc was chosen as the settlement hub for its sub-second deterministic BFT finality — once the CCTP message lands, there's zero reorg risk and the purchase is confirmed instantly. Arc also uses USDC as its native gas token, which means the merchant only holds one asset for both treasury and gas.

The only custom smart contract in production is CompoundDepositor on Ethereum Sepolia. It receives a CCTP message (calling MessageTransmitter.receiveMessage to mint USDC), then immediately supplies into Compound
V3's Comet contract — all in one atomic transaction. This is the one place where self-relay (instead of the Forwarding Service) is necessary, because we need post-mint logic that the Forwarding Service can't
execute.

Invoice state is persisted to a JSON file and tracks the full lifecycle: paid → bridging → attesting → settled, plus refunding → refunded for refunds. Each transition stores transaction hashes for both the source
chain and Arc, with clickable explorer links in the dashboard. Ebook download access is gated on invoice state — refunded invoices lose access.

background image mobile

Join the mailing list

Get the latest news and updates

Merx | ETHGlobal