> ## Documentation Index
> Fetch the complete documentation index at: https://docs.turtle.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Claim Rewards

> Integrate on-chain reward claiming for token streams into your application

## Overview

Claiming rewards from a token stream is a two-step process:

1. **Fetch the Merkle proof** from the Turtle API ([Get Merkle Proofs](/sdk/streams/get-merkle-proofs))
2. **Submit a claim transaction** to the stream's smart contract on-chain

No API key is needed for either step. Proofs are permissionless, and claiming is a direct on-chain interaction.

```
Your Application
    ↓ 1. GET /v1/streams/merkle_proofs
earn.turtle.xyz
    ↓ 2. Returns proof + amount + timestamp + contract address
Your Application
    ↓ 3. Build claim transaction
Stream Contract (on-chain)
    ↓ 4. User signs & submits
Rewards transferred to user wallet
```

## Contract ABI

The stream contract exposes the following functions for claiming and display. This ABI is sufficient for all claim integration scenarios.

### Stream contract

Each stream is a separate contract. The address is returned in the [Merkle proof API response](/sdk/streams/get-merkle-proofs) as `contractAddress`.

```typescript theme={null}
const STREAM_ABI = [
  "function claim(uint256 amount, uint40 timestamp, bytes32[] merkleProof) external returns (uint256)",
  "function canClaim(address user, uint256 amount, uint40 timestamp, bytes32[] merkleProof) external view returns (uint256)",
  "function getClaimedRewards(address user) external view returns (uint256)",
  "function getRewardToken() external view returns (address)"
];
```

| Function            | Type  | Purpose                                        |
| ------------------- | ----- | ---------------------------------------------- |
| `claim`             | Write | Claim rewards for the connected wallet         |
| `canClaim`          | View  | Returns the unclaimed amount (use for display) |
| `getClaimedRewards` | View  | Returns total already claimed by a user        |
| `getRewardToken`    | View  | Returns the reward token address               |

### StreamFactory contract

The StreamFactory owns all stream contracts and provides batch operations. Use it to claim from multiple streams in a single transaction.

```typescript theme={null}
const STREAM_FACTORY_ABI = [
  "function batchClaim((address stream, uint256 amount, uint40 rootTimestamp, bytes32[] merkleProof)[] claims, bool revertOnFailure) external returns (bool[])",
  "function batchClaimFor(address user, (address stream, uint256 amount, uint40 rootTimestamp, bytes32[] merkleProof)[] claims, bool revertOnFailure) external returns (bool[])",
  "function toggleOperatorForUser(address user, address operator) external"
];
```

| Function                | Type  | Purpose                                                |
| ----------------------- | ----- | ------------------------------------------------------ |
| `batchClaim`            | Write | Claim from multiple streams in one transaction         |
| `batchClaimFor`         | Write | Claim on behalf of a user (requires operator approval) |
| `toggleOperatorForUser` | Write | Approve or revoke an operator for a user               |

## Show claimable amount

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](/sdk/streams/get-merkle-proofs) plus the user's address.

```typescript theme={null}
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 API
const 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));
```

<Info>
  `canClaim()` returns the actual unclaimed amount. No additional math required. It accounts for previous claims automatically.
</Info>

## Claim rewards

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.

```typescript theme={null}
import { ethers } from 'ethers';

const STREAM_ABI = [
  "function claim(uint256 amount, uint40 timestamp, bytes32[] merkleProof) external returns (uint256)",
  "function canClaim(address user, uint256 amount, uint40 timestamp, bytes32[] merkleProof) external view returns (uint256)",
];

// 1. Fetch proof
const 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. Connect signer
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const contract = new ethers.Contract(proof.contractAddress, STREAM_ABI, signer);

// 4. (Optional) Show claimable amount first
const claimable = await contract.canClaim(
  userAddress,
  proof.amount,
  timestamp,
  proof.proof
);
console.log('Claimable:', ethers.formatUnits(claimable, 18));

// 5. Submit claim transaction
const tx = await contract.claim(
  proof.amount,       // total cumulative allocation
  timestamp,          // Merkle root timestamp (uint40)
  proof.proof         // Merkle proof (bytes32[])
);

const receipt = await tx.wait();
console.log('Claimed successfully:', receipt.hash);
```

## Batch claiming

If a user has rewards across multiple streams, you can claim all of them in a single transaction using `batchClaim()` on the StreamFactory instead of calling `claim()` on each stream contract separately.

```typescript theme={null}
import { ethers } from 'ethers';

const STREAM_FACTORY_ABI = [
  "function batchClaim((address stream, uint256 amount, uint40 rootTimestamp, bytes32[] merkleProof)[] claims, bool revertOnFailure) external returns (bool[])",
];

// 1. Fetch proofs for all streams
const params = new URLSearchParams({ wallet: userAddress });
streamIds.forEach((id) => params.append('streamIds', id));

const res = await fetch(
  `https://earn.turtle.xyz/v1/streams/merkle_proofs?${params}`
);
const { proofs } = await res.json();

// 2. Build batch claims array
const claims = proofs
  .filter((p) => p.amount && p.amount !== '0')
  .map((p) => ({
    stream: p.contractAddress,
    amount: p.amount,
    rootTimestamp: Math.floor(new Date(p.timestamp).getTime() / 1000),
    merkleProof: p.proof,
  }));

// 3. Submit single batch transaction to the StreamFactory
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const factory = new ethers.Contract(STREAM_FACTORY_ADDRESS, STREAM_FACTORY_ABI, signer);

const tx = await factory.batchClaim(claims, true); // revertOnFailure=true
const receipt = await tx.wait();
```

<Info>
  The StreamFactory address depends on the chain. Contact Turtle or check the block explorer for the deployed address on your target network.
</Info>

## Claim on behalf of a user

To claim on behalf of another user, use `batchClaimFor()` on the StreamFactory. The caller must first be approved as an **operator** by the user via `toggleOperatorForUser()`.

```typescript theme={null}
// User approves the operator (one-time setup)
const tx1 = await factory.toggleOperatorForUser(userAddress, operatorAddress);
await tx1.wait();

// Operator claims on behalf of the user
const tx2 = await factory.batchClaimFor(userAddress, claims, true);
await tx2.wait();
```

<Note>
  `claim()` on the stream contract can only be called by the user themselves. Delegated claiming always goes through the StreamFactory's `batchClaimFor()` with prior operator approval.
</Note>

## React component

A drop-in `<StreamsClaimButton>` component for React/Next.js applications. It fetches proofs, displays the claimable amount, and handles the claim transaction.

```tsx theme={null}
import { useState, useEffect } from 'react';
import { ethers } from 'ethers';

const STREAM_ABI = [
  "function claim(uint256 amount, uint40 timestamp, bytes32[] merkleProof) external returns (uint256)",
  "function canClaim(address user, uint256 amount, uint40 timestamp, bytes32[] merkleProof) external view returns (uint256)",
];

interface ClaimButtonProps {
  streamIds: string[];          // stream UUIDs to claim from
  walletAddress: string;        // connected wallet
  provider: ethers.Provider;    // provider for read calls
  signer: ethers.Signer;        // wallet signer for claim tx
  onSuccess?: (receipt: ethers.TransactionReceipt) => void;
  onError?: (error: Error) => void;
}

export function StreamsClaimButton({
  streamIds,
  walletAddress,
  provider,
  signer,
  onSuccess,
  onError,
}: ClaimButtonProps) {
  const [loading, setLoading] = useState(false);
  const [status, setStatus] = useState<string>('');
  const [claimable, setClaimable] = useState<string | null>(null);

  // Fetch proofs and check claimable amount on mount
  useEffect(() => {
    (async () => {
      try {
        const ids = streamIds.join('&streamIds=');
        const res = await fetch(
          `https://earn.turtle.xyz/v1/streams/merkle_proofs?wallet=${walletAddress}&streamIds=${ids}`
        );
        const { proofs } = await res.json();
        if (!proofs || proofs.length === 0) return;

        let total = 0n;
        for (const proof of proofs) {
          if (!proof.amount || proof.amount === '0') continue;
          const contract = new ethers.Contract(
            proof.contractAddress,
            STREAM_ABI,
            provider
          );
          const ts = Math.floor(new Date(proof.timestamp).getTime() / 1000);
          const amount = await contract.canClaim(
            walletAddress,
            proof.amount,
            ts,
            proof.proof
          );
          total += amount;
        }
        setClaimable(ethers.formatUnits(total, 18));
      } catch {
        // Silently fail: button still works without display amount
      }
    })();
  }, [streamIds, walletAddress, provider]);

  const handleClaim = async () => {
    setLoading(true);
    setStatus('Fetching proof...');
    try {
      const ids = streamIds.join('&streamIds=');
      const res = await fetch(
        `https://earn.turtle.xyz/v1/streams/merkle_proofs?wallet=${walletAddress}&streamIds=${ids}`
      );
      const { proofs } = await res.json();

      if (!proofs || proofs.length === 0) {
        setStatus('No claimable rewards found');
        setLoading(false);
        return;
      }

      for (const proof of proofs) {
        if (!proof.amount || proof.amount === '0') continue;

        setStatus(`Claiming from ${proof.contractAddress.slice(0, 8)}...`);
        const ts = Math.floor(new Date(proof.timestamp).getTime() / 1000);
        const contract = new ethers.Contract(
          proof.contractAddress,
          STREAM_ABI,
          signer
        );
        const tx = await contract.claim(
          proof.amount,
          ts,
          proof.proof
        );
        const receipt = await tx.wait();
        onSuccess?.(receipt);
      }

      setClaimable('0');
      setStatus('Claimed!');
    } catch (err) {
      setStatus('Claim failed');
      onError?.(err as Error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <button onClick={handleClaim} disabled={loading || claimable === '0'}>
      {loading
        ? status
        : claimable
          ? `Claim ${claimable} tokens`
          : 'Claim Rewards'}
    </button>
  );
}
```

**Usage:**

```tsx theme={null}
<StreamsClaimButton
  streamIds={['7e9c407e-3992-4587-b8e7-9a30f96b12b5']}
  walletAddress={connectedAddress}
  provider={provider}
  signer={signer}
  onSuccess={(receipt) => console.log('Claimed:', receipt)}
  onError={(err) => console.error('Failed:', err)}
/>
```

## Operational Notes

<AccordionGroup>
  <Accordion title="Cumulative amount model">
    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.
  </Accordion>

  <Accordion title="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.
  </Accordion>

  <Accordion title="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)`.
  </Accordion>

  <Accordion title="Multiple streams">
    Each stream has its own contract. When calling `claim()` directly, you need a separate transaction per stream. The React component above handles this automatically. Alternatively, use `batchClaim()` on the StreamFactory to claim from all streams in a single transaction.
  </Accordion>

  <Accordion title="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.
  </Accordion>
</AccordionGroup>
