In order to ensure easy integration with external partners Overtime V2 API is created. API returns all main data available on Overtime V2. Using Overtime V2 API endpoints someone can get data about:
More details about each API endpoint with request/response examples also can be found under Postman documentation.
Contract integration
Once all data are fetched from V2 API, the next step is integration with Overtime V2 contracts. Integration for both, a single or a parlay should be done with the Sports AMM V2 contract. Integration for live markets should be done with theLive Trading Processor contract.
The next sections describe integration with Overtime V2 API and Overtime V2 contracts together with JS code examples.
Buy a ticket
Users placing trades with THALES will get 1% extra payouts for each game they have on their ticket.
Let's say someone wants to buy a 2-game ticket with a buy-in amount of 20 THALES:
Integration with Overtime V2 API and Sports AMM V2 contract should include the following steps:
Select ticket markets and positions and get a quote for a ticket from Overtime V2 API.
NOTE: This step is not mandatory. The trading method on contract requires a total quote as a parameter, but that can be calculated by simply multiplying market odds. However, the API method returns some additional data, like ticket liquidity or validation errors, if any.
Get a Sports AMM V2 contract address for a specific network from Thales V2 contracts
Call trade method on Sports AMM V2 contract with input parameters fetched from Overtime V2 API in steps #1 and #2
The JS code snippet below implements these steps:
import axios from"axios";import dotenv from"dotenv";import { ethers } from"ethers";import w3utils from"web3-utils";import sportsAMMV2ContractAbi from"./sportsAMMV2ContractAbi.js"; // Sports AMM V2 contract ABIdotenv.config();constAPI_URL="https://overtimemarketsv2.xyz"; // base API URLconstNETWORK_ID=10; // Optimism network IDconstNETWORK="optimism"; // Optimism networkconstSPORTS_AMM_V2_CONTRACT_ADDRESS="0xFb4e4811C7A811E098A556bD79B64c20b479E431"; // Sports AMM V2 contract address on OptimismconstBUY_IN=20; // 20 THALESconstCOLLATERAL="THALES"; // THALESconstCOLLATERAL_DECIMALS=18; // THALES decimals: 18constCOLLATERAL_ADDRESS="0x217D47011b23BB961eB6D93cA9945B7501a5BB11"; // THALES contract addressconstSLIPPAGE=0.02; // slippage 2%constREFERRAL_ADDRESS="0x0000000000000000000000000000000000000000"; // referral address, set to ZERO address for testing// create instance of Infura provider for Optimism networkconstprovider=newethers.providers.InfuraProvider( { chainId:Number(NETWORK_ID), name:NETWORK },process.env.INFURA,);// create wallet instance for provided private key and providerconstwallet=newethers.Wallet(process.env.PRIVATE_KEY, provider);// create instance of Sports AMM V2 contractconstsportsAMM=newethers.Contract(SPORTS_AMM_V2_CONTRACT_ADDRESS, sportsAMMV2ContractAbi, wallet);constgetQuoteTradeData= (market, position) => {return { gameId:market.gameId, sportId:market.subLeagueId,// use subLeagueId field from API for sportId typeId:market.typeId, maturity:market.maturity, status:market.status, line:market.line, playerId:market.playerProps.playerId, odds:market.odds.map((odd) =>odd.normalizedImplied),// use normalizedImplied odds field from API for odds merkleProof:market.proof,// use proof from API for merkleProof position, combinedPositions:market.combinedPositions, live:false, };};constgetTradeData= (quoteTradeData) =>quoteTradeData.map((data) => ({...data,// multiple lines by 100 because the contract can not accept decimals line:data.line *100,// convert odds to BigNumber odds:data.odds.map((odd) =>ethers.utils.parseEther(odd.toString()).toString()),// multiple combined positions lines by 100 because the contract can not accept decimals combinedPositions:data.combinedPositions.map((combinedPositions) =>combinedPositions.map((combinedPosition) => ({ typeId:combinedPosition.typeId, position:combinedPosition.position, line:combinedPosition.line *100, })), ), }));consttrade=async () => {try {// get a EURO 2024 markets from Overtime V2 API and ungroup themconstmarketsResponse=awaitaxios.get(`${API_URL}/overtime-v2/networks/${NETWORK_ID}/markets?leagueId=50&ungroup=true`, );constmarkets=marketsResponse.data;// get a Slovakia - Romania child handicap market with line -1.5constslovakiaRomaniaHandicapMarket= markets[0].childMarkets[2];console.log(`Game: ${slovakiaRomaniaHandicapMarket.homeTeam} - ${slovakiaRomaniaHandicapMarket.awayTeam}`);// get a Ukraine - Belgium parent winner marketconstukraineBelgiumWinnerMarket= markets[1];console.log(`Game: ${ukraineBelgiumWinnerMarket.homeTeam} - ${ukraineBelgiumWinnerMarket.awayTeam}`);// get a quote from Overtime V2 API for provided trade data (markets and positions), buy-in amount and collateral on Optimism networkconstquoteTradeData= [getQuoteTradeData(slovakiaRomaniaHandicapMarket,1),getQuoteTradeData(ukraineBelgiumWinnerMarket,1), ];constquoteResponse=awaitaxios.post(`${API_URL}/overtime-v2/networks/${NETWORK_ID}/quote`, { buyInAmount:BUY_IN, tradeData: quoteTradeData, collateral:COLLATERAL, });constquote=quoteResponse.data;console.log("========== Quote ==========", quote);/* ========== Quote ========== { quoteData: { totalQuote: { american: -132.9670329668595, decimal: 1.7520661157034605, normalizedImplied: 0.5707547169808125 }, payout: { THALES: 35.04132231406921, usd: 8.913986776864496, payoutCollateral: 'THALES' }, potentialProfit: { THALES: 15.041322314069212, usd: 3.826286776864496, percentage: 0.7520661157034605 }, buyInAmountInUsd: 5.0877 }, liquidityData: { ticketLiquidityInUsd: 19098 } } */// convert total quote got from API to BigNumberconstparsedTotalQuote=ethers.utils.parseEther(quote.quoteData.totalQuote.normalizedImplied.toString());// convert buy-in amount to BigNumberconstparsedBuyInAmount=ethers.utils.parseUnits(BUY_IN.toString(),COLLATERAL_DECIMALS);// convert slippage tolerance to BigNumberconstparsedSlippage=ethers.utils.parseEther(SLIPPAGE.toString());// call trade method on Sports AMM V2 contractconsttx=awaitsportsAMM.trade(getTradeData(quoteTradeData), parsedBuyInAmount, parsedTotalQuote, parsedSlippage,REFERRAL_ADDRESS,COLLATERAL_ADDRESS,false, { type:2, maxPriorityFeePerGas:w3utils.toWei("0.00000000000000001"), }, );// wait for the resultconsttxResult=awaittx.wait();console.log(`Successfully bought a ticket from Sports AMM V2. Transaction hash: ${txResult.transactionHash}`);/* Successfully bought a ticket from Sports AMM V2. Transaction hash: 0xe65638720344cc110b77f14f4276be61e7cd767f490927c195f11813c6d39901 */ } catch (e) {console.log("Failed to buy a ticket from Sports AMM V2", e); }};trade();
Buy a position on live markets
Users placing trades with THALES will get 1% extra payouts for each game they have on their ticket.
Let's say someone wants to buy a live draw position on the game Tokyo Verdy 1969 - Consadole Sapporowith with a buy-in amount of 10 USDC:
Integration with Overtime V2 API and Live Trading Processor contract should include the following steps:
Get a Live Trading Processor contract address for a specific network from Thales V2 contracts
Get a Live Trading Processor contract ABI from the Overtime V2 contract repository
Create a Live Trading Processor contract instance
Call requestLiveTrade method on Live Trading Processor contract with input parameters fetched from Overtime V2 API in steps #1 and #2
Wait for the request to finish - fulfilled successfully or failed with some error (e.g. odds changed)
The JS code snippet below implements these steps:
import axios from"axios";import bytes32 from"bytes32";import dotenv from"dotenv";import { ethers } from"ethers";import w3utils from"web3-utils";import liveTradingProcessorContractAbi from"./liveTradingProcessorContractAbi.js"; // Live Trading Processor contract ABIdotenv.config();constAPI_URL="https://overtimemarketsv2.xyz"; // base API URLconstNETWORK_ID=10; // Optimism network IDconstNETWORK="optimism"; // Optimism networkconstLIVE_TRADING_PROCCESSOR_CONTRACT_ADDRESS="0x3b834149F21B9A6C2DDC9F6ce97F2FD1097F8EAB"; // Live Trading Processor contract address on OptimismconstBUY_IN=10; // 20 USDCconstPOSITION=2; // drawconstCOLLATERAL_DECIMALS=6; // USDC decimals: 6constCOLLATERAL_ADDRESS="0x0000000000000000000000000000000000000000"; // USDC contract address (can be ZERO address since USDC is default collateral)constSLIPPAGE=0.02; // slippage 2%constREFERRAL_ADDRESS="0x0000000000000000000000000000000000000000"; // referral address, set to ZERO address for testing// create instance of Infura provider for Optimism networkconstprovider=newethers.providers.InfuraProvider( { chainId:Number(NETWORK_ID), name:NETWORK },process.env.INFURA,);// create wallet instance for provided private key and providerconstwallet=newethers.Wallet(process.env.PRIVATE_KEY, provider);// create instance of Live Trading Processor contractconstliveTradingProcessor=newethers.Contract(LIVE_TRADING_PROCCESSOR_CONTRACT_ADDRESS, liveTradingProcessorContractAbi, wallet,);constdelay= (time) => {returnnewPromise(function (resolve) {setTimeout(resolve, time); });};constconvertFromBytes32= (value) => {constresult=bytes32({ input: value });returnresult.replace(/\0/g,"");};constbuyLivePosition=async () => {try {// get live markets from Overtime V2 APIconstmarketsResponse=awaitaxios.get(`${API_URL}/overtime-v2/networks/${NETWORK_ID}/live-markets`);constmarkets=marketsResponse.data.markets;// get a Tokyo Verdy 1969 - Consadole Sapporo marketconstmarket= markets[2];console.log(`Game: ${market.homeTeam} - ${market.awayTeam}`);// convert market odds got from API to BigNumberconstparsedQuote=ethers.utils.parseEther(market.odds[POSITION].normalizedImplied.toString());// convert buy-in amount to BigNumberconstparsedBuyInAmount=ethers.utils.parseUnits(BUY_IN.toString(),COLLATERAL_DECIMALS);// convert slippage tolerance to BigNumberconstparsedSlippage=ethers.utils.parseEther(SLIPPAGE.toString());// get max allowed execution delay from Live Trading Processor contractconstmaxAllowedExecutionDelay=Number(awaitliveTradingProcessor.maxAllowedExecutionDelay());// call trade method on Sports AMM V2 contractconsttx=awaitliveTradingProcessor.requestLiveTrade( { _gameId:convertFromBytes32(market.gameId),// use converted from bytes32 gameId field from API for gameId _sportId:market.subLeagueId,// use subLeagueId field from API for sportId _typeId:market.typeId, _position:POSITION, _line:market.line *100,// multiple lines by 100 because the contract can not accept decimals _buyInAmount: parsedBuyInAmount, _expectedQuote: parsedQuote, _additionalSlippage: parsedSlippage, _referrer:REFERRAL_ADDRESS, _collateral:COLLATERAL_ADDRESS, }, { type:2, maxPriorityFeePerGas:w3utils.toWei("0.00000000000000001"), }, );// wait for the resultconsttxResult=awaittx.wait();if (txResult) {console.log("Live trade requested. Fulfilling live trade...");constrequestId=txResult.events.find((event) =>event.event ==="LiveTradeRequested").args[2];let requestInProgress =true;conststartTime=Date.now();console.log(`Fulfill start time: ${newDate(startTime)}`);while (requestInProgress) {constisFulfilled=awaitliveTradingProcessor.requestIdToFulfillAllowed(requestId);console.log(`Is fulfilled: ${isFulfilled}`);if (isFulfilled) {console.log(`Fulfill end time: ${newDate(Date.now())}`);console.log(`Fulfill duration: ${(Date.now() - startTime) /1000} seconds`);console.log(`Successfully bought live position from Sports AMM V2`); requestInProgress =false; } else {// Add buffer of 10 seconds to wait for request to start executionif (Date.now() - startTime >= (Number(maxAllowedExecutionDelay) +10) *1000) {console.log("Odds changed while fulfilling the order. Try increasing the slippage."); requestInProgress =false; } else {awaitdelay(1000); } } } } } catch (e) {console.log("Failed to buy live position from Sports AMM V2", e); }};buyLivePosition();