Integrate

Read pools, quote swaps and build transactions with the TypeScript SDK, or work from the account layout directly. Written for wallets, aggregators and apps.

Install#

Shell
npm i @patchbay/sdk @solana/web3.js @solana/spl-token @coral-xyz/anchor

@patchbay/sdk is built on @solana/web3.js v1, @solana/spl-token and @coral-xyz/anchor. Program IDs are exported per cluster and are the same on all of them.

ExportWhat it does
listPools, listListingsEvery pool of the AMM, decoded; the module registry.
Pool, Config, Listing, Module decodersPlus the built-in modules’ per-pool states and the order book.
PDA helpersPool, vault, LP mint, hook authority, listing, module and state addresses.
quoteSwap(pool, states, amountIn, aToB)A pure TypeScript mirror of the on-chain math, dynamic fee decay included.
buildSwapTxWraps and unwraps SOL, creates token accounts, sets compute budget (400k) and priority fee, fills every module slice.
buildAddLiquidityTx, buildRemoveLiquidityTxLiquidity with the module slices for add or remove.
buildCreatePoolTx({ mintA, mintB, feeBps, modules })create_pool plus init_pool_state for each module, in one transaction.
moduleSlice(module, pool, ctx)A module’s slice: program, module, hook authority, then a built-in’s extra accounts.
mineModuleAddress(flags)Grinds a module address that ends in the flags byte.
placeOrder, cancelOrder, claimLimit order book.
twap(state, windowSecs)Mean tick from a TWAP oracle state.

Find pools and quote#

quote.tsTypeScript
import { Connection } from '@solana/web3.js';
import { listPools, quoteSwap } from '@patchbay/sdk';

const connection = new Connection('https://api.devnet.solana.com', 'confirmed');

// decoded Pool accounts; keep this pair's (mints are stored sorted)
const has = (p, mint) => p.mintA.equals(mint) || p.mintB.equals(mint);
const pools = (await listPools(connection)).filter(
  (p) => has(p, SOL_MINT) && has(p, USDC_MINT),
);

// quoteSwap(pool, states, amountIn, aToB) mirrors the on-chain math, dynamic fee
// decay included; `states` are the decoded per-pool states of the pool's modules
const aToB = pool.mintA.equals(SOL_MINT);
const quote = quoteSwap(pool, states, 10_000_000_000n, aToB); // 10 SOL in atoms
  • For pools made of built-in modules, quoteSwap reproduces the program: static fee, dynamic fee override with its decay up to now, curve rounding.
  • Limit order fills run after the user’s trade and do not change what the user receives. TWAP oracle and Lockup never change a swap.
  • For a pool with a module you cannot mirror, especially a third-party module with Returns delta, simulate the transaction and read the output from SwapEvent or the token balance change.

Without the SDK#

A pool address is a pure function of its mints, fee and patch:

pool-address.tsTypeScript
import { PublicKey } from '@solana/web3.js';
import { sha256 } from '@noble/hashes/sha2';

const PATCHBAY = new PublicKey('PbZkjLcF3Ghc6MGndLYZdXDwhbuv8YCuqZeQA8SbRgs');

/** Pool for two mints, a static fee and an ordered list of module accounts. */
export function poolAddress(
  x: PublicKey,
  y: PublicKey,
  feeBps: number,
  modules: PublicKey[],
) {
  const [a, b] = Buffer.compare(x.toBuffer(), y.toBuffer()) < 0 ? [x, y] : [y, x];
  const fee = Buffer.alloc(2);
  fee.writeUInt16LE(feeBps);
  const patchHash = sha256(Buffer.concat(modules.map((m) => m.toBuffer())));
  const seeds = [Buffer.from('pool'), a.toBuffer(), b.toBuffer(), fee, patchHash];
  return PublicKey.findProgramAddressSync(seeds, PATCHBAY)[0];
}

Swap#

swap.tsTypeScript
import { buildSwapTx } from '@patchbay/sdk';

// wraps/unwraps SOL, creates missing token accounts, sets the compute budget
// (400k) and priority fee, fills hook_account_counts and every module's slice
const tx = await buildSwapTx({
  connection,
  pool,
  owner: wallet.publicKey,
  amountIn: 10_000_000_000n,                       // 10 SOL in atoms
  minAmountOut: (quote.amountOut * 995n) / 1000n,  // 0.5% slippage
  aToB,
});

const signature = await wallet.sendTransaction(tx, connection);
  • Always pass a real minAmountOut. It is the user’s guarantee against fee changes between quote and execution, and against any module.
  • Swaps are exact input. There is no exact-output swap in v1.

Routing#

  • One pair can have many pools, one per patch and fee, each with its own liquidity. Quote all of them and take the best output.
  • A module can revert. If a simulation fails, skip that pool: the whole transaction reverts, so funds are never half-swapped.
  • The program does not route multi-hop. Chain hops in your own transaction. On mainnet the Patchbay app compares its route with Jupiter and falls back to it.
  • Account list: the swap’s named accounts, then remaining_accounts as one slice per module with a swap bit, and hook_account_counts with their lengths. The layout is on How it works. Built-in slices come from moduleSlice; a third-party module’s author publishes theirs.

Events#

Every swap emits an Anchor event in the program logs:

SwapEventRust
SwapEvent {
  pool: Pubkey,
  sender: Pubkey,
  a_to_b: bool,
  amount_in: u64,
  amount_out: u64,   // what the user received: curve output + modules' give_out
  fee_bps: u16,      // the fee this swap paid
  reserve_a: u64,
  reserve_b: u64,
}

Read the oracle#

twap.tsTypeScript
import { twap } from '@patchbay/sdk';

// mean tick over the last 30 minutes from a TWAP oracle module's per-pool state
const { meanTick, observations } = twap(twapState, 1800);

// tick → price (B atoms per A atom); adjust for decimals to get a UI price
const price = Math.pow(1.0001, meanTick) * 10 ** (decimalsA - decimalsB);

On-chain, call the TWAP oracle’s consult(window_secs) and read { mean_tick, observations } from return data. See TWAP oracle.

Limit orders#

orders.tsTypeScript
import { placeOrder, cancelOrder, claim } from '@patchbay/sdk';

// on-chain: place_order(side, price_q64, amount)
// price = B atoms per A atom as Q64.64
const toQ64 = (uiPrice: number, decimalsA: number, decimalsB: number) =>
  BigInt(Math.round(uiPrice * 10 ** (decimalsB - decimalsA) * 2 ** 32)) << 32n;

const tx = await placeOrder({
  pool,                          // a pool whose patch has a Limit order module
  module: limitOrderModule,
  owner: wallet.publicKey,
  side: 'buyA',                  // buy A with B ('sellA' sells A for B)
  priceQ64: toQ64(149.95, 9, 6), // fills at this price or better
  amount: 300_000_000n,          // escrowed in the module's hook vault
});
// cancelOrder: unfilled amount + proceeds back; claim: proceeds so far

Create a pool#

create-pool.tsTypeScript
import { buildCreatePoolTx } from '@patchbay/sdk';

// create_pool + init_pool_state for every module, in one transaction
const tx = await buildCreatePoolTx({
  mintA: SOL_MINT,
  mintB: USDC_MINT,
  feeBps: 30,
  modules: [dynamicFeeModule, limitOrderModule], // module accounts, in patch order
});

create_pool does not call modules. Modules that keep per-pool state get it from their own init_pool_state, which the SDK adds right after create_pool in the same transaction.