This guide explains how to query merkle proofs from the API for the invest flow.
The investor wave system generates merkle proofs for whitelisted investors. Frontend needs to:
- Check if a user is whitelisted
- Get their proof and allocation amount
- Pass these to the contract's
invest()function
Returns the merkle proof for a specific wallet/token combination.
Request:
GET https://api.flyingtulip.com/whitelist/invest-proof?wallet=0x...&token=0x...
Parameters:
wallet(required): User's wallet addresstoken(required): Token address they want to invest withchainId(optional): Defaults to 1 (Ethereum)
Response (user is whitelisted):
{
"proof": [
"0x1234567890abcdef...",
"0xabcdef1234567890...",
"0x..."
],
"proofAmount": "1000000000000000000000",
"merkleRoot": "0x...",
"found": true
}Response (user NOT whitelisted):
{
"proof": [],
"proofAmount": "0",
"merkleRoot": "0x...",
"found": false
}Fields:
proof: Array of bytes32 hashes for merkle proofproofAmount: Max amount user can invest (in wei, as string)merkleRoot: Current merkle root (for verification)found: Whether user is in the whitelist
Returns the current merkle root (useful for display/verification).
GET https://api.flyingtulip.com/whitelist/merkle-root
interface InvestProofResponse {
proof: string[];
proofAmount: string;
merkleRoot: string;
found: boolean;
}
async function getInvestProof(
wallet: string,
token: string
): Promise<InvestProofResponse> {
const params = new URLSearchParams({ wallet, token });
const response = await fetch(
`https://api.flyingtulip.com/whitelist/invest-proof?${params}`
);
return response.json();
}
// Usage
const { proof, proofAmount, found } = await getInvestProof(
userAddress,
usdcAddress
);
if (!found) {
// Show "not whitelisted" message
return;
}
// User is whitelisted for proofAmount
console.log(`User can invest up to ${ethers.formatUnits(proofAmount, 6)} USDC`);import { useQuery } from '@tanstack/react-query';
import { useAccount } from 'wagmi';
const INVEST_TOKEN = '0x...'; // USDC or other token
export function useInvestProof() {
const { address } = useAccount();
return useQuery({
queryKey: ['investProof', address],
queryFn: async () => {
if (!address) return null;
const params = new URLSearchParams({
wallet: address,
token: INVEST_TOKEN,
});
const res = await fetch(
`https://api.flyingtulip.com/whitelist/invest-proof?${params}`
);
return res.json();
},
enabled: !!address,
});
}
// In component
function InvestButton() {
const { data: proofData, isLoading } = useInvestProof();
if (isLoading) return <Spinner />;
if (!proofData?.found) {
return <div>You are not whitelisted for this raise</div>;
}
return (
<div>
<p>Your allocation: {formatAmount(proofData.proofAmount)}</p>
<button onClick={() => invest(proofData)}>Invest</button>
</div>
);
}import { parseUnits } from 'viem';
async function invest(
proofData: InvestProofResponse,
investAmount: string
) {
const { proof, proofAmount } = proofData;
// investAmount is what user wants to invest
// proofAmount is their max allocation from whitelist
const amountWei = parseUnits(investAmount, 6); // 6 decimals for USDC
// Call contract
await writeContract({
address: RAISE_CONTRACT,
abi: raiseAbi,
functionName: 'invest',
args: [
TOKEN_ADDRESS, // token
amountWei, // amount to invest
BigInt(proofAmount), // proofAmount from API
proof, // merkle proof array
],
});
}async function getProofSafe(wallet: string, token: string) {
try {
const data = await getInvestProof(wallet, token);
if (!data.found) {
return {
isWhitelisted: false,
error: 'Address not in whitelist'
};
}
return {
isWhitelisted: true,
proof: data.proof,
maxAmount: data.proofAmount,
};
} catch (error) {
return {
isWhitelisted: false,
error: 'Failed to fetch whitelist status'
};
}
}proofAmountis returned as a string to handle large numbers (wei)- Proof array can be empty if user is not whitelisted
- The API defaults to Ethereum - no need to pass
chainIdfor mainnet - Cache proof data - it only changes when a new wave is deployed