Build TypeScript swap flows

This guide contains complete exact-input flows for both swap directions:

  • Buy: spend an exact amount of SOL and receive at least the quoted token limit.
  • Sell: send an exact number of token base units and receive at least the quoted SOL limit.

Both directions use the same market loader, transaction builder, swap instruction, account graph, referral configuration, and submission path. Their quote math remains separate because buys charge fees on SOL input and may use launch protection, while sells charge fees on SOL output and enforce circulation and vault constraints.

Before this page:

  1. Add tribe-market.ts from Read market state onchain.
  2. Add tribe-swap.ts from Build a swap transaction.
  3. Copy the swap IDL as tribe-swap-idl.json.

AI agents can consume this guide as raw Markdown, or load it together with every dependency from the complete documentation.

Buy: SOL to token

All SOL amounts are lamports. Tribe token amounts use 9 decimal places. Use bigint throughout; JavaScript number cannot safely represent the onchain curve values.

An exact-input buy applies fees to the SOL input, sends the remaining SOL through the curve, and uses the user's slippage setting to produce the minimum token limit passed to the program.

During a token's first minute, a non-creator buy can carry a decaying anti-sniping surcharge. The calculation below reads Solana's Clock sysvar and uses a two-second earlier time only for the fee-sensitive slippage floor, making the quote conservative while the surcharge decays.

Create tribe-buy.ts:

import { Connection, PublicKey } from "@solana/web3.js";
import { activeFeeRates, loadOnchainMarket, readChainUnixTime, type FeeRates, type TribeMarket } from "./tribe-market";
import { buildExactInSwap, submitSwap, type ExactInSwapQuote } from "./tribe-swap";

const FEE_DENOMINATOR = 1_000_000n;
const Q128 = 1n << 128n;
const MIN_SQRT_PRICE = 4_295_048_016n;
const MAX_SQRT_PRICE = 79_226_673_521_066_979_257_578_248_091n;
const MIN_TRADE_LAMPORTS = 10_000n;
const U64_MAX = 18_446_744_073_709_551_615n;

function ceilDiv(numerator: bigint, denominator: bigint): bigint {
  return (numerator + denominator - 1n) / denominator;
}

export function resolveBuyRates(market: TribeMarket, trader: PublicKey, chainNow: bigint): FeeRates {
  const tier = activeFeeRates(market);
  if (trader.equals(market.creator)) return tier;
  if (
    market.antiSnipeUntil === 0n ||
    market.antiSnipeUntil <= market.createdAt ||
    chainNow >= market.antiSnipeUntil ||
    market.antiSnipeStartRate <= tier.total
  ) {
    return tier;
  }

  const effectiveTotal =
    chainNow <= market.createdAt
      ? market.antiSnipeStartRate
      : market.antiSnipeStartRate -
        ((market.antiSnipeStartRate - tier.total) * (chainNow - market.createdAt)) /
          (market.antiSnipeUntil - market.createdAt);
  const surcharge = effectiveTotal > tier.total ? effectiveTotal - tier.total : 0n;
  return { total: tier.total + surcharge, creator: tier.creator + surcharge, lp: tier.lp };
}

export function quoteExactInBuy(
  market: TribeMarket,
  amountIn: bigint,
  slippageBps: number,
  rates: FeeRates,
): ExactInSwapQuote {
  if (amountIn < MIN_TRADE_LAMPORTS || amountIn > U64_MAX) throw new Error("Invalid buy amount");
  if (!Number.isInteger(slippageBps) || slippageBps < 1 || slippageBps >= 10_000) {
    throw new Error("slippageBps must be an integer from 1 through 9999");
  }
  if (market.liquidity <= 0n) throw new Error("InsufficientLiquidity");

  const totalFee = ceilDiv(amountIn * rates.total, FEE_DENOMINATOR);
  const creatorFee = (amountIn * rates.creator) / FEE_DENOMINATOR;
  const lpFee =
    market.lpLiquidity === 0n || rates.lp === 0n
      ? 0n
      : ceilDiv(amountIn * rates.lp * market.lpLiquidity, market.liquidity * FEE_DENOMINATOR);
  const grossProtocolFee = totalFee - creatorFee - lpFee;
  if (grossProtocolFee < 0n) throw new Error("Invalid fee schedule");

  const netSol = amountIn - totalFee;
  if (netSol <= 0n) throw new Error("TradeTooSmall");

  const denominator = market.liquidity + netSol * market.sqrtPriceX64;
  const newSqrtPriceX64 = ceilDiv(market.liquidity * market.sqrtPriceX64, denominator);
  if (newSqrtPriceX64 <= MIN_SQRT_PRICE || newSqrtPriceX64 >= MAX_SQRT_PRICE) {
    throw new Error("PriceOutOfRange");
  }

  const amountOut = (market.liquidity * (market.sqrtPriceX64 - newSqrtPriceX64)) / Q128;
  if (amountOut <= 0n || amountOut > market.tokenVaultAmount) throw new Error("InsufficientLiquidity");

  const limit = (amountOut * (10_000n - BigInt(slippageBps))) / 10_000n;
  if (limit <= 0n) throw new Error("Slippage floor is zero");

  return {
    amountIn,
    amountOut,
    limit,
    creatorFee,
    grossProtocolFee,
    lpFee,
    totalFee,
    newSqrtPriceX64,
  };
}

type Wallet = Parameters<typeof buildExactInSwap>[0]["wallet"];

export async function executeExactInBuy(input: {
  connection: Connection;
  wallet: Wallet;
  mint: PublicKey;
  trader: PublicKey;
  referral: PublicKey;
  amountIn: bigint;
  slippageBps: number;
}): Promise<string> {
  const market = await loadOnchainMarket(input.connection, input.mint);
  const chainNow = await readChainUnixTime(input.connection);
  const rates = resolveBuyRates(market, input.trader, chainNow - 2n);
  const quote = quoteExactInBuy(market, input.amountIn, input.slippageBps, rates);

  const built = await buildExactInSwap({
    connection: input.connection,
    wallet: input.wallet,
    market,
    trader: input.trader,
    referral: input.referral,
    quote,
    isBuy: true,
  });
  return submitSwap({ connection: input.connection, wallet: input.wallet, ...built });
}

executeExactInBuy reloads current state, quotes, builds, simulates, asks the connected wallet to sign, submits, and confirms at confirmed commitment. Production clients must collect amountIn and slippageBps from the user rather than hardcoding them.

Buy gate behavior

The Token account exposes permissionlessBuyUnlocked:

  1. When it is true, the monotonic buy gate is already open.
  2. When it is false, the next buy can still open it if the onchain age or threshold rule has become eligible.
  3. The quote remains valid curve math in either case. The unsigned simulation in submitSwap is the authoritative gate check and happens before wallet signing.
  4. If simulation returns a gate error, show it and wait for eligibility. Do not ask the user to sign and do not replace the limit.

Sells never use this buy gate.

Sell: token to SOL

Tribe token amounts use 9 decimal places, so one token is 1_000_000_000 base units. SOL output is in lamports. Keep all arithmetic in bigint.

A sell uses the same swap instruction as a buy with these arguments:

ArgumentSell value
amountExact token base units to sell
limitMinimum net SOL lamports out
is_buyfalse
swap_mode0 for ExactIn

Sells are available from launch and do not use the permissionless-buy gate or anti-sniping surcharge.

An exact-input sell:

  1. Enforces the token's onchain virtual-circulation floor.
  2. Moves the curve price using the token input.
  3. Calculates gross SOL output with floor rounding.
  4. Deducts the active creator, LP, and gross protocol fees from that SOL output.
  5. Checks the SOL vault can fund the full gross output while retaining rent and reserved LP fees.
  6. Applies the user's slippage setting to net SOL output to produce limit.

Create tribe-sell.ts:

import { Connection, PublicKey } from "@solana/web3.js";
import { activeFeeRates, loadOnchainMarket, type TribeMarket } from "./tribe-market";
import { buildExactInSwap, submitSwap, type ExactInSwapQuote } from "./tribe-swap";

const FEE_DENOMINATOR = 1_000_000n;
const MAX_SQRT_PRICE = 79_226_673_521_066_979_257_578_248_091n;
const MIN_TRADE_LAMPORTS = 10_000n;
const TOTAL_SUPPLY = 1_000_000_000_000_000_000n;
const U64_MAX = 18_446_744_073_709_551_615n;

function ceilDiv(numerator: bigint, denominator: bigint): bigint {
  return (numerator + denominator - 1n) / denominator;
}

function maxSellableTokens(market: TribeMarket): bigint {
  if (market.tokenVaultAmount > TOTAL_SUPPLY) throw new Error("Invalid token vault balance");
  const externallyCirculating = TOTAL_SUPPLY - market.tokenVaultAmount;
  return externallyCirculating > market.virtualTokenAmount ? externallyCirculating - market.virtualTokenAmount : 0n;
}

export function quoteExactInSell(market: TribeMarket, amountIn: bigint, slippageBps: number): ExactInSwapQuote {
  if (amountIn <= 0n || amountIn > U64_MAX) throw new Error("Invalid sell amount");
  if (!Number.isInteger(slippageBps) || slippageBps < 1 || slippageBps >= 10_000) {
    throw new Error("slippageBps must be an integer from 1 through 9999");
  }
  if (amountIn > maxSellableTokens(market)) throw new Error("InsufficientLiquidity");
  if (market.liquidity <= 0n) throw new Error("InsufficientLiquidity");

  const newSqrtPriceX64 = market.sqrtPriceX64 + (amountIn << 128n) / market.liquidity;
  if (newSqrtPriceX64 <= market.sqrtPriceX64 || newSqrtPriceX64 >= MAX_SQRT_PRICE) {
    throw new Error("PriceOutOfRange");
  }

  const grossSolOut =
    (market.liquidity * (newSqrtPriceX64 - market.sqrtPriceX64)) / (market.sqrtPriceX64 * newSqrtPriceX64);
  if (grossSolOut < MIN_TRADE_LAMPORTS || grossSolOut > U64_MAX) throw new Error("TradeTooSmall");

  const rates = activeFeeRates(market);
  const totalFee = ceilDiv(grossSolOut * rates.total, FEE_DENOMINATOR);
  const creatorFee = (grossSolOut * rates.creator) / FEE_DENOMINATOR;
  const lpFee =
    market.lpLiquidity === 0n || rates.lp === 0n
      ? 0n
      : ceilDiv(grossSolOut * rates.lp * market.lpLiquidity, market.liquidity * FEE_DENOMINATOR);
  const grossProtocolFee = totalFee - creatorFee - lpFee;
  if (grossProtocolFee < 0n) throw new Error("Invalid fee schedule");

  const amountOut = grossSolOut - totalFee;
  if (amountOut <= 0n) throw new Error("TradeTooSmall");

  const protectedLamports = market.rentExemptLamports + market.lpFeeReservedSol;
  const spendableLamports =
    market.solVaultLamports > protectedLamports ? market.solVaultLamports - protectedLamports : 0n;
  if (grossSolOut > spendableLamports) throw new Error("InsufficientVaultBalance");

  const limit = (amountOut * (10_000n - BigInt(slippageBps))) / 10_000n;
  if (limit <= 0n) throw new Error("Slippage floor is zero");

  return {
    amountIn,
    amountOut,
    limit,
    creatorFee,
    grossProtocolFee,
    lpFee,
    totalFee,
    newSqrtPriceX64,
  };
}

type Wallet = Parameters<typeof buildExactInSwap>[0]["wallet"];

export async function executeExactInSell(input: {
  connection: Connection;
  wallet: Wallet;
  mint: PublicKey;
  trader: PublicKey;
  referral: PublicKey;
  amountIn: bigint;
  slippageBps: number;
}): Promise<string> {
  const market = await loadOnchainMarket(input.connection, input.mint);
  const quote = quoteExactInSell(market, input.amountIn, input.slippageBps);

  const built = await buildExactInSwap({
    connection: input.connection,
    wallet: input.wallet,
    market,
    trader: input.trader,
    referral: input.referral,
    quote,
    isBuy: false,
  });
  return submitSwap({ connection: input.connection, wallet: input.wallet, ...built });
}

executeExactInSell reloads current state, quotes, confirms the trader's Token-2022 balance, builds, simulates, asks the connected wallet to sign, submits, and confirms at confirmed commitment.

Why the sell checks differ

  1. No associated-account creation: a seller must already own the Token-2022 account containing the tokens. The shared builder verifies it instead of creating an empty account.
  2. Virtual-token floor: the program prevents sells from reducing externally circulating supply below the amount distributed at launch. maxSellableTokens mirrors that check from the live token-vault balance.
  3. Gross-output vault check: fees and trader proceeds all originate in the SOL vault. The full gross SOL output must be available after rent and already-reserved LP fees.
  4. Fees on output: the curve produces gross SOL first. Total, creator, LP, and protocol fees are then calculated from that gross amount, and the trader's amountOut is the remainder.
  5. No buy-only rules: sell quotes do not read the Clock, permissionless-buy latch, or anti-sniping settings.

Referral payout

Pass your rent-funded integration wallet as referral. The shared builder passes protocol_vault as referrer_tier_account, applying the default 15% share of Tribe's gross protocol fee in either direction. The reward splits the existing protocol fee and does not add a new fee to the trader.

Integration checklist

  1. Reload the Token account, selected fee schedule, token vault, and SOL vault immediately before quoting.
  2. Use Token-2022 for the mint, vault, and trader associated account.
  3. Keep all arithmetic in bigint and mirror the documented floor and ceiling directions.
  4. Use the user's nonzero slippage setting to calculate limit.
  5. Prefund the referral wallet and keep it rent-exempt.
  6. Add a current nonzero priority fee and a compute-unit limit.
  7. Simulate before wallet signing and reload state after any stale-state failure.
  8. For a buy, read the Clock, apply any anti-sniping surcharge, and let simulation enforce the permissionless-buy gate.
  9. For a sell, verify the trader's token balance, enforce the virtual-token floor and SOL-vault capacity, and pass isBuy: false with swapMode: 0.