Aave V4 Incentive Programs
Aave V4 reserves may offer additional incentives beyond base lending rates. These incentives are distributed through Merkl, a decentralized incentive distribution platform, or through Points programs that reward users with points multipliers.
Reward Types
Potential rewards are available in the rewards array of the Reserve Summary.
interface Reserve {
// …
summary: {
rewards: Reward[];
// …
};
}
type Reward =
| MerklSupplyReward
| MerklBorrowReward
| SupplyPointsReward
| BorrowPointsReward;
fragment Reserve on Reserve {
# …
summary {
rewards {
... on MerklSupplyReward {
...MerklSupplyReward
}
... on MerklBorrowReward {
...MerklBorrowReward
}
... on SupplyPointsReward {
...SupplyPoints
}
... on BorrowPointsReward {
...BorrowPoints
}
}
# …
}
}
Supply Rewards
Supply rewards can be present on suppliable reserves.
Merkl Supply Rewards
Merkl supply rewards represent an extra APY on top of the base reserve supply APY. The accrued extra interest is paid in the specified payout token when the incentive campaign reaches its maturity.
interface MerklSupplyReward {
__typename: "MerklSupplyReward";
id: string;
startDate: Date;
endDate: Date;
extraApy: PercentNumber;
payoutToken: Erc20Token;
criteria: MerklCriteria[];
userEligible: boolean;
}
fragment MerklSupplyReward on MerklSupplyReward {
__typename
id
startDate
endDate
extraApy {
...PercentNumber
}
payoutToken {
...Erc20Token
}
criteria {
...MerklCriteria
}
userEligible
}
Where:
extra: the additional APY earned on top of the base supply APYApy payout: the token used to pay the extra interest accrued from the rewardToken criteria: eligibility requirements for earning the reward
Points Supply Rewards
Some reserves participate in Points programs. Users who supply into these reserves accrue points over time proportional to their position size.
interface SupplyPointsReward {
__typename: "SupplyPointsReward";
id: string;
program: PointsProgram;
name: string;
startDate: Date;
endDate: Date | null;
multiplier: number;
criteria: PointsCriteria[];
userEligible: boolean;
}
fragment SupplyPoints on SupplyPointsReward {
__typename
id
program {
...PointsProgram
}
name
startDate
endDate
multiplier
criteria {
...PointsCriteria
}
userEligible
}
Where:
program: the Points program issuing the rewardmultiplier: boost factor on the base points accrual ratecriteria: eligibility requirements for earning the reward
Borrow Rewards
Borrow rewards can be present on borrowable reserves.
Merkl Borrow Rewards
Merkl borrow rewards represent an APY discount on the user’s borrow rate (which includes their Risk Premium). The accrued interest discount is paid in the specified payout token when the incentive campaign reaches its maturity.
interface MerklBorrowReward {
__typename: "MerklBorrowReward";
id: string;
startDate: Date;
endDate: Date;
discountApy: PercentNumber;
payoutToken: Erc20Token;
criteria: MerklCriteria[];
userEligible: boolean;
}
fragment MerklBorrowReward on MerklBorrowReward {
__typename
id
startDate
endDate
discountApy {
...PercentNumber
}
payoutToken {
...Erc20Token
}
criteria {
...MerklCriteria
}
userEligible
}
Where:
discount: the APY discount applied to the user’s borrow rateApy payout: the token used to pay the rewardToken criteria: eligibility requirements for earning the reward
Points Borrow Rewards
Similarly, borrowing from certain reserves can accrue points in a Points program.
interface BorrowPointsReward {
__typename: "BorrowPointsReward";
id: string;
program: PointsProgram;
name: string;
startDate: Date;
endDate: Date | null;
multiplier: number;
criteria: PointsCriteria[];
userEligible: boolean;
}
fragment BorrowPoints on BorrowPointsReward {
__typename
id
program {
...PointsProgram
}
name
startDate
endDate
multiplier
criteria {
...PointsCriteria
}
userEligible
}
Where:
program: the Points program issuing the rewardmultiplier: boost factor on the base points accrual ratecriteria: eligibility requirements for earning the reward
Points Program
A Points represents a loyalty or incentive system. Users accumulate points over time based on their supply or borrow position. The external links to the program’s website where users can view their accumulated points.
interface PointsProgram {
__typename: "PointsProgram";
id: string;
name: string;
externalUrl: string | null;
iconUrl: string | null;
}
fragment PointsProgram on PointsProgram {
__typename
id
name
externalUrl
iconUrl
}
Eligibility Criteria
Each reward may have eligibility criteria that users must meet. Both Merkl and Points criteria share the same shape (id, text, user) but use distinct GraphQL types.
// Merkl rewards use MerklCriteria
interface MerklCriteria {
__typename: "MerklGenericCriteria";
id: string;
text: string;
userPassed: boolean;
}
// Points rewards use PointsCriteria
interface PointsCriteria {
__typename: "PointsGenericCriteria";
id: string;
text: string;
userPassed: boolean;
}
fragment MerklCriteria on MerklCriteria {
... on MerklGenericCriteria {
__typename
id
text
userPassed
}
}
fragment PointsCriteria on PointsCriteria {
... on PointsGenericCriteria {
__typename
id
text
userPassed
}
}
Matured Rewards
Rewards become claimable when their incentive campaign reaches maturity. Campaigns are often renewed upon reaching their end date, so users should check for new reward opportunities periodically.
Claimable Rewards
Fetch the user’s matured rewards that are ready to claim.
Use the use hook (or the imperative use variant) in React, the user action in TypeScript, or the user query in GraphQL to fetch all rewards the user can claim.
import {
chainId,
useUserClaimableRewards,
type EvmAddress,
} from "@aave/react";
function ClaimableRewards({ user }: { user: EvmAddress }) {
const { data, loading, error } = useUserClaimableRewards({
user,
chainIds: [chainId(1)],
});
if (loading) return <div>Loading…</div>;
if (error) return <div>Error: {error.message}</div>;
if (data.length === 0) return <div>No claimable rewards</div>;
return (
<div>
{data.map((reward) => (
<div key={reward.id}>
<p>Amount: {reward.claimable.amount.value.toFixed(2)}</p>
<p>Claim until: {reward.claimUntil.toLocaleDateString()}</p>
</div>
))}
</div>
);
}
import {
chainId,
useUserClaimableRewards,
type EvmAddress,
} from "@aave/react";
function ClaimableRewards({ user }: { user: EvmAddress }) {
const { data } = useUserClaimableRewards({
user,
chainIds: [chainId(1)],
suspense: true,
});
if (data.length === 0) return <div>No claimable rewards</div>;
return (
<div>
{data.map((reward) => (
<div key={reward.id}>
<p>Amount: {reward.claimable.amount.value.toFixed(2)}</p>
<p>Claim until: {reward.claimUntil.toLocaleDateString()}</p>
</div>
))}
</div>
);
}
import {
chainId,
useUserClaimableRewardsAction,
type EvmAddress,
} from "@aave/react";
// …
const [fetch, { called, data, error, loading }] =
useUserClaimableRewardsAction();
const handler = async (user: EvmAddress) => {
const result = await fetch({
user,
chainIds: [chainId(1)],
});
if (result.isOk()) {
console.log(result.value); // UserClaimableReward[]
} else {
console.error(result.error);
}
};
import { userClaimableRewards } from "@aave/client/actions";
import { chainId, evmAddress } from "@aave/client";
import { client } from "./client";
const result = await userClaimableRewards(client, {
user: evmAddress("0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234"),
chainIds: [chainId(1)],
});
if (result.isErr()) {
console.error("claimable rewards error:", result.error);
} else {
// result.value: UserClaimableReward[]
console.log(result.value);
}
query {
userClaimableRewards(
request: {
user: "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
chainIds: [1]
}
) {
__typename
... on UserMerklClaimableReward {
id
claimChainId
claimable {
token {
address
info {
symbol
decimals
}
chain {
chainId
}
}
amount {
value
decimals
onChainValue
}
exchange(currency: USD) {
value
symbol
}
}
startDate
endDate
claimUntil
}
}
}
use doesn’t watch for updates. Use it for
on-demand fresh data, such as in an event handler.
An empty array means the user has no claimable rewards.
The User type contains details about each claimable reward:
interface UserMerklClaimableReward {
__typename: "UserMerklClaimableReward";
id: RewardId;
claimChainId: ChainId;
claimable: Erc20Amount;
startDate: Date;
endDate: Date;
claimUntil: Date;
}
fragment UserMerklClaimableReward on UserMerklClaimableReward {
__typename
id
claimChainId
claimable {
...Erc20Amount
}
startDate
endDate
claimUntil
}
Where:
id: unique identifier for the reward (used when claiming)claim: the chain on which the reward is claimedChain Id claimable: the claimable token amountstart: when the reward period startedDate end: when the reward period endedDate claim: deadline to claim the rewardUntil
Claim Rewards
Once you have claimable rewards, collect them individually or all at once in a single transaction.
Claimed rewards can stay in the claimable list until Merkl reports them as claimed, which can take a while.
To claim rewards with AaveKit, follow these steps.
1. Configure Wallet Integration
In React, first instantiate the use hook for the wallet library of your choice.
import { useWalletClient } from "wagmi";
import { useSendTransaction } from "@aave/react/viem";
// …
const { data: wallet } = useWalletClient();
const [sendTransaction] = useSendTransaction(wallet);
2. Define the Claim Flow
Then, in React, use the use hook to prepare the claim operation.
import { useClaimRewards } from "@aave/react";
const [claim, { loading, error }] = useClaimRewards((transaction) =>
sendTransaction(transaction),
);
import { useClaimRewards } from "@aave/react";
import { useState } from "react";
const [status, setStatus] = useState("");
const [claim, { loading, error }] = useClaimRewards((transaction) => {
setStatus("Please sign the claim transaction in your wallet");
return sendTransaction(transaction).andTee(() => setStatus("Claiming…"));
});
import { useClaimRewards } from "@aave/react";
const [claim, { loading, error }] = useClaimRewards(
(transaction, { cancel }) => {
if (window.confirm("Are you sure you want to claim?") === false) {
return cancel("User cancelled the operation");
}
return sendTransaction(transaction);
},
);
3. Execute the Claim Operation
Then, execute the claim operation with the reward IDs from the claimable rewards.
- TypeScript: Use the
claimaction.Rewards - GraphQL: Use the
claimquery.Rewards
import {
chainId,
useUserClaimableRewards,
evmAddress,
} from "@aave/react";
const { data: claimableRewards } = useUserClaimableRewards({
user: evmAddress(wallet.account.address),
chainIds: [chainId(1)],
suspense: true,
});
const execute = async () => {
if (claimableRewards.length > 0) {
const result = await claim({
ids: claimableRewards.map((reward) => reward.id),
user: evmAddress(wallet.account.address),
chainId: chainId(1),
});
// …
}
};
import { userClaimableRewards, claimRewards } from "@aave/client/actions";
import { chainId, evmAddress } from "@aave/client";
import { client } from "./client";
const result = await userClaimableRewards(client, {
user: evmAddress("0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234"),
chainIds: [chainId(1)],
}).andThen((rewards) =>
claimRewards(client, {
ids: rewards.map((reward) => reward.id),
user: evmAddress("0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234"),
chainId: chainId(1),
}),
);
// …
import { AaveClient } from "@aave/client";
export const client = AaveClient.create();
query {
claimRewards(
request: {
ids: ["reward-id-1", "reward-id-2"]
user: "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234"
chainId: 1
}
) {
to
from
data
value
chainId
}
}
4. Process the Transaction Request
In TypeScript, use send for the wallet library of your choice to send the transaction, and await indexing with client..
import { userClaimableRewards, claimRewards } from "@aave/client/actions";
import { sendWith } from "@aave/client/viem";
import { chainId, evmAddress } from "@aave/client";
import { client } from "./client";
import { wallet } from "./wallet";
const result = await userClaimableRewards(client, {
user: evmAddress(wallet.account.address),
chainIds: [chainId(1)],
})
.andThen((rewards) =>
claimRewards(client, {
ids: rewards.map((reward) => reward.id),
user: evmAddress(wallet.account.address),
chainId: chainId(1),
}),
)
.andThen(sendWith(wallet))
.andThen(client.waitForTransaction);
import { AaveClient } from "@aave/client";
export const client = AaveClient.create();
import { createWalletClient, http, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
export const wallet = createWalletClient({
account: privateKeyToAccount(process.env.PRIVATE_KEY as Hex),
chain: mainnet,
transport: http(),
});
In GraphQL, send the returned transaction using the wallet associated with the from address.
{
"data": {
"claimRewards": {
"to": "0x1234567890abcdef1234567890abcdef12345678",
"from": "0x742d35cc6e5c4ce3b69a2a8c7c8e5f7e9a0b1234",
"data": "0x…",
"value": "0",
"chainId": 1
}
}
}
5. Handle the Result
Finally, in React and TypeScript, handle the result.
const execute = async () => {
const result = await claim(/* … */);
if (result.isErr()) {
switch (result.error.name) {
case "CancelError":
// The user cancelled the operation
return;
case "SigningError":
console.error(
`Failed to sign the transaction: ${result.error.message}`,
);
break;
case "TimeoutError":
console.error(`Transaction timed out: ${result.error.message}`);
break;
case "TransactionError":
console.error(`Transaction failed: ${result.error.message}`);
break;
case "UnexpectedError":
console.error(result.error.message);
break;
}
return;
}
console.log("Rewards claimed successfully with hash:", result.value.txHash);
};
if (result.isErr()) {
switch (result.error.name) {
case "CancelError":
// The user cancelled the operation
return;
case "SigningError":
console.error(`Failed to sign the transaction: ${result.error.message}`);
break;
case "TimeoutError":
console.error(`Transaction timed out: ${result.error.message}`);
break;
case "TransactionError":
console.error(`Transaction failed: ${result.error.message}`);
break;
case "UnexpectedError":
console.error(result.error.message);
break;
}
} else {
console.log("Rewards claimed successfully with hash:", result.value.txHash);
}