Module 04: Trading

Buy tokens, sell tokens, open leveraged positions, simulate before executing.

Prerequisites

  • Wallet funded with the pair asset of the main you trade (USDG on the default main) (→01), client initialized (→02)
  • Understand hybrid multiplier + presale (→11) before sizing trades

Next steps after a trade

  • Bought STASIS? Stake it (→06), then borrow against it (→05)
  • Bought Floor+? Borrow against it (→05) for capital efficiency
  • Leverage is terminal: use it last in any stack (→12 for full strategies)
  • Hit an error? See the error reference below or SDK Reference for the full error index

Examples use the default main, STASIS, whose pair asset is USDG (6 decimals). On another main read "USDG" as that main's pair asset and use its decimals(). The mains of a deployment are listed in /contracts.json (mains[]).


1. Why Trade on BASIS

BASIS token mechanics differ fundamentally from standard AMMs:

  • Stable+ tokens can only go up, measured in the main's pair asset. Elastic supply, and slippage stays in the pool, so every trade increases the price. STASIS is the canonical Stable+. No creator can rug a token: there is no LP to pull, and every token in circulation was purchased at market price.
  • Floor+ tokens have a rising floor. Price moves freely above a floor that never decreases. Sell pressure creates a dip, not a death spiral. Your downside shrinks over time as the floor rises.
  • Meme+ (reversed) tokens can fall in price. Their floor is never below the launch price; at setting 100 the floor is the launch price.
  • No price liquidation on leverage. Leverage positions expire by time only: price crashes cannot trigger liquidation. Your leverage position is recorded on the token's main and has its own ids (§3c).
  • Each main's vault earns from its own pair. 10% of the base fee of every trade on a main's tokens goes to that main's vault, for its stakers. There can be several mains, each with its own pair asset and its own pool in the vault.
  • Real money. Every trade moves real funds and is final. Quote with the fee included before you send.
  • Gas is paid in ETH.

2. How Trading Works

Routing

All trades go through one SWAP contract. SWAP keeps a registry of mains: pairs(main) → (pairAsset, staking, active), listed by pairList(i) and pairCount(). A token trades against its own main (token.quoteToken()), and a main trades against its pair asset.

TradePath
Buy / sell a main[pairAsset, main] / [main, pairAsset]
Buy / sell a factory token with the pair asset[pairAsset, main, token] / [token, main, pairAsset]
Buy / sell a factory token with its main[main, token] / [token, main]
Token to token, across mains, an outside token, the native coinswapCross(path, amountIn, minOut, recipient, deadline, unwrapNative) in one transaction; quote with quoteCross(path, amountIn, trader) by eth_call (see Cross-pair swap)

A path that does not match the token's main reverts (Not pair asset, Wrong main, Not main, Bad pair or unknown pair). A factory token paired straight with the pair asset ([pairAsset, token], [token, pairAsset]) reverts Bad pair.

A factory-token trade also moves its own main's pool, and that main's vault takes its 10% of the base fee.

Token Types

TypePrice BehaviorFeeLeverage
Stable+ (multiplier 100; every main is one)Can only go up (in pair-asset units)A main: the base only. A factory token: base + its creator fee + its rewards feeUp to about 37x, depending on the token and the size of the buy; must be more than 2x
Utility+ (multiplier 50–99) and Floor+ (multiplier 1–49)Free above rising floorBase + creator fee + rewards feeLower: the loan is valued at the floor
Meme+, Yolo+ and Yolo (reversed curve: 1–50, 51–99, 100)Free; the floor is never below the launch priceBase + creator fee + rewards fee (rewards-fee cap 10%)Lowest: valued at the floor
Predict+ (market token, multiplier 100)Price only up (Stable+ subtype)Base + the market creator's creator fee; no rewards feeAs Stable+

Read the rate for your wallet: TAXES.getTaxRate(token, wallet) (basis points). Leverage reaches up to about 37x, depending on the token and the size of the buy. The position must be more than 2x (Under 2x); read simulateLeverageLocked / simulateLeverageFactoryLocked for the real figure.

Presale, then Open Trading

A token created with presaleTokens > 0 starts in a presale: presaleComplete() is false until presaleSold() reaches presaleTarget() (both are token counts with 18 decimals); the buy that reaches it emits Bonded. There is no presale method to choose: SWAP.buyTokens / sellTokens send the trade to the presale or the AMM by themselves, on the same curve. On a token created with auto-vest, a presale buy goes into a vesting schedule instead of the wallet.

Holder rewards are a separate thing: only a token created with rewardsEnabled pays its rewards fee to holders, in proportion to their balance, in the pair asset, claimable with claimRewards(). On any other token getClaimableRewards reads 0. claimRewards() never pays the creator.

AMM Pricing

BASIS prices every token on a proprietary pricing curve. The hybridMultiplier (1–49 for Floor+, 50–99 for Utility+, 100 for Stable+; Meme+, Yolo+ and Yolo use the same 1–100 dial on the reversed curve) sets where the token sits on the stability dial, for both buys and sells. This is not a standard AMM with a bolt-on sell mechanism, so every trade behaves differently from a traditional AMM.

  • Stable+ (multiplier=100): Slippage stays in the pool, so the price can only go up in its pair asset: both buys and sells increase the price.
  • Utility+ and Floor+ (multiplier 50–99 and 1–49): The price moves freely, and slippage that stays in the pool builds a rising floor. Even at multiplier 1, tokens are far more stable than on a traditional AMM.

Implication for agents: Standard AMM arbitrage assumptions do not apply. On Stable+ tokens, price increases on every trade regardless of direction. On Floor+ tokens, the floor rises with every trade. Model your strategies around these mechanics.

Fees and Where They Go

Every trade and every bet pays one base fee, TAXES.baseRate(): 0.60% today (capped at 1.5% in the contract). A wallet with an on-chain fee referrer pays 10% less (0.54%). A factory or market token adds its creator fee (0 until its creator sets it, at most 1%) and, only on a token created with holder rewards, its rewards fee (at most maxRewardsFee: 0.5% at multiplier 100, rising to 10% at multiplier 1, always 10% on a reversed token (Meme+, Yolo+, Yolo)). A main pays the base only. A fee-exempt wallet pays nothing. Each of the two fees can change once per cooldown (4 hours today). Read the live numbers: getTaxRate(token, wallet) (basis points) and getTokenTaxInfo(token).

Trades and bets: split of the base part:

RecipientShare of the base fee
SOCIAL (Alpha+ author pay)15% on trades through SWAP; on a bet this 15% goes to the buyback, to be burned
Vault of the token's main10%
Operating wallet10%
The trader's fee referrer15%
That referrer's referrer5%
Buybackthe rest (45%, plus any referral share nobody can receive: 65% with no referrer)

Loan-side fees and unwon prize money (loan origination, increase and extension, the leverage loop fees and the unused last slice of a leverage open, the 3% leverage exit tax):

RecipientShare of the base fee
Vault of the token's main10%
Operating wallet10%
Creator of the collateral token20%
Buyback60%

The creator fee goes to the token's dev wallets on every trade, and the rewards fee to the token's holders; nothing is claimed by the creator. Payouts are in the pair asset of the token's main. A bet pays getTaxRate(marketToken, bettor).


3. Actions

Setup used by the examples below:

// The default main and its pair asset; on another main take them from /contracts.json mains[]
const MAIN = client.mainTokenAddress;
const PAIR_ASSET = client.usdbAddress;
const pairDecimals = await client.publicClient.readContract({ address: PAIR_ASSET, abi: erc20Abi, functionName: "decimals" }); // 6 for USDG
MAIN = client.main_token_address
PAIR_ASSET = client.usdb_address
pair_decimals = 6  # read decimals() from the pair asset; 6 for USDG

3a. Buy: client.trading.buy()

Contract call: SWAP.buyTokens(amount, minOut, path, wrapTokens); approve path[0] to SWAP.

// JS
const result = await client.trading.buy("0xTokenAddress", parseUnits("5", pairDecimals));

// With slippage protection (recommended). getAmountsOut excludes the trade tax:
// quote the input after the tax.
const amountIn = parseUnits("5", pairDecimals);
const taxBps = await client.taxes.getTaxRate(TOKEN, wallet);
const net = amountIn - amountIn * BigInt(taxBps) / 10000n;
const preview = await client.trading.getAmountsOut(net, [PAIR_ASSET, MAIN, TOKEN]);
const minOut = preview * 98n / 100n; // 2% tolerance on a quote that already has the tax off
const result = await client.trading.buy(TOKEN, amountIn, minOut);
# Python
result = client.trading.buy("0xTokenAddress", 5 * 10**pair_decimals)

# With slippage protection (getAmountsOut excludes the trade tax)
amount_in = 5 * 10**pair_decimals
tax_bps = client.taxes.get_tax_rate(TOKEN, wallet)
net = amount_in - amount_in * tax_bps // 10000
preview = client.trading.get_amounts_out(net, [PAIR_ASSET, MAIN, TOKEN])
min_out = preview * 98 // 100
result = client.trading.buy(TOKEN, amount_in, min_out)

SWAP.quoteCross(path, amountIn, wallet) called with eth_call returns the same taxed figure in one read.

ParamTypeDescription
tokenAddressstringToken to buy
amountInbigint/intPair-asset amount of the token's main, in that asset's decimals (6 for USDG)
minOutbigint/intMin tokens to receive (slippage guard). Never pass 0 in production
wrapTokensbooleanOnly when buying a main: stake the bought tokens in that main's vault in the same transaction. Reverts Below min stake when the bought amount is under the vault's minStake(). Ignored for factory tokens. Default: false

Pre-flight checklist:

  1. Check your balance of the pair asset
  2. Check pool depth: Call client.api.getToken(tokenAddress) and compare your trade size against depthUsdE18 (divide by 1e18). If your trade exceeds 5% of pool depth, run the impact probe (Section 7) or split into smaller trades. Elastic supply AMMs are more sensitive to large orders than traditional AMMs: use 5% as the sizing threshold.
  3. Quote with the tax included (above)
  4. Set minOut to ~95–98% of that taxed quote
  5. Read the fee: client.taxes.getTaxRate(tokenAddress, wallet) (basis points). On top of the base a creator can set a creator fee of up to 1% and, on a rewards token, a rewards fee of up to 10%.

What happens: the pair asset goes to SWAP, the tax is taken off the input, and the tokens are bought on the curve (elastic supply) and sent to you. If Stable+, price increases permanently. During a presale the buy counts toward presaleSold; if the token has auto-vest on, a presale buy is put in a vesting schedule for you instead of being delivered. Holding a rewards-enabled token earns its rewards fee (→07).

Pool depth ≠ tradability. BASIS uses elastic supply: tokens are minted on buy and burned on sell. All factory tokens are tradeable from creation. depthUsdE18 from getToken() represents the pool depth that determines price impact, not whether trading is possible. A token with $100 liquidity is still fully tradeable: your price impact will just be higher per dollar spent.

Fee: TAXES.getTaxRate(token, wallet): the 0.60% base (0.54% with a fee referrer) plus the token's creator fee and rewards fee.

→ For trades exceeding 5% of pool depth, see Section 7 (Position Sizing) before executing. Elastic supply AMMs are more sensitive to large orders than traditional AMMs. Large trades relative to depth move the price sharply. Probe first, split if needed.


3b. Sell: client.trading.sell() and sellPercentage()

Contract call: SWAP.sellTokens(amount, minOut, path, false); approve the token sold to SWAP.

// JS: sell exact amount
const result = await client.trading.sell("0xTokenAddress", parseUnits("1", 18), true); // true = sell through to the pair asset

// JS: sell percentage (reads balance automatically)
const result = await client.trading.sellPercentage("0xTokenAddress", 50); // sell 50%
# Python: sell exact amount
result = client.trading.sell("0xTokenAddress", 1 * 10**18, to_pair_asset=True)

# Python: sell percentage
result = client.trading.sell_percentage("0xTokenAddress", 50)

sell() parameters:

ParamTypeDescription
tokenAddressstringToken to sell
amountbigint/intToken amount (18 decimals)
toPairAssetbooleanSell through to the main's pair asset ([token, main, pairAsset]). false stops at the main ([token, main]). Default: false
minOutbigint/intMin output. Never pass 0 in production

Native coin out goes through swapCross with a path that ends in the wrapped native coin and unwrapNative = true, only where a route from the pair asset to the wrapped native coin exists. sellTokens(…, swapToETH = true) reverts use swapCross. The sell fee is taken from the output.

sellPercentage() parameters:

ParamTypeDescription
tokenAddressstringToken to sell
percentagenumber1–100
toPairAssetbooleanSell through to the pair asset. Default: false

Pre-flight checklist:

  1. Check token balance (client.tokens.balanceOf(tokenAddress, wallet))
  2. A staked main is not in your wallet: unstake it first (Module 06)
  3. Preview: getAmountsOut(amount, path), then take getTaxRate(tokenYouSell, wallet) basis points off the result

Token-specific sell behavior:

  • Stable+: You CAN sell, and the price stays up for other holders. Your sale still receives less than the quoted price after fees and slippage, more so on a large sale.
  • Floor+: Price dips on sell, but floor holds. Other holders are protected from cascade selling.
  • Meme+: The price falls on a sell; the floor never goes below the launch price.

Sell vs Borrow decision:

  • Sell when the price is right and you want to lock in profit or fully exit the position
  • Borrow when you want liquidity but still believe in the upside: keeps your exposure intact
  • Good operators use both. Borrowing is powerful but delaying an exit on a peaked token is not a strategy.

3c. Leverage Buy: client.trading.leverageBuy()

⚠️ Leverage is not a hub loan. leverageBuy() opens an amplified position in one transaction (SWAP.leverageBuy(amount, minOut, path, numberOfDays), or mixedBuy to borrow against only part of the first buy: the rest stays in the same position as debt-free collateral, nothing goes to the wallet). Your leverage position is recorded on the token's main, one set per main: leverages(wallet, id) and leverageCount(wallet), ids from 1. A wallet has at most one open position per token (openLeverageOf(wallet, token) on the main, 0 = none): a leverage buy on a token where you already hold one adds to it and keeps its end date. Hub-loan ids and leverage ids are separate counters: never pass a leverage id to a client.loans method. Partial-close with SWAP.partialLoanSellFor(main, id, percentage, true, minOut). To extend, repay or claim a liquidated position, call that main with isLeverage = true (below).

Always simulate before executing.

// JS: Step 1: Simulate
const sim = await client.leverageSimulator.simulateLeverageLocked(
  parseUnits("10", pairDecimals),
  [PAIR_ASSET, MAIN],
  10n,  // minDaysLoan() of the main (10 today)
  100n  // leveragePct: 100 = all of it leveraged (leverageBuy); 1 to 99 quotes a mixedBuy
);
console.log("Total position:", sim.totalColleteral); // the field is spelled totalColleteral in the ABI
console.log("Total borrowed:", sim.totalBorrowed);
console.log("Total fees:", sim.totalFees);

// Step 2: Execute. leverageBuy checks minOut against the whole position
const minOut = sim.totalColleteral * 97n / 100n;
const result = await client.trading.leverageBuy(parseUnits("10", pairDecimals), minOut, [PAIR_ASSET, MAIN], 10n);
// Take the position id from the LeverageCreated event in the receipt (LeverageIncreased when the buy
// added to your open position on that token), or read openLeverageOf(wallet, token) on the main.
// There is nothing to wait for.
# Python
sim = client.leverage_simulator.simulate_leverage_locked(10 * 10**pair_decimals, [PAIR_ASSET, MAIN], 10, 100)
print(f"Position: {sim.totalColleteral}, Fees: {sim.totalFees}, Borrowed: {sim.totalBorrowed}")

min_out = sim.totalColleteral * 97 // 100  # leverageBuy checks minOut against the whole position
result = client.trading.leverage_buy(10 * 10**pair_decimals, min_out, [PAIR_ASSET, MAIN], 10)

For a factory token (3-hop path):

// Use simulateLeverageFactoryLocked for factory tokens
const sim = await client.leverageSimulator.simulateLeverageFactoryLocked(
  parseUnits("10", pairDecimals),
  [PAIR_ASSET, MAIN, "0xFactoryToken..."],
  10n,
  100n
);
const result = await client.trading.leverageBuy(
  parseUnits("10", pairDecimals), sim.totalColleteral * 97n / 100n,
  [PAIR_ASSET, MAIN, "0xFactoryToken..."],
  10n
);

leverageBuy() parameters:

ParamTypeDescription
amountbigint/intPair-asset input, in the pair asset's decimals
minOutbigint/intMinimum total position (totalColleteral of the simulation less your tolerance)
pathaddress[][pairAsset, main] or [pairAsset, main, factoryToken]; path[0] must be the pair asset of path[1]'s main, else wrong path
numberOfDaysbigint/intminDaysLoan() to maxDaysLoan() of the main (10 to 1000 today). Ignored when the buy adds to an open position on the token: the add is priced for the days that position has left

Reverts: Under 2x (the position would not be more than twice a plain buy); Extend first (an add to an open position with fewer than minDaysLoan() days left); Range (mixedBuy with a percentageLeverage outside 1 to 99); In presale (an auto-vest token cannot be leveraged before its presale ends); No WL for freeze and WL cap (a frozen token needs a whitelist entry with room); Min out.

How it works internally (you don't need to manage this): Each loop buys tokens and pays the token's swap tax at its full rate (no referral discount), borrows against them (2% plus 0.005% per day, today's values), and buys again. The loops stop when the last slice is too small for another loop; that small slice is not bought but paid in as a loan-side fee. The contract handles the entire loop in a single atomic transaction. Either it fully succeeds or fully fails: no half-built positions. You interact with the result as a single leverage position, not as separate loans.

Leverage characteristics:

Token TypeEffective LeverageWhy
Stable+ (STASIS)Up to about 37x, depending on the size of the buy; read the simulationFloor = spot (100% LTV), maximum loops
Floor+ near launchHigh (varies)Floor ≈ spot at launch: this window closes fast
Floor+ (spot >> floor)LowerLTV calculated against floor, not spot: fewer effective loops

Leverage sizing rules:

  • Smaller positions on deep pools = more loops = higher leverage
  • Larger positions = fewer effective loops (price impact reduces each loan yield)
  • The position must be more than 2x the spot amount, or the call reverts Under 2x
  • Rule: Always take minimum duration (10 days) and extend. Extensions cost 0.005%/day vs 2% to re-originate (today's values; read minDaysLoan(), staticFeePercentage() and dynamicFeePercentage() on the main).

At expiry nothing happens by itself. After liquidationTime a keeper liquidates the position (ALiquidator.liquidate; only the main's liquidator contract may do it). Assume it can happen in the first block after the deadline. Until that transaction lands a repay or a paid extension still goes through; do not plan on the gap. The keeper closes the position: the collateral that covers the debt is taken to repay it, for every token type. 10% of the rest is the liquidation fee, and you claim the remainder with claimLiquidation(id, true) on the main, in the collateral token. Claim within 365 days of the liquidation; after that the leftover may no longer be claimable. Worst case nothing is left. You never owe more than the collateral. There is no price liquidation.

Repaying or extending. The contract calls (the SDK wraps them, and SWAP.repayWith repays with the pair asset through SWAP):

ActionContract call (on the position's main)Wrapper
ExtendextendLoan(id, days, true, payInMainPair, refinance): days from minDaysLoan() to maxDaysLoan(), or 0 with refinance; with payInMainPair approve the pair asset to the main for the feeclient.trading.extendLeverage(main, id, days, payInMainPair, refinance)
RepayrepayLoan(id, amount, true): 2^256 - 1 repays all and returns all the collateral, less repays part and returns the same share of the collateral (what is left of the debt must stay at least the main's minimumLoan(), else Repay in full); approve leverages(wallet, id).fullAmount of the pair asset to the main firstclient.trading.repayLeverage(main, id)
Claim after liquidationclaimLiquidation(id, true)client.trading.claimLeverageLiquidation(...)

Otherwise take profit or exit with partialLoanSellFor (§3d).


3d. Close / Partial Close: client.trading.partialLoanSellFor(main, …, isLeverage=true)

⚠️ This method lives on client.trading, NOT client.loans. Close with SWAP.partialLoanSellFor(main, id, pct, true, minOut), naming the main the position lives on. A hub loan is closed with LOANS.hubPartialLoanSell (Module 05). Passing 100n as percentage fully closes the position: the "partial" in the name refers to the ability to close in 10% increments, not a constraint.

Close all or part of a leveraged position.

// JS: Read positions (1-indexed: loop from 1 to count inclusive).
// Reads leverageCount / leverages on the position's main: positions are per main, run the loop once per main.
const count = await client.trading.getLeverageCount(wallet);
for (let i = 1n; i <= count; i++) {
  const pos = await client.trading.getLeveragePosition(wallet, i);
  if (pos[9]) { // pos[9] = active flag
    console.log(`Active position ${i}: ${pos[2]} tokens of ${pos[1]}`);
  }
}

// Close a position: 100n = full close, or use 10-90 for partial
const positionId = 1n; // 1-indexed: find via the loop above

// Preview what the close really sells: the residual, the part of your collateral left to sell.
// The preview is on MainLens (caMAINLENS in /contracts.json), with the main as the first argument.
const [, , residualTokens] = await client.publicClient.readContract({
  address: MAIN_LENS, abi: mainLensAbi, functionName: "getPartialLoanSellAmounts",
  args: [MAIN, positionId, wallet, 100n, true],
});
const quote = await client.trading.getAmountsOut(residualTokens, [MAIN, PAIR_ASSET]); // a factory token: [token, MAIN, PAIR_ASSET]
const sellMinOut = quote * 95n / 100n; // no trade tax on the close; minOut is checked after the exit tax (at most 3%), so 3% for it plus 2% tolerance

const result = await client.trading.partialLoanSellFor(MAIN, positionId, 100n, true, sellMinOut);
# Python: Read positions (1-indexed), on the position's main
count = client.trading.get_leverage_count(wallet)
for i in range(1, count + 1):
    pos = client.trading.get_leverage_position(wallet, i)
    if pos[9]:  # active flag
        print(f"Active position {i}: {pos[2]} tokens of {pos[1]}")

# Close a position
position_id = 1
_, _, residual_tokens = main_lens.functions.getPartialLoanSellAmounts(MAIN, position_id, wallet, 100, True).call()  # MainLens: caMAINLENS
quote = client.trading.get_amounts_out(residual_tokens, [MAIN, PAIR_ASSET])  # a factory token: [token, MAIN, PAIR_ASSET]
sell_min_out = quote * 95 // 100  # no trade tax on the close; minOut is checked after the exit tax (at most 3%)
result = client.trading.partial_loan_sell_for(MAIN, position_id, 100, True, sell_min_out)

The contract compares minOut with what you receive: the pair-asset proceeds of the residual (the part of your collateral left to sell), after the exit tax (3% of the part above your remaining own cash in, so at most 3% of the proceeds). The residual sale pays no swap tax. Quoting the whole collateral instead sets minOut far too high and reverts Min out not met on every real position.

ParamTypeDescription
positionIdbigint/intLeverage position index (1-indexed). Find via getLeverageCount() + getLeveragePosition() loop. Index 0 is always empty.
percentagebigint/intMust be divisible by 10 (10, 20, 30 ... 100). 100 = full close. Any other value reverts % by 10 (not a multiple of 10) or % range (outside 10 to 100). A caller that does not own the position gets Not user.
isLeveragebooleantrue for a leverage position. With false a wallet's call reverts Not user: a plain loan belongs to the hub and is sold with LOANS.hubPartialLoanSell.
minOutbigint/intMinimum pair asset you receive from the residual, after the exit tax (see the preview above). Never pass 0.

Exit tax: 3% of the proceeds above what is left of your own cash in (leverageBuyAmount − cashedOut). The result is in the LoanSold event.

getLeveragePosition return fields (leverages(user, id) on the main):

IndexFieldTypeDescription
0useraddressWallet that opened the position
1tokenaddressToken held as collateral
2collateralAmountuint256Collateral held by the protocol until the position closes
3liquidatedAmountuint256Collateral taken by a liquidation to repay the debt
4fullAmountuint256Total repay obligation
5borrowedAmountuint256Original borrowed amount
6liquidationTimeuint256Unix timestamp of expiry
7liquidationClaimuint256What the owner can claim after a liquidation
8isLiquidatedboolWhether position was liquidated
9activeboolWhether position is still open
10creationTimeuint256Unix timestamp of creation
11timeOfClosureuint256Unix timestamp of closure
12leverageobject{ leverageBuyAmount, cashedOut }

Troubleshooting: Position returns all zeros? Leverage positions are 1-indexed and stored per main. If getLeveragePosition(wallet, 0) returns all zeros, try index 1, and read the main the position was opened on. Loop from 1 to getLeverageCount() inclusive: index 0 is always empty/cleared.


4. Simulate Before Executing

All simulation methods are read-only and free to call.

Price Preview

// Preview output for any swap: returns a single bigint (scalar), not an array. Before tax.
const expectedOut = await client.trading.getAmountsOut(parseUnits("5", pairDecimals), [PAIR_ASSET, MAIN, TOKEN]);
expected_out = client.trading.get_amounts_out(5 * 10**pair_decimals, [PAIR_ASSET, MAIN, TOKEN])
# Returns a single int, not a list. Before tax.

Leverage Simulation

// A main (2-hop)
const sim = await client.leverageSimulator.simulateLeverageLocked(amount, [PAIR_ASSET, MAIN], days, 100n); // leveragePct 100 = leverageBuy

// Factory token path (3-hop)
const sim = await client.leverageSimulator.simulateLeverageFactoryLocked(amount, [PAIR_ASSET, MAIN, TOKEN], days, 100n);

simulateLeverageLocked() return fields (EndResult, ABI spelling; money fields are in the main's pair-asset units). Only the fields you need are listed; the other fields are internal.

FieldDescription
totalRepayTotal you would need to repay to close the position
totalBorrowedPair asset borrowed across all loops
totalColleteralYour total position size in tokens after all loops
totalFeesSwap tax plus the loan fees on every loop
totalDexTaxThe swap-tax part of totalFees

Position Inspection

// Current prices: no contract returns USD
const pairPrice = await client.trading.getPairPrice(tokenAddress); // raw pair-asset units per whole token (token.getPairPrice())
const mainPrice = await client.trading.getTokenPrice(tokenAddress); // in the token's main (for a main: in its pair asset)

// Existing leverage positions (1-indexed: loop 1 to count inclusive), per main
const count = await client.trading.getLeverageCount(walletAddress);
for (let i = 1n; i <= count; i++) {
  const pos = await client.trading.getLeveragePosition(walletAddress, i);
  if (pos[9]) console.log(`Position ${i}: ${pos[2]} of ${pos[1]}, expires ${pos[6]}`);
}

Live Reads for a Token

For a live token, read the live values instead of simulating raw state: SWAP.getAmountsOut for a quote, the token's own calculateFloor() for its floor price, client.trading.getPairPrice(token) for its price in the pair asset, and the MainLens previews (getPartialLoanSellAmounts) for a close. No contract returns a USD price.


5. Fees

ActionFeeGoes To
Buy / sell any tokengetTaxRate(token, wallet): 0.60% base (0.54% with a fee referrer) + creator fee + rewards feeBase: SOCIAL 15, vault 10, operating wallet 10, referrers 15 + 5, buyback the rest (45 to 65). Creator fee: the creator. Rewards fee: holders
Leverage openThe token's full swap tax on every loop + 2% origination per loop + 0.005%/daySwap tax: as a trade, with no referral. Loan fees: vault 10, operating wallet 10, the token's creator 20, buyback 60
Leverage exit3% of the cash-out above the initial buySame loan-side split

Round-trip cost reality:

  • Round trip = 2 × getTaxRate, plus price impact: 1.2% on a main or a token with no creator or rewards fee (1.08% with a fee referrer)
  • Leverage: read totalFees and totalDexTax from the simulation

Loan duration rule: Always take minimum duration (10 days) and extend as needed. A 100-day loan costs 2% + 0.5% = 2.5%. A 10-day loan extended 90 days costs the same 2.5% but gives you the option to exit early without re-originating. (Today's values: read staticFeePercentage(), dynamicFeePercentage(), minDaysLoan() and maxDaysLoan() on the main.)


6. Floor+ Profit Scenarios

Floor+ tokens are the most interesting to unwind. The floor rises over time, which can lock in gains even after price pullbacks.

ScenarioFloor at BuyCurrent PriceBest MoveWhy
Token ran up big$1.00$3.50Sell, repay loanCaptures full spread above entry
Token near floor$1.00$1.10Hold, extend loanThe floor limits the fall: wait for volume
Token pulled back but floor rose$1.00 (floor now $1.80)$2.00Sell at $2.00Risen floor locked in gains through pullback
Token at floor, no volume$1.00$1.00Let loan expireSmall loss, mostly the fees: the floor held

Key insight: Loan LTV is calculated against the floor price, but you sell at the market price. When a Floor+ token runs up, there is a large spread between what you owe (floor-based) and what the token is actually worth. Selling captures that entire spread.

Leverage exit example: $100 input → $800 position at $1.00. Token rises to $2.00. Position worth about $1,600, debt about $700: about $900 comes out before the exit tax. The tax is 3% of the $800 above your own $100 (about $24), so about $876 reaches the wallet, before price impact. Use partialLoanSellFor() to scale out in 10% increments without closing the whole position.


7. Position Sizing

Large single buys on shallow pools move the price significantly. Always probe before committing.

// Probe price impact before entering
// getAmountsOut contains no fee: this probe measures curve impact only
const testAmount = targetAmount / 100n;  // 1% probe
const testOut = await client.trading.getAmountsOut(testAmount, path);
const testRate = testOut * 100n / testAmount;

const fullOut = await client.trading.getAmountsOut(targetAmount, path);
const fullRate = fullOut * 100n / targetAmount;

const impactBps = (testRate - fullRate) * 10000n / testRate; // basis points
// < 50bp (0.5%): good, standard trade
// 50–200bp (0.5–2%): acceptable for conviction plays
// > 200bp (2%+): split into multiple smaller trades

Factors affecting impact:

  • startLP (a creator-set depth setting from 1,000 to 100,000, not deposited capital; 10,000 is the standard depth) sets pool depth, higher = less impact per trade
  • Stable+ pools only grow over time (slippage stays in the pool): impact decreases as the token matures
  • Floor+ pools grow more slowly: impact decreases but less predictably
  • A factory-token trade also moves its own main's pool, so that main's depth matters too

Get token details before trading:

// Use getToken(address) to inspect depthUsdE18 and multiplier
// depthUsdE18 (USD x 1e18) is the current pool depth: use it to size trades

8. Complete Examples

Example: Buy, Check Balance, Sell 50%

const { BasisClient } = require("basis-sdk-js");

async function tradeTokens() {
  const client = await BasisClient.create({ privateKey: "0xYourKey..." });
  const TOKEN = "0xTokenAddress...";
  const MAIN = client.mainTokenAddress;   // the default main
  const PAIR_ASSET = client.usdbAddress;  // its pair asset (USDG, 6 decimals)
  const wallet = client.walletClient.account.address;

  // Check price: raw pair-asset units per whole token
  const price = await client.trading.getPairPrice(TOKEN);
  console.log("Price:", price, "pair-asset units");

  // Preview on the input after the tax (getAmountsOut excludes the trade tax)
  const pairDecimals = 6; // USDG; on another main read /contracts.json mains[].pairDecimals
  const amountIn = parseUnits("5", pairDecimals);
  const taxBps = await client.taxes.getTaxRate(TOKEN, wallet);
  const net = amountIn - amountIn * BigInt(taxBps) / 10000n;
  const preview = await client.trading.getAmountsOut(net, [PAIR_ASSET, MAIN, TOKEN]);

  // Buy with 2% slippage tolerance
  const minOut = preview * 98n / 100n;
  try {
    const buy = await client.trading.buy(TOKEN, amountIn, minOut);
    console.log("Bought:", buy.hash);
  } catch (e) {
    if (e.message.includes("Min out")) {
      // Retry with 5% tolerance
      const retryMin = preview * 95n / 100n;
      const buy = await client.trading.buy(TOKEN, amountIn, retryMin);
      console.log("Bought (retry):", buy.hash);
    } else throw e;
  }

  // Sell 50%: reads balance automatically
  const sell = await client.trading.sellPercentage(TOKEN, 50);
  console.log("Sold 50%:", sell.hash);
}
from basis_sdk import BasisClient

def trade_tokens():
    client = BasisClient.create(private_key="0xYourKey...")
    TOKEN = "0xTokenAddress..."
    MAIN = client.main_token_address   # the default main
    PAIR_ASSET = client.usdb_address   # its pair asset (USDG, 6 decimals)

    price = client.trading.get_pair_price(TOKEN)
    print("Price:", price, "pair-asset units")

    pair_decimals = 6  # USDG; on another main read /contracts.json mains[].pairDecimals
    amount_in = 5 * 10**pair_decimals
    tax_bps = client.taxes.get_tax_rate(TOKEN, client.wallet_address)
    net = amount_in - amount_in * tax_bps // 10000
    preview = client.trading.get_amounts_out(net, [PAIR_ASSET, MAIN, TOKEN])

    min_out = preview * 98 // 100  # 2% slippage tolerance
    buy = client.trading.buy(TOKEN, amount_in, min_out)
    print("Bought:", buy["hash"])

    sell = client.trading.sell_percentage(TOKEN, 50)
    print("Sold 50%:", sell["hash"])

Example: Leverage: Simulate, Open, Partial Close

const { BasisClient } = require("basis-sdk-js");

async function leverageTrading() {
  const client = await BasisClient.create({ privateKey: "0xYourKey..." });
  const PAIR_ASSET = client.usdbAddress;   // the default main's pair asset (USDG, 6 decimals)
  const MAIN = client.mainTokenAddress;    // the default main
  const path = [PAIR_ASSET, MAIN];
  const amount = parseUnits("10", 6);

  // 1. Simulate
  const sim = await client.leverageSimulator.simulateLeverageLocked(amount, path, 10n, 100n);
  console.log("Position:", sim.totalColleteral, "Fees:", sim.totalFees, "Borrowed:", sim.totalBorrowed);

  // 2. Open (min 10 days today); minOut is checked against the whole position
  const minOut = sim.totalColleteral * 97n / 100n;
  const open = await client.trading.leverageBuy(amount, minOut, path, 10n);
  console.log("Opened:", open.hash);

  // 3. Get position (1-indexed: loop from 1 to count inclusive), on this main
  const wallet = client.walletClient.account.address;
  const count = await client.trading.getLeverageCount(wallet);
  let activeId;
  for (let i = count; i >= 1n; i--) {
    const pos = await client.trading.getLeveragePosition(wallet, i);
    if (pos[9]) { activeId = i; break; } // pos[9] = active flag
  }

  // 4. Partial close: 50% (must be multiple of 10, 100 = full close)
  const [, , residualTokens] = await client.publicClient.readContract({
    address: MAIN_LENS, abi: mainLensAbi, functionName: "getPartialLoanSellAmounts", // MainLens: caMAINLENS
    args: [MAIN, activeId, wallet, 50n, true],
  });
  const quote = await client.trading.getAmountsOut(residualTokens, [MAIN, PAIR_ASSET]);
  const sellMin = quote * 95n / 100n; // no trade tax on the close; minOut is checked after the exit tax (at most 3%)
  const close = await client.trading.partialLoanSellFor(MAIN, activeId, 50n, true, sellMin);
  console.log("Partially closed:", close.hash);
}
from basis_sdk import BasisClient

def leverage_trading():
    client = BasisClient.create(private_key="0xYourKey...")
    PAIR_ASSET = client.usdb_address   # the default main's pair asset (USDG, 6 decimals)
    MAIN = client.main_token_address   # the default main
    path = [PAIR_ASSET, MAIN]
    amount = 10 * 10**6

    sim = client.leverage_simulator.simulate_leverage_locked(amount, path, 10, 100)
    print(f"Position: {sim.totalColleteral}, Fees: {sim.totalFees}")

    min_out = sim.totalColleteral * 97 // 100  # checked against the whole position
    open_result = client.trading.leverage_buy(amount, min_out, path, 10)
    print("Opened:", open_result["hash"])

    count = client.trading.get_leverage_count(client.wallet_address)
    active_id = None
    for i in range(count, 0, -1):
        pos = client.trading.get_leverage_position(client.wallet_address, i)
        if pos[9]:  # active flag
            active_id = i
            break

    _, _, residual_tokens = main_lens.functions.getPartialLoanSellAmounts(MAIN, active_id, client.wallet_address, 50, True).call()  # MainLens: caMAINLENS
    quote = client.trading.get_amounts_out(residual_tokens, [MAIN, PAIR_ASSET])
    sell_min = quote * 95 // 100  # no trade tax on the close; minOut is checked after the exit tax (at most 3%)
    close = client.trading.partial_loan_sell_for(MAIN, active_id, 50, True, sell_min)
    print("Partially closed:", close["hash"])

Cross-pair swap

<!-- MCP-PENDING: MCP.md §5 swap_cross, quote_cross -->

SWAP.swapCross(path, amountIn, minOut, recipient, deadline, unwrapNative) swaps between tokens of different mains, and through outside pools where a route exists, in one transaction with one base fee. Quote first with SWAP.quoteCross(path, amountIn, trader): it is not a view, so call it with eth_call (simulate), never as a transaction; the quote includes the fees trader would pay. Approve path[0] to SWAP, or send the native coin with path[0] = the wrapped native token and msg.value == amountIn. deadline is a unix time. recipient must not be the zero address. A hop between two different pair assets, or through an outside token, needs a route in the hop executor; it is available only where hopExecutor in /contracts.json is not the zero address and such a route exists.

ErrorWhen
"Deadline"The transaction landed after deadline
"no executor"The route needs an outside hop and no hop executor is set
"native in"Native coin sent, but path[0] is not the wrapped native token or amountIn != msg.value
"Transfer in"path[0] could not be pulled from the wallet (approval or balance)
"Min out"The output is below minOut
"no recipient"recipient is the zero address

9. Error Reference

ErrorTriggerFix
"Min out"A buy, sell or leverage buy returns less than minOut (for a leverage buy: the whole position)Quote again on the taxed amount, lower the size or widen the tolerance
"Min out not met"A partial loan sell returns less than minOutQuote the residual with getPartialLoanSellAmounts (§3d)
"Zero bought" / "Zero sold"The amount is too small to produce any outputRaise the amount
"Under 2x"leverageBuy() would not reach more than twice a plain buySimulate first; raise the input
"Extend first"A leverage buy that adds to your open position on the token, with fewer than minDaysLoan() days left on itExtend the position first
"Repay in full"A part repay of a leverage position would leave less than minimumLoan() of debtRepay more, or all
"wrong path"Leverage or mixed buy: path[0] is not the pair asset of the main in path[1], or the path is not 2 or 3 addressesRead SWAP.pairs(main).pairAsset
"Bad path"The path is not 2 or 3 addresses[pairAsset, main] or [pairAsset, main, token]
"Bad pair"The path does not match the token's own pair (for example [pairAsset, token])Rebuild the path from the token's main (token.quoteToken())
"Not main" / "Not MAIN"The main in the path is not a registered mainRead SWAP.pairs(main)
"Not pair asset" / "Wrong main"The pair asset or the main in the path does not belong to the token's pairBuild the path from token.quoteToken() and pairs(main).pairAsset
"unknown pair"The main's pair is unknown or pausedRead SWAP.pairs(main).active
"Alien token" / "Alien output" / "Wrong token"The input or output is not a BASIS token that SWAP acceptsCheck FACTORY.isEcosystemToken(token)
"use swapCross"sellTokens(…, swapToETH = true)Native out goes through swapCross
"In presale"Leverage buy of an auto-vest token before its presale is completeWait for presaleComplete()
"No WL for freeze" / "Overcapped on WL buying" / "WL cap"Frozen token: the wallet is not whitelisted, or the buy passes its token capAsk the creator
"% by 10" / "% range"Partial close percentage not a multiple of 10, or outside 10 to 100Use 10, 20 … 100
"Not user"Partial close of a position the caller does not own, or isLeverage = false from a walletUse your own leverage id with isLeverage = true; hub loans use LOANS.hubPartialLoanSell
"Trading is not enabled"The main's one-way launch switch is still shutRead main.tradingEnabled()
"has not bonded yet" / "Presale over"Only on a direct call to the token in the wrong phase; SWAP picks the presale or the normal function itselfTrade through SWAP.buyTokens / sellTokens

There is nothing to recover between the presale and open trading: SWAP routes a trade to the presale curve or the AMM by itself.


10. Strategy Context

SituationRecommended ActionModule
Bought STASISStake it in its vault for rewards→06
Holding a vault stakeBorrow the pair asset against it, redeploy→06
Bought Floor+ tokenBorrow against it (valued at the floor)→05
Want amplified exposureSimulate then leverageBuy() (terminal path)This module
Leveraged, want to take profitpartialLoanSellFor(main, …, isLeverage=true) in 10% incrementsThis module
Loan approaching expiryExtend cheaply (400x less than re-originate)→05
Full multi-step capital stackBuild paths A→B→C→E for maximum efficiency→12

The core insight from stacking strategies: Buying a token does not lock your capital. Borrow against almost any position to get the pair asset back, then deploy it again. Three stacked paths is the sweet spot: beyond that, fees erode the bag meaningfully. Leverage (Path E) is always the terminal step: it fully deploys your remaining pair asset and does not return capital for further stacking.


See Also