Skip to main content

SDK reference

@droppie/sdk is the TypeScript toolkit behind the Droppie app — and the same code you use to script drops, build claim bots, or integrate airdrops into your own product. Pure TypeScript, ESM + CJS builds, fully typed.

Built on viem, @openzeppelin/merkle-tree, and papaparse.

Install

pnpm add @droppie/sdk
# or
npm install @droppie/sdk

Inside the monorepo it's consumed as "@droppie/sdk": "workspace:*".

Core types

export type Recipient = {
address: `0x${string}`;
amount: bigint; // raw wei (already scaled by token decimals)
};

export type DistributionJson = {
version: 2;
chainId: number;
token: `0x${string}`;
merkleRoot: `0x${string}`;
totalAmount: string; // bigint as string
recipients: { address: `0x${string}`; amount: string }[];
createdAt: string; // ISO 8601
};

Everywhere in the SDK, amounts are raw wei bigints. Human-unit conversion happens exactly once, in parseRecipientsCsv.

Functions

parseRecipientsCsv

function parseRecipientsCsv(
csv: string,
decimals: number
): { recipients: Recipient[]; errors: string[] };

Parses an address,amount CSV (format details) into validated recipients:

  • amounts are human units, converted to raw wei using decimals
  • addresses are validated (including EIP-55 checksums)
  • duplicate addresses are deduped by summing their amounts
  • problems are returned as human-readable errors rather than thrown
import { parseRecipientsCsv } from "@droppie/sdk";

const csv = `address,amount
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266,100
0x70997970C51812dc3A010C7d01b50e0d17dc79C8,250.5`;

const { recipients, errors } = parseRecipientsCsv(csv, 18);
if (errors.length > 0) throw new Error(errors.join("\n"));

// recipients[0] = {
// address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
// amount: 100000000000000000000n,
// }

buildTree

function buildTree(recipients: Recipient[]): {
root: `0x${string}`;
tree: StandardMerkleTree<[string, string]>;
};

Builds the OpenZeppelin StandardMerkleTree with leaf encoding ["address", "uint256"] and the double-hashed leaf scheme. The returned root is what goes on-chain — it's the merkleRoot_ argument of the DroppieDrop constructor.

import { buildTree } from "@droppie/sdk";

const { root, tree } = buildTree(recipients);
console.log("merkle root:", root);

getProof

function getProof(
tree: StandardMerkleTree<[string, string]>,
address: `0x${string}`
): { proof: `0x${string}`[]; amount: bigint } | null;

Looks up an address in the tree (case-insensitive) and returns its Merkle proof and allotted amount — exactly the amount and proof arguments that claim expects. Returns null if the address isn't in the tree.

import { getProof } from "@droppie/sdk";

const result = getProof(tree, "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
if (result === null) throw new Error("address not eligible");

const { proof, amount } = result;

buildDistributionJson

function buildDistributionJson(/* chainId, token, recipients, createdAt? */): DistributionJson;

Assembles the versioned distribution file from the chain, token and recipients — the Merkle root is recomputed internally from the recipient list (so the file can't claim a root that doesn't match its contents) — serializing bigint amounts as strings and stamping createdAt. Pin the result to IPFS (or host it on any HTTPS URL) and pass its location as metadataURI when creating the drop. See the package's type definitions for the full parameter list.

import { buildDistributionJson } from "@droppie/sdk";

const distribution = buildDistributionJson({
chainId: 8453,
token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
recipients,
});

await fetch("https://droppie.notcool.in/api/pin", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ distribution }),
});

loadTreeFromDistribution

function loadTreeFromDistribution(
dist: DistributionJson
): StandardMerkleTree<[string, string]>;

Rebuilds the exact Merkle tree from a distribution file — what claim pages do after fetching metadataURI. Verify the rebuilt root against the on-chain merkleRoot() before trusting the file:

import { loadTreeFromDistribution, getProof } from "@droppie/sdk";

const dist: DistributionJson = await (await fetch(metadataUrl)).json();
const tree = loadTreeFromDistribution(dist);

if (tree.root !== onChainRoot) throw new Error("distribution file does not match drop");

const claim = getProof(tree, myAddress);

totalOf

function totalOf(recipients: Recipient[]): bigint;

Sums all recipient amounts — the totalAmount you transfer to the drop after deploying it.

formatTokenAmount

function formatTokenAmount(wei: bigint, decimals: number, precision?: number): string;

Formats a raw wei amount for display: formatTokenAmount(1500000000000000000n, 18)"1.5". Optional precision caps the number of fraction digits.

ABIs

Hand-written, exactly matching the contracts:

import {
droppieDropAbi, // DroppieDrop
droppieErc20Abi, // DroppieERC20
erc20Abi, // minimal standard ERC-20
} from "@droppie/sdk";

All are viem-compatible Abi values — plug them straight into readContract / writeContract / getLogs / deployContract and get full type inference on function names, args, and events. Both contract ABIs carry their constructor entries, so deployContract type-checks the deployment args too.

Contract bytecode

Droppie has no factories and nothing pre-deployed — the SDK ships the contracts' creation bytecode, and your wallet deploys them directly:

import { droppieDropBytecode, droppieErc20Bytecode } from "@droppie/sdk";

// Hex — 0x-prefixed creation bytecode, ready for viem/wagmi deployContract

Deploying a drop (constructor args: token, merkleRoot, startTime, endTime, metadataURI):

import { droppieDropAbi, droppieDropBytecode } from "@droppie/sdk";

const hash = await walletClient.deployContract({
abi: droppieDropAbi,
bytecode: droppieDropBytecode,
args: [token, root, 0n, 0n, metadataURI],
});

const { contractAddress: drop } = await publicClient.waitForTransactionReceipt({
hash,
});

Deploying a token for the Token Alchemist (constructor args: name, symbol, premintWei, owner_, mintable_, burnable_):

import { droppieErc20Abi, droppieErc20Bytecode } from "@droppie/sdk";
import { parseEther } from "viem";

await walletClient.deployContract({
abi: droppieErc20Abi,
bytecode: droppieErc20Bytecode,
// name, symbol, premint (raw wei), owner = you, mintable, burnable
args: ["Droppie Community", "DRPC", parseEther("1000000"), account, false, true],
});

The constants are copied verbatim from the Foundry artifacts in packages/contracts/abi/*.json (see the comment in packages/sdk/src/bytecode.ts for the regeneration command), and the SDK's test suite guards against drift. They add ~14 KB to the bundle but are tree-shakable named exports — claim-only consumers don't pay for them.

Supported chains

import { SUPPORTED_CHAINS, CHAIN_INFO } from "@droppie/sdk";

// SUPPORTED_CHAINS: number[] — chain IDs the hosted app is configured for
// CHAIN_INFO: Record<number, ChainInfo> — name, explorer URL, native symbol, testnet flag

const base = CHAIN_INFO[8453]; // { name: "Base", explorerUrl: "https://basescan.org", ... }

These are display metadata, not a permission list — since nothing is pre-deployed, the contracts work on any EVM chain. SUPPORTED_CHAINS is simply what the hosted app's wallet config offers (including 31337 for a local Anvil chain in the Docker demo); self-hosters can extend it freely.

End-to-end example

A complete script that turns a CSV into a live drop:

import {
parseRecipientsCsv,
buildTree,
buildDistributionJson,
totalOf,
droppieDropAbi,
droppieDropBytecode,
erc20Abi,
} from "@droppie/sdk";
import { readFileSync } from "node:fs";

const TOKEN = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as const;
const CHAIN_ID = 8453;

// 1. CSV → validated recipients (18 = token decimals)
const { recipients, errors } = parseRecipientsCsv(
readFileSync("recipients.csv", "utf8"),
18
);
if (errors.length > 0) throw new Error(errors.join("\n"));

// 2. Merkle tree + totals
const { root } = buildTree(recipients);
const total = totalOf(recipients);

// 3. Distribution file → pin/host it, keep the URI
const distribution = buildDistributionJson({
chainId: CHAIN_ID,
token: TOKEN,
recipients,
});
const metadataURI = await pinToIpfs(distribution); // your pinning of choice

// 4. Deploy your drop — your wallet, your contract
const hash = await walletClient.deployContract({
abi: droppieDropAbi,
bytecode: droppieDropBytecode,
args: [TOKEN, root, 0n, 0n, metadataURI], // start 0 = now, end 0 = no deadline
});
const { contractAddress: drop } = await publicClient.waitForTransactionReceipt({
hash,
});

// 5. Fund it — one plain transfer, no approve
await walletClient.writeContract({
address: TOKEN,
abi: erc20Abi,
functionName: "transfer",
args: [drop!, total],
});

That's it — share https://droppie.notcool.in/claim/{chainId}/{dropAddress} with your recipients.