Tutorials

Claim Rewards

This tutorial shows you how to implement reward claiming for Merkl programs in your application.

Overview

Claiming rewards involves:

  1. Fetching claim data (proof + transaction data) from the appropriate API
  2. Constructing or using pre-formatted transaction calldata
  3. Submitting the claim transaction onchain
  4. Handling the response and updating your UI

Prerequisites

Claiming via Merkl

Merkl is the primary distribution system for current reward programs.

Step 1: Fetch Claim Data

Endpoint:

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

The {address} must be lowercase, and chainId is required. The same endpoint returns everything you need to claim: the token, the cumulative amount, the already-claimed amount, and the merkle proofs.

Example:

async function fetchMerklRewards(
  userAddress: string,
  chainId: number
): Promise<MerklReward[]> {
  const response = await fetch(
    `https://api.merkl.xyz/v4/users/${userAddress.toLowerCase()}/rewards?chainId=${chainId}`
  );

  if (!response.ok) {
    throw new Error(`Merkl API error: ${response.status}`);
  }

  // The endpoint returns an array of per-chain objects; flatten to a flat list of rewards.
  const data: MerklChainRewards[] = await response.json();
  return data.flatMap((entry) => entry.rewards);
}

Response Structure:

[
  {
    "chain": { "id": 8453, "name": "Base" },
    "rewards": [
      {
        "token": { "address": "0xTOKEN", "symbol": "MORPHO", "decimals": 18 },
        "amount": "2000000000000000000",
        "claimed": "1000000000000000000",
        "pending": "0",
        "proofs": ["0xproof1a", "0xproof1b"]
      }
    ]
  }
]

amount is the cumulative total earned to date and claimed is the cumulative amount already claimed onchain — both are passed as-is. The distributor transfers the difference (amount - claimed), so you do not subtract them yourself. pending is reward that is not yet in a live merkle root and cannot be claimed.

Validate claim data before signing. The tokens, amounts, and proofs come from an external API and are passed straight into an onchain claim that moves funds. Treat the response as untrusted: confirm the token addresses are ones you expect and the amounts are non-zero before prompting the user to sign.

Step 2: Execute the Claim

Using viem:

import { createWalletClient, custom } from "viem";
import { mainnet } from "viem/chains";

// Same Distributor address across all supported chains
const MERKL_DISTRIBUTOR = "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae";

async function claimMerklRewards(
  userAddress: string,
  chainId: number
) {
  // 1. Fetch the user's rewards
  const rewards = await fetchMerklRewards(userAddress, chainId);

  // Keep only genuinely-claimable rewards — entries with no proofs or nothing
  // left to claim make the whole `claim` tx revert with `InvalidProof`.
  const claimable = rewards.filter(
    (r) => r.proofs.length > 0 && BigInt(r.amount) > BigInt(r.claimed)
  );

  // Build the parallel, index-aligned arrays the contract expects
  const users = claimable.map(() => userAddress);
  const tokens = claimable.map((r) => r.token.address);
  const amounts = claimable.map((r) => BigInt(r.amount)); // cumulative; pass as-is
  const proofs = claimable.map((r) => r.proofs);

  // 2. Setup wallet client
  // The claim must be sent from the user's own wallet — a third-party sender
  // reverts with `NotWhitelisted` unless it has been approved as an operator.
  const walletClient = createWalletClient({
    account: userAddress,
    chain: mainnet,
    transport: custom(window.ethereum),
  });

  // 3. Send claim transaction
  const hash = await walletClient.writeContract({
    address: MERKL_DISTRIBUTOR,
    abi: MERKL_ABI,
    functionName: "claim",
    args: [users, tokens, amounts, proofs],
  });

  // 4. Wait for confirmation
  const receipt = await walletClient.waitForTransactionReceipt({ hash });

  return receipt;
}

// Merkl Distributor ABI (claim function)
const MERKL_ABI = [
  {
    inputs: [
      { name: "users", type: "address[]" },
      { name: "tokens", type: "address[]" },
      { name: "amounts", type: "uint256[]" },
      { name: "proofs", type: "bytes32[][]" },
    ],
    name: "claim",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
] as const;

Step 3: Handle the Result

try {
  const receipt = await claimMerklRewards(userAddress, 1);

  if (receipt.status === "success") {
    console.log("✅ Merkl rewards claimed successfully!");
    // Update your UI to reflect claimed rewards
  } else {
    console.error("❌ Claim transaction failed");
  }
} catch (error) {
  console.error("Error claiming Merkl rewards:", error);
  // Show error message to user
}

For more details on Merkl claiming, see the official Merkl documentation.

Legacy Rewards (Pre-Merkl)

Legacy Rewards API Deprecated

The rewards.morpho.org API has been deprecated. If you or your users have unclaimed rewards from historical Morpho programs (pre-Merkl migration), they can be claimed directly through the legacy rewards lookup app:

👉 rewards-legacy.morpho.org

All new reward programs are distributed exclusively via Merkl. See the Merkl documentation for integration details.

Resources

On this page