User rewards

Display claimable rewards for users, build claim transactions, and surface earning history

Merkl exposes a family of /v4/users/{address}/rewards/* endpoints to read a user's reward state, build claim transactions, and surface where rewards were earned. This page is organized by use case — pick the endpoint that matches what you need to render.

Concepts: amount, pending, claimed, proofs

Every reward endpoint speaks the same vocabulary. These are the fields you will see on tokens:

  • amount — Cumulative reward credited to the user in the live Merkle tree (includes anything already claimed).
  • claimed — Cumulative amount the user has already pulled onchain.
  • pending — Earned but not yet in any Merkle root. Updates more frequently (~every 2 hours) than amount. Surface it separately — it is not claimable.
  • proofs — Merkle proofs required by the Distributor contract to claim the current amount. Pass them as-is.

The claimable amount is amount - claimed.

Whenever a new Merkle root is pushed onchain, pending resets to zero because those rewards are now folded into amount. Never sum amount + pending and display it as claimable.

Cache bypass: reloadChainId

After a user claims, Merkl needs to index the claim transaction onchain before claimed reflects the new onchain balance. Indexing typically takes up to ~5 minutes, so during that window the cached response keeps showing the old claimed value.

Pass reloadChainId={chainId} to force a fresh read for that chain and reconcile against the onchain state immediately. Supported on /rewards/summary, /rewards/breakdowns, and the legacy /v4/users/{address}/rewards. Use it only when needed — every call with reloadChainId bypasses the cache and is more expensive on the server.

GET https://api.merkl.xyz/v4/users/0x4F2BF7469Bc38d1aE779b1F4affC588f35E60973/rewards/summary?reloadChainId=324

Pick the right endpoint

Use caseEndpointNotes
Build a claim transactionGET /v4/users/{address}/rewards/summaryRecommended, fast
Show USD totalsGET /v4/users/{address}/rewards/statsLightweight USD totals
Show full reward breakdowns (small users)GET /v4/users/{address}/rewards/breakdownsCapped at ~1000 breakdowns
Show full reward breakdowns (any user, paginated)GET /v4/leaves/{recipient}/breakdownsPaginated, scales further
List opportunities a user earned on (per chain)GET /v4/users/{address}/rewards/chains/{chainId}/breakdownsPer-opportunity, single chain
List opportunities a user is currently earning onGET /v4/users/{address}/rewards/active-opportunitiesLIVE only, APR desc
Legacy combined endpointGET /v4/users/{address}/rewardsBackwards compatibility only

Build claim transactions — /rewards/summary

This is the endpoint you want for any "Claim" UI. It returns amount, claimed, pending, and proofs per token, grouped by chain, and is the cheapest and fastest server-side path because it skips the breakdown traversal.

GET https://api.merkl.xyz/v4/users/{address}/rewards/summary

Supports filtering by chain, token, and protocol — plus reloadChainId for cache bypass. See the API reference for the full schema.

Example: /v4/users/0x4F2BF7469Bc38d1aE779b1F4affC588f35E60973/rewards/summary

USD totals — /rewards/stats

A one-shot endpoint for headline numbers in dashboards or notification copy.

GET https://api.merkl.xyz/v4/users/{address}/rewards/stats

Returns totalEarnedUSD, pendingUSD, claimableUSD. Supports filtering by chain, token, and protocol. See the API reference for the full schema.

Example: /v4/users/0x4F2BF7469Bc38d1aE779b1F4affC588f35E60973/rewards/stats

Reward breakdowns — where each reward came from

Two endpoints serve breakdowns, depending on how much data you expect.

Small users — /rewards/breakdowns

Returns the same amount / claimed / pending / proofs shape as /rewards/summary, plus a breakdowns array on every token detailing which campaign each fraction of the reward came from.

GET https://api.merkl.xyz/v4/users/{address}/rewards/breakdowns

Supports filtering by chain, token, and protocol — plus reloadChainId for cache bypass. See the API reference for the full schema.

This endpoint is capped at ~1000 breakdowns per response. If a user has more than that on the queried scope, the breakdowns field comes back empty. For those users, paginate through /v4/leaves/{recipient}/breakdowns instead.

Example: /v4/users/0x4F2BF7469Bc38d1aE779b1F4affC588f35E60973/rewards/breakdowns?chainIds=324

Any user, paginated — /v4/leaves/{recipient}/breakdowns

The scalable path. Returns one page of leaf-level breakdowns at a time for a (recipient, tokenAddress, distributionChainId) triple, with pending amounts reconciled from temp leaves.

GET https://api.merkl.xyz/v4/leaves/{recipient}/breakdowns

Requires a tokenAddress and a distributionChainId. Supports additional filtering by campaign, opportunity, and protocol, plus pagination via page / items. See the API reference for the full schema.

Example: first page of breakdowns on Arbitrum for a specific token — /v4/leaves/0x4F2BF7469Bc38d1aE779b1F4affC588f35E60973/breakdowns?tokenAddress={token}&distributionChainId=42161&page=0&items=50.

Per-chain opportunity breakdown — /rewards/chains/{chainId}/breakdowns

Lists every opportunity a user has rewards on for a single chain. One row per (opportunity, token) with claimed, pending, and a server-computed claimable. Useful when expanding a chain row in a dashboard.

GET https://api.merkl.xyz/v4/users/{address}/rewards/chains/{chainId}/breakdowns

See the API reference for the full schema.

Example: /v4/users/0x4F2BF7469Bc38d1aE779b1F4affC588f35E60973/rewards/chains/42161/breakdowns

Active opportunities — /rewards/active-opportunities

Returns the opportunities where the user is currently earning — only those with LIVE campaigns and a non-zero balance (settled or pending). Aggregated per opportunity, sorted by APR desc.

GET https://api.merkl.xyz/v4/users/{address}/rewards/active-opportunities

See the API reference for the full schema.

Example: /v4/users/0x4F2BF7469Bc38d1aE779b1F4affC588f35E60973/rewards/active-opportunities

Legacy endpoint — /v4/users/{address}/rewards

Still supported for backwards compatibility. Returns rewards aggregated by chain with breakdowns embedded. Filters are limited to chain only — the newer endpoints add token and protocol filtering. New integrations should use /rewards/summary for claiming and /rewards/breakdowns or /v4/leaves/{recipient}/breakdowns for breakdown details. See the API reference for the full schema.

GET https://api.merkl.xyz/v4/users/{address}/rewards?chainId={chain_id}

Examples:

Historical rewards

For use cases that need the rewards earned by an address between two timestamps (accounting, tax reporting, retroactive analytics, etc.), dedicated API routes are available on demand. These routes are computationally heavy, so they are not publicly exposed to every integrator. Reach out to the Merkl team to request access and describe your use case.

To pull the full history of onchain claim transactions for a user, the GET /v4/claims endpoint is publicly available and easy to query. It supports filters like recipient, chainId, campaignId, token, and root — combine them to scope the history exactly to what you need. See the full filter list and response schema in the API reference.

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. It uses /rewards/summary — the recommended endpoint — so the call stays lightweight even for power users. 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.summary.get({ query: { chainIds: [String(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()
}