Merit API Documentation (0.3.1)

Download OpenAPI specification:Download

Introduction

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.

What are Merit Points?

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.

API Summary

Merit Distribution API

The Merit smart contract currently offers 3 on-chain actions via the Merit Distribution API:

  • ISSUE - The points treasury authority can issue new points to a wallet. Behind the scenes, it mints new points of a set amount to the given wallet.
  • TRANSFER - The points treasury authority can transfer points between two wallets. Behind the scenes, it burns a set amount of points from the sender's wallet, and mints new points of the same amount to the receiver's wallet.
  • SPEND The points treasury authority can allow a wallet to spend its points. Behind the scenes, it burns a set amount of points from the spender's wallet.

Merit Data API

Merit Data API also provides fetch endpoints to retrieve information related to your points token:

  • BALANCE - Fetch the points balance of a given wallet.
  • ACTIVITY - Fetch all points activity of a given wallet (aka points transactions).
  • LEADERBOARD - Fetch all holder wallets and their points balance.

Environments

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.

Prequisites

  • Create and deploy your points token on the Merit app
  • Copy and save your API keypair values in a secure place
  • Supply your points authority wallet with SOL for gas fees
  • Install tweetnacl and tweetnacl-util in your Javascript environment (or equivalent if not Javascript)

Contact

Contact Sun (Frankie Labs founder) directly if you have any questions or need assistance.

Authorization

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();

Merit Distribution API Endpoints

These endpoints emit transactions to the blockchain.

Metadata

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 properties

The 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

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.

POST /issue

Issue points to a wallet. Issuing points correlates to minting new tokens to the given wallet.

Authorizations:
(client_idmessagesignature)
query Parameters
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 or heliusPriorityLevel is required if mode is included.

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.

Request Body schema: application/json

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)

Responses

Request samples

Content type
application/json
{
  • "receiverAddress": "abc",
  • "amount": 10,
  • "metadata": {
    }
}

Response samples

Content type
application/json
{
  • "txId": "abc"
}

POST /issue/create-transaction

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.

Authorizations:
(client_idmessagesignature)
query Parameters
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 or heliusPriorityLevel is required if mode is included.

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.

Request Body schema: application/json

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)

Responses

Request samples

Content type
application/json
{
  • "receiverAddress": "abc",
  • "amount": 10,
  • "metadata": {
    }
}

Response samples

Content type
application/json
{
  • "serializedB64Tx": "VGhp...ZQ==",
  • "nonce": "Zvik...9GnW"
}

POST /transfer

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.

Authorizations:
(client_idmessagesignature)
query Parameters
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 or heliusPriorityLevel is required if mode is included.

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.

Request Body schema: application/json

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)

Responses

Request samples

Content type
application/json
{
  • "senderAddress": "abc",
  • "receiverAddress": "abc",
  • "amount": 10,
  • "metadata": {
    }
}

Response samples

Content type
application/json
{
  • "txId": "abc"
}

POST /transfer/create-transaction

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.

Authorizations:
(client_idmessagesignature)
query Parameters
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 or heliusPriorityLevel is required if mode is included.

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.

Request Body schema: application/json

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)

Responses

Request samples

Content type
application/json
{
  • "senderAddress": "abc",
  • "receiverAddress": "abc",
  • "amount": 10,
  • "metadata": {
    }
}

Response samples

Content type
application/json
{
  • "serializedB64Tx": "VGhp...ZQ==",
  • "nonce": "Zvik...9GnW"
}

POST /spend

Spends points from the given wallet. Spending points correlates to burning tokens from the given wallet.

Authorizations:
(client_idmessagesignature)
query Parameters
priorityFeeMode
string
Enum: "helius" "raw"

Priority fee mode. Including mode enables priority fee. One of rawMaxPriorityFee or heliusPriorityLevel is required if mode is included.

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.

Request Body schema: application/json

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)

Responses

Request samples

Content type
application/json
{
  • "senderAddress": "abc",
  • "amount": 10,
  • "metadata": {
    }
}

Response samples

Content type
application/json
{
  • "txId": "abc"
}

POST /spend/create-transaction

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.

Authorizations:
(client_idmessagesignature)
query Parameters
priorityFeeMode
string
Enum: "helius" "raw"

Priority fee mode. Including mode enables priority fee. One of rawMaxPriorityFee or heliusPriorityLevel is required if mode is included.

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.

Request Body schema: application/json

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)

Responses

Request samples

Content type
application/json
{
  • "senderAddress": "abc",
  • "amount": 10,
  • "metadata": {
    }
}

Response samples

Content type
application/json
{
  • "serializedB64Tx": "VGhp...ZQ==",
  • "nonce": "Zvik...9GnW"
}

Merit Data API Endpoints

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.

GET /balance

Fetch the points balance of the given wallet.

Authorizations:
(client_idmessagesignature)
query Parameters
address
required
string
Example: address=ADK8BS6KiCBYMekvoXadtgw2vyV69ZMwhtcYzFZt9g5L

The wallet address to fetch

Responses

Response samples

Content type
application/json
{
  • "balance": 10
}

GET /activity

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.

Authorizations:
(client_idmessagesignature)
query Parameters
address
required
string
Example: address=ADK8BS6KiCBYMekvoXadtgw2vyV69ZMwhtcYzFZt9g5L

The wallet address to fetch

Responses

Response samples

Content type
application/json
{
  • "activity": [
    ]
}

GET /leaderboard

Fetch all holder wallets and their balances in sorted order.

Authorizations:
(client_idmessagesignature)
query Parameters
sort
string
Default: "desc"
Enum: "desc" "asc"

The sort order

Responses

Response samples

Content type
application/json
{
  • "leaderboard": [
    ]
}

Token Account Management

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

Example Code

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>
}