Complete Integration Example
This guide provides a complete, working reference implementation for integrating Morpho rewards (both Merkl and Legacy URD) into your application. We'll walk through the Morpho + Merkl Integration Demo repository, which demonstrates best practices for production-ready rewards integration.
Repository Overview
Tech Stack
- Next.js 15 - React framework
- TypeScript - Type safety
- Morpho API - GraphQL for vault/market data
- Merkl API - REST API for Merkl rewards
- Morpho Rewards API - REST API for URD rewards
Project Structure
src/
├── app/ # Next.js app router
├── components/ # React components
│ ├── VaultMetricsPanel.tsx # Display vault APY + rewards
│ ├── UserRewardsPanel.tsx # User claimable rewards
│ ├── VaultRewardsDisplay.tsx # Rewards breakdown
│ └── ClaimImplementationPanel.tsx # Claim code examples
├── hooks/ # React hooks
│ ├── useVaultData.ts # Fetch vault data
│ └── useClipboard.ts # Utility hook
├── lib/ # Core logic
│ ├── api.ts # API clients
│ ├── claiming.ts # Claim functions
│ ├── simulation.ts # Transaction simulation
│ └── helpers/
│ └── rewards.ts # Reward calculations
└── types/ # TypeScript types
└── index.ts
Key Concepts Demonstrated
1. Combining Morpho and Merkl Data
The demo shows how to fetch and combine data from multiple sources to provide a complete picture:
File:src/lib/api.ts
export async function getVaultData(vaultAddress: string, chainId: number) {
// Fetch from Morpho API
const morphoData = await fetchMorphoVault(vaultAddress, chainId);
// Extract rewards information
const rewards = morphoData.vault.state.rewards.map((r) => ({
token: r.asset.symbol,
apr: parseFloat(r.supplyApr),
yearlySupplyTokens: r.yearlySupplyTokens,
}));
// Calculate combined APY
const baseApy = parseFloat(morphoData.vault.state.apy);
const rewardsApr = rewards.reduce((sum, r) => sum + r.apr, 0);
return {
vault: morphoData.vault,
baseApy,
rewardsApr,
totalApy: baseApy + rewardsApr,
rewards,
};
}
2. Fetching User Rewards from Both Systems
File:src/lib/helpers/rewards.ts
export async function getUserRewards(userAddress: string, chainId: number) {
const [merklRewards, urdRewards] = await Promise.all([
fetchMerklUserRewards(userAddress, chainId),
fetchURDUserRewards(userAddress),
]);
return {
merkl: merklRewards,
urd: urdRewards,
combined: combineRewardsByToken(merklRewards, urdRewards),
};
}
function combineRewardsByToken(merkl: Reward[], urd: Reward[]) {
const combined = new Map<string, Reward>();
for (const reward of [...merkl, ...urd]) {
if (combined.has(reward.token)) {
const existing = combined.get(reward.token)!;
existing.amount += reward.amount;
} else {
combined.set(reward.token, { ...reward });
}
}
return Array.from(combined.values());
}
3. Implementing Claims
File:src/lib/claiming.ts
The demo includes claim implementations for both systems:
Merkl Claim:export async function claimMerklRewards(
userAddress: string,
chainId: number,
walletClient: WalletClient
) {
// 1. Fetch claim data
const claimData = await fetch(
`https://api.merkl.xyz/v4/claim?user=${userAddress}&chainId=${chainId}`
).then((r) => r.json());
// 2. Execute claim transaction
const hash = await walletClient.writeContract({
address: MERKL_DISTRIBUTOR_ADDRESS,
abi: MERKL_ABI,
functionName: "claim",
args: [
claimData.user,
claimData.tokens,
claimData.amounts,
claimData.proofs,
],
});
// 3. Wait for confirmation
return await walletClient.waitForTransactionReceipt({ hash });
}
export async function claimURDRewards(
userAddress: string,
walletClient: WalletClient
) {
// 1. Fetch all distributions
const distributions = await fetch(
`https://rewards.morpho.org/v1/users/${userAddress}/distributions`
).then((r) => r.json());
const results = [];
// 2. Claim each distribution
for (const dist of distributions) {
// Check if already claimed
const claimed = await checkClaimedAmount(
dist.distributor,
userAddress,
dist.asset.address
);
if (BigInt(claimed) >= BigInt(dist.claimable)) {
continue; // Nothing to claim
}
// Execute claim using pre-formatted tx_data
const hash = await walletClient.sendTransaction({
to: dist.distributor,
data: dist.tx_data,
});
const receipt = await walletClient.waitForTransactionReceipt({ hash });
results.push({ distribution: dist, receipt });
}
return results;
}
4. React Components for Display
File:src/components/VaultMetricsPanel.tsx
export function VaultMetricsPanel({ vaultAddress, chainId }) {
const { data, loading, error } = useVaultData(vaultAddress, chainId);
if (loading) return <Skeleton />;
if (error) return <ErrorDisplay error={error} />;
return (
<div className="vault-metrics">
{/* Vault Header */}
<div className="header">
<h2>{data.vault.name}</h2>
<p>{data.vault.symbol}</p>
</div>
{/* APY Display */}
<div className="apy-section">
<div className="total-apy">
<span className="label">Total APY</span>
<span className="value">{data.totalApy.toFixed(2)}%</span>
</div>
<div className="breakdown">
<div>Base APY: {data.baseApy.toFixed(2)}%</div>
{data.rewardsApr > 0 && (
<div className="rewards">
Rewards: +{data.rewardsApr.toFixed(2)}%
</div>
)}
</div>
</div>
{/* Rewards List */}
{data.rewards.length > 0 && (
<VaultRewardsDisplay rewards={data.rewards} />
)}
</div>
);
}
src/components/UserRewardsPanel.tsx
export function UserRewardsPanel({ userAddress, chainId }) {
const [rewards, setRewards] = useState(null);
const [claiming, setClaiming] = useState(false);
useEffect(() => {
getUserRewards(userAddress, chainId).then(setRewards);
}, [userAddress, chainId]);
const handleClaimAll = async () => {
setClaiming(true);
try {
// Claim from both systems
await Promise.all([
claimMerklRewards(userAddress, chainId, walletClient),
claimURDRewards(userAddress, walletClient),
]);
alert("Rewards claimed successfully!");
// Refresh rewards data
getUserRewards(userAddress, chainId).then(setRewards);
} catch (error) {
console.error("Claim failed:", error);
alert("Claim failed. See console for details.");
} finally {
setClaiming(false);
}
};
return (
<div className="user-rewards">
<h3>Your Claimable Rewards</h3>
{rewards?.combined.map((reward) => (
<div key={reward.token} className="reward-row">
<span>{reward.token}</span>
<span>{formatAmount(reward.amount)} {reward.token}</span>
</div>
))}
<button onClick={handleClaimAll} disabled={claiming}>
{claiming ? "Claiming..." : "Claim All Rewards"}
</button>
{/* Show source breakdown */}
<details>
<summary>View breakdown</summary>
<div>Merkl: {rewards?.merkl.length || 0} rewards</div>
<div>URD: {rewards?.urd.length || 0} rewards</div>
</details>
</div>
);
}
Running the Demo
Prerequisites
- Node.js 18+ and yarn
- A wallet with Morpho positions (for testing user rewards)
Quick Start
# Clone the repository
git clone https://github.com/morpho-org/merkl-morpho-recipe.git
cd merkl-morpho-recipe
# Install dependencies
yarn install
# Run the development server
yarn dev
# Open http://localhost:3000
Explore via Scripts
The demo includes standalone scripts to understand the data flow:
# See vault APY + rewards calculation
yarn demo:yield
# Check user claimable rewards
yarn demo:rewards
# Complete integration flow
yarn demo:full
$ yarn demo:yield
=== Vault Metrics ===
Vault: Steakhouse USDC
Base APY: 8.45%
Rewards APR: 2.13%
Total APY: 10.58%
Reward Programs:
- MORPHO: 1.85% APR
- STEAK: 0.28% APR
Key Takeaways from the Demo
1. Error Handling
The demo implements graceful error handling:
try {
const data = await fetchVaultData(address, chain);
return data;
} catch (error) {
console.error("Failed to fetch vault data:", error);
return null; // Fallback to null, don't crash the app
}
2. Type Safety
Full TypeScript coverage ensures correctness:
export interface VaultReward {
token: string;
apr: number;
yearlySupplyTokens: string;
tokenAddress: string;
}
export interface UserClaimableReward {
token: string;
amount: bigint;
source: "merkl" | "urd";
}
3. Caching Strategy
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
const cache = new Map<string, { data: any; timestamp: number }>();
export function getCached<T>(
key: string,
fetcher: () => Promise<T>
): Promise<T> {
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return Promise.resolve(cached.data);
}
return fetcher().then((data) => {
cache.set(key, { data, timestamp: Date.now() });
return data;
});
}
4. Simulation Before Execution
// File: src/lib/simulation.ts
export async function simulateClaimTransaction(
distributorAddress: string,
txData: string,
userAddress: string
) {
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
try {
await publicClient.call({
to: distributorAddress,
data: txData,
from: userAddress,
});
return { success: true };
} catch (error) {
return { success: false, error };
}
}
Adapting the Demo for Production
When building on this demo for production, consider:
Security
- Never expose private keys: Use secure wallet connection libraries (RainbowKit, Wagmi, etc.)
- Validate all inputs: Sanitize user addresses, vault addresses, chain IDs
- Handle transaction failures: Implement retry logic with exponential backoff
- Rate limiting: Respect API rate limits and implement client-side throttling
Performance
- Lazy load components: Use React.lazy for routes
- Optimize bundle size: Code split by route and feature
- Implement pagination: For users with many rewards
- Use a state management solution: Redux, Zustand, or React Query for complex state
User Experience
- Transaction notifications: Toast notifications for claim status
- Transaction history: Show past claims
- Gas estimation: Display estimated gas costs before claiming
- Batch claiming: Allow users to claim multiple rewards in one transaction (using Bundlers)
Monitoring
- Error tracking: Sentry, Bugsnag, or similar
- Analytics: Track claim success rates, popular vaults, etc.
- Logging: Structured logging for debugging
Next Steps from the Demo
After exploring the demo:
- Fork and modify: Adapt it to your use case
- Add wallet connection: Integrate RainbowKit or ConnectKit
- Customize UI: Apply your brand's design system
- Deploy: Vercel, Netlify, or your preferred platform
Resources
- Demo Repository: github.com/morpho-org/merkl-morpho-recipe
- Morpho API: api.morpho.org/graphql
- Merkl Docs: docs.merkl.xyz
- Rewards API: rewards.morpho.org/docs