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#
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.
| Export | What it does |
|---|---|
listPools, listListings | Every pool of the AMM, decoded; the module registry. |
Pool, Config, Listing, Module decoders | Plus the built-in modules’ per-pool states and the order book. |
PDA helpers | Pool, 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. |
buildSwapTx | Wraps and unwraps SOL, creates token accounts, sets compute budget (400k) and priority fee, fills every module slice. |
buildAddLiquidityTx, buildRemoveLiquidityTx | Liquidity 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, claim | Limit order book. |
twap(state, windowSecs) | Mean tick from a TWAP oracle state. |
Find pools and quote#
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,
quoteSwapreproduces 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
SwapEventor the token balance change.
Without the SDK#
A pool address is a pure function of its mints, fee and patch:
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#
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_accountsas one slice per module with a swap bit, andhook_account_countswith their lengths. The layout is on How it works. Built-in slices come frommoduleSlice; a third-party module’s author publishes theirs.
Events#
Every swap emits an Anchor event in the program logs:
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#
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#
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 farCreate a pool#
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.