Skip to content

Claim Voting Incentives

This tutorial shows you how to claim rewards earned from your vKAT gauge voting participation using the Merkl distributor.

Goal

By the end of this tutorial, you’ll be able to:

  • Understand how the Merkl reward distribution works
  • Check your claimable rewards
  • Claim rewards with Merkle proofs
  • Set your reward token preferences

Prerequisites

  • A vKAT NFT that has been used to vote on gauges (see Vote on Gauges)

How Rewards Work

The Katana emissions voting system distributes rewards to participants across the ecosystem. Rewards come from several sources:

  • KAT emissions — Distributed to liquidity providers (LPs) in pools you vote for, not to vKAT voters directly
  • Vote incentives — Earned by vKAT voters who direct emissions to gauges
  • Trading fees — Generated by the pools you voted for
  • Exit fees — From other users who unstake their vKAT

Info

vKAT voters direct where KAT emissions go by voting on gauges, but the emissions themselves are claimed by those providing liquidity in those pools. The rewards vKAT voters earn are vote incentives, trading fees, and exit fees.

Rewards are distributed through a Merkl distributor, which uses Merkle tree proofs to enable gas-efficient claiming. A new Merkle root is published periodically (each epoch), and users can claim accumulated rewards at any time.

Exit Fee Distribution During Stabilization

During the stabilization window (Day 0-60), exit fees are accumulated, not distributed in real-time. This means displayed yield during this period reflects bribes and protocol revenue only.

After Day 60, accumulated exit fees are distributed to Founding Stakers over several epochs, proportional to each staker’s founding-eligible amount. From Day 61 onward, exit fees revert to real-time distribution to all active vKAT holders.

Note

avKAT holders do not need to claim rewards manually. The CompoundStrategy contract handles reward claims and reinvestment for the vault’s master position. This section is only relevant for vKAT holders.

Contract Addresses

const MERKL_DISTRIBUTOR = "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae";
const VKAT_METADATA = "0xb2143cFC740356E5FeFB4488e01026cfBb0A328F";

ABIs

Merkl Distributor ABI (click to expand)
const merklDistributorAbi = [
  {
    name: "claim",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "users", type: "address[]" },
      { name: "tokens", type: "address[]" },
      { name: "amounts", type: "uint256[]" },
      { name: "proofs", type: "bytes32[][]" },
    ],
    outputs: [],
  },
  {
    name: "claimed",
    type: "function",
    stateMutability: "view",
    inputs: [
      { name: "user", type: "address" },
      { name: "token", type: "address" },
    ],
    outputs: [
      { name: "amount", type: "uint256" },
    ],
  },
  {
    name: "getMerkleRoot",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "bytes32" }],
  },
  {
    name: "epochDuration",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "uint256" }],
  },
  {
    name: "endOfDisputePeriod",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "uint256" }],
  },
] as const;
vKAT Metadata ABI (click to expand)
const vkatMetadataAbi = [
  {
    name: "setPreferences",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "tokens", type: "address[]" },
      { name: "weights", type: "uint256[]" },
    ],
    outputs: [],
  },
  {
    name: "getPreferences",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "account", type: "address" }],
    outputs: [
      { name: "rewardTokens", type: "address[]" },
      { name: "rewardTokenWeights", type: "uint256[]" },
    ],
  },
  {
    name: "getAllowedRewardTokens",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "address[]" }],
  },
] as const;

Step 1: Check Epoch and Distribution Timing

import { formatEther } from "viem";

// Current Merkle root
const root = await publicClient.readContract({
  address: MERKL_DISTRIBUTOR,
  abi: merklDistributorAbi,
  functionName: "getMerkleRoot",
});
console.log(`Current Merkle root: ${root}`);

// Epoch duration
const epochDuration = await publicClient.readContract({
  address: MERKL_DISTRIBUTOR,
  abi: merklDistributorAbi,
  functionName: "epochDuration",
});
console.log(`Epoch duration: ${Number(epochDuration) / 3600} hours`);

// End of current dispute period
const disputeEnd = await publicClient.readContract({
  address: MERKL_DISTRIBUTOR,
  abi: merklDistributorAbi,
  functionName: "endOfDisputePeriod",
});
console.log(`Dispute ends: ${new Date(Number(disputeEnd) * 1000)}`);

Step 2: Check Past Claims

See what you’ve already claimed for a specific reward token:

const rewardTokenAddress = "0x..."; // The reward token address

const claimedAmount = await publicClient.readContract({
  address: MERKL_DISTRIBUTOR,
  abi: merklDistributorAbi,
  functionName: "claimed",
  args: [account.address, rewardTokenAddress],
});

console.log(`Previously claimed: ${formatEther(claimedAmount)}`);

Step 3: Claim Rewards

To claim rewards, you need Merkle proofs for your claimable amounts. These proofs are published each epoch and can be fetched from the Merkl API or generated from the published Merkle tree.

// Claim with Merkle proofs
// You'll need the cumulative amounts and proofs from the Merkl distribution
const claimHash = await walletClient.writeContract({
  address: MERKL_DISTRIBUTOR,
  abi: merklDistributorAbi,
  functionName: "claim",
  args: [
    [account.address],      // users
    [rewardTokenAddress],   // tokens
    [cumulativeAmount],     // cumulative amounts (total earned, not just new)
    [merkleProof],          // Merkle proofs (bytes32[][])
  ],
});

await publicClient.waitForTransactionReceipt({ hash: claimHash });
console.log("Rewards claimed!");

Note

The amounts parameter in the claim function represents cumulative amounts — the total ever earned, not just the new rewards since last claim. The contract calculates the difference internally and sends only the unclaimed portion.

Set Reward Token Preferences

vKAT holders can customize which tokens they want to receive as rewards. The protocol maintains a whitelist of allowed reward tokens.

View Allowed Tokens

const allowedTokens = await publicClient.readContract({
  address: VKAT_METADATA,
  abi: vkatMetadataAbi,
  functionName: "getAllowedRewardTokens",
});

console.log("Allowed reward tokens:");
for (const token of allowedTokens) {
  console.log(`  ${token}`);
}

Set Your Preferences

const KAT_ADDRESS = "0x7f1f4b4b29f5058fa32cc7a97141b8d7e5abdc2d";
const OTHER_TOKEN = "0x..."; // Another whitelisted reward token

// Receive 50% in KAT and 50% in another token
const prefHash = await walletClient.writeContract({
  address: VKAT_METADATA,
  abi: vkatMetadataAbi,
  functionName: "setPreferences",
  args: [
    [KAT_ADDRESS, OTHER_TOKEN], // tokens
    [50n, 50n],                  // relative weights
  ],
});

await publicClient.waitForTransactionReceipt({ hash: prefHash });
console.log("Reward preferences updated");

View Current Preferences

const [rewardTokens, rewardWeights] = await publicClient.readContract({
  address: VKAT_METADATA,
  abi: vkatMetadataAbi,
  functionName: "getPreferences",
  args: [account.address],
});

console.log("Current preferences:");
for (let i = 0; i < rewardTokens.length; i++) {
  console.log(`  Token: ${rewardTokens[i]}, Weight: ${rewardWeights[i]}`);
}

What’s Next

  • Unstake and Exit — Learn how to exit your vKAT position and understand the fee schedule
  • Deposit KAT to avKAT — Deposit into the avKAT vault if you prefer not to claim manually