Skip to main content

Distribute rewards to users

Overview

This guide describes how to integrate the Reward Distribution service into your project to distribute rewards to users. The integration involves the following steps:

  1. Define daily, weekly, and monthly token distribution limits for rewards.
  2. Get an API key and fund wallet address from Sky Mavis.
  3. Top up the fund wallet with tokens.
  4. Implement token distribution APIs.
  5. Implement reward claiming by calling the claimReward function in the Reward Distributor contract.

Token order

A token order is a transaction that represents the distribution of tokens to a user. Token orders are central to the Reward Distribution service, acting as the primary unit of record for each distribution event. They enable precise tracking, validation, and auditing of all token movements within the system.

Each token order includes the following information:

  • Quantity: the number of tokens to be distributed.
  • Token Type: the specific ERC-20 token being distributed.
  • Reference ID: a unique identifier used to deduplicate orders and ensure each distribution occurs only once.
  • Extra data: additional information stored for audit.
  • Reference context: the reason for the distribution, such as a campaign or event.

A token order can be in one of the following states:

  • Pending: right after the order is created, it is held in a pending state for a specified duration, as indicated by the valid_from field returned in the API response.
  • Completed: the order is ready for user claim after the current time exceeds the valid_from field.

Sequence diagram

Steps

Step 1. Set token distribution limits and pending period

  1. Define daily, weekly, and monthly token distribution limits for each ERC-20 token you want to distribute as rewards. For example, the following configuration sets the limits for distributing AXS and SLP tokens:

    # AXS
    0x97a9107c1793bc407d6f527b77e7fff4d812bece:
    daily_limit: 16000e18 # ~ 16k AXS
    weekly_limit: 16000e18
    monthly_limit: 16000e18

    # SLP
    0xa8754b9fa15fc18bb59458815510e40a12cd2014:
    daily_limit: 5e6 # ~ 5M SLP
    weekly_limit: 5e6
    monthly_limit: 5e6

    The limits are reset at the beginning of each day, week, and month, in UTC time, respectively. If the daily limit is reached, no more tokens can be distributed until the next day. The same applies to the weekly and monthly limits.

  2. Set the duration for which token orders are held in a pending state, after which users can claim their rewards. The default value is 3 days.

Step 2. Get an API key and fund wallet address

  1. Request an API key from Sky Mavis to authenticate your requests to the Reward Distribution service.
  2. Receive the fund wallet address (a reward distribution contract) from Sky Mavis for your game. Each game has its own distribution contract.

Step 3. Top up the fund wallet

  1. Transfer the desired amount of tokens to the fund wallet.
  2. Provide your IP address to allowlist it for the wallet.
note

The wallet is used to distribute rewards to users. Make sure to top it up with enough tokens to prevent any issues with reward distribution.

Step 4. Implement token distribution APIs

Create a token order

Whenever a player earns a reward, create a token order by forming a POST request to the /token-orders endpoint with the following parameters:

  • ref_id: a unique identifier for the order to ensure each distribution occurs only once.
  • token_address: the address of the token to distribute.
  • user_address: the address of the user to receive the tokens.
  • ref_context: a context for the order, such as a campaign or event.
  • amount: the amount of tokens to distribute.
  • extra_data: additional data you want to store with the order and track on the signature side. For example, user_id, rank, and so on.

Example request:

curl --location -g --request POST '{{api_host}}/v1/external/token-orders' \
--header 'Authorization: Basic <basic_api_key>' \
--header 'Content-Type: application/json' \
--data-raw '{
"token_orders": [
{
"ref_id": "b93c206e-8ac3-4111-808b-298d61246c31",
"token_address": "0x97a9107c1793bc407d6f527b77e7fff4d812bece",
"user_address": "0xe07d7e56588a6fd860c5073c70a099658c060f3d",
"ref_context": "leaderboard_season_5",
"amount": 3e18,
"extra_data": { // any data you want to provide and track on signature side
"user_id": "3b541bee-8189-467b-805e-f3d50e06f583",
"rank": 1
}
},
{
"ref_id": "b93c206e-8ac3-4111-808b-298d61246c32",
"token_address": "0x97a9107c1793bc407d6f527b77e7fff4d812bece",
"user_address": "0xe07d7e56588a6fd860c5073c70a099658c060f3f",
"amount": 2e18,
"ref_context": "leaderboard_season_5",
"extra_data": {
"user_id": "3b541bee-8189-467b-805e-f3d50e06f581",
"rank": 2
}
}
]
}'

Example response:

{
"num_success": 1,
"num_failed": 1,
"failed_token_orders": [
{
"ref_id": "b93c206e-8ac3-4111-808b-298d61246c32",
"ref_context": "example_context",
"ref_service_code": "land",
"reason": "duplicate field abcxyz"
},
]
}

After a token order is created, the service generates a signature for the order. The order is held in a pending state for the specified duration, after which the user can claim it.

Get token order by ref_id

When a user queries information about a token order, form a GET request to the /token-orders endpoint, including the ref_id identifier of the order in the URL path to retrieve the order details.

Example request:

curl --location '{{api_host}}/v1/external/token-orders?ref_ids=b93c206e-8ac3-4111-808b-298d61246c04&ref_ids=b93c206e-8ac3-4111-808b-298d61246c03' \
--header 'Authorization: Basic <basic_api_key>' \
--header 'Content-Type: application/json'

Example response:

{
"token_orders": [
{
"id": 31,
"ref_context": "leaderboard_season_5",
"ref_id": "b93c206e-8ac3-4111-808b-298d61246c03",
"user_address": "0x1fedd249251ef7187155354807b861310af1516f",
"token_address": "0x82f5483623d636bc3deba8ae67e1751b6cf2bad2",
"amount": 1,
"extra_data": {
"rank": 1,
"user_id": "3b541bee-8189-467b-805e-f3d50e06f583"
},
"created_at": 1708501128,
"valid_from": 1708501133,
"total_amount": 3,
"total_amount_txt": "3",
"signature": "0x8fc5ac33d7a6cae24121d1646c845778e67feecdca85e05535f2096057f46a240ee7ff00168fc7bc4cf27019798f5256c0b311d95549205ae04cd837772798741b",
"nonce": 0
},
{
"id": 32,
"ref_context": "leaderboard_season_5",
"ref_id": "b93c206e-8ac3-4111-808b-298d61246c04",
"user_address": "0xd4b8e723a0517eb5905e38ba82470a05c51abe5e",
"token_address": "0x82f5483623d636bc3deba8ae67e1751b6cf2bad2",
"amount": 1,
"extra_data": {
"rank": 1,
"user_id": "3b541bee-8189-467b-805e-f3d50e06f583"
},
"created_at": 1708501655,
"valid_from": 1708501660,
"total_amount": 1,
"total_amount_txt": "1",
"signature": "0x8fc5ac33d7a6cae24121d1646c845778e67feecdca85e05535f2096057f46a240ee7ff00168fc7bc4cf27019798f5256c0b311d95549205ae04cd837772798741b",
"nonce": 0
}
]
}

Get user's signatures by token address

Use the /get-signatures endpoint to retrieve the signature for the token order. Include the user's wallet address in the URL path and pass the token addresses in the request body of the POST request. Use the signature along with other parameters to call the claimReward function in the Reward Distributor contract as part of the reward claiming process.

Example request:

curl --location '{{api_host}}/v1/users/0xd4b8e723a0517eb5905e38ba82470a05c51abe5e/get-signatures' \
--header 'Authorization: Basic <api_key>' \
--header 'Content-Type: application/json' \
--data '{
"request": {
"token_addresses": ["0x82f5483623d636bc3deba8ae67e1751b6cf2bad2", "0x97a9107c1793bc407d6f527b77e7fff4d812bece", "0x3c4e17b9056272ce1b49f6900d8cfd6171a1869d"]
}
}'

Example response:

{
"data": [
{
"signature": "0xfb9b4e3e2983ac25eb2bdcbbee3671b219831d840b8c17a2524dcce5c0b7570253fdaa65d64e06c6ffbbb13fc6d1e00943a1464681d1ec9b78027faf7ae101b81c",
"total_amount": 100,
"total_amount_txt": "100",
"updated_at": 1721631296,
"nonce": 0,
"valid_from": 1721631356,
"ref_service_code": "wallet_quest",
"token_address": "0x3c4e17b9056272ce1b49f6900d8cfd6171a1869d"
}
]
}

Step 5. Implement reward claiming

Claiming rewards means calling a function on a Reward Distributor contract created for your game and managed by Sky Mavis with the signature and other parameters. Experienced Web3 developers can implement their own logic for claiming rewards by directly calling the smart contract. Alternatively, you can use a claim user interface provided by Sky Mavis, similar to the Claim Tokens page on the App.axie website.

Through a smart contract

  1. Call the /get-signatures endpoint to retrieve the signature for the token order.
  2. Use the signature along with other parameters (total_amount, user_address, and token_address) to call the claimReward function, such as the one in the Axie Quest contract.

Example of calling the claimReward function in Python, using the web3.py library:

pip install web3
from web3 import Web3
from web3.middleware import geth_poa_middleware
import json

REWARD_DISTRIBUTOR_CONTRACT_ABI_PATH = "<PATH_TO_ABI_JSON>"
SUBMIT_TX_PRIVATE_KEY = "<YOUR_PRIVATE_KEY>"

REWARD_DISTRIBUTOR_CONTRACT_ADDRESS = "<CONTRACT_ADDRESS>"
USER_CLAIM_ADDRESS = Web3.to_checksum_address(
"0x6e1e4d95940d12a24016e73cdd207ba8fe138806")
USER_CLAIM_TOKEN_ADDRESS = Web3.to_checksum_address(
"0x3c4e17b9056272ce1b49f6900d8cfd6171a1869d") # AXS
TESTNET_RPC_URL = "https://saigon-testnet.roninchain.com/rpc"
MAINNET_RPC_URL = "https://api.roninchain.com/rpc"

# Get this information from the /get-signatures endpoint of Reward Distribution service
user_claim_signature = "0x8fc5ac33d7a6cae24121d1646c845778e67feecdca85e05535f2096057f46a240ee7ff00168fc7bc4cf27019798f5256c0b311d95549205ae04cd837772798741b"
total_amount = int(84844756000000000000)
valid_from = 1722580266
nonce_operator = 0

with open(REWARD_DISTRIBUTOR_CONTRACT_ABI_PATH, "r") as f:
abi = json.loads(f.read())

ronin_testnet_provider = Web3.HTTPProvider(
endpoint_uri=TESTNET_RPC_URL
)

w3 = Web3(ronin_testnet_provider)
w3.middleware_onion.inject(geth_poa_middleware, layer=0)

contract_instance = w3.eth.contract(
address=Web3.to_checksum_address(REWARD_DISTRIBUTOR_CONTRACT_ADDRESS), abi=abi
)

submit_tx_address = w3.to_checksum_address(
w3.eth.account.from_key(SUBMIT_TX_PRIVATE_KEY))

nonce = w3.eth.get_transaction_count(submit_tx_address)

claim_reward = contract_instance.functions.claimReward(
[
USER_CLAIM_ADDRESS,
USER_CLAIM_TOKEN_ADDRESS,
total_amount,
valid_from,
nonce_operator
],
user_claim_signature,
)
unicorn_txn = claim_reward.build_transaction(
{
"chainId": 2021, # 2021 testnet, 2020 for mainnet
"nonce": nonce,
"gas": 100000,
"gasPrice": 200000000000,
}
)
signed_txn = w3.eth.account.sign_transaction(
unicorn_txn, private_key=SUBMIT_TX_PRIVATE_KEY)
tx = w3.eth.send_raw_transaction(signed_txn.rawTransaction)
print("claim tx_hash:", tx.hex())

Example of calling the claimReward function in JavaScript, using the Ethers.js library:

npm install ethers
const ethers = require('ethers');
const fs = require('fs');

const REWARD_DISTRIBUTOR_CONTRACT_ABI_PATH = "<PATH_TO_ABI_JSON>";
const SUBMIT_TX_PRIVATE_KEY = "<PRIVATE_KEY>";

const REWARD_DISTRIBUTOR_CONTRACT_ADDRESS = "<CONTRACT_ADDRESS>";
const USER_CLAIM_ADDRESS = "0x6e1e4d95940d12a24016e73cdd207ba8fe138806";
const USER_CLAIM_TOKEN_ADDRESS = "0x3c4e17b9056272ce1b49f6900d8cfd6171a1869d"; // AXS
const TESTNET_RPC_URL = "https://saigon-testnet.roninchain.com/rpc";
const MAINNET_RPC_URL = "https://api.roninchain.com/rpc";

// Get this information from the /get-signatures endpoint of Reward Distribution service
const user_claim_signature = "0x8fc5ac33d7a6cae24121d1646c845778e67feecdca85e05535f2096057f46a240ee7ff00168fc7bc4cf27019798f5256c0b311d95549205ae04cd837772798741b";
const total_amount = ethers.BigNumber.from("84844756000000000000");
const valid_from = 1722580266;
const nonce_operator = 0;

async function main() {
const abi = JSON.parse(fs.readFileSync(REWARD_DISTRIBUTOR_CONTRACT_ABI_PATH, 'utf8'));

const provider = new ethers.providers.JsonRpcProvider(TESTNET_RPC_URL);

const wallet = new ethers.Wallet(SUBMIT_TX_PRIVATE_KEY, provider);

const contract = new ethers.Contract(REWARD_DISTRIBUTOR_CONTRACT_ADDRESS, abi, wallet);

const submit_tx_address = wallet.address;
const nonce = await provider.getTransactionCount(submit_tx_address);

const tx = await contract.claimReward(
[
USER_CLAIM_ADDRESS,
USER_CLAIM_TOKEN_ADDRESS,
total_amount,
valid_from,
nonce_operator
],
user_claim_signature,
{
chainId: 2021, // 2021 for testnet, 2020 for mainnet
nonce: nonce,
gasLimit: 100000,
gasPrice: ethers.utils.parseUnits("200", "gwei"),
}
);

console.log("claim tx_hash:", tx.hash);

// Wait for the transaction to be mined
const receipt = await tx.wait();
console.log("Transaction confirmed in block:", receipt.blockNumber);
}

main().catch((error) => {
console.error(error);
process.exit(1);
});

See an example function call in the Axie Quest contract, which can be used to claim rewards for any user address.

Through a GUI

If you prefer not to implement your own logic for claiming rewards, you can use a graphic user interface (GUI) provided by Sky Mavis. This method requires Sky Mavis to integrate your game into a claim reward UI. For example, in case of Axie Infinity, users can claim their tokens through the Claim Tokens page on the App.axie website. This initiates a call to the same smart contract function as the one used in the smart contract integration method.

To use this method, contact your Sky Mavis point of contact.

Error handling

When creating a token order, you may encounter the following errors:

Error codeDescriptionSolution
REF_ID_DUPLICATEDThe ref_id provided in the token order already exists in the system.Use a unique ref_id for each token order.
INTERNALAn internal error occurred during the token order creation process.Retry the request.
SIGN_DISABLEDThe token order endpoint is temporarily disabled.Try again later.
REACH_LIMIT_DISTRIBUTION_RULEThe token distribution limit has been reached.Optionally, contact Sky Mavis to increase the limit or wait until the next distribution period.