Build a Blockchain-Based Fantasy Sports Game Using a Chainlink External Adapter

At the 2021 ETHDenver Hackathon, Chainlink Bounty Winner Pr!ce used a Chainlink External Adapter to connect off-chain NBA player performance data to a Chainlink oracle for powering a blockchain-based fantasy sports game. This marketplace model can also be applied to other zero-sum performance assets and is a great demonstration of how Chainlink’s configurable and secure oracle infrastructure can be used to connect unique off-chain APIs and custom datasets to smart contracts, enabling competitive applications for sports, gaming, DeFi, and more.

In this post, the Pr!ce team members, Anweshi Anavadya, Ishan Ghimire, Sam Orend, and Jasraj Bedi, explain how they used Chainlink to accurately calculate and deliver NBA player metadata on-chain to create “shares” in the form of semi-fungible ERC-1155 tokens. They also walk through how their dApp called the Chainlink ETH-USD Price Feed to securely distribute prizes to game players.


By SamIshanAnweshi, and Jasraj

Meet Pr!ce, a seasonal blockchain-based marketplace that enables users to buy and sell shares in NBA players and even receive dividends for their performance. This post will explain how Pr!ce works from a technical perspective and how our team used Chainlink External Adapters and price oracles to securely fetch external data for our on-chain marketplace.

The Pr!ce dApp demo launch screen.
The Pr!ce dApp demo launch screen.

How Does a Seasonal Marketplace Work?

At the start of the NBA season, Pr!ce issues “shares” of NBA players. What do these shares look like? We built Pr!ce on the Ethereum network, so our shares are ERC-1155 (semi-fungible) tokens. Each token contains metadata that ties it to a specific player. We then visualize this metadata by pulling it with web3.js and creating visualizations of it with React components.

Pr!ce dApp interface featuring NBA players represented as ERC-1155 tokens.
Pr!ce ERC-1155 semi-fungible tokens built with NBA player metadata.

At the start of the NBA season, Pr!ce mints 100 ERC-1155 tokens for each NBA player. The metadata for these tokens are fetched using Chainlink, which calls an off-chain API we developed to manage league data that does not need to be on-chain, as it remains immutable during the season, yet varies by season. In the first week, users can bid on the players whose shares they wish to purchase using wrapped ERC-20 tokens. After the first week, the ERC-1155 tokens are transferred to the wallets of each user and the season begins.

Pr!ce user interface purchasing shares of Kevin Durant using MetaMask browser wallet on the Ethereum network.
A Pr!ce user purchasing shares of Kevin Durant for the season.

In season, users can buy/sell additional shares of any player at market price. The market price of a given player is determined by our AMM (Automated Market-Maker). The AMM determines the market price of one share based on liquidity pools, similar to Uniswap.

Pr!ce user interface showing NBA player tokens owned and player metadata.
Pr!ce platform Shareholder Portal UI showing the shares owned for the season.

Finally, depending on how well a user’s holdings perform, they may be eligible to collect dividends. Performance in our system is defined as quantifiable output. This is different for every asset type — for the NBA we used the statistical performance of the players. Dividends are something that make Pr!ce unique. We used Chainlink Price Feeds to calculate the dividend distribution at the end of the season.

Calculating Dividends Off-Chain

Much like a financial market, Pr!ce rewards you for holding shares in top-performing NBA players. We do this by paying out a portion of the commissions to shareholders as dividends. Dividend payouts are based on the fantasy sports performance of the athletes that a user holds.

To first gather player performance data, we built a custom web scraping API, using Flask, to obtain the fantasy sports data of the top 50 NBA fantasy players on basketballmonster.com. However, calculating the value of our dividends is more complicated than just fetching data. It requires a statistical calculation involving the number of shares that the user owns, the gross number of shares in circulation, the value of our dividend fund, and the player’s fantasy output.

When we were developing Pr!ce, we hit an issue around how to cost-effectively calculate the dividend. If we did the dividend computation on-chain, the gas fees for processing our dividend payout would be significant, and we would be putting ourselves at risk of the gas fees potentially costing more than the value of the actual payout depending on network congestion.

For this reason, we decided to leverage Chainlink to both fetch the off-chain data we needed and also compute the value of our dividend payouts before bringing it on-chain. This allowed us to keep gas fees low and give users larger payouts. Here’s how we did it.

function giveDividendPerPlayer(string memory request_uri) public hasSeasonStarted {
  require(lastDividendWithdrawn[msg.sender] < currWeekStart);
        
  lastDividendWithdrawn[msg.sender] = block.timestamp;
  requestDividendWorthyEntities(request_uri);
}

When the user clicks “Collect Dividends” in the frontend of our application, we run this method in our Solidity smart contract to begin the process. We first require that the user hasn’t already withdrawn their dividends for this player this week. We then record that the user is withdrawing their dividends today, at the current block.timestamp, and then make our call to see if their holdings are eligible for dividends.

function requestDividendWorthyEntities(string memory request_uri) public onlyOwner {
  Chainlink.Request memory request = buildChainlinkRequest(nba_JOBID, address(this), this.fulfill.selector);
  // Set the URL to perform the request on
  request.add("get", request_uri);
  request.add("path", "to_send");

  bytes32 reqID = sendChainlinkRequestTo(nba_ORACLE, request, fee);
  jobIdMapping[reqID] = msg.sender;
}

function fulfill(bytes32 reqID, uint256 payout) public recordChainlinkFulfillment(reqID)
{
  require(jobIdMapping[reqID] != address(0x0));
  payable(jobIdMapping[reqID]).transfer(payout);
  jobIdMapping[reqID] = address(0x0);
  //payout to jobIdMapping[randomID]
}

In this function, we build our Chainlink request, which will determine the value of our dividend payout to the user. The single parameter, request_uri, is constructed elsewhere, and contains the necessary data to compute our dividend in query parameters, as will be seen later. Of note are the following lines:

We add a "get" request to our Chainlink request. This will determine the value of our dividend payout.

request.add("get", request_uri);

We also add "path" to our request, which specifies our desired data, to_send. Our endpoint returns the value of the payout we will send to the user in to_send. Adding this as path helps us cast our desired data into what we return to the user.

request.add("path", "to_send");

We store a mapping from request IDs to user addresses that are requesting the dividend payout. When fulfill is run, we transfer the calculated payout to the message sender (the user who requested the dividend).

We send the Chainlink request that we just built to this endpoint in our Flask API. Let’s break this down, because this is where our gas fees are saved!

@app.route('/to_send_per_entity/<int:entity_id>/<int:shares_owned>/<int:shares_in_circulation>/<int:dividend_fund>', methods=['GET'])
@cross_origin()
def to_send(entity_id, shares_owned, shares_in_circulation, dividend_fund):
    if entity_id > LEAGUE_SIZE:
        entity_id -= LEAGUE_SIZE   

    league = league_data()
    entity_held_score = float(league[entity_id - 1]['fantasy_points'])

    sum_scores = 0.0 
    for entity in league:
        sum_scores += float(entity["fantasy_points"])

    res = (shares_owned/shares_in_circulation)*(entity_held_score / sum_scores)*dividend_fund
           
    return {"to_send": int(res)}

In short, this endpoint is calculating the amount of ETH “to_send” to the user as a dividend payment. This payment calculation first involves summing up the cumulative fantasy points earned by each NBA player during the week.

for entity in league: sum_scores += float(entity["fantasy_points"])

We then calculate the resulting payout based on the cumulative fantasy points, shares owned, gross shares in circulation and the value of our dividend fund.

res=(shares_owned/shares_in_circulation)*(entity_held_score/ sum_scores)*dividend_fund

The amount of the dividend payment is then returned in “to_send”

return {"to_send": int(res)}

But since we added “path” to our Chainlink request above, it will initiate the process of sending “to_send” ETH to the user, right away.

By executing this off-chain we avoid the gas fees related to this computation, which we would normally incur from handling this computation in a separate method in our Solidity smart contract.

Finally, at the end of the season, we issue “Champion NFTs” to the top stakeholders of each player asset. This encourages competition and healthy volatility in the marketplace and allows users to retain an item post-season.

What’s Next for Pr!ce?

Looking to the future, we plan to expand on the hybrid smart contract architecture that we built with this project and continue creating semi-fungible token marketplaces in the crypto ecosystem. We are currently working on an exciting project that we hope to release soon, so stay tuned!

More on this Topic

If you’re a developer and want to connect your smart contract to existing data and infrastructure outside the underlying blockchain, reach out here or visit the developer documentation.

Website | Twitter | Discord | Reddit | YouTube | Telegram | Events | GitHub | Price Feeds | DeFi