Download OpenAPI specification:Download
Welcome to the Merit API documentation!
Merit is an on-chain tokenized points protocol & infrastructure solution on Solana, leveraging Token Extension to create and manage distribution of non-liquid on-chain points as tokens.
Here you will find all available API endpoints to integrate Merit into your existing tech stack.
Merit's on-chain points are tokens with no monetary value associated with them. We call the underlying standard soul-bound utility tokens, or SBU tokens for short. Their utility is like coins at an arcade, or tickets at a carnival. They can be used to exchange for rewards, but cannot act as a liquid currency in the open market.
Behind the scenes, our propietary smart contract initializes a points manager (i.e. points treasury authority) for secure token management. The tokens are made non-tradable via the non-transferable extension of the Token 2022 program. This means the token owner cannot transfer the tokens to a liquidity pool, effectively preventing monetary value associated with the token.
We also leverage the permanent delegate extension of the Token 2022 program to retain minting authority solely held by the points treasury authority wallet. Since points are non-monetary, we are free to keep the token supply open (i.e. not fixed supply) and mint the tokens when issuing points, burn the tokens when spending points, and burn from one wallet and mint into the other when tranferring points. See the API Summary section below for more information.
Points holders can see their points in their wallets as an on-chain token asset, and all records of events related to their points activity are stored on-chain as blockchain transactions.
At Frankie Labs, we understand that points act more as assets than records in a database, and that is why we set out to build Merit - to tokenize points, as how they should be in Web3.
The Merit smart contract currently offers 3 on-chain actions via the Merit Distribution API:
Merit Data API also provides fetch endpoints to retrieve information related to your points token:
When launching your points token on Merit, you have the option to deploy on either devnet
or mainnet-beta
. The Merit API handles either network intelligently - when you receive your API keypair upon points token creation, that API keypair correlates directly with the token you deployed. Our system will know which points token, and subsequently which network, to handle depending on the API keypair you supply in your outgoing requests to Merit API.
See the Authorization section below for more information on auth handling.
tweetnacl
and tweetnacl-util
in your Javascript environment (or equivalent if not Javascript)Contact Sun (Frankie Labs founder) directly if you have any questions or need assistance.
When you create your points token on Merit app, an API keypair will be generated for you. All endpoints are protected via API keypair authorization. When making your HTTP requests, use your provided API client ID and secret key to sign a secure message using tweetnacl
and include your client ID, message, and signature in the request header as follows:
// Javascript code sample for `/leaderboard` endpoint
import nacl from 'tweetnacl';
import naclUtil from 'tweetnacl-util';
const {CLIENT_ID, SECRET_KEY} = process.env;
const secretKey = naclUtil.decodeBase64(SECRET_KEY);
const message = new Date().toISOString();
const messageBuffer = Buffer.from(message);
const signatureBuffer = nacl.sign.detached(messageBuffer, secretKey);
const signature = naclUtil.encodeBase64(signatureBuffer);
const request = await fetch(
'https://merit-api.frankielabs.com/leaderboard',
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Client-Id': CLIENT_ID,
'X-Signed-Message': message,
'X-Signature': signature,
}
}
);
const data = await request.json();
These endpoints emit transactions to the blockchain.
Merit's smart contract instructions were built to include useful metadata in the instruction data for categorizing different transactions.
By default, the points transactions are already distinguishable between issue
, transfer
and spend
. You can use the following properties to create further nested categorizations.
mainType
- what is the main categorization of your transaction?subType
- what is the secondary categorization of your transaction?location
- where did the action take place? e.g. "IN_APP"
, "DISCORD"
, etc.additionalData
- an array of objects with key
and value
propertiesThe GET /activity
endpoint below is already configured to parse these metadata properties for your benefit.
SUGGESTION: Treat the values as enums. Once these values are written to the blockchain, there is no undoing. Carefully strategize what your enums will be and how you will use them before sending your first transaction to mainnet-beta.
Priority fees are a way to prioritize your transaction in a given block by paying more compute unit price, allowing transactions to be dynamically dispatched at a different price range during times of network congestion to provide a higher success rate of landing transactions.
There are three behaviors (modes) related to priority fees: 'raw'
, 'helius'
, or no priority fees at all.
While using 'raw'
, the RPC returns the latest 150 slots and priority fees paid in them. Currently, the default strategy is calculating an average that doesn't exceed a maximum value to avoid draining the wallet.
On the other hand, when using 'helius'
you must provide a priority fee mode ranging from 'Min'
to 'VeryHigh'
, which uses Helius's API to determine a reasonable price depending on the given priority fee mode. See heliusPriorityLevel
parameter definition below to view all possible modes available for Helius.
Issue points to a wallet. Issuing points correlates to minting new tokens to the given wallet.
openAta | boolean Enables the creation of the associated token account to deposit the tokens in. Please check usage in-app since left always on true could lead to token account closing/reopening, hence a possible loss of token account rent funds. |
priorityFeeMode | string Enum: "helius" "raw" Priority fee mode. Including mode enables priority fee. One of |
rawMaxPriorityFee | number The maximum priority fee to pay for raw configuration mode (defaults to average don't exceed max). |
heliusPriorityLevel | string Enum: "Min" "Low" "Medium" "High" "VeryHigh" The priority level from Helius API usage. |
Issue points of a given amount to the provided wallet.
receiverAddress | string <base58> The points receiver's wallet address |
amount | integer >= 1 The amount of points to issue |
object (IssueMetadata) |
{- "receiverAddress": "abc",
- "amount": 10,
- "metadata": {
- "mainType": "EARN",
- "subType": "DailyClaim",
- "location": "App",
- "additionalData": [
- {
- "key": "some_key_name",
- "value": "some value"
}
]
}
}
{- "txId": "abc"
}
Creates a transaction to be signed by the client to issue points to a wallet, using the client as fee payer for both opening the ata if specified and the transaction gas. Issuing points correlates to minting new tokens to the given wallet.
openAta | boolean Enables the creation of the associated token account to deposit the tokens in. |
priorityFeeMode | string Enum: "helius" "raw" Priority fee mode. Including mode enables priority fee. One of |
rawMaxPriorityFee | number The maximum priority fee to pay for raw configuration mode (defaults to average don't exceed max). |
heliusPriorityLevel | string Enum: "Min" "Low" "Medium" "High" "VeryHigh" The priority level from Helius API usage. |
Issue points of a given amount to the provided wallet.
receiverAddress | string <base58> The points receiver's wallet address |
amount | integer >= 1 The amount of points to issue |
object (IssueMetadata) |
{- "receiverAddress": "abc",
- "amount": 10,
- "metadata": {
- "mainType": "EARN",
- "subType": "DailyClaim",
- "location": "App",
- "additionalData": [
- {
- "key": "some_key_name",
- "value": "some value"
}
]
}
}
{- "serializedB64Tx": "VGhp...ZQ==",
- "nonce": "Zvik...9GnW"
}
Transfer points from one wallet to another wallet. Transferring points correlates to burning tokens from one wallet of a given amount and minting new tokens to another wallet of the same amount.
openAta | boolean Enables the creation of the associated token account to deposit the tokens in. Please check usage in-app since left always on true could lead to token account closing/reopening, hence a possible loss of token account rent funds. |
priorityFeeMode | string Enum: "helius" "raw" Priority fee mode. Including mode enables priority fee. One of |
rawMaxPriorityFee | number The maximum priority fee to pay for raw configuration mode (defaults to average don't exceed max). |
heliusPriorityLevel | string Enum: "Min" "Low" "Medium" "High" "VeryHigh" The priority level from Helius API usage. |
Transfer points of a given amount from wallet to another wallet.
senderAddress | string <base58> The points sender's wallet address |
receiverAddress | string <base58> The points receiver's wallet address |
amount | integer >= 1 The amount of points to transfer |
object (TransferMetadata) |
{- "senderAddress": "abc",
- "receiverAddress": "abc",
- "amount": 10,
- "metadata": {
- "mainType": "TRANSFER",
- "subType": "Gift",
- "location": "Discord",
- "additionalData": [
- {
- "key": "some_key_name",
- "value": "some value"
}
]
}
}
{- "txId": "abc"
}
Creates a transaction to be signed by the client to transfer points from one wallet to another wallet, using the client as fee payer for both opening the ata if specified and the transaction gas. Transferring points correlates to burning tokens from one wallet of a given amount and minting new tokens to another wallet of the same amount.
openAta | boolean Enables the creation of the associated token account to deposit the tokens in. Please check usage in-app since left always on true could lead to token account closing/reopening, hence a possible loss of token account rent funds. |
priorityFeeMode | string Enum: "helius" "raw" Priority fee mode. Including mode enables priority fee. One of |
rawMaxPriorityFee | number The maximum priority fee to pay for raw configuration mode (defaults to average don't exceed max). |
heliusPriorityLevel | string Enum: "Min" "Low" "Medium" "High" "VeryHigh" The priority level from Helius API usage. |
Transfer points of a given amount from wallet to another wallet.
senderAddress | string <base58> The points sender's wallet address |
receiverAddress | string <base58> The points receiver's wallet address |
amount | integer >= 1 The amount of points to transfer |
object (TransferMetadata) |
{- "senderAddress": "abc",
- "receiverAddress": "abc",
- "amount": 10,
- "metadata": {
- "mainType": "TRANSFER",
- "subType": "Gift",
- "location": "Discord",
- "additionalData": [
- {
- "key": "some_key_name",
- "value": "some value"
}
]
}
}
{- "serializedB64Tx": "VGhp...ZQ==",
- "nonce": "Zvik...9GnW"
}
Spends points from the given wallet. Spending points correlates to burning tokens from the given wallet.
priorityFeeMode | string Enum: "helius" "raw" Priority fee mode. Including mode enables priority fee. One of |
rawMaxPriorityFee | number The maximum priority fee to pay for raw configuration mode (defaults to average don't exceed max). |
heliusPriorityLevel | string Enum: "Min" "Low" "Medium" "High" "VeryHigh" The priority level from Helius API usage. |
Spend points of a given amount from a given wallet.
senderAddress | string <base58> The points sender's wallet address |
amount | integer >= 1 The amount of points to spend |
object (SpendMetadata) |
{- "senderAddress": "abc",
- "amount": 10,
- "metadata": {
- "mainType": "SPEND",
- "subType": "NftClaim",
- "location": "App",
- "additionalData": [
- {
- "key": "some_key_name",
- "value": "some value"
}
]
}
}
{- "txId": "abc"
}
Creates a transaction to be signed and by the client to spend points from the given wallet, using the client as fee payer for both opening the ata if specified and the transaction gas. Spending points correlates to burning tokens from the given wallet.
priorityFeeMode | string Enum: "helius" "raw" Priority fee mode. Including mode enables priority fee. One of |
rawMaxPriorityFee | number The maximum priority fee to pay for raw configuration mode (defaults to average don't exceed max). |
heliusPriorityLevel | string Enum: "Min" "Low" "Medium" "High" "VeryHigh" The priority level from Helius API usage. |
Spend points of a given amount from a given wallet.
senderAddress | string <base58> The points sender's wallet address |
amount | integer >= 1 The amount of points to spend |
object (SpendMetadata) |
{- "senderAddress": "abc",
- "amount": 10,
- "metadata": {
- "mainType": "SPEND",
- "subType": "NftClaim",
- "location": "App",
- "additionalData": [
- {
- "key": "some_key_name",
- "value": "some value"
}
]
}
}
{- "serializedB64Tx": "VGhp...ZQ==",
- "nonce": "Zvik...9GnW"
}
These endpoints read transactions from the blockchain. They translate the raw transaction data to a custom format that is easier to handle. Review each endpoint's sample response bodies for examples. If any data properties are missing or you would like to see them included, contact us and we will work on upgrades for you.
Fetch the points balance of the given wallet.
address required | string Example: address=ADK8BS6KiCBYMekvoXadtgw2vyV69ZMwhtcYzFZt9g5L The wallet address to fetch |
{- "balance": 10
}
Fetch the points activity of the given wallet. Each row correlates to an on-chain transactions. Each row includes parsed metadata from the Merit program instruction data.
address required | string Example: address=ADK8BS6KiCBYMekvoXadtgw2vyV69ZMwhtcYzFZt9g5L The wallet address to fetch |
{- "activity": [
- {
- "transactionId": "sfhrN3ti4w7tBX9ZM1tdBaKFDZigaoWe1GDJGziAitSojiN7anHkxyiiiB39MUr9zvdgg1py5NLVh2zJjvF4zeo",
- "mainType": "TRANSFER",
- "subType": "Gift",
- "location": "Postman",
- "senderAddress": "ADK8BS6KiCBYMekvoXadtgw2vyV69ZMwhtcYzFZt9g5L",
- "receiverAddress": "FeikG7Kui7zw8srzShhrPv2TJgwAn61GU7m8xmaK9GnW",
- "amount": 5,
- "timestamp": 1705859110000,
- "additionalData": [
- {
- "key": "some_key_name",
- "value": "some value"
}
]
}
]
}
Fetch all holder wallets and their balances in sorted order.
sort | string Default: "desc" Enum: "desc" "asc" The sort order |
{- "leaderboard": [
- {
- "address": "ADK8BS6KiCBYMekvoXadtgw2vyV69ZMwhtcYzFZt9g5L",
- "balance": 95
}
]
}
Merit Distribution API includes the option to open ATAs (associated token accounts) on behalf of the user, meaning you would pay for the account rent upon opening the points token account. It is useful for scenarios like airdropping points to users.
However, we do not recommend using this option for common use cases like continuous issuing or transferring of points. There is a chance a user could abuse the expensed token account rent by closing the token account and having your system pay the rent to open the account again, then repeating this cyclically.
In this situation, we recommend having the user open their own token accounts and provide their own rent (which can be refunded back to them if they choose to close the token account).
Here we have sample frontend React code to allow your users to open thier own token accounts, using the @solana/web3.js
SDK and Solana's default wallet adapter.
This code renders a button that, when clicked, prompts a transaction sign request. The user approves the transaction request, and the transaction opens the token account.
import { FC } from 'react';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import * as web3 from '@solana/web3.js';
import * as SPLToken from "@solana/spl-token";
// Store your points token mint as an environment variable
const treasuryMintPubkey = new web3.PublicKey(
process.env.REACT_APP_POINTS_TOKEN_MINT as string
);
const Button: FC = () => {
// You should have set up the Connection provider towards the root of your React app
const { connection } = useConnection();
// You should have set up the Wallet provider towards the root of your React app
const { publicKey } = useWallet();
const [isLoading, setIsLoading] = useState<boolean>(true);
const [ata, setAta] = useState<web3.PublicKey|null>(null);
const [ataExists, setAtaExists] = useState<boolean>(false);
// First, check if ATA already exists for the connected wallet
useEffect(() => {
// Derive the ATA (associated token account) key
const ata = SPLToken.getAssociatedTokenAddressSync(
treasuryMintPubkey,
publicKey,
false,
SPLToken.TOKEN_2022_PROGRAM_ID,
SPLToken.ASSOCIATED_TOKEN_PROGRAM_ID
);
// Save the ATA for later when building the transaction
setAta(ata);
// Check if the ATA already exists
SPLToken.getAccount(
connection,
ata,
"confirmed",
SPLToken.TOKEN_2022_PROGRAM_ID
).then(() => {
setIsLoading(false);
setAtaExists(true);
}).catch((error) => {
// otherwise, the user does not have the ATA yet
if (
!(error instanceof SPLToken.TokenAccountNotFoundError) &&
!(error instanceof SPLToken.TokenInvalidAccountOwnerError)
) {
console.error("Failed to check for points token account.");
}
setIsLoading(false);
});
}, []);
// If ATA doesn't exist yet, create the transaction to open a new one
const openAta = async () => {
if (ataExists) {
console.warn("Points token account already exists.");
return;
}
setIsLoading(true);
// Build the open ATA instruction
const ataIx = SPLToken.createAssociatedTokenAccountInstruction(
publicKey, // token account rent payer
ata, // the token account derived key
publicKey, // the owner of the token account
treasuryMintPubkey, // the mint for the token account
SPLToken.TOKEN_2022_PROGRAM_ID,
SPLToken.ASSOCIATED_TOKEN_PROGRAM_ID
);
// Fetch the latest blockhash
const { blockhash } = await connection.getLatestBlockhash();
// Build a versioned transaction
const tx = new web3.VersionedTransaction(
new web3.TransactionMessage({
payerKey: publicKey,
instructions: [ataIx!],
recentBlockhash: blockhash,
}).compileToV0Message()
);
try {
// this will trigger the wallet popup for the user to sign the tx
const signature = await sendTransaction!(tx, connection);
console.log("Opened points token account!");
setAtaExists(true);
} catch(error) {
console.error(error.message);
}
setIsLoading(false);
}
if (isLoading) {
return <button disabled>Loading...</button>
}
if (!ataExists) {
return <button onClick={openAta}>Open Token Account</button>;
}
return <button>Do Something Else!</button>
}