Architecture: web, API & Sepolia

Everything the other technical documents describe — splits, order matching, resolution — ultimately lands on a handful of contracts on the Sepolia testnet. This document maps the path: which layer talks to which, exactly which contracts are deployed where, and why the browser never signs anything.

Four layers, one direction

Verex is a monorepo of four runtime layers. A request only ever flows one way — browser to web server to API to chain — and only one layer holds keys or an RPC connection:

LayerPackageRole
Webpackages/webNext.js app. Renders markets, books, and the portfolio from the API's REST endpoints. Holds no keys, opens no RPC connection — it cannot touch the chain even by accident.
APIpackages/apiFastify server. Owns the Postgres mirror (markets, orders, fills), runs the matching engine and the market-maker, and is the only process that signs and sends transactions.
SDKpackages/sdkTyped viem clients — one per contract (CTClient, ExchangeClient, UsdcClient, UmaAdapterClient). The API never handles raw ABIs; every chain call goes through these.
Contractspackages/contractsFoundry project holding the Solidity sources and the deployment scripts, plus deployments.json — the manifest of what is live on Sepolia.

The database is a mirror, not the source of truth. Balances and positions are read from the chain (balanceOf, batched over every outcome token in one balanceOfBatch call); the database holds what the chain cannot: order books, price history, copy, and categories.

The specific contracts

Five contracts make up an environment. Two are third-party primitives used unmodified — the same ones Polymarket runs on mainnet — and three are written for Verex:

ContractOriginWhat it does
ConditionalTokens (CTF)Gnosis, unmodifiedThe custody core. Registers conditions (prepareCondition), splits $1 of collateral into a full outcome set (splitPosition), merges it back, records the result (reportPayouts), and pays winners (redeemPositions). Positions are ERC-1155 tokens.
CTFExchangePolymarket, unmodifiedAtomic settlement. Takes two signed EIP-712 orders that the off-chain book matched and swaps outcome tokens against collateral in one transaction (matchOrders).
MockUSDCVerexThe demo dollar — a mintable ERC-20 with 6 decimals standing in for USDC, so wallets can be funded without a faucet queue.
UmaCtfAdapterVerex (UMA's design)Bridges UMA's Optimistic Oracle to the CTF: registers the question (initialize), and once the oracle settles, translates the answer into the payout vector (resolve). It — not the operator — is the oracle address of UMA markets.
MockOptimisticOracleV2VerexA faithful mock of UMA's propose/dispute/vote lifecycle where the demo wallets sit as the jury — so the full dispute flow is walkable without real UMA tokens. Local and staging only.
The split matters for trust: the contracts that hold money (CTF, exchange) are unmodified, audited primitives. The contracts written for Verex are the demo scaffolding around them.

Deployed addresses on Sepolia

Staging and production each run their own full backbone on Sepolia (chain id 11155111) — separate instances so the two environments cannot interfere. Every address is verifiable on Etherscan:

Production (verex.jaylabs.xyz):

ContractAddress
MockUSDC0xAc03…b6B6
ConditionalTokens0xEB10…Fb04
CTFExchange0xcB22…99Cf

Staging:

ContractAddress
MockUSDC0xF0AB…3edD
ConditionalTokens0xCa4d…aCBB
CTFExchange0x19f3…DD22
UmaCtfAdapter0x1B45…00AC
MockOptimisticOracleV20x9f12…e88C

Local development deploys a fresh backbone (plus the mock oracle stack) onto an anvil chain on every seed run, so local addresses are new each time by design. The canonical record of the Sepolia addresses is packages/contracts/deployments.json.

Who holds the keys

This is where Verex deliberately differs from a production exchange. There is no wallet-connect: the demo wallets (#1–9) and the operator (#0) are derived from a publicly known development mnemonic, and their keys live server-side, in the API. When you trade as wallet #3, the API signs as wallet #3.

Every write to the chain goes through one queue. The API answers from the database immediately, enqueues a ChainJob, and a single worker executes jobs strictly one at a time — which doubles as nonce management, since every transaction comes from server-held accounts. Jobs are claimed atomically, retried with backoff, and idempotent, so a crash mid-settlement re-runs safely.

A production build would invert this: signing moves to the user's own wallet in the browser (wagmi/viem), the operator keeps only its market-maker key, and the queue keeps only the operator's jobs. The one-way layering above is what makes that swap possible without redesign — the browser already never talks to the chain.

Which layer makes which call

The three chain-touching flows, end to end — the mechanisms are covered in their own documents; this is the map of who calls what:

  1. Create — web posts the form to the API; the API derives the question id and calls prepareCondition (operator markets) or the adapter's initialize (UMA markets) through the SDK. The condition id comes back derived, not assigned — see Custody & on-chain settlement.
  2. Trade — web posts the order; the API matches it in the database book instantly, then a SETTLE_MATCH job submits both signed orders to the exchange's matchOrders. The UI's settling on-chain… chip is that job in flight.
  3. Resolve & redeem — the operator path calls reportPayouts directly; the UMA path runs propose → dispute → vote on the oracle and the adapter copies the settled answer in. Either way, redemption is the holder's own redeemPositions call — see Resolution & the UMA oracle.

Reads follow the same discipline: the portfolio page asks the API, the API asks the chain — one balanceOfBatch across every outcome token, netted against fills that are still settling, so what you see immediately after a trade equals what the chain confirms a block later.