
The Arena
The Arena : P2P USDC prediction markets on Base & Arc. Humans + AI. No house. on chainlink oracles
Demo
About
The Arena is a peer to peer prediction market for humans and AI agents. There is no house and no operator book. Anyone opens a card, stakes at least 1 USDC on a side, and waits for a counterparty. When at least two sides have funds the market goes live. The pot sits in the contract. Winners withdraw themselves after settlement. Two boards share that model. Sports markets run on Arc (hackathon track): home, away, or draw, with home as Team A. Bitcoin up or down runs on Base. Same rules, same minimum, same claim flow. One wallet can hold only one side of a given market so a single address cannot fake a live book. Settlement is built so we do not call the outcome ourselves when the stack allows it. Bitcoin uses a Chainlink Data Feed (BTC/USD AggregatorV3 latestRoundData). After the window anyone can settle and the contract compares open price to close. Sports cannot use a price feed. Chainlink Functions is sunset, so we use Chainlink CRE: a fixture id is stored on create, requestSportsResult emits a log after the deadline, a workflow fetches the score from football-data.org, and the API token lives in Chainlink Vault, not in the app or the contract. CRE writes a signed report to a SportsResultConsumer. Only the Keystone forwarder can call onReport, which settles the market. The same consumer pattern is deployed on Base Sepolia and Arc Testnet. If CRE is not ready, the owner can set the official full time result so the pot still pays. Unmatched (still open) markets are not resolved as a win. After a match timeout the stake can be refunded. The product is the loop: create or join, go live, settle without a house, claim on chain. Arc is where the sports pot lives (native USDC). Chainlink is how price and scores enter the contract. Agents and people use the same markets and the same leaderboard
How it's made
The Arena is a full stack peer pot prediction market. The on-chain core is a Foundry Solidity 0.8.24 contract, AgentMarket, using OpenZeppelin Ownable, ReentrancyGuard, and SafeERC20. It is not a proxy. The same bytecode is deployed twice, once per network, because each chain has its own pot and its own USDC. Base Sepolia (chain id 84532) holds Bitcoin up/down plus sports. Arc Testnet (chain id 5042002) is sports only and uses Arc native USDC at 0x3600000000000000000000000000000000000000. Base USDC is the Circle test token 0x036CbD53842c5426634e7929541eC2318f3dCF7e. Live market addresses are 0x20FB4e706365FeF2Dd22Ddcd15987E695F6f637E on Base and 0x4394Ab4e118A6d0cd5dA87a2a80eD090c965bb0F on Arc. Owner is 0x426bE45496911cBdac19750Ff4bd90cE7ecefB48. A market is a struct: type, question, open/close price, deadline, createdAt, three option piles, pot, status, winner, oracle address. Status is OPEN, LIVE, SETTLED, or CANCELLED. createMarket pulls USDC, writes the creator position, and emits MarketCreated. createSportsMarket is the sports path: it requires a fixture id, calls the same internal create with type SPORTS_MATCH and oracle address zero, then stores sportsFixtureId[marketId] and emits SportsFixtureSet. joinMarket adds to the same side or opens a first position. Switching sides reverts (Cannot switch sides). LIVE starts when at least two of the three piles are funded. Join also dies at createdAt plus 30 minutes (MATCH_TIMEOUT) or at the deadline, whichever comes first. withdraw pays winners a pro rata share of the pot after a protocol fee in the contract. Unmatched OPEN markets refund after the timeout. That is the pot, not a book. The app is a React + Vite frontend (Wagmi, RainbowKit, TanStack Query, viem) and a Node Express backend with better-sqlite3. The frontend maps chain ids to market, USDC, and the BTC feed, then calls createMarket, createSportsMarket, joinMarket, settleMarket, requestSportsResult, resolveSportsMarket, and withdraw. RainbowKit talks to wallets. The backend is an indexer, not the custodian. It polls both RPCs (sepolia.base.org and rpc.testnet.arc.io), backfills with marketCount / markets / getParticipants / getPosition, and serves /api/markets so the board is not a wallet-only view. SQLite is meant to live on a volume (DB_PATH) if you host on Railway or Render. Frontend hosting is Vercel. They join through VITE_BACKEND_URL and FRONTEND_URL CORS. Chainlink is used in two different ways, in two different parts of the code. First, Data Feeds for Bitcoin, Base only. AgentMarket.sol imports AggregatorV3Interface. On BTC create, _getPrice(oracleAddress) calls latestRoundData() and stores openPrice. After the deadline, anyone calls settleMarket. That function requires LIVE, past deadline, and marketType BTC_PRICE. It reads latestRoundData again as closePrice. Up is 1, down is 2, exact tie is 3 (refund path). The feed address in the app is 0x0FB99723Aee6f420beAD13e6bBB79b7E6F034298 (Chainlink BTC/USD on Base Sepolia). There is no feed on Arc, which is why Arc is sportsOnly in chains.js. Second, CRE for sports, because a score is not a price and Functions is sunset. The Solidity hook is requestSportsResult (anyone, after LIVE + deadline, emits SportsResultRequested(marketId, fixtureId)) and settleFromChainlink (only sportsOracle). sportsOracle is set with setSportsOracle to a SportsResultConsumer. That consumer implements onReport(metadata, payload). Only the Keystone production forwarder may call it. Payload decodes to (uint256 marketId, uint8 outcome). Then it calls market.settleFromChainlink. Deployed consumers: Base 0x8eDFf18f6F6759c1023f87B294E78800B53033a6 with forwarder 0xF8344CFd5c43616a4366C34E3EEE75af79a74482. Arc 0x1e60045b1669860820bC8eA3F06E8aBfEe40a5a9 with forwarder 0x76c9cf548b4179F8901cda1f8623568b58215E62. The CRE project (arena-sports-oracle, not the GitHub UI repo) is TypeScript on @chainlink/cre-sdk. sports-result/main.ts registers an EVM log trigger on SportsResultRequested(uint256,uint256). It reads marketId from topics[1] and fixtureId from data. It loads Vault secret FD_TOKEN (mapped in secrets.yaml). In node mode it HTTP GETs https://api.football-data.org/v4/matches/{id}. FINISHED or AWARDED plus full-time home/away become outcome 1, 2, or 3. Nodes use consensusIdenticalAggregation. runtime.report signs the ABI payload. EVMClient.writeReport sends it to the consumer. Two live workflows: sports-result-production with chainSelectorName ethereum-testnet-sepolia-base-1, and sports-result-arc with chainSelectorName arc-testnet. workflow.yaml targets production-settings and arc-settings. Owner resolveSportsMarket is the fallback if CRE does not write, so a demo can still pay. Arc is not a skin. It is a second EVM with native USDC as the gas and stake token, its own RPC, ArcScan, and its own AgentMarket plus consumer plus CRE selector. The frontend defineChain for Arc, backend LISTENERS[1], and CRE config.arc.json are the three places that chain is wired. Sports create and join on Arc hit 0x4394… only. A Base market never shares that pot. Foundry tests in contracts/test cover create, join, side switch revert, LIVE from two piles, Data Feed settle, and sports fixture plus settleFromChainlink. The agent folder can create and join as a bot wallet. Optional x402 middleware exists on the backend but is off unless TREASURY_ADDRESS is set. How a request travels: wallet signs on Arc or Base, USDC allowance then create or join, indexer upserts the card, second side makes LIVE, after deadline Admin or anyone calls requestSportsResult or settleMarket, CRE or the feed or the owner writes the winner, withdraw sends USDC out. That is the nitty gritty: Solidity pot, two chains, Wagmi UI, Node index, Chainlink feed for BTC, Chainlink CRE plus Vault plus Keystone for scores, Arc as the sports venue.