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:
- Define daily, weekly, and monthly token distribution limits for rewards.
- Get an API key and fund wallet address from Sky Mavis.
- Top up the fund wallet with tokens.
- Implement token distribution APIs.
- 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
-
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: 5e6The 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.
-
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
- Request an API key from Sky Mavis to authenticate your requests to the Reward Distribution service.
- 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
- Transfer the desired amount of tokens to the fund wallet.
- Provide your IP address to allowlist it for the wallet.
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
- Call the
/get-signatures
endpoint to retrieve the signature for the token order. - Use the signature along with other parameters (
total_amount
,user_address
, andtoken_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 code | Description | Solution |
---|---|---|
REF_ID_DUPLICATED | The ref_id provided in the token order already exists in the system. | Use a unique ref_id for each token order. |
INTERNAL | An internal error occurred during the token order creation process. | Retry the request. |
SIGN_DISABLED | The token order endpoint is temporarily disabled. | Try again later. |
REACH_LIMIT_DISTRIBUTION_RULE | The token distribution limit has been reached. | Optionally, contact Sky Mavis to increase the limit or wait until the next distribution period. |