# Multisender SDK — Full Reference > TypeScript SDK for the Multisender Enterprise API. Batches ERC-20 and native token distributions on EVM chains — the SDK generates transaction calldata that consumers sign and submit themselves. Package name: `@multisender.app/multisender-sdk`. Package version: `1.0.0`. This document is a single self-contained reference intended to be fetched once by an LLM. It includes installation, configuration, every public method, every type, every error class, examples, and architectural notes. Anchor links in `llms.txt` point to headings here. --- ## Installation ```bash npm install @multisender.app/multisender-sdk # or yarn add @multisender.app/multisender-sdk # or bun add @multisender.app/multisender-sdk ``` **Runtime requirements:** Node.js 18+ or Bun 1+. Works in modern browsers that support native `fetch`. For development, TypeScript 5+ is recommended. The SDK is shipped as dual ESM/CJS with bundled TypeScript types. The package also exposes a CLI binary named `multisender` at `./dist/cli/index.cjs` when installed globally. --- ## Quick start ```typescript import { Multisender } from '@multisender.app/multisender-sdk' const sdk = new Multisender({ apiKey: process.env.MULTISENDER_API_KEY!, }) // One-shot distribution: create + prepare + return calldata const result = await sdk.distributions.distribute({ chainId: 1, // Ethereum tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC tokenSymbol: 'USDC', recipients: [ ['0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', '100.5'], ['0x1234567890123456789012345678901234567890', '50.25'], ], account: '0xYourWallet...', }) // result.distribution — Distribution object (id, status, totals...) // result.calldata.transactions — transactions to sign and submit // result.calldata.summary — totalRecipients, totalAmount, estimatedGas, estimatedCost for (const tx of result.calldata.transactions) { await wallet.sendTransaction({ to: tx.to, data: tx.data, value: tx.value, gasLimit: tx.gasLimit, }) } // Monitor progress const stats = await sdk.distributions.getStats(result.distribution.id) ``` For ERC-20 tokens you must first submit an `approve()` transaction — call `sdk.distributions.getApproveCalldata(...)` or `getApproveCalldataForDistribution(id)` to obtain it. For native currency (ETH, POL, BNB, …) pass `0x0000000000000000000000000000000000000000` as `tokenAddress` and skip the approval step — see [Native tokens](#native-tokens). --- ## Configuration The `Multisender` constructor takes a `MultisenderConfig` object: ```typescript interface MultisenderConfig { apiKey: string // required — API key from the Multisender dashboard baseUrl?: string // optional — override only for staging/local timeout?: number // default: 30000 (ms) headers?: Record // additional headers merged into every request } ``` The API key is validated inside the constructor via `validateApiKey()` — it must be non-empty and at least 10 characters, otherwise a `ValidationError` is thrown synchronously. The fetch layer enforces the timeout using `AbortSignal.timeout()` (modern) or a `setTimeout` fallback. Slow networks that don't respond within `timeout` produce a `TimeoutError`. ### Instance properties Once constructed, the client exposes four services as properties: ```typescript const sdk = new Multisender({ apiKey: 'ms_...' }) sdk.distributions // DistributionsService sdk.lists // ListsService sdk.project // ProjectService sdk.catalogs // CatalogsService ``` ### Static ```typescript Multisender.getVersion() // returns '1.0.0' (hardcoded constant in the SDK build) ``` --- ## Authentication The SDK authenticates every request using the API key you pass to the `Multisender` constructor. The transport details are an internal concern — do not attempt to set auth headers manually via the `headers` config option; the SDK always applies the configured key itself and will not let you override it. Obtain an API key from the Multisender dashboard at `dashboard.multisender.app/api-keys`. Each API key carries a set of scopes. Typical scopes used by the SDK: - `project:read`, `project:write` — project info and API key management - `distributions:read`, `distributions:write` — distribution CRUD - `lists:read`, `lists:write` — recipient list CRUD - `members:read`, `members:write` — project membership - `api-keys:read`, `api-keys:write` — API key management Server-side enforcement is via `ApiScopesGuard` decorators on the backend controllers. If the key lacks the required scope, the API returns `403` and the SDK throws an `ApiError` with `status: 403`. --- ## Native tokens For native currency (ETH on Ethereum, POL on Polygon, BNB on BSC, …) pass the zero address as `tokenAddress`: ```typescript await sdk.distributions.distribute({ chainId: 1, tokenAddress: '0x0000000000000000000000000000000000000000', tokenSymbol: 'ETH', // any placeholder — the server normalizes this recipients: [['0xabc...', '0.5']], account: '0xYourWallet...', }) ``` - **No approval step is needed.** Native tokens cannot be ERC-20 `approve()`d. Skip `getApproveCalldata()` entirely. - **`tokenSymbol` is normalized.** When the server sees the zero address it replaces your `tokenSymbol` with the chain's `nativeSymbol` (e.g. `ETH`, `POL`, `BNB`). You can pass any placeholder string. - **Calldata includes `value`.** The generated transaction includes the required native `value` per batch — set the `value` field from `tx.value` when submitting. --- ## CatalogsService Discovery of supported chains. ```typescript class CatalogsService { getChains(): Promise getChain(chainId: string): Promise } ``` | Method | Purpose | |--------|---------| | `getChains()` | List every supported blockchain. Returns `Chain[]`. | | `getChain(chainId)` | Retrieve details for a single chain. Throws `ApiError(404)` if unknown. | --- ## DistributionsService The core of the SDK. 12 methods covering both the single-step flow (`distribute`) and the two-step draft flow (`createDraft` → `updateDraft`/`replaceRecipients` → `prepare`). ```typescript class DistributionsService { distribute(request: CreateDistributeRequest): Promise createDraft(request: CreateDistributionRequest): Promise updateDraft(id: string, request: UpdateDistributionRequest): Promise replaceRecipients(id: string, request: UpdateDistributionRecipientsRequest): Promise prepare(id: string, request: PrepareDistributionRequest): Promise getApproveCalldata(request: ApproveCalldataRequest): Promise getApproveCalldataForDistribution(id: string): Promise list(params?: DistributionsQueryParams): Promise> get(id: string): Promise getTransactions(id: string, params?: TransactionsQueryParams): Promise> getStats(id: string): Promise cancel(id: string): Promise } ``` ### Method details | Method | Description | |--------|-------------| | `distribute(request)` | Create a distribution and return calldata in one call. Accepts inline `recipients` or `csv`, not `listId`. Returns `DistributeResult = { distribution, calldata }` — use this when you need the transactions to sign immediately. | | `createDraft(request)` | Create a distribution in `DRAFT` status. Accepts `listId` (existing list) or inline `recipients`, mutually exclusive. | | `updateDraft(id, request)` | Edit metadata (name, chain, token, account, notes, strategy, isDeflationary) of a `DRAFT`. Throws `ApiError(409)` on non-draft. | | `replaceRecipients(id, request)` | Fully replace the recipients array on a `DRAFT`. | | `prepare(id, request)` | Generate calldata for a `DRAFT` and transition it to `PREPARED`. Accepts optional `account` + `rpcUrl`. | | `getApproveCalldata(request)` | Build `approve(spender, amount)` calldata from chainId + tokenAddress + amount. `amount` can be a wei string or the literal `"max"`. | | `getApproveCalldataForDistribution(id)` | Same but calculates the exact amount from an existing distribution — use in place of manual amount math. | | `list(params?)` | Paginated list of distributions for the authenticated project. | | `get(id)` | Fetch a distribution by ID. | | `getTransactions(id, params?)` | Paginated list of underlying batch transactions with txHashes and status. | | `getStats(id)` | Aggregate counts: `total`, `pending`, `submitted`, `confirmed`, `failed`. | | `cancel(id)` | Cancel a distribution. Allowed only from `DRAFT`, `PREPARED`, or `IN_PROGRESS` (admin can cancel the latter). | --- ## ListsService Recipient list management, 14 methods. ```typescript class ListsService { list(params?: ListsQueryParams): Promise> create(request: CreateRecipientListRequest): Promise get(listId: string): Promise update(listId: string, request: UpdateRecipientListRequest): Promise delete(listId: string): Promise getRecipients(listId: string, params?: RecipientsQueryParams): Promise> addRecipient(listId: string, request: AddRecipientRequest): Promise addRecipientsBulk(listId: string, request: AddRecipientsBulkRequest): Promise removeListItem(listId: string, listItemId: string): Promise bulkDeleteRecipients(listId: string, request: BulkDeleteRecipientsRequest): Promise importFromCsv(listId: string, file: File | Blob, options?: ImportFromCsvOptions): Promise createDistributionList(request: CreateDistributionListRequest): Promise importCsvDistribution(request: ImportCsvDistributionRequest): Promise validateDistributionRecipients(request: ValidateDistributionRecipientsRequest): Promise } ``` ### Method details | Method | Description | |--------|-------------| | `list(params?)` | Paginated list of recipient lists in the current project. | | `create({ name, notes? })` | Create an empty list. Use `addRecipient` / `addRecipientsBulk` / `importFromCsv` to populate it later. | | `get(listId)` | Fetch list metadata including `totalItems`. | | `update(listId, { name?, notes? })` | Partial update. Pass `notes: null` to clear existing notes. | | `delete(listId)` | Hard-delete. Returns `{ message }`. Existing distributions keep their own recipient snapshot and are not affected. | | `getRecipients(listId, params?)` | Paginated list items with the linked `Recipient`. | | `addRecipient(listId, dto)` | Add a single recipient. Returns `{ recipient, listItem }`. ENS names are supported for EVM. | | `addRecipientsBulk(listId, { items })` | Bulk-add. Returns `{ added, errors }`. Duplicates are silently skipped. | | `removeListItem(listId, listItemId)` | Remove one list item by its ID (from `getRecipients()` response). Returns `{ message }`. | | `bulkDeleteRecipients(listId, { ids })` | Remove many list items at once by their IDs. Returns `{ deletedCount }`. | | `importFromCsv(listId, file, options?)` | Import a CSV `File` or `Blob` into an existing list (`multipart/form-data`). `options.filename` overrides the multipart filename. Returns `{ added, totalItems, totalFailed, failed }`. Node consumers holding a `Buffer` should use [`importFromCsvBuffer`](#node-entrypoint) from the `./node` subpath. | | `createDistributionList({ name, recipients, notes? })` | Create a list with recipients in one step. Returns `{ list, added, errors }`. | | `importCsvDistribution({ name, csvData, ... })` | Create a list from a CSV string in one step. Returns `{ list, added, errors }`. | | `validateDistributionRecipients({ recipients })` | Validate recipients without saving a list — checks addresses/amounts and reports duplicates. Returns `{ valid, warnings }`. | --- ## ProjectService ```typescript class ProjectService { getInfo(): Promise getMembers(): Promise } ``` | Method | Description | |--------|-------------| | `getInfo()` | Full project object. Use as a health check — a successful response confirms API key and base URL are correctly configured. | | `getMembers()` | Flat list of project members with role (`OWNER` > `ADMIN` > `MANAGER` > `VIEWER`). | --- ## Common types ### Pagination ```typescript interface PaginationParams { page?: number // 1-indexed limit?: number // 1..100; capped server-side orderBy?: string // field name orderDir?: 'ASC' | 'DESC' search?: string // free-text search where supported } interface PaginatedResponse { data: T[] meta: { page: number limit: number total: number totalPages: number } } ``` Query-parameter aliases that extend `PaginationParams`: `ListsQueryParams`, `RecipientsQueryParams`, `DistributionsQueryParams`, `TransactionsQueryParams`. ### Address and transaction enums ```typescript type AddressType = 'EVM' | 'SOLANA' | 'TRON' | 'MOVE_EVM' | 'TON' type TransactionStatus = 'PENDING' | 'SUBMITTED' | 'CONFIRMED' | 'FAILED' ``` Only `EVM` is fully supported end-to-end today; the others are present in the schema but marked "coming soon" across the platform. ```typescript interface MessageResponse { message: string } ``` --- ## Distribution types ```typescript type DistributionStatus = | 'DRAFT' | 'PREPARED' | 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'PARTIALLY_COMPLETED' | 'FAILED' | 'CANCELLED' interface Distribution { id: string projectId: string listId?: string | null name: string notes?: string | null chainId: number tokenAddress: string tokenSymbol: string status: DistributionStatus totalRecipients?: number | null totalAmount?: string | null // human-readable token units customerFee?: string | null feeData?: DistributionFeeData | null // estimatedGas, estimatedCost, gasPrice, maxFeePerGas, maxPriorityFeePerGas totalInUSDT?: string | null account?: string | null idempotencyKey?: string | null createdBy?: string | null createdByApiKeyId?: string | null createdAt: string // ISO timestamp updatedAt: string executedAt?: string | null completedAt?: string | null } interface CreateDistributeRequest { recipients?: Array<[address: string, amount: string]> csv?: string // mutually exclusive with recipients chainId: number tokenAddress: string // 0x0 for native currency tokenSymbol: string account?: string idempotencyKey?: string } interface CreateDistributionRequest { // draft create name: string notes?: string chainId: number tokenAddress: string tokenSymbol: string listId?: string // mutually exclusive with recipients recipients?: RecipientDto[] account?: string isDeflationary?: boolean strategy?: string idempotencyKey?: string } interface UpdateDistributionRequest { // draft update name?: string notes?: string chainId?: number tokenAddress?: string tokenSymbol?: string account?: string isDeflationary?: boolean strategy?: string } interface UpdateDistributionRecipientsRequest { recipients: RecipientDto[] } interface PrepareDistributionRequest { account?: string rpcUrl?: string // custom RPC endpoint } interface RecipientDto { address: string amount: string // human-readable, NEVER number label?: string tags?: string[] } interface DistributeResult { distribution: Distribution calldata: { transactions: TransactionCalldata[] summary: { totalRecipients: number totalAmount: string estimatedGas: string estimatedCost: string } } } interface TransactionCalldata { to: string data: string value: string // native value in hex gasLimit: string recipientAddress?: string amount?: string } interface DistributionStats { total: number pending: number submitted: number confirmed: number failed: number } interface DistributionTransaction { id: string distributionId: string batchIndex: number contractAddress?: string | null value?: string | null recipientCount?: number | null txHash?: string | null status: TransactionStatus submittedAt?: string | null confirmedAt?: string | null } ``` --- ## Recipient list types ```typescript interface RecipientList { id: string projectId: string name: string notes?: string | null totalItems: number createdBy?: string | null createdByApiKeyId?: string | null createdAt: string updatedAt: string } interface CreateRecipientListRequest { name: string; notes?: string } interface UpdateRecipientListRequest { name?: string; notes?: string | null } interface Recipient { id: string address: string addressType: AddressType label?: string | null metadata?: Record | null createdAt: string } interface ListItem { id: string listId: string recipientId: string tags: string[] amount?: string | null createdAt: string recipient?: Recipient | null } interface AddRecipientRequest { address: string // ENS name accepted for EVM addressType: AddressType amount: string // must be > 0 label?: string metadata?: Record tags?: string[] } interface AddRecipientResult { recipient: Recipient listItem: ListItem } interface AddRecipientsBulkRequest { items: AddRecipientRequest[] } interface AddRecipientsBulkResult { added: number errors: string[] // validation errors for failed rows } interface CreateDistributionListRequest { name: string notes?: string recipients: RecipientDto[] // DistributionRecipientDto shape } interface DistributionListCreateResult { list: RecipientList added: number errors: string[] } interface ImportCsvDistributionRequest { name: string notes?: string csvData: string delimiter?: string // default ',' skipRows?: number // default 0 hasHeader?: boolean // default true } interface BulkDeleteRecipientsRequest { ids: string[] // list item IDs to delete } interface BulkDeleteRecipientsResult { deletedCount: number // number of list items deleted } interface ValidateDistributionRecipient { address?: string // missing/malformed values surface in warnings amount?: string // human-readable; non-positive values flagged } interface ValidateDistributionRecipientsRequest { recipients: ValidateDistributionRecipient[] } interface ValidateDistributionRecipientsResult { valid: boolean warnings: string[] // includes duplicate-address warnings } interface ImportFromCsvOptions { filename?: string // overrides the multipart filename } interface ImportListResponse { added: number // actually imported totalItems: number // total recipients in list after import totalFailed: number failed: ImportListFailedRow[] // first 100 failed rows } interface ImportListFailedRow { row: number // 1-based address?: string reason: ImportListFailedReason } type ImportListFailedReason = | 'invalid_chainId' | 'invalid_address' | 'missing_amount' | 'invalid_amount' | 'metadata_too_long' | 'invalid_metadata_json' | 'unknown_error' ``` --- ## Project types ```typescript interface Project { id: string name: string slug: string description?: string | null isDefault: boolean userId?: string | null createdAt: string updatedAt: string memberships: ProjectMember[] apiKeys: ApiKey[] } interface ProjectMember { id: string projectId: string userId: string role: 'OWNER' | 'ADMIN' | 'MANAGER' | 'VIEWER' createdAt: string user: { id: string email?: string | null displayName?: string | null } } interface ApiKey { id: string projectId: string name: string scopes: string[] status: 'ACTIVE' | 'INACTIVE' lastUsedAt?: string | null usageCount: number createdAt: string updatedAt: string } interface CreateApiKeyRequest { name: string; scopes: string[] } interface CreateApiKeyResult { apiKey: string; apiKeyEntity: ApiKey } // apiKey shown once interface UpdateApiKeyRequest { name?: string; scopes?: string[] } ``` --- ## Catalog types ```typescript interface Chain { chainId: number name: string addressType: AddressType rpcUrl: string explorerUrl: string nativeSymbol: string multisenderContractAddress?: string | null changeReceiverContractAddress?: string | null blockGasLimit?: number | null logoUri?: string | null rpcUrls?: string[] | null isSupportEstimateFees?: boolean createdAt: string updatedAt: string } interface Token { id: string chainId: number address: string symbol: string decimals: number name: string logoUri?: string | null createdAt: string updatedAt: string } ``` --- ## Approve calldata types ```typescript interface ApproveCalldataRequest { chainId: number tokenAddress: string amount: string // wei string or literal 'max' } interface ApproveCalldata { to: string // token contract address data: string // encoded approve(spender, amount) value: string // always '0x0' — no native value spender: string // multisender contract address amount: string // wei } ``` --- ## CSV utility types ```typescript interface CsvParseOptions { delimiter?: string // default ',' skipRows?: number // default 0 hasHeader?: boolean // default true } ``` `ImportListResponse`, `ImportListFailedRow`, and `ImportListFailedReason` are defined in [Recipient list types](#recipient-list-types). --- ## Error classes All errors extend `MultisenderError`, which extends the native `Error`. ```typescript class MultisenderError extends Error { name: 'MultisenderError' constructor(message: string) } class ApiError extends MultisenderError { name: 'ApiError' status: number body: unknown constructor(status: number, body: unknown, message?: string) } class NetworkError extends MultisenderError { name: 'NetworkError' cause?: Error constructor(message: string, cause?: Error) } class ValidationError extends MultisenderError { name: 'ValidationError' fields?: Record constructor(message: string, fields?: Record) } class TimeoutError extends MultisenderError { name: 'TimeoutError' constructor(message?: string) // defaults to 'Request timeout' } ``` Error mapping happens inside `src/core/generated-transport.ts` in the `executeGenerated()` helper, which wraps every raw OpenAPI operation and converts fetch/HTTP failures into the typed classes above before propagating them. ### Error handling ```typescript import { ApiError, NetworkError, ValidationError, TimeoutError } from '@multisender.app/multisender-sdk' try { await sdk.distributions.distribute({ /* ... */ }) } catch (error) { if (error instanceof ValidationError) { console.error('Invalid input:', error.fields) // per-field errors } else if (error instanceof ApiError) { console.error(`API ${error.status}:`, error.body) // server rejected } else if (error instanceof NetworkError) { console.error('Network problem:', error.cause) } else if (error instanceof TimeoutError) { console.error('Request timed out — retry later') } else { throw error // unknown } } ``` Common `ApiError` statuses you should handle: - `400` — validation failed on the server (invalid address, amount not > 0, etc.) - `401` — missing or invalid API key - `403` — API key lacks the required scope - `404` — distribution or list not found - `409` — state conflict (e.g. trying to update a non-draft distribution) --- ## CSV helpers ```typescript parseCsv(csvData: string, options?: CsvParseOptions): [string, string][] toCsv(recipients: [string, string][], includeHeader?: boolean): string validateCsv(csvData: string, options?: CsvParseOptions): boolean ``` - **`parseCsv`** throws `ValidationError` on empty input or malformed rows. Output tuples are `[address, amount]`. - **`toCsv`** is the inverse. When `includeHeader: true` it prepends `address,amount`. - **`validateCsv`** does not throw — it returns `false` on any problem. Use when you want to pre-check input. CSV format (default options): ```csv address,amount 0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6,100 0x1234567890123456789012345678901234567890,200 ``` --- ## Node entrypoint Node-only CSV helpers that accept a `Buffer` / `Uint8Array` / `ArrayBuffer`. They live on the `./node` subpath because they import from `node:buffer`; the main entrypoint stays browser-safe. ```typescript import { createCsvBlobFromBuffer, importFromCsvBuffer } from '@multisender.app/multisender-sdk/node' import type { CsvUploadBuffer, CreateCsvBlobFromBufferOptions } from '@multisender.app/multisender-sdk/node' type CsvUploadBuffer = Buffer | Uint8Array | ArrayBuffer interface CreateCsvBlobFromBufferOptions { type?: string } createCsvBlobFromBuffer(value: CsvUploadBuffer, options?: CreateCsvBlobFromBufferOptions): Blob importFromCsvBuffer( lists: Pick, listId: string, value: CsvUploadBuffer, options?: ImportFromCsvOptions & CreateCsvBlobFromBufferOptions, ): Promise ``` - **`createCsvBlobFromBuffer`** wraps the buffer in a `Blob`. Pass `options.type` to override the MIME type. - **`importFromCsvBuffer`** is the one-shot helper: pass any object that exposes `importFromCsv` (the SDK's `lists` service qualifies), the target `listId`, and the buffer. Returns the same `ImportListResponse` shape as `lists.importFromCsv`. Node example: ```typescript import { readFile } from 'node:fs/promises' import { Multisender } from '@multisender.app/multisender-sdk' import { importFromCsvBuffer } from '@multisender.app/multisender-sdk/node' const client = new Multisender({ apiKey: process.env.MS_API_KEY! }) const csv = await readFile('./recipients.csv') const result = await importFromCsvBuffer(client.lists, listId, csv, { filename: 'recipients.csv' }) ``` --- ## Validation helpers ```typescript validateEthereumAddress(address: string): boolean // no-throw validatePositiveNumber(value: string | number): boolean // no-throw validateChainId(chainId: number): void // throws ValidationError validateTokenAddress(address: string): void // throws ValidationError validateApiKey(apiKey: string): void // throws ValidationError (empty or < 10 chars) validateRecipients(recipients: [string, string][]): void // throws ValidationError ``` Use the no-throw variants (`validateEthereumAddress`, `validatePositiveNumber`) for input filtering. Use the throwing variants (`validateChainId`, etc.) for fail-fast checks before submitting to the API. --- ## Distribution lifecycle ``` DRAFT ──prepare──▶ PREPARED ──execute──▶ IN_PROGRESS ──auto──▶ COMPLETED │ │ │ │ │ │ │ └─▶ PARTIALLY_COMPLETED │ │ │ └─▶ FAILED ▼ ▼ ▼ CANCELLED CANCELLED CANCELLED (admin only) ``` | Transition | Trigger | Constraint | |------------|---------|------------| | `DRAFT → PREPARED` | `prepare(id, ...)` | Must have ≥ 1 recipient | | `DRAFT → CANCELLED` | `cancel(id)` | — | | `PREPARED → IN_PROGRESS` | Dashboard `execute()` or manual submission | Sets `executedAt` | | `PREPARED → CANCELLED` | `cancel(id)` | — | | `IN_PROGRESS → COMPLETED` | Automatic when all batch txs confirmed | Sets `completedAt` | | `IN_PROGRESS → PARTIALLY_COMPLETED` | Some batches confirmed, some failed | Sets `completedAt` | | `IN_PROGRESS → FAILED` | All batches failed | Sets `completedAt` | | `IN_PROGRESS → CANCELLED` | Admin only (`cancel(id)`) | Cannot cancel from `COMPLETED` | **Important rules:** - Only `DRAFT` distributions can have their `recipients` replaced (`replaceRecipients`) or metadata updated (`updateDraft`). - `prepare()` deletes any previously generated transactions for the draft before generating new ones. - `idempotencyKey` on `createDraft`/`distribute` returns the existing distribution when seen a second time, preventing duplicates on retry. --- ## Flows ### Single-step flow (recommended for SDK integrations) Use when you are creating a one-off distribution programmatically and don't need user review. ```typescript const result = await sdk.distributions.distribute({ chainId: 1, tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', tokenSymbol: 'USDC', recipients: [['0xabc...', '100'], ['0xdef...', '200']], account: '0xYourWallet...', idempotencyKey: `airdrop-${batchId}`, }) for (const tx of result.calldata.transactions) { await wallet.sendTransaction({ to: tx.to, data: tx.data, value: tx.value, gasLimit: tx.gasLimit }) } ``` Only inline recipients (`recipients` or `csv`) are accepted. `listId` is **not** supported by `distribute()` — use the two-step flow for lists. ### Two-step flow (for review before execution) Use when you want to show the distribution in your UI, let the user edit recipients, and only then prepare. ```typescript // 1. Create a draft const draft = await sdk.distributions.createDraft({ name: 'Q1 Airdrop', chainId: 1, tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', tokenSymbol: 'USDC', listId: existingListId, // OR recipients: [...] }) // 2. Optionally edit metadata or recipients await sdk.distributions.updateDraft(draft.id, { name: 'Q1 Airdrop — revised' }) await sdk.distributions.replaceRecipients(draft.id, { recipients: [ /* ... */ ] }) // 3. Prepare when ready const prepared = await sdk.distributions.prepare(draft.id, { account: '0xYourWallet...' }) // 4. Fetch calldata + sign + submit — the `prepared` response includes calldata for execution ``` --- ## ERC-20 approval Before distributing ERC-20 tokens, the wallet needs to `approve(spender, amount)` the Multisender contract. Native tokens (ETH, POL, BNB, …) do **not** need this step — skip it when `tokenAddress === '0x0000000000000000000000000000000000000000'`. There are two ways to get approval calldata: ```typescript // Manual — you know the exact amount you want to approve const manual = await sdk.distributions.getApproveCalldata({ chainId: 1, tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', amount: 'max', // or a specific wei string like '100000000' }) // Automatic — compute the exact amount from an existing distribution const auto = await sdk.distributions.getApproveCalldataForDistribution(distributionId) ``` Both return an `ApproveCalldata` object: ```typescript { to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // token contract data: '0x095ea7b3...', // approve(spender, amount) encoded value: '0x0', // native value spender: '0x5B3E5F6B...', // multisender contract amount: '100000000' // wei } ``` Submit this transaction from the wallet that owns the tokens, wait for one confirmation, then call `distribute` / `prepare`. --- ## Idempotency Pass an `idempotencyKey` to `createDraft()` or `distribute()` to make retries safe. If the server sees the same key twice within the retention window, it returns the existing distribution instead of creating a duplicate. ```typescript const key = `payroll-${year}-${month}-${teamId}` const result = await sdk.distributions.distribute({ chainId: 1, tokenAddress: '0x...', tokenSymbol: 'USDC', recipients: [...], idempotencyKey: key, }) // Calling again with the same key returns the previous distribution — no duplicates. ``` Rules of thumb: - Derive the key from domain facts, not timestamps or random UUIDs (otherwise retries produce new keys). - Keep the key stable across the entire retry window on your side — days, not minutes. - Combine with exponential backoff for `NetworkError` / `TimeoutError` / `ApiError(5xx)`. --- ## Pagination List methods accept `PaginationParams` and return `PaginatedResponse`. ```typescript const page1 = await sdk.distributions.list({ page: 1, limit: 50 }) // page1.data — Distribution[] // page1.meta — { page, limit, total, totalPages } ``` - **`page`** is 1-indexed. - **`limit`** is capped at 100 by the server; anything larger is rejected with `ApiError(400)`. - **`orderBy`** accepts field names like `createdAt`, `name`, `status` depending on the method. - **`orderDir`** is `'ASC' | 'DESC'`. - **`search`** is a free-text filter applied to names where supported. Aliases: `ListsQueryParams`, `RecipientsQueryParams`, `DistributionsQueryParams`, `TransactionsQueryParams` — all extend `PaginationParams`. --- ## Examples ### Basic Node.js distribution ```typescript import { Multisender } from '@multisender.app/multisender-sdk' async function main() { const sdk = new Multisender({ apiKey: process.env.MULTISENDER_API_KEY! }) const result = await sdk.distributions.distribute({ chainId: 1, tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', tokenSymbol: 'USDC', recipients: [ ['0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6', '100.5'], ['0x1234567890123456789012345678901234567890', '50.25'], ], }) console.log('Distribution created:', result.distribution.id) console.log('Transactions to sign:', result.calldata.transactions.length) const stats = await sdk.distributions.getStats(result.distribution.id) console.log('Stats:', stats) } main().catch((err) => { console.error(err) process.exit(1) }) ``` ### CSV-driven distribution ```typescript import { Multisender, parseCsv } from '@multisender.app/multisender-sdk' import { readFileSync } from 'fs' const sdk = new Multisender({ apiKey: process.env.MULTISENDER_API_KEY! }) const csv = readFileSync('./recipients.csv', 'utf-8') const recipients = parseCsv(csv, { hasHeader: true, delimiter: ',' }) await sdk.distributions.distribute({ chainId: 137, // Polygon tokenAddress: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', // USDC on Polygon tokenSymbol: 'USDC', recipients, }) ``` Expected CSV: ```csv address,amount 0xabc...,100 0xdef...,200 ``` ### Managing recipient lists ```typescript const list = await sdk.lists.create({ name: 'VIP Customers', notes: 'High-value customers for token airdrop', }) const result = await sdk.lists.addRecipientsBulk(list.id, { items: [ { address: '0x742d35Cc...', addressType: 'EVM', amount: '100', label: 'Customer A', tags: ['vip'] }, { address: '0x12345678...', addressType: 'EVM', amount: '200', label: 'Customer B', tags: ['vip', 'early'] }, ], }) console.log(`Added ${result.added}, errors: ${result.errors.length}`) const recipients = await sdk.lists.getRecipients(list.id, { page: 1, limit: 50 }) console.log(`List has ${recipients.meta.total} recipients`) ``` ### Native token (ETH) distribution ```typescript await sdk.distributions.distribute({ chainId: 1, tokenAddress: '0x0000000000000000000000000000000000000000', tokenSymbol: 'ETH', recipients: [ ['0xabc...', '0.5'], ['0xdef...', '0.25'], ], account: '0xYourWallet...', }) ``` No `approve()` step is needed. The generated transactions carry the required native `value` per batch. --- ## CLI The SDK ships a CLI binary named `multisender`. Install the package globally to expose it on your `PATH`: ```bash npm install -g @multisender.app/multisender-sdk ``` Every command accepts `-k, --api-key ` (required) and `--base-url ` (optional override of the API endpoint). ### `multisender distribute` Create and execute a distribution from a CSV file. ```bash multisender distribute \ --api-key $MULTISENDER_API_KEY \ --chain 1 \ --token 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \ --symbol USDC \ --recipients ./recipients.csv ``` Flags: | Flag | Required | Description | |------|----------|-------------| | `-c, --chain ` | yes | Chain ID (1, 10, 137, 8453, 42161, …) | | `-t, --token
` | yes | Token contract | | `-s, --symbol ` | yes | Token symbol | | `-r, --recipients ` | yes | CSV path | | `-a, --account
` | no | Sender wallet | ### `multisender lists ` | Sub | Usage | |-----|-------| | `list` | `multisender lists list --api-key $KEY --page 1 --limit 20` | | `create` | `multisender lists create --api-key $KEY --name "My List" [--notes "..."]` | | `get ` | `multisender lists get lst_abc123 --api-key $KEY` | | `delete ` | `multisender lists delete lst_abc123 --api-key $KEY` | ### `multisender distributions ` | Sub | Usage | |-----|-------| | `list` | `multisender distributions list --api-key $KEY --page 1 --limit 20` | | `get ` | `multisender distributions get dist_abc123 --api-key $KEY` | | `stats ` | `multisender distributions stats dist_abc123 --api-key $KEY` | | `cancel ` | `multisender distributions cancel dist_abc123 --api-key $KEY` | ### Local development against a local API ```bash multisender lists list --api-key $KEY --base-url http://localhost:3000 multisender distributions list --api-key $KEY --base-url http://localhost:3000 ``` --- ## Supported chains | Chain | Chain ID | |-------|----------| | Ethereum | 1 | | Optimism | 10 | | Polygon | 137 | | Base | 8453 | | Arbitrum | 42161 | Call `sdk.catalogs.getChains()` at runtime for the full current list — the backend adds chains over time. Each `Chain` response includes `multisenderContractAddress` (the spender for ERC-20 `approve()`), `rpcUrl`, `explorerUrl`, and `nativeSymbol`. --- ## Links - [NPM Package](https://www.npmjs.com/package/@multisender.app/multisender-sdk) - [API Documentation](https://docs.multisender.app) - [GitHub Repository](https://github.com/multisender/sdk) - [Issue Tracker](https://github.com/multisender/sdk/issues) - [Support email](mailto:team@multisender.app) --- ## License MIT.