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