Call canClaim() as a staticCall (no gas, no transaction) to display the unclaimed balance before the user clicks “Claim.” It takes the same parameters from the Merkle proof API response plus the user’s address.
import { ethers } from 'ethers';const STREAM_ABI = [ "function canClaim(address user, uint256 amount, uint40 timestamp, bytes32[] merkleProof) external view returns (uint256)",];// 1. Fetch proof from Turtle APIconst res = await fetch( `https://earn.turtle.xyz/v1/streams/merkle_proofs?wallet=${userAddress}&streamIds=${streamId}`);const { proofs } = await res.json();const proof = proofs[0];// 2. Convert timestamp from ISO 8601 to Unix epoch (uint40)const timestamp = Math.floor(new Date(proof.timestamp).getTime() / 1000);// 3. Call canClaim (staticCall — no gas)const provider = new ethers.BrowserProvider(window.ethereum);const contract = new ethers.Contract(proof.contractAddress, STREAM_ABI, provider);const claimable = await contract.canClaim( userAddress, proof.amount, timestamp, proof.proof);console.log('Claimable:', ethers.formatUnits(claimable, 18));
canClaim() returns the actual unclaimed amount — no additional math required. It accounts for previous claims automatically.
Submit a claim() transaction using the proof data. The contract uses a cumulative model: amount is the user’s total allocation across all snapshots, and the contract releases only the difference between the total and what has already been claimed.
The claimFor() function lets a relayer or backend service claim on behalf of a user. The rewards are still sent to the user’s wallet — the caller just submits the transaction.
TypeScript
const timestamp = Math.floor(new Date(proof.timestamp).getTime() / 1000);const tx = await contract.claimFor( userAddress, // recipient — rewards go to this wallet proof.amount, timestamp, proof.proof);
A drop-in <StreamsClaimButton> component for React/Next.js applications. It fetches proofs, displays the claimable amount, and handles the claim transaction.
The amount parameter in claim() is the user’s total cumulative allocation, not the unclaimed delta. The contract tracks how much has already been claimed and releases only the difference. Users can claim at any time and always receive their full outstanding balance in a single transaction.
Amount is in raw token units
The amount from the API is in raw token units. Use ethers.formatUnits(amount, decimals) to convert to a human-readable number for display. Call getRewardToken() on the contract to get the token address, then query the token’s decimals() if needed — most stream reward tokens use 18 decimals. Always pass the raw value to the contract — do not format it before sending the transaction.
Timestamp conversion
The API returns timestamp as an ISO 8601 string (e.g. "2026-05-20T13:05:10Z"), but the contract expects a uint40 Unix epoch in seconds. Convert before passing to the contract: Math.floor(new Date(proof.timestamp).getTime() / 1000).
Multiple streams require separate transactions
Each stream has its own contract. If a user has rewards in multiple streams, you need to submit a separate claim() transaction for each one. The React component above handles this automatically.
Claiming is idempotent
Calling claim() when there is nothing new to claim will succeed but transfer zero tokens. There is no penalty for calling it multiple times.