OddsX is a next-generation pari-mutuel prediction market running on Arc Testnet. Traders take YES or NO positions using native USDC, market odds emerge from the distribution of liquidity between outcome pools, and winning positions receive a proportional share of the distributable pool after resolution.
The repository is a pnpm monorepo containing the Next.js trading interface, shared Arc/contract configuration, and the Foundry smart contract workspace.
OddsX is deployed for testnet development. The contracts have not undergone an independent production security audit. Do not treat testnet behavior, reference prices, or this repository as financial advice.
| Setting | Value |
|---|---|
| Network | Arc Testnet |
| Chain ID | 5042002 |
| Native currency | USDC, 18 decimals |
| OddsX contract | 0x6C9fD55355e83190363842693867826d4eCd94C5 |
| Protocol admin | 0x8010d267f4df81ff2beD3D0De1C0bC4da5CA05f5 |
| RPC endpoint | https://rpc.testnet.arc.network |
| Block explorer | ArcScan Testnet |
| Deployment block | 54065221 |
The frontend reads its RPC endpoint and contract address from environment variables. The values above are the current audit-remediated Arc Testnet deployment. It was deployed with the deployer as admin, a 1.5% default fee, and zero resolution delay.
The default ETH_ABOVE_5000 native-USDC market was created at block 54065766 and expires on August 11, 2026 at 09:09:34 UTC.
- Connect wallet — use an injected wallet, Coinbase Wallet, or configured WalletConnect connection and switch it to Arc Testnet.
- Analyze odds — inspect the live YES/NO pool distribution, reference asset chart, and estimated payout multiplier.
- Cast a prediction — choose an outcome and submit native USDC to its pool through
placeBet. - Settle or cancel — the designated oracle or resolver reports the outcome after the optional delay; authorized cancellers can stop an invalid market.
- Claim or refund — winners call
claimReward; cancelled-market participants reclaim each outcome stake throughemergencyRefund.
The live BTC/ETH chart is informational. Binance supplies the reference candles and ticker stream, while the market's designated on-chain oracle remains the sole settlement authority.
OddsX/
├── apps/
│ └── web/ Next.js App Router trading interface
│ └── src/
│ ├── app/ Root layout, metadata, and page entry
│ ├── components/ Market, betting, portfolio, chart, and onboarding UI
│ ├── hooks/ Typed Wagmi reads, writes, receipts, and event history
│ ├── lib/ Formatting, RPC resilience, parsing, and error safety
│ └── providers/ Arc Wagmi and TanStack Query configuration
├── packages/
│ ├── config/ Shared Arc chain definition, address registry, and web ABI
│ └── contracts/ OddsX Solidity source, Foundry tests, and deployment scripts
├── package.json Workspace command entry points
└── pnpm-workspace.yaml Workspace package discovery
Arc RPC
├── getMarketWithPools ──────────> coherent active market book and implied odds
├── getUserStake + previewReward ──> connected-wallet portfolio
└── bounded contract events ──────> featured markets and activity tape
Connected wallet
├── placeBetWithBounds ──────────> deadline/slippage-bounded native USDC stake
├── claimReward ──────────────────> resolved-market payout
├── emergencyRefund ──────────────> cancelled-market stake recovery
├── resolveMarket / cancelMarket ─> role-aware settlement controls
└── createMarket ─────────────────> role-gated Arc test market
All frontend contract calls use the ABI in packages/config/src/abi/oddsXAbi.ts. The Solidity protocol source of truth is packages/contracts/src/OddsX.sol, with shared market types and events in packages/contracts/src/interfaces/IOddsX.sol.
OddsX does not use an order book or constant-product AMM. Every outcome has a stake pool. The distribution of the total pool determines the displayed implied probability, while final payouts depend on the winning pool's share of total liquidity.
For a two-outcome market:
totalPool = yesPool + noPool
protocolFee = floor(totalPool × feeBps / 10,000)
distributablePool = totalPool - protocolFee
userReward = floor(
userWinningStake × distributablePool / winningPool
)
The frontend preview includes the proposed wager in both the selected outcome pool and total pool before calculating the estimated return. Estimates can change as later bets enter the market and are not guaranteed until resolution.
The UI treats an empty pool as unpriced rather than displaying a synthetic probability. Web bets use a five-minute deadline and require the execution-time expected reward to remain within 1% of the displayed estimate. These bounds protect transaction inclusion, not the final pari-mutuel payout: later bets can still change every participant's eventual return.
| State | Meaning |
|---|---|
None |
The market ID has not been created. Reads revert with MarketDoesNotExist. |
Open |
Betting is allowed until endTime. |
Resolved |
The oracle selected a funded winning outcome and rewards are claimable. |
Cancelled |
Trading stopped and users can recover stakes with emergencyRefund. This also occurs automatically when the objectively winning outcome has no stake. |
Resolution cannot occur before the configured end time plus the market's snapshotted resolution delay. If the selected winning outcome has no stake, resolution atomically changes the market to Cancelled, emits a dedicated event, and enables full refunds instead of selecting an incorrect funded outcome.
| Function | Purpose |
|---|---|
createMarket |
Creates a unique market ID, oracle, expiry, outcome count, fee, and settlement asset. |
placeBet |
Collects native currency or an ERC-20 stake and updates user/outcome/market pools. |
placeBetWithBounds |
Places a bet with a deadline and minimum execution-time expected reward. |
resolveMarket |
Finalizes an expired market with its winning outcome. |
claimReward |
Transfers a winner's proportional distributable-pool share. |
cancelMarket |
Cancels an open market under the emergency role. |
emergencyRefund |
Returns a user's stake for one outcome in a cancelled market. |
setDefaultProtocolFee |
Changes the fee applied to subsequently created markets. |
withdrawProtocolFees |
Transfers accrued fees to an authorized recipient. |
getMarket / getOutcomePool |
Returns current market configuration, accounting, and pool liquidity. |
getMarketWithPools |
Returns the market and every pool from one coherent RPC snapshot. |
getUserStake / previewReward |
Returns wallet-specific exposure and claimable reward previews. |
The current web trader supports native-USDC betting. It detects non-native settlement markets and disables the native execution form instead of submitting an incompatible transaction.
OddsX.sol uses OpenZeppelin AccessControl with explicit operational roles:
| Role | Authority |
|---|---|
DEFAULT_ADMIN_ROLE |
Grants and revokes protocol roles. Assigned to the deployment administrator. |
MARKET_CREATOR_ROLE |
Creates new markets. The frontend checks this role before enabling its test-market launchpad. |
RESOLVER_ROLE |
Resolves expired markets in addition to each market's designated oracle. |
CANCELLER_ROLE |
Cancels disrupted or invalid markets so participants can request refunds. |
FEE_MANAGER_ROLE |
Changes the default fee and withdraws accrued protocol fees. |
Additional contract protections include:
ReentrancyGuardon stake, claim, refund, and fee-withdrawal paths;SafeERC20transfers and balance-delta validation for unsupported fee-on-transfer tokens;- checks-effects-interactions ordering before native or token payouts;
- immutable per-market fee snapshots;
- duplicate market, zero-address, invalid amount, invalid outcome, and expiry validation;
- explicit rejection of unsolicited native transfers;
- automatic cancellation and refund availability when the reported winner has no stake;
- optional per-market resolution-delay snapshots capped at seven days;
- execution deadlines and minimum expected-reward bounds for protected bets;
- Solidity custom errors for deterministic failure handling.
The deployment script accepts ODDSX_ADMIN, allowing every initial role to be assigned directly to a multisig while a separate funded EOA broadcasts deployment. A configurable ODDSX_RESOLUTION_DELAY provides a timelock-ready settlement window. Before production deployment, define a dispute-capable oracle policy, allowlist settlement assets, add Arc fork invariants, perform an independent audit, and document emergency governance procedures.
The public Arc RPC is intentionally treated as a constrained shared resource:
- Wagmi polling is set to 12 seconds.
- Viem HTTP requests use a 10-second timeout and one bounded transport retry.
- TanStack Query keeps reads fresh for one block interval, caches inactive data for five minutes, and disables automatic window-focus refetching.
- Compatible reads are grouped through Wagmi multicall batching.
- Public activity starts at the later of deployment block
54065221orlatest - 9,999, keeping the live tape bounded. - Featured markets and wallet-filtered bet/reward history are fetched from deployment in sequential 10,000-block pages.
- Portfolio market states are refreshed with a multicall after paginated wallet history is loaded.
- Selected market snapshots refetch on matching lifecycle events and every 12 seconds as a fallback.
- Live activity pauses its watcher after a rate-limit response and retries history after 30 seconds.
- The in-memory activity tape is capped at the latest 100 deduplicated bets.
- Raw RPC and Viem JSON errors are replaced with concise rate-limit, timeout, wallet-rejection, and network fallback messages.
These controls reduce request bursts, but a dedicated RPC provider or indexer is recommended for production traffic and complete historical analytics.
- Next.js 16 App Router
- React 19
- TypeScript with strict, exact optional, unchecked-index, and unused-symbol checks
- Tailwind CSS
- Framer Motion
- Lucide React icons
- Recharts for session pool-odds visualization
- Lightweight Charts for BTC/ETH reference candles
- Wagmi v2
- Viem
- TanStack Query
- Injected EIP-1193, Coinbase Wallet, and optional WalletConnect connectors
- Solidity
0.8.30 - Foundry: Forge, Cast, and Anvil
- OpenZeppelin Contracts
- pnpm workspaces
- Node.js 22 or newer
- pnpm 10.15 or a compatible pnpm 10 release
- Foundry with Solidity
0.8.30support - A browser, Coinbase, or WalletConnect-compatible wallet for transactions
- Arc Testnet native USDC for transactions and test wagers
Confirm the toolchain:
node --version
pnpm --version
forge --version
cast --versionFrom the repository root:
pnpm installCreate apps/web/.env.local:
NEXT_PUBLIC_ARC_TESTNET_RPC_URL=https://rpc.testnet.arc.network
NEXT_PUBLIC_ODDSX_ADDRESS_ARC_TESTNET=0x6C9fD55355e83190363842693867826d4eCd94C5
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=<optional-reown-project-id>Create packages/config/.env.local with the same public values when running configuration tooling directly:
NEXT_PUBLIC_ARC_TESTNET_RPC_URL=https://rpc.testnet.arc.network
NEXT_PUBLIC_ODDSX_ADDRESS_ARC_TESTNET=0x6C9fD55355e83190363842693867826d4eCd94C5Never place a private key in either frontend environment file. Next.js exposes variables prefixed with NEXT_PUBLIC_ to the browser.
Start the web application:
pnpm devOpen http://localhost:3000. The application still reads live market state from Arc Testnet; the local process only serves the frontend.
Run the production-quality gates:
pnpm typecheck
pnpm lint
pnpm buildpnpm build compiles the shared configuration and creates the optimized Next.js production bundle with TypeScript and framework checks enabled.
Run the 24-test Foundry suite through the requested root command:
pnpm testAdditional contract commands:
pnpm contracts:build
pnpm contracts:test
pnpm contracts:coverageThe Foundry profile enables the optimizer with 10,000 runs, via_ir, 1,000 fuzz runs, and 256 invariant runs at depth 64.
-
Create
packages/contracts/.envand add a funded deployer key:PRIVATE_KEY=<funded-arc-testnet-private-key> ODDSX_ADMIN=<admin-or-multisig-address> ODDSX_RESOLUTION_DELAY=0
-
Keep the file private. It is consumed by
DeployOddsX.s.soland must never be committed or exposed to the frontend. -
Deploy using the configured
arc_testnetFoundry RPC alias:pnpm contracts:deploy:arc-testnet
-
Record the confirmed contract address from the Forge output and ArcScan receipt.
-
Update
NEXT_PUBLIC_ODDSX_ADDRESS_ARC_TESTNETin both frontend/config environment files, then restart or rebuild the web application.
The deployment script assigns DEFAULT_ADMIN_ROLE, MARKET_CREATOR_ROLE, RESOLVER_ROLE, CANCELLER_ROLE, and FEE_MANAGER_ROLE to ODDSX_ADMIN (or the deploying address when omitted), initializes the default protocol fee to 150 basis points, and snapshots the configured default resolution delay into new markets.
| Command | Purpose |
|---|---|
pnpm install |
Installs all workspace dependencies. |
pnpm dev |
Starts the Next.js development server. |
pnpm test |
Runs the Foundry smart contract test suite in offline mode. |
pnpm typecheck |
Runs strict TypeScript checks and a Foundry build across workspaces. |
pnpm lint |
Runs ESLint, config typechecking, and forge fmt --check. |
pnpm build |
Builds shared config and the production Next.js application. |
pnpm format |
Formats workspace files with Prettier. |
pnpm format:check |
Checks Prettier formatting without modifying files. |
pnpm contracts:build |
Compiles OddsX.sol with Forge. |
pnpm contracts:test |
Runs offline contract tests with verbose Forge output. |
pnpm contracts:coverage |
Generates Solidity coverage data. |
pnpm contracts:deploy:local |
Broadcasts the deployment script to a local Anvil RPC. |
pnpm contracts:deploy:arc-testnet |
Broadcasts the deployment script to Arc Testnet. |
Confirm NEXT_PUBLIC_ODDSX_ADDRESS_ARC_TESTNET is present in apps/web/.env.local, then restart the development server. Next.js reads public environment variables at process start and build time.
Use the header's Switch to Arc action. Contract reads remain pinned to Arc for market visibility, while writes are blocked until the connected wallet reports chain ID 5042002.
The public RPC returned a rate-limit response. The UI stops the log watcher, keeps raw RPC details out of the page, and retries after 30 seconds. Wait for the clean fallback state to recover or configure a higher-capacity Arc-compatible RPC endpoint.
Market labels are hashed with keccak256 and are case-sensitive. Confirm the exact creation label or provide its 32-byte market ID. getMarket intentionally reverts for an unknown ID.
The Binance REST or WebSocket endpoint may be blocked or disconnected. Trading and settlement remain available because the chart is not used by the OddsX contract or oracle.
No project-level license has been declared. Third-party dependencies retain their respective licenses. Add an explicit repository license before public production distribution.