🔥Overtime V2 integration

Step-by-step Overtime V2 integration guide.

Overtime V2 API

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 the Live 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:

  1. Get markets from Overtime V2 API

  2. 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.

  3. Get a Sports AMM V2 contract address for a specific network from Thales V2 contracts

  4. Get a Sports AMM V2 contract ABI from the Overtime V2 contract repository

  5. Create a Sports AMM V2 contract instance

  6. 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 ABI

dotenv.config();

const API_URL = "https://overtimemarketsv2.xyz"; // base API URL

const NETWORK_ID = 10; // Optimism network ID
const NETWORK = "optimism"; // Optimism network
const SPORTS_AMM_V2_CONTRACT_ADDRESS = "0xFb4e4811C7A811E098A556bD79B64c20b479E431"; // Sports AMM V2 contract address on Optimism

const BUY_IN = 20; // 20 THALES
const COLLATERAL = "THALES"; // THALES
const COLLATERAL_DECIMALS = 18; // THALES decimals: 18
const COLLATERAL_ADDRESS = "0x217D47011b23BB961eB6D93cA9945B7501a5BB11"; // THALES contract address
const SLIPPAGE = 0.02; // slippage 2%
const REFERRAL_ADDRESS = "0x0000000000000000000000000000000000000000"; // referral address, set to ZERO address for testing

// create instance of Infura provider for Optimism network
const provider = new ethers.providers.InfuraProvider(
  { chainId: Number(NETWORK_ID), name: NETWORK },
  process.env.INFURA,
);

// create wallet instance for provided private key and provider
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

// create instance of Sports AMM V2 contract
const sportsAMM = new ethers.Contract(SPORTS_AMM_V2_CONTRACT_ADDRESS, sportsAMMV2ContractAbi, wallet);

const getQuoteTradeData = (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,
  };
};

const getTradeData = (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,
      })),
    ),
  }));

const trade = async () => {
  try {
    // get a EURO 2024 markets from Overtime V2 API and ungroup them
    const marketsResponse = await axios.get(
      `${API_URL}/overtime-v2/networks/${NETWORK_ID}/markets?leagueId=50&ungroup=true`,
    );
    const markets = marketsResponse.data;

    // get a Slovakia - Romania child handicap market with line -1.5
    const slovakiaRomaniaHandicapMarket = markets[0].childMarkets[2];
    console.log(`Game: ${slovakiaRomaniaHandicapMarket.homeTeam} - ${slovakiaRomaniaHandicapMarket.awayTeam}`);
    // get a Ukraine - Belgium parent winner market
    const ukraineBelgiumWinnerMarket = 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 network
    const quoteTradeData = [
      getQuoteTradeData(slovakiaRomaniaHandicapMarket, 1),
      getQuoteTradeData(ukraineBelgiumWinnerMarket, 1),
    ];
    const quoteResponse = await axios.post(`${API_URL}/overtime-v2/networks/${NETWORK_ID}/quote`, {
      buyInAmount: BUY_IN,
      tradeData: quoteTradeData,
      collateral: COLLATERAL,
    });
    const quote = 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 BigNumber
    const parsedTotalQuote = ethers.utils.parseEther(quote.quoteData.totalQuote.normalizedImplied.toString());
    // convert buy-in amount to BigNumber
    const parsedBuyInAmount = ethers.utils.parseUnits(BUY_IN.toString(), COLLATERAL_DECIMALS);
    // convert slippage tolerance to BigNumber
    const parsedSlippage = ethers.utils.parseEther(SLIPPAGE.toString());

    // call trade method on Sports AMM V2 contract
    const tx = await sportsAMM.trade(
      getTradeData(quoteTradeData),
      parsedBuyInAmount,
      parsedTotalQuote,
      parsedSlippage,
      REFERRAL_ADDRESS,
      COLLATERAL_ADDRESS,
      false,
      {
        type: 2,
        maxPriorityFeePerGas: w3utils.toWei("0.00000000000000001"),
      },
    );
    // wait for the result
    const txResult = await tx.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:

  1. Get live markets from Overtime V2 API

  2. Select live market and position

  3. Get a Live Trading Processor contract address for a specific network from Thales V2 contracts

  4. Get a Live Trading Processor contract ABI from the Overtime V2 contract repository

  5. Create a Live Trading Processor contract instance

  6. Call requestLiveTrade method on Live Trading Processor contract with input parameters fetched from Overtime V2 API in steps #1 and #2

  7. 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 ABI

dotenv.config();

const API_URL = "https://overtimemarketsv2.xyz"; // base API URL

const NETWORK_ID = 10; // Optimism network ID
const NETWORK = "optimism"; // Optimism network
const LIVE_TRADING_PROCCESSOR_CONTRACT_ADDRESS = "0x3b834149F21B9A6C2DDC9F6ce97F2FD1097F8EAB"; // Live Trading Processor contract address on Optimism

const BUY_IN = 10; // 20 USDC
const POSITION = 2; // draw
const COLLATERAL_DECIMALS = 6; // USDC decimals: 6
const COLLATERAL_ADDRESS = "0x0000000000000000000000000000000000000000"; // USDC contract address (can be ZERO address since USDC is default collateral)
const SLIPPAGE = 0.02; // slippage 2%
const REFERRAL_ADDRESS = "0x0000000000000000000000000000000000000000"; // referral address, set to ZERO address for testing

// create instance of Infura provider for Optimism network
const provider = new ethers.providers.InfuraProvider(
  { chainId: Number(NETWORK_ID), name: NETWORK },
  process.env.INFURA,
);

// create wallet instance for provided private key and provider
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);

// create instance of Live Trading Processor contract
const liveTradingProcessor = new ethers.Contract(
  LIVE_TRADING_PROCCESSOR_CONTRACT_ADDRESS,
  liveTradingProcessorContractAbi,
  wallet,
);

const delay = (time) => {
  return new Promise(function (resolve) {
    setTimeout(resolve, time);
  });
};

const convertFromBytes32 = (value) => {
  const result = bytes32({ input: value });
  return result.replace(/\0/g, "");
};

const buyLivePosition = async () => {
  try {
    // get live markets from Overtime V2 API
    const marketsResponse = await axios.get(`${API_URL}/overtime-v2/networks/${NETWORK_ID}/live-markets`);
    const markets = marketsResponse.data.markets;

    // get a Tokyo Verdy 1969 - Consadole Sapporo market
    const market = markets[2];
    console.log(`Game: ${market.homeTeam} - ${market.awayTeam}`);

    // convert market odds got from API to BigNumber
    const parsedQuote = ethers.utils.parseEther(market.odds[POSITION].normalizedImplied.toString());
    // convert buy-in amount to BigNumber
    const parsedBuyInAmount = ethers.utils.parseUnits(BUY_IN.toString(), COLLATERAL_DECIMALS);
    // convert slippage tolerance to BigNumber
    const parsedSlippage = ethers.utils.parseEther(SLIPPAGE.toString());

    // get max allowed execution delay from Live Trading Processor contract
    const maxAllowedExecutionDelay = Number(await liveTradingProcessor.maxAllowedExecutionDelay());

    // call trade method on Sports AMM V2 contract
    const tx = await liveTradingProcessor.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 result
    const txResult = await tx.wait();
    if (txResult) {
      console.log("Live trade requested. Fulfilling live trade...");

      const requestId = txResult.events.find((event) => event.event === "LiveTradeRequested").args[2];

      let requestInProgress = true;
      const startTime = Date.now();
      console.log(`Fulfill start time: ${new Date(startTime)}`);

      while (requestInProgress) {
        const isFulfilled = await liveTradingProcessor.requestIdToFulfillAllowed(requestId);
        console.log(`Is fulfilled: ${isFulfilled}`);
        if (isFulfilled) {
          console.log(`Fulfill end time: ${new Date(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 execution
          if (Date.now() - startTime >= (Number(maxAllowedExecutionDelay) + 10) * 1000) {
            console.log("Odds changed while fulfilling the order. Try increasing the slippage.");
            requestInProgress = false;
          } else {
            await delay(1000);
          }
        }
      }
    }
  } catch (e) {
    console.log("Failed to buy live position from Sports AMM V2", e);
  }
};

buyLivePosition();

Last updated