Cookbook

The shortest correct form of each operation an integrator ships. For swaps, route first: Quotes & Routing. Addresses come from GET /v1/venues, ABIs from GET /v1/abis/{name}, both keyed by chain id and both listed in the API & SDK Reference.


1. Setup

LangStack
TypeScriptviem / ethers + @btr-protocol/sdk
Pythonweb3.py
Rustalloy / ethers-rs
SolidityFoundry; declare the interface from Basic Operations §1
API=https://api.btr.markets curl -s $API/v1/venues | jq 'keys' curl -s $API/v1/abis/Pool | jq 'length'

Solidity callers: IPool.sol does not declare the trading functions. Use the IBtrPool block in Basic Operations §1; every Solidity snippet below assumes it.


2. Swap

2.1. TypeScript, routed

import { planToLegs, buildSwapCalls } from '@btr-protocol/sdk/router'; const plan = await fetch('https://api.btr.markets/v1/route', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ pools, token_in, token_out, amount_in: hexAmount }), }).then((r) => r.json()); const legs = planToLegs(plan, { slippageFrac: 0.005, tokenOf, nativeIn: true, amountInUnits: amountIn, // exact bigint; omitting it can overshoot the balance by 1 wei }); if (!legs) throw new Error('unroutable'); const calls = buildSwapCalls(legs, { recipient: user, wrappedNative }); // send from the user account: EIP-5792 wallet_sendCalls, or sequentially

2.2. TypeScript, single pool

import { getSwapQuote, swap, defaultDeadline } from '@btr-protocol/sdk/pool'; // First argument is an EIP-1193 provider (window.ethereum), not a viem client. const q = await getSwapQuote(provider, pool, tokenIn, tokenOut, amountIn); await swap(provider, pool, { tokenIn, tokenOut, amountIn, minAmountOut: (q.amountOut * 9950n) / 10000n, // 50 bps recipient: user, deadline: defaultDeadline(), });

2.3. Python (web3.py, single pool)

from web3 import Web3 w3 = Web3(Web3.HTTPProvider(rpc)) pool = w3.eth.contract(address=pool_addr, abi=pool_abi) # GET /v1/abis/Pool token = w3.eth.contract(address=token_in, abi=erc20_abi) quote = pool.functions.getSwapQuote(token_in, token_out, amount_in).call() min_out = quote[0] * 9950 // 10000 # quote[0] = amountOut token.functions.approve(pool_addr, amount_in).transact({"from": user}) pool.functions.swap( token_in, token_out, amount_in, min_out, user, 0xFFFFFFFF, # deadline opt-out sentinel ).transact({"from": user})

For a routed fill, POST /v1/route and encode each leg’s approve + swap the same way. Legs must be sent from the user account, no Multicall3.

2.4. Rust (alloy)

// Route off-chain, then send one approve + swap per leg from the user's wallet. let plan: RouteResponse = reqwest::Client::new() .post("https://api.btr.markets/v1/route") .json(&serde_json::json!({ "pools": pools, "token_in": token_in, "token_out": token_out, "amount_in": format!("0x{amount_in:x}"), })) .send().await?.json().await?; // Bind the ABI from GET /v1/abis/Pool, then per leg. // U256 has no Mul<u64>: lift the scalars, or use mul_div from your bigint crate. let min_out = amount_out * U256::from(9950u64) / U256::from(10000u64); let tx = pool.swap(token_in, token_out, amount_in, min_out, recipient, U256::MAX);

2.5. Solidity (vault / strategy)

using SafeERC20 for IERC20; // OpenZeppelin; forceApprove lives here, not on IERC20 IERC20(tokenIn).forceApprove(pool, amountIn); uint256 out = IBtrPool(pool).swap( tokenIn, tokenOut, amountIn, minOut, address(this), block.timestamp );

On-chain strategies normally fix a pool: no off-chain routing inside the vault. Aggregators still discover venues per chain via /v1/venues, off chain.


3. Liquidity

3.1. Deposit / withdraw (TypeScript)

import { deposit, withdraw, defaultDeadline, NATIVE_TOKEN } from '@btr-protocol/sdk/pool'; await deposit(provider, pool, { token, amount }); // token = NATIVE_TOKEN → sent as msg.value await withdraw(provider, pool, { token, lpAmount, minAmountOut, deadline: defaultDeadline(), });

3.2. Dual-route mint (TypeScript)

import { rankDeposit, buildDepositCalls } from '@btr-protocol/sdk/router'; // Args 2 and 3 are SYMBOLS, not addresses. amountIn is a human-units number, not bigint. // Returns { best, routes }: `best` is null when every route is gated off. const { best } = rankDeposit(pools, 'USDC', 'WETH', 1000, opts); if (!best) throw new Error('no feasible mint route'); // buildDepositCalls takes MarketMintArgs | TransferMintArgs, built from `best`: const calls = buildDepositCalls( poolAddress, best.mode === 'market' ? { mode: 'market', legs, depositToken, depositAmount } // Σ per-part minOut : { mode: 'transfer', token, amount, targetToken, lpAmountIn, minLpAmountOut }, { recipient: user, wrappedNative }, );

best describes the winning route; the MarketMintArgs / TransferMintArgs union is what buildDepositCalls consumes. Market mode is [approve?, swap(X→target)…, deposit(target)]; transfer mode is [approve?, deposit(X), swapLiability] and takes one approval total, since the LP burn needs no allowance. Fresh deposits in transfer mode are not batchable: the anti-JIT cooldown gates them.

3.3. Cross-asset exit: withdrawTo

// Burn LP on tokenFrom, receive tokenTo, priced along the anchor path. IBtrPool(pool).withdrawTo(tokenFrom, tokenTo, lpAmount, minAmountOut, deadline);

3.4. Transfer liability (swapLiability)

Moves the LP claim from one leg to another. Reserves do not move; a haircut may apply on an under-covered input leg, and both legs need LIABILITY_SWAP_ENABLED_BIT (Basic Operations §2).

uint256 lpOut = IBtrPool(pool).swapLiability( tokenIn, // leg to leave tokenOut, // leg to enter lpAmountIn, minLpAmountOut, deadline );
import { encodeFunctionData } from 'viem'; import { POOL_ABI } from '@btr-protocol/sdk/abis'; // No first-class sender: encode it. const data = encodeFunctionData({ abi: POOL_ABI, functionName: 'swapLiability', args: [tokenIn, tokenOut, lpIn, minLpOut, deadline], });

The front end labels this LP action swap (action matrix).

3.5. Donate

IBtrPool(pool).donate(token, amount);

Raises the leg’s LP index; reserves and liabilities are credited equally. Deposit into a leg before donating to it: donating into an unopened leg strands the gift permanently. See Incentivization §3.


4. Flash loan

BTR uses an ERC-3156-style callback named postFlashLoan, not the canonical onFlashLoan, and the first argument to flashLoan is the pool. Repay by transferring to the pool: Flash checks the pool’s balance and never calls transferFrom on the receiver.

IFlash(flash).flashLoan(pool, IERC3156FlashBorrower(address(this)), token, amount, data); function postFlashLoan(address initiator, address token, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32) { require(msg.sender == flash, "untrusted caller"); // … use funds … IERC20(token).transfer(pool, amount + fee); // NOT approve(flash) return keccak256("ERC3156FlashBorrower.postFlashLoan"); }

Views: maxFlashLoan(pool, token), flashFee(pool, token, amount). Full interface, fee arithmetic and the safety pattern: Composability §1.


5. Deploy a pool

address pool = IPoolFactory(factory).createPool( baseToken, tokens, // MUST be non-empty - createPool reverts InvalidInput on an empty array initdata // abi.encodeCall(IPool.initialize, (base, wnative, feeParams)) );

tokens seeds the factory’s discovery index and the deployment salt. It does not list assets for trading: that is a separate, gated ADD_ASSET operation on Admin, and createPool grants the deployer no administrative authority. Full walkthrough: Pool Deployment & Curation.


6. Read state

import { getPoolData, getCoverageRatio, getLPBalance } from '@btr-protocol/sdk/pool'; const data = await getPoolData(provider, pool, tokens, poolName); // 4 args const coverage = await getCoverageRatio(provider, pool, token); const shares = await getLPBalance(provider, pool, user, token); const activity = await fetch('https://api.btr.markets/v1/activity?limit=50').then((r) => r.json());

Integration examples, the longer form of this page: every snippet below as a runnable file with a test that executes it against a live deployment.

API & SDK Reference · Basic Operations · Pool · Admin · Glossary