User rewards
Display claimable rewards for users and build claim transactions
To retrieve reward data for a specific user, use the following endpoint:
https://api.merkl.xyz/v4/users/{address}/rewards?chainId={chain_id}
Example for a single chain - Checking a user's rewards on zkSync: https://api.merkl.xyz/v4/users/0x4F2BF7469Bc38d1aE779b1F4affC588f35E60973/rewards?chainId=324
Example for multiple chains - Checking a user's rewards on zkSync, Ethereum and Arbitrum: https://api.merkl.xyz/v4/users/0x4F2BF7469Bc38d1aE779b1F4affC588f35E60973/rewards?chainId=324,1,42161
This endpoint returns:
amount: Total tokens credited to the user onchainpending: Pending rewards that will be credited on the next Merkle root updateclaimed: Tokens already claimed by the userproofs: Cryptographic proofs needed for claiming rewards (detailed in the next section)breakdowns: Campaign attribution for earned rewards (may be incomplete and not show all campaigns)
The claimable amount equals amount - claimed.
pending rewards update more frequently (about every 2 hours) than the amount. Integrating pending rewards in your app lets you show more timely updates to your users. However, always display pending rewards separately to avoid confusion because pending rewards are not claimable. Also note that whenever tokens are credited onchain, the pending rewards reset to zero because they are added to the amount.
Important notes about this endpoint:
-
Historical data: The API doesn't return time-series data. To build historical datasets, take daily snapshots of the API responses.
-
Caching behavior: This route is cached. If called immediately after a user claims rewards, the data may be stale. Use the
reloadChainIdparameter to force a cache refresh:https://api.merkl.xyz/v4/users/{address}/rewards?chainId=324&reloadChainId=324
Building claim transactions
Rewards are claimed through the Distributor contract. Find contract addresses for each chain on the Chains & Contracts page.
If the token or amount doesn't match the proof when calling the Distributor contract, the transaction will revert.
Rewards on Merkl are claimable per token. Users can claim rewards for a single token or all tokens at once.
Distributor contract interface
The full source is available in the merkl-contracts repository. The functions and events most relevant to integrators are:
interface IDistributor {
/// @notice Cumulative claim record stored per (user, token).
struct Claim {
uint208 amount;
uint48 timestamp;
bytes32 merkleRoot;
}
/*//////////////////////////////////// CLAIM ////////////////////////////////////*/
/// @notice Claim rewards for a batch of (user, token) pairs.
/// @dev `amounts` are cumulative — pass the `amount` returned by the API as-is.
/// Tokens are sent to `users[i]` (or to a recipient previously set via
/// `setClaimRecipient`). Reverts with `InvalidProof` if a leaf does not
/// match the active Merkle root.
function claim(
address[] calldata users,
address[] calldata tokens,
uint256[] calldata amounts,
bytes32[][] calldata proofs
) external;
/// @notice Claim with explicit recipients and optional `onClaim` callback data.
/// @dev Only `msg.sender` claiming for itself can override the recipient.
function claimWithRecipient(
address[] calldata users,
address[] calldata tokens,
uint256[] calldata amounts,
bytes32[][] calldata proofs,
address[] calldata recipients,
bytes[] memory datas
) external;
/*//////////////////////////////////// USER ADMIN ////////////////////////////////////*/
/// @notice Authorize / deauthorize an operator to claim on behalf of `user`.
/// Pass `address(0)` to allow anyone to claim for the user.
function toggleOperator(address user, address operator) external;
/// @notice Set a default recipient for `user`'s claims of `token`.
/// `token = address(0)` sets the default for all tokens.
function setClaimRecipient(address recipient, address token) external;
/*//////////////////////////////////// VIEWS ////////////////////////////////////*/
/// @notice Returns the Merkle root currently used to verify proofs.
/// Equals `lastTree.merkleRoot` while a dispute window is open.
function getMerkleRoot() external view returns (bytes32);
/// @notice Cumulative amount already claimed by `user` for `token`.
function claimed(address user, address token)
external
view
returns (uint208 amount, uint48 timestamp, bytes32 merkleRoot);
/// @notice Operator authorizations (1 = authorized, 0 = not authorized).
function operators(address user, address operator) external view returns (uint256);
/// @notice Per-(user, token) custom recipient set via `setClaimRecipient`.
function claimRecipient(address user, address token) external view returns (address);
/*//////////////////////////////////// EVENTS ////////////////////////////////////*/
event Claimed(address indexed user, address indexed token, uint256 amount);
event ClaimRecipientUpdated(address indexed user, address indexed token, address indexed recipient);
event OperatorToggled(address indexed user, address indexed operator, bool isWhitelisted);
event TreeUpdated(bytes32 merkleRoot, bytes32 ipfsHash, uint48 endOfDisputePeriod);
}
The full ABI (including governance functions, dispute flow, and errors) is available as a JSON file: Download the Distributor ABI.
Example claiming script
Here is a script to claim all token rewards for a user on a chain. The Distributor address is the same on most EVM chains — see the Chains & Contracts page for per-chain values.
import type { JsonRpcSigner } from '@ethersproject/providers'
import { MerklApi } from '@merkl/api'
import { Distributor__factory } from '@sdk' // Or load the ABI from /images/distributor-abi.json
const DISTRIBUTOR_ADDRESS = '0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae'
export const claim = async (chainId: number, signer: JsonRpcSigner) => {
const { status, data } = await MerklApi('https://api.merkl.xyz')
.v4.users({ address: signer._address })
.rewards.get({ query: { chainId: [chainId] } })
if (status !== 200) throw 'Failed to fetch rewards'
const users = []
const tokens = []
const amounts = []
const proofs = []
for (const rewards of data) {
if (rewards.chain.id !== chainId) continue
for (const reward of rewards.rewards) {
users.push(signer._address)
tokens.push(reward.token.address)
amounts.push(reward.amount)
proofs.push(reward.proofs)
}
}
if (tokens.length === 0) throw 'No tokens to claim'
const contract = Distributor__factory.connect(DISTRIBUTOR_ADDRESS, signer)
await (await contract.claim(users, tokens, amounts, proofs)).wait()
}