§8 · Building
The TypeScript SDK
@cohort/sdk reads pods, quotes wraps and unwraps, and builds unsigned v0 transactions for a wallet to sign.
@cohort/sdk is the TypeScript client for the Cohort program. Its main export, createCohortClient, returns an object that reads pods, quotes wraps and unwraps with the program's own integer math, and builds unsigned v0 transactions. Signing and sending stay with your wallet. Amounts are raw base units as bigint; addresses are base58 strings.
Install§
The package lives in the repository's sdk/ directory and isn't published to npm yet. Build it and depend on it by path:
cd sdk
npm install
npm run build # emits sdk/dist (ESM + .d.ts)
# in your project
npm install ../path/to/peasv2/sdk @solana/web3.jsIt depends on @solana/web3.js 1.x, @solana/spl-token 0.4 and @coral-xyz/anchor 0.29, and runs in Node 18+ and in browsers.
Create a client§
import { Connection } from "@solana/web3.js";
import { createCohortClient } from "@cohort/sdk";
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const client = createCohortClient(connection);
// A different deployment: createCohortClient(connection, "<program id>")createCohortClient(connection, programId?) returns a CohortSdkClient. Without a program ID it uses COHORT_PROGRAM_ID, the ID compiled into the program (see §7 Program reference). The devnet deployment uses that same ID; its pods and mints are listed on §4 The pods.
Reading pods§
listPods§
const pods = await client.listPods(); // PodView[], sorted by address
for (const p of pods) console.log(p.symbol, p.supply, p.components.map((c) => c.symbol));Finds every Pod account by its discriminator with getProgramAccounts, then fetches the pod mints, fee vaults, component mints and vaults in batches of 100. Public RPC endpoints may rate-limit getProgramAccounts; use a dedicated RPC for anything busy.
getPod§
const pod = await client.getPod(podAddress);
pod.wrapFeeBps; // 30
pod.pendingFees; // cohorts waiting in the FeeVault, raw
pod.components[0].issuerPowers; // { permanentDelegate, freezeAuthority, pausable, transferHook, scaledUiAmount }
pod.components[0].paused; // live: the mint is paused
pod.components[0].frozen; // live: the pod's vault is frozenA PodView carries the pod's fees and shares, its supply and pending fees, and per component the mint, token program, decimals, recipeUnits, trackedBalance, shortfall, live pause and freeze state, and the issuer powers present on the mint. symbol comes from the mint's Token-2022 metadata, or a shortened address if it has none.
Quotes§
const q = await client.quoteWrap(podAddress, 10_000_000_000n); // 10 cohorts, gross
q.amountsIn; // bigint[], raw component units, indexed like pod.components
q.fee; // cohorts to the FeeVault
q.podOut; // cohorts you receive = podAmount − fee
const u = await client.quoteUnwrap(podAddress, 10_000_000_000n);
u.fee; // cohorts to the FeeVault
u.amountsOut; // bigint[], raw component unitsQuotes replay the program's arithmetic exactly: ceil on the way in, floor on the way out, the fee rounded up. They also apply the seizure check the program runs first, so a seizure that hasn't been reconciled yet is already reflected. The math is exported for offline use as wrapAmounts, unwrapAmounts, feeFor, effectiveTracked and splitFees.
Quotes are raw units. For display, divide by 10 ** decimals, and for a ScaledUiAmount component multiply by its current multiplier. quoteUnwrap throws if the amount exceeds supply.
Building and sending transactions§
Every builder takes TxParams (pod, user, podAmount, slippageBps) or the addresses it needs, and returns an unsigned VersionedTransaction with the user as fee payer and a fresh blockhash. Sign and send it promptly.
buildWrapTx§
const tx = await client.buildWrapTx({ pod: podAddress, user: owner, podAmount: 10_000_000_000n, slippageBps: 50 });
const sig = await wallet.sendTransaction(tx, connection); // wallet-adapter
await connection.confirmTransaction(sig, "confirmed");Sets max_in to each quoted amount plus slippageBps, rounded up, and creates your cohort token account if it doesn't exist. You need every component in your associated token accounts.
buildUnwrapTx§
const tx = await client.buildUnwrapTx({ pod: podAddress, user: owner, podAmount: 4_000_000_000n, slippageBps: 50 });Sets min_out to each quoted amount minus slippageBps, rounded down, and creates any missing component token accounts.
buildUnwrapPartialTx§
const tx = await client.buildUnwrapPartialTx({ pod: podAddress, user: owner, podAmount: 1_000_000_000n, slippageBps: 50, skip: [2] });skip lists component indices to turn into claim tickets. It must not be empty. The SDK picks a random 64-bit nonce and appends the ticket accounts. See §6 Partial unwrap and claim tickets.
listClaims and buildRedeemClaimTx§
const claims = await client.listClaims(owner); // ClaimTicketView[]
const ready = claims.filter((c) => c.redeemable);
const tx = await client.buildRedeemClaimTx(ready[0].address, owner);redeemable is true when the component's mint isn't paused and neither the pod's vault nor your token account is frozen. buildRedeemClaimTx throws if the ticket belongs to someone else.
buildDistributeFeesTx§
const tx = await client.buildDistributeFeesTx(podAddress, payer);Burns the FeeVault after paying the creator and protocol shares. Anyone may send it; it fails with NoFees when the FeeVault is empty, so check pod.pendingFees first. It creates the creator's and treasury's cohort token accounts if they are missing, at the payer's expense.
Signing without a wallet adapter§
import { Keypair } from "@solana/web3.js";
const tx = await client.buildWrapTx({ pod, user: keypair.publicKey.toBase58(), podAmount, slippageBps: 50 });
tx.sign([keypair]);
const sig = await connection.sendTransaction(tx);What the builders take care of§
- Compute budget. Each transaction starts with a compute-unit limit sized to the work: 60,000 plus 25,000 per token transfer or hook call plus 30,000 per account created, capped at 1.4M. No priority fee is added.
- Missing token accounts. Associated token accounts are created with the idempotent instruction, so a race with another transaction is harmless.
- The 64-instruction trace limit. Creating accounts costs inner instructions. If a large pod plus several new accounts would exceed Solana's limit, the builder throws and tells you to create the token accounts first.
Transfer hooks§
When a component mint has a transfer hook enabled, every transfer of it needs the hook program's extra accounts. The builders resolve them for you with addExtraAccountMetasForExecute from @solana/spl-token, for the exact source, destination and amount of each transfer, and pass them to the program with a per-component count in hook_extras. The program forwards them to the token program.
Resolve them at build time and never cache them: an issuer can change what its hook requires. If you build instructions yourself with client.ix (the CohortInstructions class), pass hookExtras as one AccountMeta[] per component, in component order; componentRemainingAccounts shows the layout the program expects.
Lookup tables§
A wrap of a seven-component pod names dozens of accounts, more than a legacy transaction can hold. The program records one address lookup table per pod with set_lookup_table, and the builders compile against it automatically when pod.lookup_table is set.
Anyone can register the table, once. It must be frozen (no authority, so nobody can close or change it) and contain the pod, the pod mint, the FeeVault and every component's mint and vault:
import { AddressLookupTableProgram, PublicKey } from "@solana/web3.js";
import { decodePod, podPda } from "@cohort/sdk";
const podKey = podPda(podMint);
const state = decodePod((await connection.getAccountInfo(podKey))!.data);
const addresses = [podKey, state.podMint, state.feeVault, ...state.components.flatMap((c) => [c.mint, c.vault])];
const slot = await connection.getSlot("finalized");
const [create, lut] = AddressLookupTableProgram.createLookupTable({ authority: payer, payer, recentSlot: slot });
const extend = AddressLookupTableProgram.extendLookupTable({ lookupTable: lut, authority: payer, payer, addresses });
const freeze = AddressLookupTableProgram.freezeLookupTable({ lookupTable: lut, authority: payer });
// Send create + extend, then freeze, then:
const register = await client.ix.setLookupTable({ pod: podKey, lookupTable: lut });The program's tests run every instruction on a ten-component pod with a lookup table in one transaction each, using less than 60% of the compute limit.
Lower-level exports§
| Export | What it is |
|---|---|
CohortSdkClient | The class behind createCohortClient; exposes connection, programId and ix |
CohortInstructions | An instruction builder for every program instruction, with no network calls |
configPda, issuerPda, podPda, claimPda | PDA derivation |
decodePod, decodeClaimTicket, decodeProtocolConfig, DISCRIMINATORS | Account decoding |
parseMint, parseTokenAccount | Token and Token-2022 parsing, including pause, hook and issuer powers |
wrapAmounts, unwrapAmounts, feeFor, effectiveTracked, splitFees, withMaxSlippage, withMinSlippage | The share math |
IDL, COHORT_PROGRAM_ID, POD_DECIMALS, ONE_POD, MAX_FEE_BPS, … | The IDL and constants |
The CohortClient interface in sdk/src/types.ts is the contract between the SDK and the Cohort app; changing it means changing both.