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
| Lang | Stack |
|---|---|
| TypeScript | viem / ethers + @btr-protocol/sdk |
| Python | web3.py |
| Rust | alloy / ethers-rs |
| Solidity | Foundry; 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 sequentially2.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());7. Related
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